diff --git a/src/sentry/workflow_engine/handlers/action/notification/issue_alert.py b/src/sentry/workflow_engine/handlers/action/notification/issue_alert.py index 2c2e7c85fa5ba5..2bf845ac971a45 100644 --- a/src/sentry/workflow_engine/handlers/action/notification/issue_alert.py +++ b/src/sentry/workflow_engine/handlers/action/notification/issue_alert.py @@ -1,6 +1,6 @@ import logging import uuid -from abc import ABC, abstractmethod +from abc import ABC from collections.abc import Callable, Collection, Sequence from typing import Any @@ -20,6 +20,8 @@ ActionFieldMapping, ActionFieldMappingKeys, DiscordDataBlob, + OnCallDataBlob, + SlackDataBlob, ) logger = logging.getLogger(__name__) @@ -33,12 +35,16 @@ class BaseIssueAlertHandler(ABC): @staticmethod def get_integration_id(action: Action, mapping: ActionFieldMapping) -> dict[str, Any]: if mapping.get(ActionFieldMappingKeys.INTEGRATION_ID_KEY.value): + if action.integration_id is None: + raise ValueError(f"No integration id found for action type: {action.type}") return {mapping[ActionFieldMappingKeys.INTEGRATION_ID_KEY.value]: action.integration_id} raise ValueError(f"No integration id key found for action type: {action.type}") @classmethod def get_target_identifier(cls, action: Action, mapping: ActionFieldMapping) -> dict[str, Any]: if mapping.get(ActionFieldMappingKeys.TARGET_IDENTIFIER_KEY.value): + if action.target_identifier is None: + raise ValueError(f"No target identifier found for action type: {action.type}") return { mapping[ ActionFieldMappingKeys.TARGET_IDENTIFIER_KEY.value @@ -49,16 +55,15 @@ def get_target_identifier(cls, action: Action, mapping: ActionFieldMapping) -> d @classmethod def get_target_display(cls, action: Action, mapping: ActionFieldMapping) -> dict[str, Any]: if mapping.get(ActionFieldMappingKeys.TARGET_DISPLAY_KEY.value): + if action.target_display is None: + raise ValueError(f"No target display found for action type: {action.type}") return {mapping[ActionFieldMappingKeys.TARGET_DISPLAY_KEY.value]: action.target_display} raise ValueError(f"No target display key found for action type: {action.type}") @classmethod - @abstractmethod def get_additional_fields(cls, action: Action, mapping: ActionFieldMapping) -> dict[str, Any]: """Add additional fields to the blob""" - raise NotImplementedError( - f"get_additional_fields not implemented for action type: {action.type}" - ) + return {} @classmethod def build_rule_action_blob( @@ -186,3 +191,43 @@ def get_additional_fields(cls, action: Action, mapping: ActionFieldMapping) -> d @classmethod def get_target_display(cls, action: Action, mapping: ActionFieldMapping) -> dict[str, Any]: return {} + + +@issue_alert_handler_registry.register(Action.Type.MSTEAMS) +class MSTeamsIssueAlertHandler(BaseIssueAlertHandler): + pass + + +@issue_alert_handler_registry.register(Action.Type.SLACK) +class SlackIssueAlertHandler(BaseIssueAlertHandler): + @classmethod + def get_additional_fields(cls, action: Action, mapping: ActionFieldMapping) -> dict[str, Any]: + blob = SlackDataBlob(**action.data) + return { + "tags": blob.tags, + "notes": blob.notes, + } + + +@issue_alert_handler_registry.register(Action.Type.PAGERDUTY) +class PagerDutyIssueAlertHandler(BaseIssueAlertHandler): + @classmethod + def get_target_display(cls, action: Action, mapping: ActionFieldMapping) -> dict[str, Any]: + return {} + + @classmethod + def get_additional_fields(cls, action: Action, mapping: ActionFieldMapping) -> dict[str, Any]: + blob = OnCallDataBlob(**action.data) + return {"severity": blob.priority} + + +@issue_alert_handler_registry.register(Action.Type.OPSGENIE) +class OpsgenieIssueAlertHandler(BaseIssueAlertHandler): + @classmethod + def get_target_display(cls, action: Action, mapping: ActionFieldMapping) -> dict[str, Any]: + return {} + + @classmethod + def get_additional_fields(cls, action: Action, mapping: ActionFieldMapping) -> dict[str, Any]: + blob = OnCallDataBlob(**action.data) + return {"priority": blob.priority} diff --git a/tests/sentry/workflow_engine/handlers/action/notification/test_issue_alert.py b/tests/sentry/workflow_engine/handlers/action/notification/test_issue_alert.py index 28578670df7ad6..78962e49ff0e69 100644 --- a/tests/sentry/workflow_engine/handlers/action/notification/test_issue_alert.py +++ b/tests/sentry/workflow_engine/handlers/action/notification/test_issue_alert.py @@ -8,6 +8,10 @@ from sentry.workflow_engine.handlers.action.notification.issue_alert import ( BaseIssueAlertHandler, DiscordIssueAlertHandler, + MSTeamsIssueAlertHandler, + OpsgenieIssueAlertHandler, + PagerDutyIssueAlertHandler, + SlackIssueAlertHandler, ) from sentry.workflow_engine.models import Action from sentry.workflow_engine.types import WorkflowJob @@ -75,6 +79,11 @@ def test_create_rule_instance_from_action(self): assert rule.status == ObjectStatus.ACTIVE assert rule.source == RuleSource.ISSUE + def test_create_rule_instance_from_action_missing_action_properties_raises_value_error(self): + action = self.create_action(type=Action.Type.DISCORD) + with pytest.raises(ValueError): + self.handler.create_rule_instance_from_action(action, self.detector, self.job) + def test_create_rule_instance_from_action_no_environment(self): """Test that create_rule_instance_from_action creates a Rule with correct attributes""" workflow = self.create_workflow() @@ -163,3 +172,136 @@ def test_build_rule_action_blob_no_tags(self): "channel_id": "channel456", "tags": "", } + + +class TestMSTeamsIssueAlertHandler(BaseWorkflowTest): + def setUp(self): + super().setUp() + self.handler = MSTeamsIssueAlertHandler() + self.action = self.create_action( + type=Action.Type.MSTEAMS, + integration_id="1234567890", + target_identifier="channel789", + target_display="General Channel", + ) + + def test_build_rule_action_blob(self): + """Test that build_rule_action_blob creates correct MSTeams action data""" + blob = self.handler.build_rule_action_blob(self.action) + + assert blob == { + "id": "sentry.integrations.msteams.notify_action.MsTeamsNotifyServiceAction", + "team": "1234567890", + "channel_id": "channel789", + "channel": "General Channel", + } + + +class TestSlackIssueAlertHandler(BaseWorkflowTest): + def setUp(self): + super().setUp() + self.handler = SlackIssueAlertHandler() + self.action = self.create_action( + type=Action.Type.SLACK, + integration_id="1234567890", + target_identifier="channel789", + target_display="#general", + data={"tags": "environment,user", "notes": "Important alert"}, + ) + + def test_build_rule_action_blob(self): + """Test that build_rule_action_blob creates correct Slack action data""" + blob = self.handler.build_rule_action_blob(self.action) + + assert blob == { + "id": "sentry.integrations.slack.notify_action.SlackNotifyServiceAction", + "workspace": "1234567890", + "channel_id": "channel789", + "channel": "#general", + "tags": "environment,user", + "notes": "Important alert", + } + + def test_build_rule_action_blob_no_data(self): + """Test that build_rule_action_blob handles missing data""" + self.action.data = {} + blob = self.handler.build_rule_action_blob(self.action) + + assert blob == { + "id": "sentry.integrations.slack.notify_action.SlackNotifyServiceAction", + "workspace": "1234567890", + "channel_id": "channel789", + "channel": "#general", + "tags": "", + "notes": "", + } + + +class TestPagerDutyIssueAlertHandler(BaseWorkflowTest): + def setUp(self): + super().setUp() + self.handler = PagerDutyIssueAlertHandler() + self.action = self.create_action( + type=Action.Type.PAGERDUTY, + integration_id="1234567890", + target_identifier="service789", + data={"priority": "P1"}, + ) + + def test_build_rule_action_blob(self): + """Test that build_rule_action_blob creates correct PagerDuty action data""" + blob = self.handler.build_rule_action_blob(self.action) + + assert blob == { + "id": "sentry.integrations.pagerduty.notify_action.PagerDutyNotifyServiceAction", + "account": "1234567890", + "service": "service789", + "severity": "P1", + } + + def test_build_rule_action_blob_no_priority(self): + """Test that build_rule_action_blob handles missing priority""" + self.action.data = {} + blob = self.handler.build_rule_action_blob(self.action) + + assert blob == { + "id": "sentry.integrations.pagerduty.notify_action.PagerDutyNotifyServiceAction", + "account": "1234567890", + "service": "service789", + "severity": "", + } + + +class TestOpsgenieIssueAlertHandler(BaseWorkflowTest): + def setUp(self): + super().setUp() + self.handler = OpsgenieIssueAlertHandler() + self.action = self.create_action( + type=Action.Type.OPSGENIE, + integration_id="1234567890", + target_identifier="team789", + data={"priority": "P1"}, + ) + + def test_build_rule_action_blob(self): + """Test that build_rule_action_blob creates correct Opsgenie action data""" + blob = self.handler.build_rule_action_blob(self.action) + + assert blob == { + "id": "sentry.integrations.opsgenie.notify_action.OpsgenieNotifyTeamAction", + "account": "1234567890", + "team": "team789", + "priority": "P1", + } + + def test_build_rule_action_blob_no_priority(self): + """Test that build_rule_action_blob handles missing priority""" + self.action.data = {} + blob = self.handler.build_rule_action_blob(self.action) + + assert blob == { + "id": "sentry.integrations.opsgenie.notify_action.OpsgenieNotifyTeamAction", + "account": "1234567890", + "team": "team789", + "priority": "", + }