Skip to content

Commit 1df0db5

Browse files
ceorourkegetsantry[bot]
authored andcommitted
feat(ACI): Update metric issue occurrence (#92702)
Update the metric issue occurrence to use real data instead of hard coded data. We might still need to add things like `evidence_display` and more `event_data` but this should be a solid start. --------- Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com>
1 parent c3f1ad8 commit 1df0db5

File tree

7 files changed

+414
-28
lines changed

7 files changed

+414
-28
lines changed

src/sentry/incidents/grouptype.py

Lines changed: 124 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,30 @@
22

33
from dataclasses import dataclass
44
from datetime import UTC, datetime
5-
from typing import Any
65

76
from sentry import features
7+
from sentry.constants import CRASH_RATE_ALERT_AGGREGATE_ALIAS
88
from sentry.incidents.handlers.condition import * # noqa
99
from sentry.incidents.metric_alert_detector import MetricAlertsDetectorValidator
1010
from sentry.incidents.models.alert_rule import AlertRuleDetectionType, ComparisonDeltaChoices
11+
from sentry.incidents.utils.format_duration import format_duration_idiomatic
12+
from sentry.incidents.utils.metric_issue_poc import QUERY_AGGREGATION_DISPLAY
1113
from sentry.incidents.utils.types import QuerySubscriptionUpdate
14+
from sentry.integrations.metric_alerts import TEXT_COMPARISON_DELTA
1215
from sentry.issues.grouptype import GroupCategory, GroupType
1316
from sentry.models.organization import Organization
1417
from sentry.ratelimits.sliding_windows import Quota
18+
from sentry.snuba.metrics import format_mri_field, is_mri_field
19+
from sentry.snuba.models import QuerySubscription, SnubaQuery
20+
from sentry.types.actor import parse_and_validate_actor
1521
from sentry.types.group import PriorityLevel
1622
from sentry.workflow_engine.handlers.detector import DetectorOccurrence, StatefulDetectorHandler
17-
from sentry.workflow_engine.handlers.detector.base import EvidenceData
23+
from sentry.workflow_engine.handlers.detector.base import EventData, EvidenceData
24+
from sentry.workflow_engine.models.alertrule_detector import AlertRuleDetector
25+
from sentry.workflow_engine.models.data_condition import Condition, DataCondition
1826
from sentry.workflow_engine.models.data_source import DataPacket
1927
from sentry.workflow_engine.processors.data_condition_group import ProcessedDataConditionGroup
20-
from sentry.workflow_engine.types import DetectorPriorityLevel, DetectorSettings
28+
from sentry.workflow_engine.types import DetectorException, DetectorPriorityLevel, DetectorSettings
2129

2230
COMPARISON_DELTA_CHOICES: list[None | int] = [choice.value for choice in ComparisonDeltaChoices]
2331
COMPARISON_DELTA_CHOICES.append(None)
@@ -28,29 +36,130 @@ class MetricIssueEvidenceData(EvidenceData):
2836
alert_id: int
2937

3038

31-
class MetricAlertDetectorHandler(StatefulDetectorHandler[QuerySubscriptionUpdate, int]):
39+
class MetricIssueDetectorHandler(StatefulDetectorHandler[QuerySubscriptionUpdate, int]):
3240
def create_occurrence(
3341
self,
3442
evaluation_result: ProcessedDataConditionGroup,
3543
data_packet: DataPacket[QuerySubscriptionUpdate],
3644
priority: DetectorPriorityLevel,
37-
) -> tuple[DetectorOccurrence, dict[str, Any]]:
38-
# Returning a placeholder for now, this may require us passing more info
39-
occurrence = DetectorOccurrence(
40-
issue_title="Some Issue Title",
41-
subtitle="An Issue Subtitle",
42-
type=MetricIssue,
43-
level="error",
44-
culprit="Some culprit",
45+
) -> tuple[DetectorOccurrence, EventData]:
46+
try:
47+
alert_rule_detector = AlertRuleDetector.objects.get(detector=self.detector)
48+
alert_id = alert_rule_detector.alert_rule_id
49+
except AlertRuleDetector.DoesNotExist:
50+
alert_id = None
51+
52+
try:
53+
detector_trigger = DataCondition.objects.get(
54+
condition_group=self.detector.workflow_condition_group, condition_result=priority
55+
)
56+
except DataCondition.DoesNotExist:
57+
raise DetectorException(
58+
f"Failed to find detector trigger for detector id {self.detector.id}, cannot create metric issue occurrence"
59+
)
60+
61+
try:
62+
query_subscription = QuerySubscription.objects.get(id=data_packet.source_id)
63+
except QuerySubscription.DoesNotExist:
64+
raise DetectorException(
65+
f"Failed to find query subscription for detector id {self.detector.id}, cannot create metric issue occurrence"
66+
)
67+
68+
try:
69+
snuba_query = SnubaQuery.objects.get(id=query_subscription.snuba_query_id)
70+
except SnubaQuery.DoesNotExist:
71+
raise DetectorException(
72+
f"Failed to find snuba query for detector id {self.detector.id}, cannot create metric issue occurrence"
73+
)
74+
75+
try:
76+
assignee = parse_and_validate_actor(
77+
str(self.detector.created_by_id), self.detector.project.organization_id
78+
)
79+
except Exception:
80+
assignee = None
81+
82+
title = self.construct_title(snuba_query, detector_trigger, priority)
83+
event_data = {
84+
"environment": self.detector.config.get("environment"),
85+
"platform": None,
86+
"sdk": None,
87+
} # XXX: may need to add to this
88+
89+
return (
90+
DetectorOccurrence(
91+
issue_title=self.detector.name,
92+
subtitle=title,
93+
resource_id=None,
94+
evidence_data={
95+
"alert_id": alert_id,
96+
},
97+
evidence_display=[], # XXX: may need to pass more info here for the front end
98+
type=MetricIssue,
99+
level="error",
100+
culprit="",
101+
priority=priority,
102+
assignee=assignee,
103+
),
104+
event_data,
45105
)
46-
return occurrence, {}
47106

48107
def extract_dedupe_value(self, data_packet: DataPacket[QuerySubscriptionUpdate]) -> int:
49108
return int(data_packet.packet.get("timestamp", datetime.now(UTC)).timestamp())
50109

51110
def extract_value(self, data_packet: DataPacket[QuerySubscriptionUpdate]) -> int:
52111
return data_packet.packet["values"]["value"]
53112

113+
def construct_title(
114+
self,
115+
snuba_query: SnubaQuery,
116+
detector_trigger: DataCondition,
117+
priority: DetectorPriorityLevel,
118+
) -> str:
119+
comparison_delta = self.detector.config.get("comparison_delta")
120+
agg_display_key = snuba_query.aggregate
121+
122+
if is_mri_field(agg_display_key):
123+
aggregate = format_mri_field(agg_display_key)
124+
elif CRASH_RATE_ALERT_AGGREGATE_ALIAS in agg_display_key:
125+
agg_display_key = agg_display_key.split(f"AS {CRASH_RATE_ALERT_AGGREGATE_ALIAS}")[
126+
0
127+
].strip()
128+
aggregate = QUERY_AGGREGATION_DISPLAY.get(agg_display_key, agg_display_key)
129+
else:
130+
aggregate = QUERY_AGGREGATION_DISPLAY.get(agg_display_key, agg_display_key)
131+
132+
# Determine the higher or lower comparison
133+
higher_or_lower = ""
134+
if detector_trigger.type == Condition.GREATER:
135+
higher_or_lower = "greater than" if comparison_delta else "above"
136+
else:
137+
higher_or_lower = "less than" if comparison_delta else "below"
138+
139+
label = "Warning" if priority == DetectorPriorityLevel.MEDIUM else "Critical"
140+
141+
# Format the time window for the threshold
142+
time_window = format_duration_idiomatic(snuba_query.time_window // 60)
143+
144+
# If the detector_trigger has a comparison delta, format the comparison string
145+
comparison: str | int | float = "threshold"
146+
if comparison_delta:
147+
comparison_delta_minutes = comparison_delta // 60
148+
comparison = TEXT_COMPARISON_DELTA.get(
149+
comparison_delta_minutes, f"same time {comparison_delta_minutes} minutes ago "
150+
)
151+
else:
152+
comparison = detector_trigger.comparison
153+
154+
template = "{label}: {metric} in the last {time_window} {higher_or_lower} {comparison}"
155+
return template.format(
156+
label=label.capitalize(),
157+
metric=aggregate,
158+
higher_or_lower=higher_or_lower,
159+
comparison=comparison,
160+
time_window=time_window,
161+
)
162+
54163

55164
# Example GroupType and detector handler for metric alerts. We don't create these issues yet, but we'll use something
56165
# like these when we're sending issues as alerts
@@ -65,8 +174,9 @@ class MetricIssue(GroupType):
65174
default_priority = PriorityLevel.HIGH
66175
enable_auto_resolve = False
67176
enable_escalation_detection = False
177+
enable_status_change_workflow_notifications = False
68178
detector_settings = DetectorSettings(
69-
handler=MetricAlertDetectorHandler,
179+
handler=MetricIssueDetectorHandler,
70180
validator=MetricAlertsDetectorValidator,
71181
config_schema={
72182
"$schema": "https://json-schema.org/draft/2020-12/schema",

src/sentry/workflow_engine/handlers/detector/stateful.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -525,8 +525,8 @@ def _evaluation_detector_conditions(
525525
if condition_result.result is not None
526526
and isinstance(condition_result.result, DetectorPriorityLevel)
527527
]
528-
529-
new_priority = max(new_priority, *validated_condition_results)
528+
if validated_condition_results:
529+
new_priority = max(new_priority, *validated_condition_results)
530530

531531
return condition_evaluation, new_priority
532532

src/sentry/workflow_engine/types.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
T = TypeVar("T")
2323

2424

25+
class DetectorException(Exception):
26+
pass
27+
28+
2529
class DetectorPriorityLevel(IntEnum):
2630
OK = 0
2731
LOW = PriorityLevel.LOW

tests/sentry/workflow_engine/handlers/detector/test_base.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ def tearDown(self):
186186
self.uuid_patcher.stop()
187187
self.sm_comp_patcher.stop()
188188

189-
def create_detector_and_conditions(self, type: str | None = None):
189+
def create_detector_and_condition(self, type: str | None = None):
190190
if type is None:
191191
type = "handler_with_state"
192192
self.project = self.create_project()
@@ -195,19 +195,19 @@ def create_detector_and_conditions(self, type: str | None = None):
195195
workflow_condition_group=self.create_data_condition_group(),
196196
type=type,
197197
)
198-
self.create_data_condition(
198+
data_condition = self.create_data_condition(
199199
type=Condition.GREATER,
200200
comparison=5,
201201
condition_result=DetectorPriorityLevel.HIGH,
202202
condition_group=detector.workflow_condition_group,
203203
)
204-
return detector
204+
return detector, data_condition
205205

206206
def build_handler(
207207
self, detector: Detector | None = None, detector_type=None
208208
) -> MockDetectorStateHandler:
209209
if detector is None:
210-
detector = self.create_detector_and_conditions(detector_type)
210+
detector, _ = self.create_detector_and_condition(detector_type)
211211
return MockDetectorStateHandler(detector)
212212

213213
def assert_updates(

0 commit comments

Comments
 (0)