Skip to content
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

🔧 chore(aci): Add support to invoke rule registry email handler from NOA #85130

Draft
wants to merge 2 commits into
base: raj/invoke-rule-registry-rest-of-ticketing
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions src/sentry/testutils/helpers/data_blobs.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Any

"""
Contains data blobs that we store in the Rule.action json field.

Expand Down Expand Up @@ -752,3 +754,61 @@
"uuid": "22222222-2222-2222-2222-222222222222",
},
]

EMAIL_ACTION_DATA_BLOBS: list[dict[str, Any]] = [
# IssueOwners (targetIdentifier is "None")
{
"targetType": "IssueOwners",
"id": "sentry.mail.actions.NotifyEmailAction",
"targetIdentifier": "None",
"fallthroughType": "ActiveMembers",
"uuid": "2e8847d7-8fe4-44d2-8a16-e25040329790",
},
# NoOne Fallthrough (targetIdentifier is "")
{
"targetType": "IssueOwners",
"targetIdentifier": "",
"id": "sentry.mail.actions.NotifyEmailAction",
"fallthroughType": "NoOne",
"uuid": "fb039430-0848-4fc4-89b4-bc7689a9f851",
},
# AllMembers Fallthrough (targetIdentifier is None)
{
"targetType": "IssueOwners",
"id": "sentry.mail.actions.NotifyEmailAction",
"targetIdentifier": None,
"fallthroughType": "AllMembers",
"uuid": "41f13756-8f90-4afe-b162-55268c6e3cdb",
},
# NoOne Fallthrough (targetIdentifier is "None")
{
"targetType": "IssueOwners",
"id": "sentry.mail.actions.NotifyEmailAction",
"targetIdentifier": "None",
"fallthroughType": "NoOne",
"uuid": "99c9b517-0a0f-47f0-b3ff-2a9cd2fd9c49",
},
# ActiveMembers Fallthrough
{
"targetType": "Member",
"fallthroughType": "ActiveMembers",
"id": "sentry.mail.actions.NotifyEmailAction",
"targetIdentifier": 3234013,
"uuid": "6e83337b-9561-4167-a208-27d6bdf5e613",
},
# Member Email
{
"id": "sentry.mail.actions.NotifyEmailAction",
"targetIdentifier": 2160509,
"targetType": "Member",
"uuid": "42c3e1d6-4004-4a51-a90b-13d3404f1e55",
},
# Team Email
{
"targetType": "Team",
"id": "sentry.mail.actions.NotifyEmailAction",
"fallthroughType": "AllMembers",
"uuid": "71b445cf-573b-4e0c-86bc-8dfbad93c480",
"targetIdentifier": 188022,
},
]
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from sentry.constants import ObjectStatus
from sentry.eventstore.models import GroupEvent
from sentry.models.rule import Rule, RuleSource
from sentry.notifications.models.notificationaction import ActionTarget
from sentry.rules.processing.processor import activate_downstream_actions
from sentry.types.rules import RuleFuture
from sentry.utils.registry import Registry
Expand All @@ -20,6 +21,9 @@
ActionFieldMapping,
ActionFieldMappingKeys,
DiscordDataBlob,
EmailActionHelper,
EmailDataBlob,
EmailFieldMappingKeys,
OnCallDataBlob,
SlackDataBlob,
TicketFieldMappingKeys,
Expand Down Expand Up @@ -262,3 +266,49 @@ def get_additional_fields(cls, action: Action, mapping: ActionFieldMapping) -> d
}

return final_blob


@issue_alert_handler_registry.register(Action.Type.EMAIL)
class EmailIssueAlertHandler(BaseIssueAlertHandler):
@classmethod
def get_integration_id(cls, action: Action, mapping: ActionFieldMapping) -> dict[str, Any]:
return {}

@classmethod
def get_target_display(cls, action: Action, mapping: ActionFieldMapping) -> dict[str, Any]:
return {}

@classmethod
def get_target_identifier(cls, action: Action, mapping: ActionFieldMapping) -> dict[str, Any]:
# this would be when the target_type is IssueOwners
if action.target_identifier is None:
if action.target_type != ActionTarget.ISSUE_OWNERS.value:
raise ValueError(
f"No target identifier found for {action.type} action {action.id}, target_type: {action.target_type}"
)
return {}
else:
return {
mapping[
ActionFieldMappingKeys.TARGET_IDENTIFIER_KEY.value
]: action.target_identifier
}

@classmethod
def get_additional_fields(cls, action: Action, mapping: ActionFieldMapping) -> dict[str, Any]:
if action.target_type is None:
raise ValueError(f"No target type found for {action.type} action {action.id}")

target_type = ActionTarget(action.target_type)

final_blob = {
EmailFieldMappingKeys.TARGET_TYPE_KEY.value: EmailActionHelper.get_target_type_string(
target_type
),
}

if target_type == ActionTarget.ISSUE_OWNERS.value:
blob = EmailDataBlob(**action.data)
final_blob[EmailFieldMappingKeys.FALLTHROUGH_TYPE_KEY.value] = blob.fallthroughType

return final_blob
58 changes: 44 additions & 14 deletions src/sentry/workflow_engine/typings/notification_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ class TicketFieldMappingKeys(StrEnum):
ADDITIONAL_FIELDS_KEY = "additional_fields"


class EmailFieldMappingKeys(StrEnum):
"""
EmailFieldMappingKeys is an enum that represents the keys of an email field mapping.
"""

FALLTHROUGH_TYPE_KEY = "fallthroughType"
TARGET_TYPE_KEY = "targetType"


class ActionFieldMapping(TypedDict):
"""Mapping between Action model fields and Rule Action blob fields"""

Expand Down Expand Up @@ -87,7 +96,6 @@ class ActionFieldMapping(TypedDict):
Action.Type.GITHUB: ActionFieldMapping(
id="sentry.integrations.github.notify_action.GitHubCreateTicketAction",
integration_id_key="integration",
target_identifier_key="repo",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mistake found with test updates

),
Action.Type.GITHUB_ENTERPRISE: ActionFieldMapping(
id="sentry.integrations.github_enterprise.notify_action.GitHubEnterpriseCreateTicketAction",
Expand Down Expand Up @@ -465,32 +473,50 @@ def action_type(self) -> Action.Type:
return Action.Type.JIRA_SERVER


class EmailActionHelper(ABC):
target_type_mapping = {
ActionTarget.USER: "Member",
ActionTarget.TEAM: "Team",
ActionTarget.ISSUE_OWNERS: "IssueOwners",
}

reverse_target_type_mapping = {v: k for k, v in target_type_mapping.items()}

@staticmethod
def get_target_type_object(target_type: str) -> ActionTarget:
return EmailActionHelper.reverse_target_type_mapping[target_type]

@staticmethod
def get_target_type_string(target_type: ActionTarget) -> str:
return EmailActionHelper.target_type_mapping[target_type]


@issue_alert_action_translator_registry.register(ACTION_FIELD_MAPPINGS[Action.Type.EMAIL]["id"])
class EmailActionTranslator(BaseActionTranslator):
class EmailActionTranslator(BaseActionTranslator, EmailActionHelper):
@property
def action_type(self) -> Action.Type:
return Action.Type.EMAIL

@property
def required_fields(self) -> list[str]:
return ["targetType"]
return [
EmailFieldMappingKeys.TARGET_TYPE_KEY.value,
]

@property
def target_type(self) -> ActionTarget:
# If the targetType is Member, then set the target_type to User,
# if the targetType is Team, then set the target_type to Team,
# otherwise return None (this would be for IssueOwners (suggested assignees))

target_type = self.action.get("targetType")
if target_type == ActionTargetType.MEMBER.value:
return ActionTarget.USER
elif target_type == ActionTargetType.TEAM.value:
return ActionTarget.TEAM
return ActionTarget.ISSUE_OWNERS
if (target_type := self.action.get(EmailFieldMappingKeys.TARGET_TYPE_KEY.value)) is None:
raise ValueError("Target type is required for email actions")

return EmailActionHelper.get_target_type_object(target_type)

@property
def target_identifier(self) -> str | None:
target_type = self.action.get("targetType")
target_type = self.action.get(EmailFieldMappingKeys.TARGET_TYPE_KEY.value)
if target_type in [ActionTargetType.MEMBER.value, ActionTargetType.TEAM.value]:
return self.action.get(
ACTION_FIELD_MAPPINGS[Action.Type.EMAIL][
Expand All @@ -501,7 +527,7 @@ def target_identifier(self) -> str | None:

@property
def blob_type(self) -> type[DataBlob] | None:
target_type = self.action.get("targetType")
target_type = self.action.get(EmailFieldMappingKeys.TARGET_TYPE_KEY.value)
if target_type == ActionTargetType.ISSUE_OWNERS.value:
return EmailDataBlob
return None
Expand All @@ -510,12 +536,16 @@ def get_sanitized_data(self) -> dict[str, Any]:
"""
Override to handle the special case of IssueOwners target type
"""
if self.action.get("targetType") == ActionTargetType.ISSUE_OWNERS.value:
if (
self.action.get(EmailFieldMappingKeys.TARGET_TYPE_KEY.value)
== ActionTargetType.ISSUE_OWNERS.value
):
return dataclasses.asdict(
EmailDataBlob(
fallthroughType=self.action.get(
"fallthroughType", FallthroughChoiceType.ACTIVE_MEMBERS.value
)
EmailFieldMappingKeys.FALLTHROUGH_TYPE_KEY.value,
FallthroughChoiceType.ACTIVE_MEMBERS.value,
),
)
)
return {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
from sentry.models.rule import Rule, RuleSource
from sentry.testutils.helpers.data_blobs import (
AZURE_DEVOPS_ACTION_DATA_BLOBS,
EMAIL_ACTION_DATA_BLOBS,
GITHUB_ACTION_DATA_BLOBS,
JIRA_ACTION_DATA_BLOBS,
JIRA_SERVER_ACTION_DATA_BLOBS,
)
from sentry.workflow_engine.handlers.action.notification.issue_alert import (
BaseIssueAlertHandler,
DiscordIssueAlertHandler,
EmailIssueAlertHandler,
MSTeamsIssueAlertHandler,
OpsgenieIssueAlertHandler,
PagerDutyIssueAlertHandler,
Expand All @@ -27,6 +29,7 @@
EXCLUDED_ACTION_DATA_KEYS,
ActionFieldMapping,
ActionFieldMappingKeys,
EmailActionHelper,
)
from tests.sentry.workflow_engine.test_base import BaseWorkflowTest

Expand Down Expand Up @@ -353,17 +356,24 @@ def _test_build_rule_action_blob(self, expected, action_type: Action.Type):
)
blob = self.handler.build_rule_action_blob(action)

# pop uuid from blob
# (we don't store it anymore since its a legacy artifact when we didn't have the action model)
expected.pop("uuid")

assert blob == {
"id": expected["id"],
"integration": expected["integration"],
**blob,
**expected,
}


class TestGithubIssueAlertHandler(TestTicketingIssueAlertHandlerBase):
def test_build_rule_action_blob(self):
for expected in GITHUB_ACTION_DATA_BLOBS:
self._test_build_rule_action_blob(expected, Action.Type.GITHUB)
if expected["id"] == ACTION_FIELD_MAPPINGS[Action.Type.GITHUB]["id"]:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

something else found with test updates. we have to do this since github/github enterprise config looks identical (based on code, currently there are no prod actions set up for ghe).

self._test_build_rule_action_blob(expected, Action.Type.GITHUB)
else:
self._test_build_rule_action_blob(expected, Action.Type.GITHUB_ENTERPRISE)


class TestAzureDevopsIssueAlertHandler(TestTicketingIssueAlertHandlerBase):
Expand All @@ -382,3 +392,80 @@ class TestJiraServerIssueAlertHandler(TestTicketingIssueAlertHandlerBase):
def test_build_rule_action_blob(self):
for expected in JIRA_SERVER_ACTION_DATA_BLOBS:
self._test_build_rule_action_blob(expected, Action.Type.JIRA_SERVER)


class TestEmailIssueAlertHandler(BaseWorkflowTest):
def setUp(self):
super().setUp()
self.handler = EmailIssueAlertHandler()
# These are the actions that are healed from the old email action data blobs
# It removes targetIdentifier for IssueOwner targets (since that shouldn't be set for those)
# It also removes the fallthroughType for Team and Member targets (since that shouldn't be set for those)
self.HEALED_EMAIL_ACTION_DATA_BLOBS = [
# IssueOwners (targetIdentifier is "None")
{
"targetType": "IssueOwners",
"id": "sentry.mail.actions.NotifyEmailAction",
"fallthroughType": "ActiveMembers",
},
# NoOne Fallthrough (targetIdentifier is "")
{
"targetType": "IssueOwners",
"id": "sentry.mail.actions.NotifyEmailAction",
"fallthroughType": "NoOne",
},
# AllMembers Fallthrough (targetIdentifier is None)
{
"targetType": "IssueOwners",
"id": "sentry.mail.actions.NotifyEmailAction",
"fallthroughType": "AllMembers",
},
# NoOne Fallthrough (targetIdentifier is "None")
{
"targetType": "IssueOwners",
"id": "sentry.mail.actions.NotifyEmailAction",
"fallthroughType": "NoOne",
},
# ActiveMembers Fallthrough
{
"targetType": "Member",
"id": "sentry.mail.actions.NotifyEmailAction",
"targetIdentifier": 3234013,
},
# Member Email
{
"id": "sentry.mail.actions.NotifyEmailAction",
"targetIdentifier": 2160509,
"targetType": "Member",
},
# Team Email
{
"targetType": "Team",
"id": "sentry.mail.actions.NotifyEmailAction",
"targetIdentifier": 188022,
},
]

def test_build_rule_action_blob(self):
for expected, healed in zip(EMAIL_ACTION_DATA_BLOBS, self.HEALED_EMAIL_ACTION_DATA_BLOBS):
action_data = pop_keys_from_data_blob(expected, Action.Type.EMAIL)

# pop the targetType from the action_data
target_type = EmailActionHelper.get_target_type_object(action_data.pop("targetType"))

# Handle all possible targetIdentifier formats
target_identifier = expected["targetIdentifier"]
if target_identifier in ("None", "", None):
target_identifier = None
elif str(target_identifier).isnumeric():
target_identifier = int(target_identifier)

action = self.create_action(
type=Action.Type.EMAIL,
data=action_data,
target_type=target_type,
target_identifier=target_identifier,
)
blob = self.handler.build_rule_action_blob(action)

assert blob == healed
Loading
Loading