-
-
Notifications
You must be signed in to change notification settings - Fork 4.4k
♻️ feat(workflow_engine): issue alert action migration util for slack #83123
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
ef7859f
fc203b5
4de89b9
1c0317d
0088734
4d390bb
c5bde4b
41d2ac6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
import logging | ||
from typing import Any | ||
|
||
from sentry.notifications.models.notificationaction import ActionTarget | ||
from sentry.workflow_engine.models.action import Action | ||
from sentry.workflow_engine.typings.notification_action import ( | ||
ACTION_TYPE_TO_INTEGRATION_ID_KEY, | ||
ACTION_TYPE_TO_TARGET_DISPLAY_KEY, | ||
ACTION_TYPE_TO_TARGET_IDENTIFIER_KEY, | ||
ACTION_TYPE_TO_TARGET_TYPE_RULE_REGISTRY, | ||
EXCLUDED_ACTION_DATA_KEYS, | ||
RULE_REGISTRY_ID_TO_INTEGRATION_PROVIDER, | ||
SlackDataBlob, | ||
) | ||
|
||
_logger = logging.getLogger(__name__) | ||
|
||
|
||
def build_slack_data_blob(action: dict[str, Any]) -> SlackDataBlob: | ||
""" | ||
Builds a SlackDataBlob from the action data. | ||
Only includes the keys that are not None. | ||
""" | ||
return SlackDataBlob( | ||
tags=action.get("tags", ""), | ||
notes=action.get("notes", ""), | ||
) | ||
|
||
|
||
def sanitize_action(action: dict[str, Any], action_type: Action.Type) -> dict[str, Any]: | ||
""" | ||
Pops the keys we don't want to save inside the JSON field of the Action model. | ||
|
||
:param action: action data (Rule.data.actions) | ||
:param action_type: action type (Action.Type) | ||
:return: action data without the excluded keys | ||
""" | ||
|
||
# # If we have a specific blob type, we need to sanitize the action data to the blob type | ||
if action_type == Action.Type.SLACK: | ||
return build_slack_data_blob(action).__dict__ | ||
iamrajjoshi marked this conversation as resolved.
Show resolved
Hide resolved
iamrajjoshi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
# # Otherwise, we can just return the action data as is, removing the keys we don't want to save | ||
else: | ||
return { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. popping keys we save elsewhere or don't want to save |
||
k: v | ||
for k, v in action.items() | ||
if k | ||
not in [ | ||
ACTION_TYPE_TO_INTEGRATION_ID_KEY.get(action_type), | ||
ACTION_TYPE_TO_TARGET_IDENTIFIER_KEY.get(action_type), | ||
ACTION_TYPE_TO_TARGET_DISPLAY_KEY.get(action_type), | ||
*EXCLUDED_ACTION_DATA_KEYS, | ||
] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we remove these because these are used in metric alerts? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we remove them b/c we already store them in some other field. i want to keep the blob as small as possible, containing extra context that we need to send a notification. |
||
} | ||
|
||
|
||
def build_notification_actions_from_rule_data_actions( | ||
actions: list[dict[str, Any]] | ||
) -> list[Action]: | ||
""" | ||
Builds notification actions from action field in Rule's data blob. | ||
|
||
:param actions: list of action data (Rule.data.actions) | ||
:return: list of notification actions (Action) | ||
""" | ||
|
||
notification_actions: list[Action] = [] | ||
|
||
for action in actions: | ||
# Use Rule.integration.provider to get the action type | ||
action_type = RULE_REGISTRY_ID_TO_INTEGRATION_PROVIDER.get(action.get("id")) | ||
if action_type is None: | ||
_logger.warning( | ||
"Action type not found for action", | ||
extra={ | ||
"action_id": action.get("id"), | ||
"action_uuid": action.get("uuid"), | ||
}, | ||
) | ||
continue | ||
|
||
# For all integrations, the target type is specific | ||
# For email, the target type is user | ||
# For sentry app, the target type is sentry app | ||
# FWIW, we don't use target type for issue alerts | ||
target_type = ACTION_TYPE_TO_TARGET_TYPE_RULE_REGISTRY.get(action_type) | ||
|
||
if target_type == ActionTarget.SPECIFIC: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. since we're not using the |
||
|
||
# Get the integration_id | ||
integration_id_key = ACTION_TYPE_TO_INTEGRATION_ID_KEY.get(action_type) | ||
if integration_id_key is None: | ||
# we should always have an integration id key if target type is specific | ||
# TODO(iamrajjoshi): Should we fail loudly here? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it depends on how this method is being used to migrate a rule. I'd think of this as flow control, if we want to continue creating the new action / workflow then we should just log and continue like you have. If we want to stop creation of the workflow because it will be malformed, we should throw an error to stop the migration (or handle the issue in another area) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. talked with cathy, we will log the error and continue to build actions that we can. actions that we store in Rule that are malformed don't fire in the first place and we don't have any system that heals these actions, so we just won't migrate them. |
||
_logger.warning( | ||
"Integration ID key not found for action type", | ||
extra={ | ||
"action_type": action_type, | ||
"action_id": action.get("id"), | ||
"action_uuid": action.get("uuid"), | ||
}, | ||
) | ||
continue | ||
integration_id = action.get(integration_id_key) | ||
|
||
# Get the target_identifier if it exists | ||
target_identifier_key = ACTION_TYPE_TO_TARGET_IDENTIFIER_KEY.get(action_type) | ||
if target_identifier_key is not None: | ||
target_identifier = action.get(target_identifier_key) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think |
||
|
||
# Get the target_display if it exists | ||
target_display_key = ACTION_TYPE_TO_TARGET_DISPLAY_KEY.get(action_type) | ||
if target_display_key is not None: | ||
target_display = action.get(target_display_key) | ||
|
||
notification_action = Action( | ||
type=action_type, | ||
data=( | ||
# If the target type is specific, sanitize the action data | ||
# Otherwise, use the action data as is | ||
sanitize_action(action, action_type) | ||
if target_type == ActionTarget.SPECIFIC | ||
else action | ||
), | ||
integration_id=integration_id, | ||
target_identifier=target_identifier, | ||
target_display=target_display, | ||
target_type=target_type, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. would we want to create the action if any of these weren't set? (given the current code flow, that could be a possibility) |
||
) | ||
|
||
notification_actions.append(notification_action) | ||
|
||
# Bulk create the actions, note: this won't call save(), not sure if we need to | ||
iamrajjoshi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Action.objects.bulk_create(notification_actions) | ||
|
||
return notification_actions |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
from dataclasses import dataclass | ||
|
||
from sentry.notifications.models.notificationaction import ActionTarget | ||
from sentry.workflow_engine.models.action import Action | ||
|
||
""" | ||
Keys that are excluded from the action data blob. | ||
We don't want to save these keys because: | ||
- uuid: maps to action id | ||
- id: maps to action type | ||
""" | ||
EXCLUDED_ACTION_DATA_KEYS = ["uuid", "id"] | ||
|
||
""" | ||
Action types that are integrations | ||
""" | ||
INTEGRATION_ACTION_TYPES = [ | ||
Action.Type.SLACK, | ||
Action.Type.DISCORD, | ||
Action.Type.MSTEAMS, | ||
Action.Type.PAGERDUTY, | ||
Action.Type.OPSGENIE, | ||
Action.Type.GITHUB, | ||
Action.Type.GITHUB_ENTERPRISE, | ||
Action.Type.JIRA, | ||
Action.Type.JIRA_SERVER, | ||
Action.Type.AZURE_DEVOPS, | ||
] | ||
|
||
ACTION_TYPE_TO_TARGET_TYPE_RULE_REGISTRY: dict[Action.Type, ActionTarget] = { | ||
Action.Type.SLACK: ActionTarget.SPECIFIC, | ||
iamrajjoshi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Action.Type.DISCORD: ActionTarget.SPECIFIC, | ||
Action.Type.MSTEAMS: ActionTarget.SPECIFIC, | ||
Action.Type.PAGERDUTY: ActionTarget.SPECIFIC, | ||
Action.Type.OPSGENIE: ActionTarget.SPECIFIC, | ||
Action.Type.GITHUB: ActionTarget.SPECIFIC, | ||
Action.Type.GITHUB_ENTERPRISE: ActionTarget.SPECIFIC, | ||
Action.Type.JIRA: ActionTarget.SPECIFIC, | ||
Action.Type.JIRA_SERVER: ActionTarget.SPECIFIC, | ||
Action.Type.AZURE_DEVOPS: ActionTarget.SPECIFIC, | ||
Action.Type.SENTRY_APP: ActionTarget.SENTRY_APP, | ||
Action.Type.EMAIL: ActionTarget.USER, | ||
Action.Type.PLUGIN: ActionTarget.USER, | ||
Action.Type.WEBHOOK: ActionTarget.USER, | ||
} | ||
|
||
INTEGRATION_PROVIDER_TO_RULE_REGISTRY_ID: dict[Action.Type, str] = { | ||
Action.Type.SLACK: "sentry.integrations.slack.notify_action.SlackNotifyServiceAction", | ||
Action.Type.DISCORD: "sentry.integrations.discord.notify_action.DiscordNotifyServiceAction", | ||
Action.Type.MSTEAMS: "sentry.integrations.msteams.notify_action.MsTeamsNotifyServiceAction", | ||
Action.Type.PAGERDUTY: "sentry.integrations.pagerduty.notify_action.PagerDutyNotifyServiceAction", | ||
Action.Type.OPSGENIE: "sentry.integrations.opsgenie.notify_action.OpsgenieNotifyTeamAction", | ||
Action.Type.GITHUB: "sentry.integrations.github.notify_action.GitHubCreateTicketAction", | ||
Action.Type.GITHUB_ENTERPRISE: "sentry.integrations.github_enterprise.notify_action.GitHubEnterpriseCreateTicketAction", | ||
Action.Type.JIRA: "sentry.integrations.jira.notify_action.JiraCreateTicketAction", | ||
Action.Type.JIRA_SERVER: "sentry.integrations.jira_server.notify_action.JiraServerCreateTicketAction", | ||
Action.Type.AZURE_DEVOPS: "sentry.integrations.vsts.notify_action.AzureDevopsCreateTicketAction", | ||
Action.Type.SENTRY_APP: "sentry.rules.actions.notify_event_sentry_app.NotifyEventSentryAppAction", | ||
Action.Type.EMAIL: "sentry.mail.actions.NotifyEmailAction", | ||
Action.Type.PLUGIN: "sentry.rules.actions.notify_event.NotifyEventAction", | ||
Action.Type.WEBHOOK: "sentry.rules.actions.notify_event_service.NotifyEventServiceAction", | ||
} | ||
|
||
RULE_REGISTRY_ID_TO_INTEGRATION_PROVIDER: dict[str, Action.Type] = { | ||
v: k for k, v in INTEGRATION_PROVIDER_TO_RULE_REGISTRY_ID.items() | ||
} | ||
|
||
ACTION_TYPE_TO_INTEGRATION_ID_KEY: dict[Action.Type, str] = { | ||
Action.Type.SLACK: "workspace", | ||
Action.Type.DISCORD: "server", | ||
Action.Type.MSTEAMS: "team", | ||
Action.Type.PAGERDUTY: "account", | ||
Action.Type.OPSGENIE: "account", | ||
Action.Type.GITHUB: "integration", | ||
Action.Type.GITHUB_ENTERPRISE: "integration", | ||
Action.Type.JIRA: "integration", | ||
Action.Type.JIRA_SERVER: "integration", | ||
Action.Type.AZURE_DEVOPS: "integration", | ||
} | ||
|
||
INTEGRATION_ID_KEY_TO_ACTION_TYPE: dict[str, Action.Type] = { | ||
v: k for k, v in ACTION_TYPE_TO_INTEGRATION_ID_KEY.items() | ||
} | ||
|
||
|
||
ACTION_TYPE_TO_TARGET_IDENTIFIER_KEY: dict[Action.Type, str] = { | ||
Action.Type.SLACK: "channel_id", | ||
Action.Type.DISCORD: "channel_id", | ||
Action.Type.MSTEAMS: "channel_id", | ||
Action.Type.PAGERDUTY: "service", | ||
Action.Type.OPSGENIE: "team", | ||
} | ||
|
||
TARGET_IDENTIFIER_KEY_TO_ACTION_TYPE: dict[str, Action.Type] = { | ||
v: k for k, v in ACTION_TYPE_TO_TARGET_IDENTIFIER_KEY.items() | ||
} | ||
|
||
ACTION_TYPE_TO_TARGET_DISPLAY_KEY: dict[Action.Type, str] = { | ||
Action.Type.SLACK: "channel", | ||
Action.Type.MSTEAMS: "channel", | ||
} | ||
|
||
TARGET_DISPLAY_KEY_TO_ACTION_TYPE: dict[str, Action.Type] = { | ||
v: k for k, v in ACTION_TYPE_TO_TARGET_DISPLAY_KEY.items() | ||
} | ||
|
||
|
||
@dataclass | ||
class DataBlob: | ||
iamrajjoshi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" | ||
DataBlob is a generic type that represents the data blob for a notification action. | ||
""" | ||
|
||
pass | ||
|
||
|
||
@dataclass | ||
class SlackDataBlob(DataBlob): | ||
""" | ||
SlackDataBlob is a specific type that represents the data blob for a Slack notification action. | ||
""" | ||
|
||
tags: str = "" | ||
notes: str = "" | ||
|
||
|
||
ACTION_TYPE_2_BLOB_TYPE: dict[Action.Type, type[DataBlob]] = { | ||
iamrajjoshi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Action.Type.SLACK: SlackDataBlob, | ||
} |
Uh oh!
There was an error while loading. Please reload this page.