Skip to content

Commit 5fbd175

Browse files
committed
ref(investigations): Share the breached monitor snapshot
Part of a series that adds follow-up actions to Seer investigations: when a run completes, Seer suggests one next step, and the user can apply a typed action, such as a monitor change, with one click. This moves building the monitor snapshot we send Seer, and reading a monitor's detection type, out of the breached-metric source code into two functions. The stale re-check can then send Seer the monitor in the same shape as when the investigation started.
1 parent 0cdea46 commit 5fbd175

2 files changed

Lines changed: 131 additions & 40 deletions

File tree

‎src/sentry/investigations/services/breached_metrics.py‎

Lines changed: 69 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,14 @@
1515
from sentry.models.groupopenperiod import GroupOpenPeriod
1616
from sentry.models.organization import Organization
1717
from sentry.snuba.dataset import Dataset
18-
from sentry.snuba.models import QuerySubscription, SnubaQueryEventType
19-
from sentry.workflow_engine.models import DataCondition, DataSourceDetector, DetectorGroup
18+
from sentry.snuba.models import QuerySubscription, SnubaQuery, SnubaQueryEventType
19+
from sentry.workflow_engine.models import (
20+
DataCondition,
21+
DataConditionGroup,
22+
DataSourceDetector,
23+
Detector,
24+
DetectorGroup,
25+
)
2026
from sentry.workflow_engine.models.data_condition import Condition
2127
from sentry.workflow_engine.types import DetectorPriorityLevel
2228

@@ -97,6 +103,59 @@ def _condition_snapshot(
97103
return snapshot
98104

99105

106+
def breached_metric_detection_type(detector: Detector) -> AlertRuleDetectionType | None:
107+
try:
108+
detection_type = AlertRuleDetectionType(detector.config.get("detection_type"))
109+
except (TypeError, ValueError):
110+
return None
111+
if (
112+
detection_type == AlertRuleDetectionType.STATIC
113+
and detector.config.get("comparison_delta") is not None
114+
):
115+
return AlertRuleDetectionType.PERCENT
116+
return detection_type
117+
118+
119+
def breached_metric_monitor_snapshot(
120+
detector: Detector,
121+
snuba_query: SnubaQuery,
122+
condition_group: DataConditionGroup,
123+
detection_type: AlertRuleDetectionType,
124+
) -> dict[str, Any] | None:
125+
"""The monitor settings we share with Seer, or None if Seer can't query the monitor."""
126+
127+
try:
128+
dataset = Dataset(snuba_query.dataset)
129+
except ValueError:
130+
return None
131+
code_mode_dataset = _code_mode_dataset(dataset, snuba_query.event_types)
132+
if code_mode_dataset is None:
133+
return None
134+
conditions = [
135+
_condition_snapshot(condition, detection_type)
136+
# Sort in Python, so a prefetch can still serve it.
137+
for condition in sorted(condition_group.conditions.all(), key=lambda c: c.id)
138+
]
139+
if not conditions:
140+
return None
141+
return {
142+
"id": str(detector.id),
143+
"name": detector.name,
144+
"dataset": code_mode_dataset,
145+
"query": snuba_query.query,
146+
"aggregate": snuba_query.aggregate,
147+
"groupBy": snuba_query.group_by or [],
148+
"timeWindowSeconds": snuba_query.time_window,
149+
"detectionType": detection_type.value,
150+
"comparisonDeltaSeconds": detector.config.get("comparison_delta"),
151+
"direction": _direction(conditions),
152+
"conditions": conditions,
153+
"environment": (
154+
snuba_query.environment.name if snuba_query.environment is not None else None
155+
),
156+
}
157+
158+
100159
def _analysis_window(open_period: GroupOpenPeriod, now: datetime) -> dict[str, str]:
101160
end = open_period.date_ended or now
102161
breach_start = max(open_period.date_started, end - MAX_BREACH_WINDOW)
@@ -189,15 +248,9 @@ def resolve_breached_metric_sources(
189248
detector = detector_group.detector
190249
if detector.type != "metric_issue" or detector.project_id != group.project_id:
191250
continue
192-
try:
193-
detection_type = AlertRuleDetectionType(detector.config.get("detection_type"))
194-
except (TypeError, ValueError):
251+
detection_type = breached_metric_detection_type(detector)
252+
if detection_type is None:
195253
continue
196-
if (
197-
detection_type == AlertRuleDetectionType.STATIC
198-
and detector.config.get("comparison_delta") is not None
199-
):
200-
detection_type = AlertRuleDetectionType.PERCENT
201254
data_source_link = data_source_links.get(detector.id)
202255
if data_source_link is None:
203256
continue
@@ -208,43 +261,19 @@ def resolve_breached_metric_sources(
208261
subscription = subscriptions.get(subscription_id)
209262
if subscription is None or subscription.project_id != group.project_id:
210263
continue
211-
snuba_query = subscription.snuba_query
212-
try:
213-
dataset = Dataset(snuba_query.dataset)
214-
except ValueError:
215-
continue
216-
code_mode_dataset = _code_mode_dataset(dataset, snuba_query.event_types)
217-
if code_mode_dataset is None:
218-
continue
219264
condition_group = detector.workflow_condition_group
220265
if condition_group is None:
221266
continue
222-
conditions = [
223-
_condition_snapshot(condition, detection_type)
224-
for condition in condition_group.conditions.all()
225-
]
226-
if not conditions:
267+
monitor = breached_metric_monitor_snapshot(
268+
detector, subscription.snuba_query, condition_group, detection_type
269+
)
270+
if monitor is None:
227271
continue
228272
snapshot = {
229273
"groupId": str(group.id),
230274
"groupTitle": group.title,
231275
"openPeriodId": str(open_period.id),
232-
"monitor": {
233-
"id": str(detector.id),
234-
"name": detector.name,
235-
"dataset": code_mode_dataset,
236-
"query": snuba_query.query,
237-
"aggregate": snuba_query.aggregate,
238-
"groupBy": snuba_query.group_by or [],
239-
"timeWindowSeconds": snuba_query.time_window,
240-
"detectionType": detection_type.value,
241-
"comparisonDeltaSeconds": detector.config.get("comparison_delta"),
242-
"direction": _direction(conditions),
243-
"conditions": conditions,
244-
"environment": (
245-
snuba_query.environment.name if snuba_query.environment is not None else None
246-
),
247-
},
276+
"monitor": monitor,
248277
"project": {"id": str(group.project_id), "slug": group.project.slug},
249278
"analysisWindow": _analysis_window(open_period, current_time),
250279
}
@@ -258,7 +287,7 @@ def resolve_breached_metric_sources(
258287
}
259288
resolved[source_ref] = BreachedMetricSource(
260289
project_id=group.project_id,
261-
dataset=code_mode_dataset,
290+
dataset=monitor["dataset"],
262291
source=source,
263292
)
264293
return resolved
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from __future__ import annotations
2+
3+
from datetime import timedelta
4+
5+
from sentry.incidents.models.alert_rule import AlertRuleDetectionType
6+
from sentry.investigations.services.breached_metrics import breached_metric_monitor_snapshot
7+
from sentry.snuba.dataset import Dataset
8+
from sentry.snuba.models import SnubaQuery, SnubaQueryEventType
9+
from sentry.snuba.subscriptions import create_snuba_query
10+
from sentry.testutils.cases import TestCase
11+
from sentry.workflow_engine.models.data_condition import Condition
12+
from sentry.workflow_engine.types import DetectorPriorityLevel
13+
14+
15+
class BreachedMetricMonitorSnapshotTest(TestCase):
16+
def test_snapshot_has_the_monitor_settings_and_conditions_in_id_order(self) -> None:
17+
snuba_query = create_snuba_query(
18+
query_type=SnubaQuery.Type.ERROR,
19+
dataset=Dataset.Events,
20+
query="level:error",
21+
aggregate="count()",
22+
time_window=timedelta(minutes=5),
23+
resolution=timedelta(minutes=1),
24+
environment=self.environment,
25+
event_types=[SnubaQueryEventType.EventType.ERROR],
26+
)
27+
group = self.create_data_condition_group()
28+
high = self.create_data_condition(
29+
condition_group=group,
30+
type=Condition.GREATER,
31+
comparison=500,
32+
condition_result=DetectorPriorityLevel.HIGH,
33+
)
34+
ok = self.create_data_condition(
35+
condition_group=group,
36+
type=Condition.LESS_OR_EQUAL,
37+
comparison=500,
38+
condition_result=DetectorPriorityLevel.OK,
39+
)
40+
detector = self.create_detector(name="Checkout errors")
41+
42+
snapshot = breached_metric_monitor_snapshot(
43+
detector, snuba_query, group, AlertRuleDetectionType.STATIC
44+
)
45+
46+
assert snapshot == {
47+
"id": str(detector.id),
48+
"name": "Checkout errors",
49+
"dataset": "errors",
50+
"query": "level:error",
51+
"aggregate": "count()",
52+
"groupBy": [],
53+
"timeWindowSeconds": 300,
54+
"detectionType": "static",
55+
"comparisonDeltaSeconds": None,
56+
"direction": "above",
57+
"conditions": [
58+
{"type": high.type, "comparison": 500, "result": DetectorPriorityLevel.HIGH},
59+
{"type": ok.type, "comparison": 500, "result": DetectorPriorityLevel.OK},
60+
],
61+
"environment": self.environment.name,
62+
}

0 commit comments

Comments
 (0)