Skip to content

Commit e8370df

Browse files
authored
🔧 chore(aci): Organize Action Handler Directory (#88059)
## there is no logic change in this pr ### to review this pr, id recommend looking at the changes to the folder structure i have been running into a lot of circular dependency issues and merge conflicts which stem from me stuffing everything in the action handler directory. with this pr, i bring some sense of organizations for actions and action handlers: 1. we have generic action handlers that live in the workflow_engine folder 2. other actions and registries live in a separate `notifications/notification_action` folder organized by registry and follow the same folder structure as workflow enginer this will allow me to work quickly and keep things organized. i also found some SOLID principle violations that i can fix in the future. ## Before ``` workflow_engine/handlers/action/ ├── __init__.py ├── __pycache__/ └── notification/ ├── __init__.py ├── __pycache__/ ├── handler.py ├── issue_alert.py └── metric_alert.py ``` ## After ``` notifications/notification_action/ ├── __init__.py ├── __pycache__/ ├── exceptions.py ├── registry.py ├── types.py ├── utils.py ├── group_type_notification_registry/ │ ├── __init__.py │ ├── __pycache__/ │ └── handlers/ ├── issue_alert_registry/ │ ├── __init__.py │ ├── __pycache__/ │ └── handlers/ └── metric_alert_registry/ ├── __init__.py ├── __pycache__/ └── handlers/ ``` ``` workflow_engine/handlers/action/ ├── __init__.py ├── __pycache__/ └── notification/ ├── __init__.py ├── __pycache__/ ├── azure_devops_handler.py ├── base.py ├── common.py ├── discord_handler.py ├── email_handler.py ├── github_enterprise_handler.py ├── github_handler.py ├── jira_handler.py ├── jira_server_handler.py ├── msteams_handler.py ├── opsgenie_handler.py ├── pagerduty_handler.py ├── plugin_handler.py ├── sentry_app_handler.py ├── slack_handler.py └── webhook_handler.py ```
1 parent 5cbbe2e commit e8370df

File tree

55 files changed

+1524
-1228
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+1524
-1228
lines changed

src/sentry/incidents/endpoints/serializers/workflow_engine_action.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
)
77
from sentry.incidents.models.alert_rule import AlertRuleTriggerAction
88
from sentry.notifications.models.notificationaction import ActionService
9-
from sentry.workflow_engine.handlers.action.notification.handler import MetricAlertRegistryInvoker
9+
from sentry.notifications.notification_action.group_type_notification_registry.handlers.metric_alert_registry_handler import (
10+
MetricAlertRegistryHandler,
11+
)
1012
from sentry.workflow_engine.models import Action, ActionAlertRuleTriggerAction
1113

1214

@@ -20,7 +22,7 @@ def serialize(self, obj: Action, attrs, user, **kwargs):
2022
aarta = ActionAlertRuleTriggerAction.objects.get(action=obj.id)
2123
priority = obj.data.get("priority")
2224
type_value = ActionService.get_value(obj.type)
23-
target = MetricAlertRegistryInvoker.target(obj)
25+
target = MetricAlertRegistryHandler.target(obj)
2426

2527
target_type = obj.config.get("target_type")
2628
target_identifier = obj.config.get("target_identifier")

src/sentry/notifications/notification_action/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
class NotificationHandlerException(Exception):
2+
pass
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
__all__ = [
2+
"IssueAlertRegistryHandler",
3+
"MetricAlertRegistryHandler",
4+
]
5+
6+
from .handlers.issue_alert_registry_handler import IssueAlertRegistryHandler
7+
from .handlers.metric_alert_registry_handler import MetricAlertRegistryHandler
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import logging
2+
3+
from sentry.grouping.grouptype import ErrorGroupType
4+
from sentry.notifications.notification_action.exceptions import NotificationHandlerException
5+
from sentry.notifications.notification_action.registry import (
6+
group_type_notification_registry,
7+
issue_alert_handler_registry,
8+
)
9+
from sentry.notifications.notification_action.types import LegacyRegistryHandler
10+
from sentry.utils.registry import NoRegistrationExistsError
11+
from sentry.workflow_engine.models import Action, Detector
12+
from sentry.workflow_engine.types import WorkflowEventData
13+
14+
logger = logging.getLogger(__name__)
15+
16+
17+
@group_type_notification_registry.register(ErrorGroupType.slug)
18+
class IssueAlertRegistryHandler(LegacyRegistryHandler):
19+
@staticmethod
20+
def handle_workflow_action(job: WorkflowEventData, action: Action, detector: Detector) -> None:
21+
try:
22+
handler = issue_alert_handler_registry.get(action.type)
23+
handler.invoke_legacy_registry(job, action, detector)
24+
except NoRegistrationExistsError:
25+
logger.exception(
26+
"No issue alert handler found for action type: %s",
27+
action.type,
28+
extra={"action_id": action.id},
29+
)
30+
raise
31+
except Exception as e:
32+
logger.exception(
33+
"Error invoking issue alert handler",
34+
extra={"action_id": action.id},
35+
)
36+
raise NotificationHandlerException(e)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import logging
2+
3+
from sentry.issues.grouptype import MetricIssuePOC
4+
from sentry.models.organizationmember import OrganizationMember
5+
from sentry.models.team import Team
6+
from sentry.notifications.models.notificationaction import ActionTarget
7+
from sentry.notifications.notification_action.exceptions import NotificationHandlerException
8+
from sentry.notifications.notification_action.registry import (
9+
group_type_notification_registry,
10+
metric_alert_handler_registry,
11+
)
12+
from sentry.notifications.notification_action.types import LegacyRegistryHandler
13+
from sentry.utils.registry import NoRegistrationExistsError
14+
from sentry.workflow_engine.models import Action, DataConditionGroupAction, Detector
15+
from sentry.workflow_engine.types import WorkflowEventData
16+
17+
logger = logging.getLogger(__name__)
18+
19+
20+
@group_type_notification_registry.register(MetricIssuePOC.slug)
21+
class MetricAlertRegistryHandler(LegacyRegistryHandler):
22+
@staticmethod
23+
def handle_workflow_action(job: WorkflowEventData, action: Action, detector: Detector) -> None:
24+
try:
25+
handler = metric_alert_handler_registry.get(action.type)
26+
handler.invoke_legacy_registry(job, action, detector)
27+
except NoRegistrationExistsError:
28+
logger.exception(
29+
"No metric alert handler found for action type: %s",
30+
action.type,
31+
extra={"action_id": action.id},
32+
)
33+
raise
34+
except Exception as e:
35+
logger.exception(
36+
"Error invoking metric alert handler",
37+
extra={"action_id": action.id},
38+
)
39+
raise NotificationHandlerException(e)
40+
41+
@staticmethod
42+
def target(action: Action) -> OrganizationMember | Team | str | None:
43+
target_identifier = action.config.get("target_identifier")
44+
if target_identifier is None:
45+
return None
46+
47+
target_type = action.config.get("target_type")
48+
if target_type == ActionTarget.USER.value:
49+
dcga = DataConditionGroupAction.objects.get(action=action)
50+
return OrganizationMember.objects.get(
51+
user_id=int(target_identifier),
52+
organization=dcga.condition_group.organization,
53+
)
54+
elif target_type == ActionTarget.TEAM.value:
55+
try:
56+
return Team.objects.get(id=int(target_identifier))
57+
except Team.DoesNotExist:
58+
pass
59+
elif target_type == ActionTarget.SPECIFIC.value:
60+
# TODO: This is only for email. We should have a way of validating that it's
61+
# ok to contact this email.
62+
return target_identifier
63+
return None
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
__all__ = [
2+
"AzureDevopsIssueAlertHandler",
3+
"DiscordIssueAlertHandler",
4+
"EmailIssueAlertHandler",
5+
"GithubIssueAlertHandler",
6+
"GithubEnterpriseIssueAlertHandler",
7+
"JiraIssueAlertHandler",
8+
"JiraServerIssueAlertHandler",
9+
"MSTeamsIssueAlertHandler",
10+
"OpsgenieIssueAlertHandler",
11+
"PagerdutyIssueAlertHandler",
12+
"PluginIssueAlertHandler",
13+
"SentryAppIssueAlertHandler",
14+
"SlackIssueAlertHandler",
15+
"WebhookIssueAlertHandler",
16+
"PagerDutyIssueAlertHandler",
17+
]
18+
19+
from .handlers.azure_devops_issue_alert_handler import AzureDevopsIssueAlertHandler
20+
from .handlers.discord_issue_alert_handler import DiscordIssueAlertHandler
21+
from .handlers.email_issue_alert_handler import EmailIssueAlertHandler
22+
from .handlers.github_enterprise_issue_alert_handler import GithubEnterpriseIssueAlertHandler
23+
from .handlers.github_issue_alert_handler import GithubIssueAlertHandler
24+
from .handlers.jira_issue_alert_handler import JiraIssueAlertHandler
25+
from .handlers.jira_server_issue_alert_handler import JiraServerIssueAlertHandler
26+
from .handlers.msteams_issue_alert_handler import MSTeamsIssueAlertHandler
27+
from .handlers.opsgenie_issue_alert_handler import OpsgenieIssueAlertHandler
28+
from .handlers.pagerduty_issue_alert_handler import PagerDutyIssueAlertHandler
29+
from .handlers.plugin_issue_alert_handler import PluginIssueAlertHandler
30+
from .handlers.sentry_app_issue_alert_handler import SentryAppIssueAlertHandler
31+
from .handlers.slack_issue_alert_handler import SlackIssueAlertHandler
32+
from .handlers.webhook_issue_alert_handler import WebhookIssueAlertHandler
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from sentry.notifications.notification_action.registry import issue_alert_handler_registry
2+
from sentry.notifications.notification_action.types import TicketingIssueAlertHandler
3+
from sentry.workflow_engine.models import Action
4+
5+
6+
@issue_alert_handler_registry.register(Action.Type.AZURE_DEVOPS)
7+
class AzureDevopsIssueAlertHandler(TicketingIssueAlertHandler):
8+
pass
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from typing import Any
2+
3+
from sentry.notifications.notification_action.registry import issue_alert_handler_registry
4+
from sentry.notifications.notification_action.types import BaseIssueAlertHandler
5+
from sentry.workflow_engine.models import Action
6+
from sentry.workflow_engine.typings.notification_action import ActionFieldMapping, DiscordDataBlob
7+
8+
9+
@issue_alert_handler_registry.register(Action.Type.DISCORD)
10+
class DiscordIssueAlertHandler(BaseIssueAlertHandler):
11+
@classmethod
12+
def get_additional_fields(cls, action: Action, mapping: ActionFieldMapping) -> dict[str, Any]:
13+
blob = DiscordDataBlob(**action.data)
14+
return {"tags": blob.tags}
15+
16+
@classmethod
17+
def get_target_display(cls, action: Action, mapping: ActionFieldMapping) -> dict[str, Any]:
18+
return {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from typing import Any
2+
3+
from sentry.notifications.models.notificationaction import ActionTarget
4+
from sentry.notifications.notification_action.registry import issue_alert_handler_registry
5+
from sentry.notifications.notification_action.types import BaseIssueAlertHandler
6+
from sentry.workflow_engine.models import Action
7+
from sentry.workflow_engine.typings.notification_action import (
8+
ActionFieldMapping,
9+
ActionFieldMappingKeys,
10+
EmailActionHelper,
11+
EmailDataBlob,
12+
EmailFieldMappingKeys,
13+
)
14+
15+
16+
@issue_alert_handler_registry.register(Action.Type.EMAIL)
17+
class EmailIssueAlertHandler(BaseIssueAlertHandler):
18+
@classmethod
19+
def get_integration_id(cls, action: Action, mapping: ActionFieldMapping) -> dict[str, Any]:
20+
return {}
21+
22+
@classmethod
23+
def get_target_display(cls, action: Action, mapping: ActionFieldMapping) -> dict[str, Any]:
24+
return {}
25+
26+
@classmethod
27+
def get_target_identifier(
28+
cls, action: Action, mapping: ActionFieldMapping, organization_id: int
29+
) -> dict[str, Any]:
30+
target_id = action.config.get("target_identifier")
31+
target_type = action.config.get("target_type")
32+
33+
# this would be when the target_type is IssueOwners
34+
if target_id is None:
35+
if target_type != ActionTarget.ISSUE_OWNERS.value:
36+
raise ValueError(
37+
f"No target identifier found for {action.type} action {action.id}, target_type: {target_type}"
38+
)
39+
return {}
40+
else:
41+
return {mapping[ActionFieldMappingKeys.TARGET_IDENTIFIER_KEY.value]: target_id}
42+
43+
@classmethod
44+
def get_additional_fields(cls, action: Action, mapping: ActionFieldMapping) -> dict[str, Any]:
45+
target_type = ActionTarget(action.config.get("target_type"))
46+
47+
final_blob = {
48+
EmailFieldMappingKeys.TARGET_TYPE_KEY.value: EmailActionHelper.get_target_type_string(
49+
target_type
50+
),
51+
}
52+
53+
if target_type == ActionTarget.ISSUE_OWNERS.value:
54+
blob = EmailDataBlob(**action.data)
55+
final_blob[EmailFieldMappingKeys.FALLTHROUGH_TYPE_KEY.value] = blob.fallthroughType
56+
57+
return final_blob
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from sentry.notifications.notification_action.registry import issue_alert_handler_registry
2+
from sentry.notifications.notification_action.types import TicketingIssueAlertHandler
3+
from sentry.workflow_engine.models import Action
4+
5+
6+
@issue_alert_handler_registry.register(Action.Type.GITHUB_ENTERPRISE)
7+
class GithubEnterpriseIssueAlertHandler(TicketingIssueAlertHandler):
8+
pass
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from sentry.notifications.notification_action.registry import issue_alert_handler_registry
2+
from sentry.notifications.notification_action.types import TicketingIssueAlertHandler
3+
from sentry.workflow_engine.models import Action
4+
5+
6+
@issue_alert_handler_registry.register(Action.Type.GITHUB)
7+
class GithubIssueAlertHandler(TicketingIssueAlertHandler):
8+
pass
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from sentry.notifications.notification_action.registry import issue_alert_handler_registry
2+
from sentry.notifications.notification_action.types import TicketingIssueAlertHandler
3+
from sentry.workflow_engine.models import Action
4+
5+
6+
@issue_alert_handler_registry.register(Action.Type.JIRA)
7+
class JiraIssueAlertHandler(TicketingIssueAlertHandler):
8+
pass
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from sentry.notifications.notification_action.registry import issue_alert_handler_registry
2+
from sentry.notifications.notification_action.types import TicketingIssueAlertHandler
3+
from sentry.workflow_engine.models import Action
4+
5+
6+
@issue_alert_handler_registry.register(Action.Type.JIRA_SERVER)
7+
class JiraServerIssueAlertHandler(TicketingIssueAlertHandler):
8+
pass
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from sentry.notifications.notification_action.registry import issue_alert_handler_registry
2+
from sentry.notifications.notification_action.types import BaseIssueAlertHandler
3+
from sentry.workflow_engine.models import Action
4+
5+
6+
@issue_alert_handler_registry.register(Action.Type.MSTEAMS)
7+
class MSTeamsIssueAlertHandler(BaseIssueAlertHandler):
8+
pass
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from typing import Any
2+
3+
from sentry.notifications.notification_action.registry import issue_alert_handler_registry
4+
from sentry.notifications.notification_action.types import BaseIssueAlertHandler
5+
from sentry.workflow_engine.models import Action
6+
from sentry.workflow_engine.typings.notification_action import ActionFieldMapping, OnCallDataBlob
7+
8+
9+
@issue_alert_handler_registry.register(Action.Type.OPSGENIE)
10+
class OpsgenieIssueAlertHandler(BaseIssueAlertHandler):
11+
@classmethod
12+
def get_target_display(cls, action: Action, mapping: ActionFieldMapping) -> dict[str, Any]:
13+
return {}
14+
15+
@classmethod
16+
def get_additional_fields(cls, action: Action, mapping: ActionFieldMapping) -> dict[str, Any]:
17+
blob = OnCallDataBlob(**action.data)
18+
return {"priority": blob.priority}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from typing import Any
2+
3+
from sentry.notifications.notification_action.registry import issue_alert_handler_registry
4+
from sentry.notifications.notification_action.types import BaseIssueAlertHandler
5+
from sentry.workflow_engine.models import Action
6+
from sentry.workflow_engine.typings.notification_action import ActionFieldMapping, OnCallDataBlob
7+
8+
9+
@issue_alert_handler_registry.register(Action.Type.PAGERDUTY)
10+
class PagerDutyIssueAlertHandler(BaseIssueAlertHandler):
11+
@classmethod
12+
def get_target_display(cls, action: Action, mapping: ActionFieldMapping) -> dict[str, Any]:
13+
return {}
14+
15+
@classmethod
16+
def get_additional_fields(cls, action: Action, mapping: ActionFieldMapping) -> dict[str, Any]:
17+
blob = OnCallDataBlob(**action.data)
18+
return {"severity": blob.priority}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from typing import Any
2+
3+
from sentry.notifications.notification_action.registry import issue_alert_handler_registry
4+
from sentry.notifications.notification_action.types import BaseIssueAlertHandler
5+
from sentry.workflow_engine.models import Action
6+
from sentry.workflow_engine.typings.notification_action import ActionFieldMapping
7+
8+
9+
@issue_alert_handler_registry.register(Action.Type.PLUGIN)
10+
class PluginIssueAlertHandler(BaseIssueAlertHandler):
11+
@classmethod
12+
def get_integration_id(cls, action: Action, mapping: ActionFieldMapping) -> dict[str, Any]:
13+
return {}
14+
15+
@classmethod
16+
def get_target_identifier(
17+
cls, action: Action, mapping: ActionFieldMapping, organization_id: int
18+
) -> dict[str, Any]:
19+
return {}
20+
21+
@classmethod
22+
def get_target_display(cls, action: Action, mapping: ActionFieldMapping) -> dict[str, Any]:
23+
return {}

0 commit comments

Comments
 (0)