Skip to content

Commit 8420950

Browse files
authored
fix(aci): support notifications for non-dual written metric detectors (#103938)
Should probably be merged before #103937. If an open period comes in without a corresponding entry in the IGOP table, create a fake ID for the incident and pass it to the frontend, where it will be used for redirect logic. Do the same for detector IDs that don't have entries in the AlertRuleDetector table.
1 parent dd321dd commit 8420950

File tree

3 files changed

+182
-6
lines changed

3 files changed

+182
-6
lines changed

src/sentry/incidents/action_handlers.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
DetailedIncidentSerializerResponse,
2828
IncidentSerializer,
2929
)
30+
from sentry.incidents.endpoints.serializers.utils import get_fake_id_from_object_id
3031
from sentry.incidents.grouptype import MetricIssue
3132
from sentry.incidents.models.alert_rule import (
3233
AlertRuleDetectionType,
@@ -603,15 +604,19 @@ def generate_incident_trigger_email_context(
603604
incident_group_open_period = IncidentGroupOpenPeriod.objects.get(
604605
group_open_period_id=metric_issue_context.open_period_identifier
605606
)
607+
incident_identifier = incident_group_open_period.incident_identifier
606608
except IncidentGroupOpenPeriod.DoesNotExist:
607-
raise ValueError("IncidentGroupOpenPeriod does not exist")
609+
# the corresponding metric detector was not dual written
610+
incident_identifier = get_fake_id_from_object_id(
611+
metric_issue_context.open_period_identifier
612+
)
608613

609614
alert_link = organization.absolute_url(
610615
reverse(
611616
"sentry-metric-alert",
612617
kwargs={
613618
"organization_slug": organization.slug,
614-
"incident_id": incident_group_open_period.incident_identifier,
619+
"incident_id": incident_identifier,
615620
},
616621
),
617622
query=urlencode(alert_link_params),

src/sentry/integrations/metric_alerts.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44
from typing import NotRequired, TypedDict
55
from urllib import parse
66

7-
import sentry_sdk
87
from django.db.models import Max
98
from django.urls import reverse
109
from django.utils.translation import gettext as _
1110

1211
from sentry import features
1312
from sentry.constants import CRASH_RATE_ALERT_AGGREGATE_ALIAS
13+
from sentry.incidents.endpoints.serializers.utils import get_fake_id_from_object_id
1414
from sentry.incidents.logic import GetMetricIssueAggregatesParams, get_metric_issue_aggregates
1515
from sentry.incidents.models.alert_rule import AlertRule, AlertRuleThresholdType
1616
from sentry.incidents.models.incident import (
@@ -239,7 +239,8 @@ def incident_attachment_info(
239239
if alert_rule_id is None:
240240
raise ValueError("Alert rule id not found when querying for AlertRuleDetector")
241241
except AlertRuleDetector.DoesNotExist:
242-
raise ValueError("Alert rule detector not found when querying for AlertRuleDetector")
242+
# the corresponding metric detector was not dual written
243+
alert_rule_id = get_fake_id_from_object_id(alert_context.action_identifier_id)
243244

244245
workflow_engine_params = title_link_params.copy()
245246

@@ -248,8 +249,11 @@ def incident_attachment_info(
248249
group_open_period_id=metric_issue_context.open_period_identifier
249250
)
250251
workflow_engine_params["alert"] = str(open_period_incident.incident_identifier)
251-
except IncidentGroupOpenPeriod.DoesNotExist as e:
252-
sentry_sdk.capture_exception(e)
252+
except IncidentGroupOpenPeriod.DoesNotExist:
253+
# the corresponding metric detector was not dual written
254+
workflow_engine_params["alert"] = str(
255+
get_fake_id_from_object_id(metric_issue_context.open_period_identifier)
256+
)
253257

254258
title_link = build_title_link(alert_rule_id, organization, workflow_engine_params)
255259

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import uuid
2+
3+
import pytest
4+
5+
from sentry.incidents.endpoints.serializers.utils import get_fake_id_from_object_id
6+
from sentry.incidents.grouptype import MetricIssue
7+
from sentry.incidents.logic import CRITICAL_TRIGGER_LABEL
8+
from sentry.incidents.models.alert_rule import AlertRuleDetectionType, AlertRuleThresholdType
9+
from sentry.incidents.models.incident import IncidentStatus
10+
from sentry.incidents.typings.metric_detector import AlertContext, MetricIssueContext
11+
from sentry.integrations.metric_alerts import incident_attachment_info
12+
from sentry.models.groupopenperiod import GroupOpenPeriod
13+
from sentry.testutils.cases import BaseIncidentsTest, TestCase
14+
15+
pytestmark = pytest.mark.sentry_metrics
16+
17+
18+
def incident_attachment_info_with_metric_value(incident, new_status, metric_value):
19+
return incident_attachment_info(
20+
organization=incident.organization,
21+
alert_context=AlertContext.from_alert_rule_incident(incident.alert_rule),
22+
metric_issue_context=MetricIssueContext.from_legacy_models(
23+
incident, new_status, metric_value
24+
),
25+
)
26+
27+
28+
class IncidentAttachmentInfoTest(TestCase, BaseIncidentsTest):
29+
def setUp(self) -> None:
30+
super().setUp()
31+
self.group = self.create_group(type=MetricIssue.type_id)
32+
33+
def test_returns_correct_info_with_workflow_engine_dual_write(self) -> None:
34+
"""
35+
This tests that we look up the correct incident and alert rule during dual write ACI migration.
36+
"""
37+
alert_rule = self.create_alert_rule()
38+
date_started = self.now
39+
incident = self.create_incident(
40+
self.organization,
41+
title="Incident #1",
42+
projects=[self.project],
43+
alert_rule=alert_rule,
44+
status=IncidentStatus.CLOSED.value,
45+
date_started=date_started,
46+
)
47+
trigger = self.create_alert_rule_trigger(alert_rule, CRITICAL_TRIGGER_LABEL, 100)
48+
self.create_alert_rule_trigger_action(
49+
alert_rule_trigger=trigger, triggered_for_incident=incident
50+
)
51+
metric_value = 123
52+
referrer = "metric_alert_custom"
53+
notification_uuid = str(uuid.uuid4())
54+
55+
detector = self.create_detector(project=self.project)
56+
self.create_alert_rule_detector(alert_rule_id=alert_rule.id, detector=detector)
57+
58+
open_period = (
59+
GroupOpenPeriod.objects.filter(group=self.group).order_by("-date_started").first()
60+
)
61+
assert open_period is not None
62+
self.create_incident_group_open_period(incident, open_period)
63+
64+
metric_issue_context = MetricIssueContext.from_legacy_models(
65+
incident, IncidentStatus.CLOSED, metric_value
66+
)
67+
# Setting the open period identifier to the open period id, since we are testing the lookup
68+
metric_issue_context.open_period_identifier = open_period.id
69+
metric_issue_context.group = self.group
70+
assert metric_issue_context.group is not None
71+
72+
data = incident_attachment_info(
73+
organization=incident.organization,
74+
alert_context=AlertContext(
75+
name=alert_rule.name,
76+
# Setting the action identifier id to the detector id since that's what the NOA does
77+
action_identifier_id=detector.id,
78+
threshold_type=AlertRuleThresholdType(alert_rule.threshold_type),
79+
detection_type=AlertRuleDetectionType(alert_rule.detection_type),
80+
comparison_delta=alert_rule.comparison_delta,
81+
sensitivity=alert_rule.sensitivity,
82+
resolve_threshold=alert_rule.resolve_threshold,
83+
alert_threshold=None,
84+
),
85+
metric_issue_context=metric_issue_context,
86+
notification_uuid=notification_uuid,
87+
referrer=referrer,
88+
)
89+
90+
assert data["title"] == f"Resolved: {alert_rule.name}"
91+
assert data["status"] == "Resolved"
92+
assert data["text"] == "123 events in the last 10 minutes"
93+
# We still build the link using the alert_rule_id and the incident identifier
94+
assert (
95+
data["title_link"]
96+
== f"http://testserver/organizations/baz/alerts/rules/details/{alert_rule.id}/?alert={incident.identifier}&referrer={referrer}&detection_type=static&notification_uuid={notification_uuid}"
97+
)
98+
assert (
99+
data["logo_url"]
100+
== "http://testserver/_static/{version}/sentry/images/sentry-email-avatar.png"
101+
)
102+
103+
def test_returns_correct_info_placeholder_incident(self) -> None:
104+
"""
105+
Test that we use the fake incident ID to build the title link if no IGOP entry exists (if the detector was not dual written).
106+
"""
107+
alert_rule = self.create_alert_rule()
108+
date_started = self.now
109+
incident = self.create_incident(
110+
self.organization,
111+
title="Incident #1",
112+
projects=[self.project],
113+
alert_rule=alert_rule,
114+
status=IncidentStatus.CLOSED.value,
115+
date_started=date_started,
116+
)
117+
trigger = self.create_alert_rule_trigger(alert_rule, CRITICAL_TRIGGER_LABEL, 100)
118+
self.create_alert_rule_trigger_action(
119+
alert_rule_trigger=trigger, triggered_for_incident=incident
120+
)
121+
metric_value = 123
122+
referrer = "metric_alert_custom"
123+
notification_uuid = str(uuid.uuid4())
124+
125+
detector = self.create_detector(project=self.project)
126+
127+
open_period = (
128+
GroupOpenPeriod.objects.filter(group=self.group).order_by("-date_started").first()
129+
)
130+
assert open_period is not None
131+
132+
metric_issue_context = MetricIssueContext.from_legacy_models(
133+
incident, IncidentStatus.CLOSED, metric_value
134+
)
135+
# Setting the open period identifier to the open period id, since we are testing the lookup
136+
metric_issue_context.open_period_identifier = open_period.id
137+
metric_issue_context.group = self.group
138+
assert metric_issue_context.group is not None
139+
140+
# XXX: for convenience, we populate the AlertContext with alert rule/incident information. In this test,
141+
# we're just interested in how the method handles missing AlertRuleDetectors/IGOPs.
142+
data = incident_attachment_info(
143+
organization=incident.organization,
144+
alert_context=AlertContext(
145+
name=alert_rule.name,
146+
# Setting the action identifier id to the detector id since that's what the NOA does
147+
action_identifier_id=detector.id,
148+
threshold_type=AlertRuleThresholdType(alert_rule.threshold_type),
149+
detection_type=AlertRuleDetectionType(alert_rule.detection_type),
150+
comparison_delta=alert_rule.comparison_delta,
151+
sensitivity=alert_rule.sensitivity,
152+
resolve_threshold=alert_rule.resolve_threshold,
153+
alert_threshold=None,
154+
),
155+
metric_issue_context=metric_issue_context,
156+
notification_uuid=notification_uuid,
157+
referrer=referrer,
158+
)
159+
160+
fake_alert_rule_id = get_fake_id_from_object_id(detector.id)
161+
fake_incident_identifier = get_fake_id_from_object_id(open_period.id)
162+
163+
# Build the link using the fake alert_rule_id and the fake incident identifier
164+
assert (
165+
data["title_link"]
166+
== f"http://testserver/organizations/baz/alerts/rules/details/{fake_alert_rule_id}/?alert={fake_incident_identifier}&referrer={referrer}&detection_type=static&notification_uuid={notification_uuid}"
167+
)

0 commit comments

Comments
 (0)