|
10 | 10 | MetricIssueContext, |
11 | 11 | OpenPeriodContext, |
12 | 12 | ) |
| 13 | +from sentry.models.activity import Activity |
13 | 14 | from sentry.models.group import Group |
14 | 15 | from sentry.models.organization import Organization |
15 | 16 | from sentry.notifications.platform.registry import template_registry |
@@ -107,37 +108,24 @@ def to_open_period_context(self) -> OpenPeriodContext: |
107 | 108 | ) |
108 | 109 |
|
109 | 110 |
|
110 | | -class MetricAlertNotificationData(NotificationData): |
111 | | - source: NotificationSource = NotificationSource.METRIC_ALERT |
| 111 | +class BaseMetricAlertNotificationData(NotificationData): |
| 112 | + """ |
| 113 | + Shared fields and properties for metric alert notification data. |
112 | 114 |
|
113 | | - # For re-fetching GroupEvent via eventstore (MetricIssueContext has ORM instances) |
114 | | - event_id: str |
115 | | - project_id: int |
116 | | - group_id: int |
| 115 | + Subclasses differ only in how they source MetricIssueContext |
| 116 | + - MetricAlertNotificationData: re-fetches GroupEvent from Snuba |
| 117 | + - ActivityMetricAlertNotificationData: re-fetches Activity |
| 118 | + """ |
117 | 119 |
|
118 | | - # For feature flag check(chartcuterie) + message builder |
| 120 | + group_id: int |
119 | 121 | organization_id: int |
120 | | - # To rebuild the contexts |
121 | 122 | detector_id: int |
122 | 123 |
|
123 | | - # Pre-computed serializable contexts |
124 | 124 | alert_context: SerializableAlertContext |
125 | 125 | open_period_context: SerializableOpenPeriodContext |
126 | 126 |
|
127 | 127 | notification_uuid: str |
128 | 128 |
|
129 | | - @property |
130 | | - def event(self) -> GroupEvent: |
131 | | - event = eventstore.backend.get_event_by_id( |
132 | | - self.project_id, self.event_id, group_id=self.group_id |
133 | | - ) |
134 | | - if event is None: |
135 | | - raise ValueError(f"Event {self.event_id} not found") |
136 | | - elif not isinstance(event, GroupEvent): |
137 | | - raise ValueError(f"Event {self.event_id} is not a GroupEvent") |
138 | | - |
139 | | - return event |
140 | | - |
141 | 129 | @property |
142 | 130 | def organization(self) -> Organization: |
143 | 131 | return Organization.objects.get_from_cache(id=self.organization_id) |
@@ -166,41 +154,97 @@ def serialized_detector(self) -> DetectorSerializerResponse: |
166 | 154 |
|
167 | 155 | return get_detector_serializer(self.detector) |
168 | 156 |
|
169 | | - @classmethod |
170 | | - def get_metric_issue_context(cls, event: GroupEvent) -> MetricIssueContext: |
| 157 | + def build_metric_issue_context(self) -> MetricIssueContext: |
| 158 | + raise NotImplementedError |
| 159 | + |
| 160 | + |
| 161 | +class MetricAlertNotificationData(BaseMetricAlertNotificationData): |
| 162 | + source: NotificationSource = NotificationSource.METRIC_ALERT |
| 163 | + |
| 164 | + event_id: str |
| 165 | + project_id: int |
| 166 | + |
| 167 | + @property |
| 168 | + def event(self) -> GroupEvent: |
| 169 | + event = eventstore.backend.get_event_by_id( |
| 170 | + self.project_id, self.event_id, group_id=self.group_id |
| 171 | + ) |
| 172 | + if event is None: |
| 173 | + raise ValueError(f"Event {self.event_id} not found") |
| 174 | + elif not isinstance(event, GroupEvent): |
| 175 | + raise ValueError(f"Event {self.event_id} is not a GroupEvent") |
| 176 | + |
| 177 | + return event |
| 178 | + |
| 179 | + def build_metric_issue_context(self) -> MetricIssueContext: |
171 | 180 | from sentry.notifications.notification_action.types import BaseMetricAlertHandler |
172 | 181 |
|
| 182 | + event = self.event |
173 | 183 | evidence_data, priority = BaseMetricAlertHandler._extract_from_group_event(event) |
174 | 184 | return MetricIssueContext.from_group_event(event.group, evidence_data, priority) |
175 | 185 |
|
176 | 186 |
|
| 187 | +class ActivityMetricAlertNotificationData(BaseMetricAlertNotificationData): |
| 188 | + source: NotificationSource = NotificationSource.ACTIVITY_METRIC_ALERT |
| 189 | + |
| 190 | + activity_id: int |
| 191 | + |
| 192 | + @property |
| 193 | + def activity(self) -> Activity: |
| 194 | + return Activity.objects.get(id=self.activity_id) |
| 195 | + |
| 196 | + def build_metric_issue_context(self) -> MetricIssueContext: |
| 197 | + from sentry.notifications.notification_action.types import BaseMetricAlertHandler |
| 198 | + |
| 199 | + evidence_data, priority = BaseMetricAlertHandler._extract_from_activity(self.activity) |
| 200 | + return MetricIssueContext.from_group_event(self.group, evidence_data, priority) |
| 201 | + |
| 202 | + |
| 203 | +_EXAMPLE_ALERT_CONTEXT = SerializableAlertContext( |
| 204 | + name="Example Alert", |
| 205 | + action_identifier_id=1, |
| 206 | + detection_type="static", |
| 207 | +) |
| 208 | +_EXAMPLE_OPEN_PERIOD_CONTEXT = SerializableOpenPeriodContext( |
| 209 | + id=1, |
| 210 | + date_started=datetime(2024, 1, 1, 0, 0, 0), |
| 211 | +) |
| 212 | + |
| 213 | + |
177 | 214 | @template_registry.register(NotificationSource.METRIC_ALERT) |
178 | 215 | class MetricAlertNotificationTemplate(NotificationTemplate[MetricAlertNotificationData]): |
179 | 216 | category = NotificationCategory.METRIC_ALERT |
180 | | - # hide_from_debugger because this template uses a custom renderer that bypasses |
181 | | - # the standard NotificationRenderedTemplate rendering path so wouldn't load correctly in the debugger. |
182 | 217 | hide_from_debugger = True |
183 | 218 | example_data = MetricAlertNotificationData( |
184 | 219 | event_id="abc123", |
185 | 220 | project_id=1, |
186 | 221 | group_id=1, |
187 | 222 | organization_id=1, |
188 | 223 | detector_id=1, |
189 | | - alert_context=SerializableAlertContext( |
190 | | - name="Example Alert", |
191 | | - action_identifier_id=1, |
192 | | - detection_type="static", |
193 | | - ), |
194 | | - open_period_context=SerializableOpenPeriodContext( |
195 | | - id=1, |
196 | | - date_started=datetime(2024, 1, 1, 0, 0, 0), |
197 | | - ), |
| 224 | + alert_context=_EXAMPLE_ALERT_CONTEXT, |
| 225 | + open_period_context=_EXAMPLE_OPEN_PERIOD_CONTEXT, |
198 | 226 | notification_uuid="test-uuid", |
199 | 227 | ) |
200 | 228 |
|
201 | 229 | def render(self, data: MetricAlertNotificationData) -> NotificationRenderedTemplate: |
202 | | - # The actual rendering is handled by the provider-specific custom renderer |
203 | | - # (e.g. SlackMetricAlertRenderer), which rebuilds MetricIssueContext from the |
204 | | - # re-fetched GroupEvent and builds the full payload via SlackIncidentsMessageBuilder. |
205 | | - # This method returns a minimal fallback for providers without a custom renderer. |
| 230 | + return NotificationRenderedTemplate(subject="Metric Alert", body=[]) |
| 231 | + |
| 232 | + |
| 233 | +@template_registry.register(NotificationSource.ACTIVITY_METRIC_ALERT) |
| 234 | +class ActivityMetricAlertNotificationTemplate( |
| 235 | + NotificationTemplate[ActivityMetricAlertNotificationData] |
| 236 | +): |
| 237 | + category = NotificationCategory.METRIC_ALERT |
| 238 | + hide_from_debugger = True |
| 239 | + example_data = ActivityMetricAlertNotificationData( |
| 240 | + group_id=1, |
| 241 | + organization_id=1, |
| 242 | + detector_id=1, |
| 243 | + alert_context=_EXAMPLE_ALERT_CONTEXT, |
| 244 | + open_period_context=_EXAMPLE_OPEN_PERIOD_CONTEXT, |
| 245 | + notification_uuid="test-uuid", |
| 246 | + activity_id=1, |
| 247 | + ) |
| 248 | + |
| 249 | + def render(self, data: ActivityMetricAlertNotificationData) -> NotificationRenderedTemplate: |
206 | 250 | return NotificationRenderedTemplate(subject="Metric Alert", body=[]) |
0 commit comments