Skip to content

Commit 9227fd0

Browse files
fix(webhooks): New task for metric alerts to fire in process (#112234)
1 parent 7d524e9 commit 9227fd0

File tree

11 files changed

+528
-52
lines changed

11 files changed

+528
-52
lines changed

src/sentry/incidents/action_handlers.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -462,22 +462,15 @@ def send_alert(
462462

463463
incident_serialized_response = serialize(incident, serializer=IncidentSerializer())
464464

465-
success = send_incident_alert_notification(
465+
send_incident_alert_notification(
466466
notification_context=notification_context,
467467
alert_context=alert_context,
468468
metric_issue_context=metric_issue_context,
469469
incident_serialized_response=incident_serialized_response,
470470
organization=incident.organization,
471+
project_id=project.id,
471472
notification_uuid=notification_uuid,
472473
)
473-
if success:
474-
self.record_alert_sent_analytics(
475-
organization_id=incident.organization.id,
476-
project_id=project.id,
477-
alert_id=incident.alert_rule.id,
478-
external_id=action.sentry_app_id,
479-
notification_uuid=notification_uuid,
480-
)
481474

482475

483476
def format_duration(minutes):

src/sentry/integrations/services/integration/impl.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@
4545
serialize_integration_external_project,
4646
serialize_organization_integration,
4747
)
48-
from sentry.rules.actions.notify_event_service import find_alert_rule_action_ui_component
4948
from sentry.sentry_apps.api.serializers.app_platform_event import AppPlatformEvent
5049
from sentry.sentry_apps.metrics import (
5150
SentryAppEventType,
@@ -54,7 +53,11 @@
5453
)
5554
from sentry.sentry_apps.models.sentry_app import SentryApp
5655
from sentry.sentry_apps.models.sentry_app_installation import SentryAppInstallation
57-
from sentry.sentry_apps.utils.webhooks import MetricAlertActionType, SentryAppResourceType
56+
from sentry.sentry_apps.utils.webhooks import (
57+
MetricAlertActionType,
58+
SentryAppResourceType,
59+
find_alert_rule_action_ui_component,
60+
)
5861
from sentry.shared_integrations.exceptions import ApiError
5962
from sentry.utils import json
6063
from sentry.utils.sentry_apps import send_and_save_webhook_request

src/sentry/notifications/notification_action/metric_alert_registry/handlers/sentry_app_metric_alert_handler.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,6 @@ def send_alert(
5454
metric_issue_context=metric_issue_context,
5555
incident_serialized_response=incident_serialized_response,
5656
organization=organization,
57+
project_id=project.id,
5758
notification_uuid=notification_uuid,
5859
)

src/sentry/rules/actions/notify_event_service.py

Lines changed: 10 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,13 @@
1313
NotificationContext,
1414
)
1515
from sentry.integrations.metric_alerts import incident_attachment_info
16-
from sentry.integrations.services.integration import integration_service
1716
from sentry.models.organization import Organization
1817
from sentry.plugins.base import plugins
1918
from sentry.rules.actions.base import EventAction
2019
from sentry.rules.actions.services import PluginService
2120
from sentry.rules.base import CallbackFuture
22-
from sentry.sentry_apps.api.serializers.app_platform_event import AppPlatformEvent
2321
from sentry.sentry_apps.services.app import RpcSentryAppService, app_service
24-
from sentry.sentry_apps.tasks.sentry_apps import notify_sentry_app
22+
from sentry.sentry_apps.tasks.sentry_apps import notify_sentry_app, send_metric_alert_webhook
2523
from sentry.services.eventstore.models import GroupEvent
2624
from sentry.utils import json, metrics
2725
from sentry.utils.forms import set_field_choices
@@ -64,15 +62,15 @@ def send_incident_alert_notification(
6462
metric_issue_context: MetricIssueContext,
6563
incident_serialized_response: IncidentSerializerResponse,
6664
organization: Organization,
65+
project_id: int,
6766
notification_uuid: str | None = None,
68-
) -> bool:
67+
) -> None:
6968
"""
7069
When a metric alert is triggered, send incident data to the SentryApp's webhook.
7170
:param action: The triggered `AlertRuleTriggerAction`.
7271
:param incident: The `Incident` for which to build a payload.
73-
:param metric_value: The value of the metric that triggered this alert to
74-
fire.
75-
:return:
72+
:param metric_value: The value of the metric that triggered this alert to fire.
73+
:param project_id: project id will be used for analytics after sending the webhook.
7674
"""
7775
incident_attachment = build_incident_attachment(
7876
alert_context,
@@ -85,40 +83,16 @@ def send_incident_alert_notification(
8583
if notification_context.sentry_app_id is None:
8684
raise ValueError("Sentry app ID is required")
8785

88-
success = integration_service.send_incident_alert_notification(
89-
sentry_app_id=notification_context.sentry_app_id,
86+
send_metric_alert_webhook.delay(
87+
sentry_app_id=int(notification_context.sentry_app_id),
9088
new_status=metric_issue_context.new_status.value,
9189
incident_attachment_json=json.dumps(incident_attachment),
9290
organization_id=organization.id,
93-
# TODO(iamrajjoshi): The rest of the params are unused
94-
action_id=-1,
95-
incident_id=-1,
96-
metric_value=-1,
97-
)
98-
return success
99-
100-
101-
def find_alert_rule_action_ui_component(app_platform_event: AppPlatformEvent) -> bool:
102-
"""
103-
Loop through the triggers for the alert rule event. For each trigger, check
104-
if an action is an alert rule UI Component
105-
"""
106-
triggers = (
107-
getattr(app_platform_event, "data", {})
108-
.get("metric_alert", {})
109-
.get("alert_rule", {})
110-
.get("triggers", [])
91+
project_id=project_id,
92+
alert_id=alert_context.action_identifier_id,
93+
notification_uuid=notification_uuid,
11194
)
11295

113-
actions = [
114-
action
115-
for trigger in triggers
116-
for action in trigger.get("actions", {})
117-
if (action.get("type") == "sentry_app" and action.get("settings") is not None)
118-
]
119-
120-
return bool(len(actions))
121-
12296

12397
class NotifyEventServiceForm(forms.Form):
12498
service = forms.ChoiceField(choices=())

src/sentry/sentry_apps/metrics.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ class SentryAppWebhookFailureReason(StrEnum):
6060
EVENT_NOT_IN_SERVCEHOOK = "event_not_in_servicehook"
6161
MISSING_ISSUE_OCCURRENCE = "missing_issue_occurrence"
6262
MISSING_USER = "missing_user"
63+
MULTIPLE_INSTALLATIONS = "multiple_installations"
6364

6465

6566
class SentryAppWebhookHaltReason(StrEnum):

src/sentry/sentry_apps/tasks/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
regenerate_service_hooks_for_installation,
99
send_alert_webhook,
1010
send_alert_webhook_v2,
11+
send_metric_alert_webhook,
1112
send_resource_change_webhook,
1213
workflow_notification,
1314
)
@@ -26,4 +27,5 @@
2627
"send_alert_webhook_v2",
2728
"send_resource_change_webhook",
2829
"workflow_notification",
30+
"send_metric_alert_webhook",
2931
)

src/sentry/sentry_apps/tasks/sentry_apps.py

Lines changed: 116 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from sentry.analytics.events.alert_rule_ui_component_webhook_sent import (
1818
AlertRuleUiComponentWebhookSentEvent,
1919
)
20+
from sentry.analytics.events.alert_sent import AlertSentEvent
2021
from sentry.analytics.events.comment_webhooks import (
2122
CommentCreatedEvent,
2223
CommentDeletedEvent,
@@ -36,6 +37,7 @@
3637
from sentry.db.models.base import Model
3738
from sentry.exceptions import RestrictedIPAddress
3839
from sentry.hybridcloud.rpc.caching import cell_caching_service
40+
from sentry.incidents.models.incident import INCIDENT_STATUS, IncidentStatus
3941
from sentry.issues.issue_occurrence import IssueOccurrence
4042
from sentry.models.activity import Activity
4143
from sentry.models.group import Group
@@ -63,7 +65,12 @@
6365
)
6466
from sentry.sentry_apps.services.hook.service import hook_service
6567
from sentry.sentry_apps.utils.errors import SentryAppSentryError
66-
from sentry.sentry_apps.utils.webhooks import IssueAlertActionType, SentryAppResourceType
68+
from sentry.sentry_apps.utils.webhooks import (
69+
IssueAlertActionType,
70+
MetricAlertActionType,
71+
SentryAppResourceType,
72+
find_alert_rule_action_ui_component,
73+
)
6774
from sentry.services.eventstore.models import BaseEvent, Event, GroupEvent
6875
from sentry.shared_integrations.exceptions import ApiHostError, ApiTimeoutError, ClientError
6976
from sentry.silo.base import SiloMode
@@ -72,7 +79,7 @@
7279
from sentry.types.rules import RuleFuture
7380
from sentry.users.services.user.model import RpcUser
7481
from sentry.users.services.user.service import user_service
75-
from sentry.utils import metrics
82+
from sentry.utils import json, metrics
7683
from sentry.utils.function_cache import cache_func_for_models
7784
from sentry.utils.http import absolute_uri
7885
from sentry.utils.sentry_apps import send_and_save_webhook_request
@@ -908,6 +915,113 @@ def regenerate_service_hooks_for_installation(
908915
)
909916

910917

918+
def _record_metric_alert_sent_analytics(
919+
organization_id: int,
920+
project_id: int,
921+
alert_id: int,
922+
sentry_app_id: int,
923+
notification_uuid: str | None,
924+
) -> None:
925+
try:
926+
analytics.record(
927+
AlertSentEvent(
928+
organization_id=organization_id,
929+
project_id=project_id,
930+
alert_id=alert_id,
931+
alert_type="metric_alert",
932+
provider="sentry_app",
933+
external_id=str(sentry_app_id),
934+
notification_uuid=notification_uuid,
935+
)
936+
)
937+
except Exception as e:
938+
sentry_sdk.capture_exception(e)
939+
940+
941+
def _record_metric_alert_ui_component_analytics(
942+
organization_id: int,
943+
sentry_app_id: int,
944+
app_platform_event: AppPlatformEvent,
945+
) -> None:
946+
if not find_alert_rule_action_ui_component(app_platform_event):
947+
return
948+
try:
949+
analytics.record(
950+
AlertRuleUiComponentWebhookSentEvent(
951+
organization_id=organization_id,
952+
sentry_app_id=sentry_app_id,
953+
event=f"{app_platform_event.resource}.{app_platform_event.action}",
954+
)
955+
)
956+
except Exception as e:
957+
sentry_sdk.capture_exception(e)
958+
959+
960+
@instrumented_task(
961+
name="sentry.sentry_apps.tasks.sentry_apps.send_metric_alert_webhook",
962+
namespace=sentryapp_tasks,
963+
retry=Retry(times=3, delay=60 * 5),
964+
silo_mode=SiloMode.CELL,
965+
)
966+
@retry_decorator
967+
def send_metric_alert_webhook(
968+
sentry_app_id: int,
969+
new_status: int,
970+
incident_attachment_json: str,
971+
organization_id: int,
972+
project_id: int,
973+
alert_id: int,
974+
notification_uuid: str | None = None,
975+
**kwargs: Any,
976+
) -> None:
977+
try:
978+
new_status_str = INCIDENT_STATUS[IncidentStatus(new_status)].lower()
979+
event = SentryAppEventType(
980+
f"{SentryAppResourceType.METRIC_ALERT}.{MetricAlertActionType(new_status_str)}"
981+
)
982+
except ValueError as e:
983+
sentry_sdk.capture_exception(e)
984+
return
985+
986+
with SentryAppInteractionEvent(
987+
operation_type=SentryAppInteractionType.PREPARE_WEBHOOK,
988+
event_type=event,
989+
).capture() as lifecycle:
990+
sentry_app = app_service.get_sentry_app_by_id(id=sentry_app_id)
991+
if sentry_app is None:
992+
lifecycle.record_failure(SentryAppWebhookFailureReason.MISSING_SENTRY_APP)
993+
return
994+
995+
installations = app_service.get_many(
996+
filter=dict(
997+
organization_id=organization_id,
998+
app_ids=[sentry_app.id],
999+
status=SentryAppInstallationStatus.INSTALLED,
1000+
)
1001+
)
1002+
if not installations:
1003+
lifecycle.record_failure(SentryAppWebhookFailureReason.MISSING_INSTALLATION)
1004+
return
1005+
1006+
if len(installations) > 1:
1007+
lifecycle.record_failure(SentryAppWebhookFailureReason.MULTIPLE_INSTALLATIONS)
1008+
return
1009+
1010+
app_platform_event = AppPlatformEvent(
1011+
resource=SentryAppResourceType.METRIC_ALERT,
1012+
action=MetricAlertActionType(new_status_str),
1013+
install=installations[0],
1014+
data=json.loads(incident_attachment_json),
1015+
)
1016+
1017+
send_and_save_webhook_request(sentry_app, app_platform_event)
1018+
1019+
_record_metric_alert_sent_analytics(
1020+
organization_id, project_id, alert_id, sentry_app.id, notification_uuid
1021+
)
1022+
_record_metric_alert_ui_component_analytics(organization_id, sentry_app.id, app_platform_event)
1023+
1024+
9111025
@instrumented_task(
9121026
name="sentry.sentry_apps.tasks.sentry_apps.broadcast_webhooks_for_organization",
9131027
namespace=sentryapp_tasks,

src/sentry/sentry_apps/utils/webhooks.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
from __future__ import annotations
2+
13
from enum import StrEnum
2-
from typing import Final
4+
from typing import TYPE_CHECKING, Any, Final
5+
6+
if TYPE_CHECKING:
7+
from sentry.sentry_apps.api.serializers.app_platform_event import AppPlatformEvent
38

49

510
class SentryAppActionType(StrEnum):
@@ -106,3 +111,25 @@ def map_sentry_app_webhook_events(
106111
# per-event-type (issue.created, project.deleted, etc.). These are valid
107112
# resources a Sentry App may subscribe to.
108113
VALID_EVENT_RESOURCES = EVENT_EXPANSION.keys()
114+
115+
116+
def find_alert_rule_action_ui_component(
117+
app_platform_event: AppPlatformEvent[dict[str, Any]],
118+
) -> bool:
119+
"""
120+
Returns True if the metric alert event contains a sentry app action with UI component settings.
121+
Used to gate recording of AlertRuleUiComponentWebhookSentEvent analytics.
122+
"""
123+
triggers = (
124+
getattr(app_platform_event, "data", {})
125+
.get("metric_alert", {})
126+
.get("alert_rule", {})
127+
.get("triggers", [])
128+
)
129+
actions = [
130+
action
131+
for trigger in triggers
132+
for action in trigger.get("actions", [])
133+
if (action.get("type") == "sentry_app" and action.get("settings") is not None)
134+
]
135+
return bool(len(actions))

tests/sentry/notifications/notification_action/metric_alert_registry/test_sentry_app_metric_alert_handler.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ def test_send_alert(self, mock_send_incident_alert_notification: mock.MagicMock)
8585
alert_context=alert_context,
8686
metric_issue_context=metric_issue_context,
8787
organization=self.detector.project.organization,
88+
project_id=self.detector.project.id,
8889
notification_uuid=notification_uuid,
8990
incident_serialized_response=get_incident_serializer(self.open_period),
9091
)

0 commit comments

Comments
 (0)