Skip to content

Commit a269db2

Browse files
authored
feat(alerts): Emit alert.sent analytic on incidents (#55479)
fixes #54417
1 parent 55f2cca commit a269db2

File tree

16 files changed

+182
-23
lines changed

16 files changed

+182
-23
lines changed

Diff for: src/sentry/analytics/events/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from .alert_created import * # noqa: F401,F403
33
from .alert_edited import * # noqa: F401,F403
44
from .alert_rule_ui_component_webhook_sent import * # noqa: F401,F403
5+
from .alert_sent import * # noqa: F401,F403
56
from .api_token_created import * # noqa: F401,F403
67
from .api_token_deleted import * # noqa: F401,F403
78
from .artifactbundle_assemble import * # noqa: F401,F403

Diff for: src/sentry/analytics/events/alert_sent.py

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from sentry import analytics
2+
3+
4+
class AlertSentEvent(analytics.Event):
5+
type = "alert.sent"
6+
7+
attributes = (
8+
analytics.Attribute("organization_id"),
9+
analytics.Attribute("project_id"),
10+
# The id of the Alert or AlertRule
11+
analytics.Attribute("alert_id"),
12+
# "issue_alert" or "metric_alert"
13+
analytics.Attribute("alert_type"),
14+
# Slack, msteams, email, etc.
15+
analytics.Attribute("provider"),
16+
# User_id if sent via email, channel id if sent via slack, etc.
17+
analytics.Attribute("external_id", type=str, required=False),
18+
analytics.Attribute("notification_uuid", required=False),
19+
)
20+
21+
22+
analytics.register(AlertSentEvent)

Diff for: src/sentry/incidents/action_handlers.py

+44-6
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from django.template.defaultfilters import pluralize
1010
from django.urls import reverse
1111

12-
from sentry import features
12+
from sentry import analytics, features
1313
from sentry.charts.types import ChartSize
1414
from sentry.constants import CRASH_RATE_ALERT_AGGREGATE_ALIAS
1515
from sentry.incidents.charts import build_metric_alert_chart
@@ -33,6 +33,7 @@
3333

3434
class ActionHandler(metaclass=abc.ABCMeta):
3535
status_display = {TriggerStatus.ACTIVE: "Fired", TriggerStatus.RESOLVED: "Resolved"}
36+
provider: str
3637

3738
def __init__(self, action, incident, project):
3839
self.action = action
@@ -57,6 +58,20 @@ def resolve(
5758
):
5859
pass
5960

61+
def record_alert_sent_analytics(
62+
self, external_id: int | str | None = None, notification_uuid: str | None = None
63+
):
64+
analytics.record(
65+
"alert.sent",
66+
organization_id=self.incident.organization_id,
67+
project_id=self.project.id,
68+
provider=self.provider,
69+
alert_id=self.incident.alert_rule_id,
70+
alert_type="metric_alert",
71+
external_id=str(external_id) if external_id is not None else "",
72+
notification_uuid=notification_uuid or "",
73+
)
74+
6075

6176
class DefaultActionHandler(ActionHandler):
6277
def fire(
@@ -93,6 +108,8 @@ def send_alert(
93108
[AlertRuleTriggerAction.TargetType.USER, AlertRuleTriggerAction.TargetType.TEAM],
94109
)
95110
class EmailActionHandler(ActionHandler):
111+
provider = "email"
112+
96113
def _get_targets(self) -> Set[int]:
97114
target = self.action.target
98115
if not target:
@@ -160,6 +177,7 @@ def email_users(
160177
notification_uuid,
161178
)
162179
self.build_message(email_context, trigger_status, user_id).send_async(to=[email])
180+
self.record_alert_sent_analytics(user_id, notification_uuid)
163181

164182
def build_message(self, context, status, user_id) -> MessageBuilder:
165183
display = self.status_display[status]
@@ -182,6 +200,8 @@ def build_message(self, context, status, user_id) -> MessageBuilder:
182200
integration_provider="slack",
183201
)
184202
class SlackActionHandler(DefaultActionHandler):
203+
provider = "slack"
204+
185205
def send_alert(
186206
self,
187207
metric_value: int | float,
@@ -190,9 +210,11 @@ def send_alert(
190210
):
191211
from sentry.integrations.slack.utils import send_incident_alert_notification
192212

193-
send_incident_alert_notification(
213+
success = send_incident_alert_notification(
194214
self.action, self.incident, metric_value, new_status, notification_uuid
195215
)
216+
if success:
217+
self.record_alert_sent_analytics(self.action.target_identifier, notification_uuid)
196218

197219

198220
@AlertRuleTriggerAction.register_type(
@@ -202,6 +224,8 @@ def send_alert(
202224
integration_provider="msteams",
203225
)
204226
class MsTeamsActionHandler(DefaultActionHandler):
227+
provider = "msteams"
228+
205229
def send_alert(
206230
self,
207231
metric_value: int | float,
@@ -210,9 +234,11 @@ def send_alert(
210234
):
211235
from sentry.integrations.msteams.utils import send_incident_alert_notification
212236

213-
send_incident_alert_notification(
237+
success = send_incident_alert_notification(
214238
self.action, self.incident, metric_value, new_status, notification_uuid
215239
)
240+
if success:
241+
self.record_alert_sent_analytics(self.action.target_identifier, notification_uuid)
216242

217243

218244
@AlertRuleTriggerAction.register_type(
@@ -222,6 +248,8 @@ def send_alert(
222248
integration_provider="pagerduty",
223249
)
224250
class PagerDutyActionHandler(DefaultActionHandler):
251+
provider = "pagerduty"
252+
225253
def send_alert(
226254
self,
227255
metric_value: int | float,
@@ -230,9 +258,11 @@ def send_alert(
230258
):
231259
from sentry.integrations.pagerduty.utils import send_incident_alert_notification
232260

233-
send_incident_alert_notification(
261+
success = send_incident_alert_notification(
234262
self.action, self.incident, metric_value, new_status, notification_uuid
235263
)
264+
if success:
265+
self.record_alert_sent_analytics(self.action.target_identifier, notification_uuid)
236266

237267

238268
@AlertRuleTriggerAction.register_type(
@@ -242,6 +272,8 @@ def send_alert(
242272
integration_provider="opsgenie",
243273
)
244274
class OpsgenieActionHandler(DefaultActionHandler):
275+
provider = "opsgenie"
276+
245277
def send_alert(
246278
self,
247279
metric_value: int | float,
@@ -250,9 +282,11 @@ def send_alert(
250282
):
251283
from sentry.integrations.opsgenie.utils import send_incident_alert_notification
252284

253-
send_incident_alert_notification(
285+
success = send_incident_alert_notification(
254286
self.action, self.incident, metric_value, new_status, notification_uuid
255287
)
288+
if success:
289+
self.record_alert_sent_analytics(self.action.target_identifier, notification_uuid)
256290

257291

258292
@AlertRuleTriggerAction.register_type(
@@ -261,6 +295,8 @@ def send_alert(
261295
[AlertRuleTriggerAction.TargetType.SENTRY_APP],
262296
)
263297
class SentryAppActionHandler(DefaultActionHandler):
298+
provider = "sentry_app"
299+
264300
def send_alert(
265301
self,
266302
metric_value: int | float,
@@ -269,9 +305,11 @@ def send_alert(
269305
):
270306
from sentry.rules.actions.notify_event_service import send_incident_alert_notification
271307

272-
send_incident_alert_notification(
308+
success = send_incident_alert_notification(
273309
self.action, self.incident, new_status, metric_value, notification_uuid
274310
)
311+
if success:
312+
self.record_alert_sent_analytics(self.action.sentry_app_id, notification_uuid)
275313

276314

277315
def format_duration(minutes):

Diff for: src/sentry/integrations/msteams/utils.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -102,18 +102,19 @@ def send_incident_alert_notification(
102102
metric_value: int | None,
103103
new_status: IncidentStatus,
104104
notification_uuid: str | None = None,
105-
) -> None:
105+
) -> bool:
106106
from .card_builder import build_incident_attachment
107107

108108
if action.target_identifier is None:
109109
raise ValueError("Can't send without `target_identifier`")
110110

111111
attachment = build_incident_attachment(incident, new_status, metric_value, notification_uuid)
112-
integration_service.send_msteams_incident_alert_notification(
112+
success = integration_service.send_msteams_incident_alert_notification(
113113
integration_id=action.integration_id,
114114
channel=action.target_identifier,
115115
attachment=attachment,
116116
)
117+
return success
117118

118119

119120
def get_preinstall_client(service_url):

Diff for: src/sentry/integrations/opsgenie/utils.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -62,19 +62,19 @@ def send_incident_alert_notification(
6262
metric_value: int,
6363
new_status: IncidentStatus,
6464
notification_uuid: str | None = None,
65-
) -> None:
65+
) -> bool:
6666
integration, org_integration = integration_service.get_organization_context(
6767
organization_id=incident.organization_id, integration_id=action.integration_id
6868
)
6969
if org_integration is None or integration is None or integration.status != ObjectStatus.ACTIVE:
7070
logger.info("Opsgenie integration removed, but the rule is still active.")
71-
return
71+
return False
7272

7373
team = get_team(org_integration=org_integration, team_id=action.target_identifier)
7474
if not team:
7575
# team removed, but the rule is still active
7676
logger.info("Opsgenie team removed, but the rule is still active.")
77-
return
77+
return False
7878

7979
integration_key = team["integration_key"]
8080
client = OpsgenieClient(
@@ -85,6 +85,7 @@ def send_incident_alert_notification(
8585
attachment = build_incident_attachment(incident, new_status, metric_value, notification_uuid)
8686
try:
8787
client.send_notification(attachment)
88+
return True
8889
except ApiError as e:
8990
logger.info(
9091
"rule.fail.opsgenie_notification",

Diff for: src/sentry/integrations/pagerduty/utils.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def send_incident_alert_notification(
6262
metric_value: int,
6363
new_status: IncidentStatus,
6464
notification_uuid: str | None = None,
65-
) -> None:
65+
) -> bool:
6666
integration_id = action.integration_id
6767
organization_id = incident.organization_id
6868

@@ -107,6 +107,7 @@ def send_incident_alert_notification(
107107
)
108108
try:
109109
client.send_trigger(attachment)
110+
return True
110111
except ApiError as e:
111112
logger.info(
112113
"rule.fail.pagerduty_metric_alert",

Diff for: src/sentry/integrations/slack/utils/notifications.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@ def send_incident_alert_notification(
2424
metric_value: int,
2525
new_status: IncidentStatus,
2626
notification_uuid: str | None = None,
27-
) -> None:
27+
) -> bool:
2828
# Make sure organization integration is still active:
2929
integration, org_integration = integration_service.get_organization_context(
3030
organization_id=incident.organization_id, integration_id=action.integration_id
3131
)
3232
if org_integration is None or integration is None or integration.status != ObjectStatus.ACTIVE:
3333
# Integration removed, but rule is still active.
34-
return
34+
return False
3535

3636
chart_url = None
3737
if features.has("organizations:metric-alert-chartcuterie", incident.organization):
@@ -64,8 +64,10 @@ def send_incident_alert_notification(
6464
client = SlackClient(integration_id=integration.id)
6565
try:
6666
client.post("/chat.postMessage", data=payload, timeout=5)
67+
return True
6768
except ApiError:
6869
logger.info("rule.fail.slack_post", exc_info=True)
70+
return False
6971

7072

7173
def send_slack_response(

Diff for: src/sentry/rules/actions/notify_event_service.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def send_incident_alert_notification(
5656
new_status: IncidentStatus,
5757
metric_value: str | None = None,
5858
notification_uuid: str | None = None,
59-
) -> None:
59+
) -> bool:
6060
"""
6161
When a metric alert is triggered, send incident data to the SentryApp's webhook.
6262
:param action: The triggered `AlertRuleTriggerAction`.
@@ -70,7 +70,7 @@ def send_incident_alert_notification(
7070
incident, new_status, metric_value, notification_uuid
7171
)
7272

73-
integration_service.send_incident_alert_notification(
73+
success = integration_service.send_incident_alert_notification(
7474
sentry_app_id=action.sentry_app_id,
7575
action_id=action.id,
7676
incident_id=incident.id,
@@ -79,6 +79,7 @@ def send_incident_alert_notification(
7979
incident_attachment_json=json.dumps(incident_attachment),
8080
metric_value=metric_value,
8181
)
82+
return success
8283

8384

8485
def find_alert_rule_action_ui_component(app_platform_event: AppPlatformEvent) -> bool:

Diff for: src/sentry/services/hybrid_cloud/integration/impl.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,7 @@ def send_incident_alert_notification(
348348
incident_attachment_json: str,
349349
metric_value: Optional[str] = None,
350350
notification_uuid: str | None = None,
351-
) -> None:
351+
) -> bool:
352352
sentry_app = SentryApp.objects.get(id=sentry_app_id)
353353

354354
metrics.incr("notifications.sent", instance=sentry_app.slug, skip_internal=False)
@@ -370,7 +370,7 @@ def send_incident_alert_notification(
370370
},
371371
exc_info=True,
372372
)
373-
return None
373+
return False
374374

375375
app_platform_event = AppPlatformEvent(
376376
resource="metric_alert",
@@ -395,16 +395,19 @@ def send_incident_alert_notification(
395395
sentry_app_id=sentry_app.id,
396396
event=f"{app_platform_event.resource}.{app_platform_event.action}",
397397
)
398+
return alert_rule_action_ui_component
398399

399400
def send_msteams_incident_alert_notification(
400401
self, *, integration_id: int, channel: str, attachment: Dict[str, Any]
401-
) -> None:
402+
) -> bool:
402403
integration = Integration.objects.get(id=integration_id)
403404
client = MsTeamsClient(integration)
404405
try:
405406
client.send_card(channel, attachment)
407+
return True
406408
except ApiError:
407409
logger.info("rule.fail.msteams_post", exc_info=True)
410+
return False
408411

409412
def delete_integration(self, *, integration_id: int) -> None:
410413
integration = Integration.objects.filter(id=integration_id).first()

Diff for: src/sentry/services/hybrid_cloud/integration/service.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -264,14 +264,14 @@ def send_incident_alert_notification(
264264
incident_attachment_json: str,
265265
metric_value: Optional[str] = None,
266266
notification_uuid: Optional[str] = None,
267-
) -> None:
267+
) -> bool:
268268
pass
269269

270270
@rpc_method
271271
@abstractmethod
272272
def send_msteams_incident_alert_notification(
273273
self, *, integration_id: int, channel: str, attachment: Dict[str, Any]
274-
) -> None:
274+
) -> bool:
275275
raise NotImplementedError
276276

277277
@rpc_method

Diff for: tests/sentry/incidents/action_handlers/test_email.py

+14
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,20 @@ def test_fire_metric_alert(self):
6060
def test_resolve_metric_alert(self):
6161
self.run_fire_test("resolve")
6262

63+
@patch("sentry.analytics.record")
64+
def test_alert_sent_recorded(self, mock_record):
65+
self.run_fire_test()
66+
mock_record.assert_called_with(
67+
"alert.sent",
68+
organization_id=self.organization.id,
69+
project_id=self.project.id,
70+
provider="email",
71+
alert_id=self.alert_rule.id,
72+
alert_type="metric_alert",
73+
external_id=str(self.user.id),
74+
notification_uuid="",
75+
)
76+
6377

6478
class EmailActionHandlerGetTargetsTest(TestCase):
6579
@cached_property

0 commit comments

Comments
 (0)