diff --git a/src/sentry/incidents/endpoints/serializers/workflow_engine_action.py b/src/sentry/incidents/endpoints/serializers/workflow_engine_action.py index 38a85f3c68b7..37c6053c31bb 100644 --- a/src/sentry/incidents/endpoints/serializers/workflow_engine_action.py +++ b/src/sentry/incidents/endpoints/serializers/workflow_engine_action.py @@ -1,3 +1,4 @@ +import logging from collections.abc import Mapping from typing import Any, Sequence @@ -18,6 +19,8 @@ from sentry.users.services.user.model import RpcUser from sentry.workflow_engine.models import Action, ActionAlertRuleTriggerAction +logger = logging.getLogger(__name__) + class WorkflowEngineActionSerializer(Serializer[dict[str, Any]]): def get_attrs( @@ -57,10 +60,16 @@ def serialize( type_value: int | None = ActionService.get_value(obj.type) target = attrs.get("target") - target_type: int = obj.config.get("target_type") + target_type: int | None = obj.config.get("target_type") target_identifier: str | None = obj.config.get("target_identifier") target_display: str | None = obj.config.get("target_display") + if target_type is None: + logger.warning( + "workflow_engine_action.missing_target_type", + extra={"action_id": obj.id, "action_type": obj.type}, + ) + sentry_app_id = None sentry_app_config = None if obj.type == Action.Type.SENTRY_APP.value: @@ -75,7 +84,9 @@ def serialize( ), "alertRuleTriggerId": str(alert_rule_trigger_id), "type": obj.type, - "targetType": ACTION_TARGET_TYPE_TO_STRING[ActionTarget(target_type)], + "targetType": ACTION_TARGET_TYPE_TO_STRING[ActionTarget(target_type)] + if target_type is not None + else None, "targetIdentifier": get_identifier_from_action( type_value, str(target_identifier), target_display ), diff --git a/tests/sentry/incidents/serializers/test_workflow_engine_action.py b/tests/sentry/incidents/serializers/test_workflow_engine_action.py index fbce43b7f2cb..22a56013836f 100644 --- a/tests/sentry/incidents/serializers/test_workflow_engine_action.py +++ b/tests/sentry/incidents/serializers/test_workflow_engine_action.py @@ -188,6 +188,25 @@ def test_sentry_app_action(self) -> None: sentry_app_expected["desc"] = f"Send a notification via {sentry_app.name}" assert serialized_action == sentry_app_expected + def test_missing_target_type_does_not_raise(self) -> None: + """ + Regression test for: ValueError: None is not a valid ActionTarget. + Actions whose config dict lacks a 'target_type' key (e.g. actions created + before the field was required) should serialize without raising. + """ + # Mutate the migrated action's config to remove target_type, simulating + # the DB state that triggers the bug in production. + self.critical_action.config.pop("target_type", None) + self.critical_action.save() + + serialized_action = serialize( + self.critical_action, + self.user, + WorkflowEngineActionSerializer(), + alert_rule_trigger_id=self.critical_trigger.id, + ) + assert serialized_action["targetType"] is None + def test_slack_action(self) -> None: self.integration = self.create_slack_integration( self.organization,