Skip to content

feat(alerts): Add notification_uuid to incident actions #54891

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Aug 29, 2023
Merged
134 changes: 108 additions & 26 deletions src/sentry/incidents/action_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,25 +40,50 @@ def __init__(self, action, incident, project):
self.project = project

@abc.abstractmethod
def fire(self, metric_value: int | float, new_status: IncidentStatus):
def fire(
self,
metric_value: int | float,
new_status: IncidentStatus,
notification_uuid: str | None = None,
):
pass

@abc.abstractmethod
def resolve(self, metric_value: int | float, new_status: IncidentStatus):
def resolve(
self,
metric_value: int | float,
new_status: IncidentStatus,
notification_uuid: str | None = None,
):
pass


class DefaultActionHandler(ActionHandler):
def fire(self, metric_value: int | float, new_status: IncidentStatus):
def fire(
self,
metric_value: int | float,
new_status: IncidentStatus,
notification_uuid: str | None = None,
):
if not RuleSnooze.objects.is_snoozed_for_all(alert_rule=self.incident.alert_rule):
self.send_alert(metric_value, new_status)

def resolve(self, metric_value: int | float, new_status: IncidentStatus):
self.send_alert(metric_value, new_status, notification_uuid)

def resolve(
self,
metric_value: int | float,
new_status: IncidentStatus,
notification_uuid: str | None = None,
):
if not RuleSnooze.objects.is_snoozed_for_all(alert_rule=self.incident.alert_rule):
self.send_alert(metric_value, new_status)
self.send_alert(metric_value, new_status, notification_uuid)

@abc.abstractmethod
def send_alert(self, metric_value: int | float, new_status: IncidentStatus):
def send_alert(
self,
metric_value: int | float,
new_status: IncidentStatus,
notification_uuid: str | None = None,
):
pass


Expand Down Expand Up @@ -99,13 +124,28 @@ def _get_targets(self) -> Set[int]:
def get_targets(self) -> Sequence[Tuple[int, str]]:
return list(get_email_addresses(self._get_targets(), project=self.project).items())

def fire(self, metric_value: int | float, new_status: IncidentStatus):
self.email_users(TriggerStatus.ACTIVE, new_status)

def resolve(self, metric_value: int | float, new_status: IncidentStatus):
self.email_users(TriggerStatus.RESOLVED, new_status)

def email_users(self, trigger_status: TriggerStatus, incident_status: IncidentStatus) -> None:
def fire(
self,
metric_value: int | float,
new_status: IncidentStatus,
notification_uuid: str | None = None,
):
self.email_users(TriggerStatus.ACTIVE, new_status, notification_uuid)

def resolve(
self,
metric_value: int | float,
new_status: IncidentStatus,
notification_uuid: str | None = None,
):
self.email_users(TriggerStatus.RESOLVED, new_status, notification_uuid)

def email_users(
self,
trigger_status: TriggerStatus,
incident_status: IncidentStatus,
notification_uuid: str | None = None,
) -> None:
targets = [(user_id, email) for user_id, email in self.get_targets()]
users = user_service.get_many(filter={"user_ids": [user_id for user_id, _ in targets]})
for index, (user_id, email) in enumerate(targets):
Expand All @@ -117,6 +157,7 @@ def email_users(self, trigger_status: TriggerStatus, incident_status: IncidentSt
trigger_status,
incident_status,
user,
notification_uuid,
)
self.build_message(email_context, trigger_status, user_id).send_async(to=[email])

Expand All @@ -141,10 +182,17 @@ def build_message(self, context, status, user_id) -> MessageBuilder:
integration_provider="slack",
)
class SlackActionHandler(DefaultActionHandler):
def send_alert(self, metric_value: int | float, new_status: IncidentStatus):
def send_alert(
self,
metric_value: int | float,
new_status: IncidentStatus,
notification_uuid: str | None = None,
):
from sentry.integrations.slack.utils import send_incident_alert_notification

send_incident_alert_notification(self.action, self.incident, metric_value, new_status)
send_incident_alert_notification(
self.action, self.incident, metric_value, new_status, notification_uuid
)


@AlertRuleTriggerAction.register_type(
Expand All @@ -154,10 +202,17 @@ def send_alert(self, metric_value: int | float, new_status: IncidentStatus):
integration_provider="msteams",
)
class MsTeamsActionHandler(DefaultActionHandler):
def send_alert(self, metric_value: int | float, new_status: IncidentStatus):
def send_alert(
self,
metric_value: int | float,
new_status: IncidentStatus,
notification_uuid: str | None = None,
):
from sentry.integrations.msteams.utils import send_incident_alert_notification

send_incident_alert_notification(self.action, self.incident, metric_value, new_status)
send_incident_alert_notification(
self.action, self.incident, metric_value, new_status, notification_uuid
)


@AlertRuleTriggerAction.register_type(
Expand All @@ -167,10 +222,17 @@ def send_alert(self, metric_value: int | float, new_status: IncidentStatus):
integration_provider="pagerduty",
)
class PagerDutyActionHandler(DefaultActionHandler):
def send_alert(self, metric_value: int | float, new_status: IncidentStatus):
def send_alert(
self,
metric_value: int | float,
new_status: IncidentStatus,
notification_uuid: str | None = None,
):
from sentry.integrations.pagerduty.utils import send_incident_alert_notification

send_incident_alert_notification(self.action, self.incident, metric_value, new_status)
send_incident_alert_notification(
self.action, self.incident, metric_value, new_status, notification_uuid
)


@AlertRuleTriggerAction.register_type(
Expand All @@ -180,10 +242,17 @@ def send_alert(self, metric_value: int | float, new_status: IncidentStatus):
integration_provider="opsgenie",
)
class OpsgenieActionHandler(DefaultActionHandler):
def send_alert(self, metric_value: int | float, new_status: IncidentStatus):
def send_alert(
self,
metric_value: int | float,
new_status: IncidentStatus,
notification_uuid: str | None = None,
):
from sentry.integrations.opsgenie.utils import send_incident_alert_notification

send_incident_alert_notification(self.action, self.incident, metric_value, new_status)
send_incident_alert_notification(
self.action, self.incident, metric_value, new_status, notification_uuid
)


@AlertRuleTriggerAction.register_type(
Expand All @@ -192,10 +261,17 @@ def send_alert(self, metric_value: int | float, new_status: IncidentStatus):
[AlertRuleTriggerAction.TargetType.SENTRY_APP],
)
class SentryAppActionHandler(DefaultActionHandler):
def send_alert(self, metric_value: int | float, new_status: IncidentStatus):
def send_alert(
self,
metric_value: int | float,
new_status: IncidentStatus,
notification_uuid: str | None = None,
):
from sentry.rules.actions.notify_event_service import send_incident_alert_notification

send_incident_alert_notification(self.action, self.incident, new_status, metric_value)
send_incident_alert_notification(
self.action, self.incident, new_status, metric_value, notification_uuid
)


def format_duration(minutes):
Expand Down Expand Up @@ -226,6 +302,7 @@ def generate_incident_trigger_email_context(
trigger_status,
incident_status,
user: User | RpcUser | None = None,
notification_uuid: str | None = None,
):
trigger = alert_rule_trigger
alert_rule = trigger.alert_rule
Expand Down Expand Up @@ -272,6 +349,11 @@ def generate_incident_trigger_email_context(
tz = options[0].value

organization = incident.organization
alert_link_params = {
"referrer": "metric_alert_email",
}
if notification_uuid:
alert_link_params["notification_uuid"] = notification_uuid

alert_link = organization.absolute_url(
reverse(
Expand All @@ -281,7 +363,7 @@ def generate_incident_trigger_email_context(
"incident_id": incident.identifier,
},
),
query="referrer=alert_email",
query=urlencode(alert_link_params),
)

snooze_alert_url = None
Expand Down
2 changes: 2 additions & 0 deletions src/sentry/incidents/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from dataclasses import replace
from datetime import datetime, timedelta
from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple, Union
from uuid import uuid4

from django.db import router, transaction
from django.db.models.signals import post_save
Expand Down Expand Up @@ -252,6 +253,7 @@ def create_incident_activity(
value=value,
previous_value=previous_value,
comment=comment,
notification_uuid=uuid4(),
**kwargs,
)

Expand Down
8 changes: 4 additions & 4 deletions src/sentry/incidents/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -612,15 +612,15 @@ def build_handler(self, action, incident, project):
else:
metrics.incr(f"alert_rule_trigger.unhandled_type.{self.type}")

def fire(self, action, incident, project, metric_value, new_status):
def fire(self, action, incident, project, metric_value, new_status, notification_uuid=None):
handler = self.build_handler(action, incident, project)
if handler:
return handler.fire(metric_value, new_status)
return handler.fire(metric_value, new_status, notification_uuid)

def resolve(self, action, incident, project, metric_value, new_status):
def resolve(self, action, incident, project, metric_value, new_status, notification_uuid=None):
handler = self.build_handler(action, incident, project)
if handler:
return handler.resolve(metric_value, new_status)
return handler.resolve(metric_value, new_status, notification_uuid)

@classmethod
def register_type(cls, slug, type, supported_target_types, integration_provider=None):
Expand Down
14 changes: 13 additions & 1 deletion src/sentry/incidents/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,14 +208,26 @@ def handle_trigger_action(
metrics.incr("incidents.alert_rules.action.skipping_missing_project")
return

incident_activity = (
IncidentActivity.objects.filter(incident=incident, value=new_status).order_by("-id").first()
)
notification_uuid = str(incident_activity.notification_uuid) if incident_activity else None
if notification_uuid is None:
metrics.incr("incidents.alert_rules.action.incident_activity_missing")

metrics.incr(
"incidents.alert_rules.action.{}.{}".format(
AlertRuleTriggerAction.Type(action.type).name.lower(), method
)
)

getattr(action, method)(
action, incident, project, metric_value=metric_value, new_status=IncidentStatus(new_status)
action,
incident,
project,
metric_value=metric_value,
new_status=IncidentStatus(new_status),
notification_uuid=notification_uuid,
)


Expand Down
17 changes: 15 additions & 2 deletions src/sentry/integrations/metric_alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,13 @@ def get_incident_status_text(alert_rule: AlertRule, metric_value: str) -> str:
}


def incident_attachment_info(incident: Incident, new_status: IncidentStatus, metric_value=None):
def incident_attachment_info(
incident: Incident,
new_status: IncidentStatus,
metric_value=None,
notification_uuid=None,
referrer="metric_alert",
):
alert_rule = incident.alert_rule

status = INCIDENT_STATUS[new_status]
Expand All @@ -111,6 +117,13 @@ def incident_attachment_info(incident: Incident, new_status: IncidentStatus, met
text = get_incident_status_text(alert_rule, metric_value)
title = f"{status}: {alert_rule.name}"

title_link_params = {
"alert": str(incident.identifier),
"referrer": referrer,
}
if notification_uuid:
title_link_params["notification_uuid"] = notification_uuid

title_link = alert_rule.organization.absolute_url(
reverse(
"sentry-metric-alert-details",
Expand All @@ -119,7 +132,7 @@ def incident_attachment_info(incident: Incident, new_status: IncidentStatus, met
"alert_rule_id": alert_rule.id,
},
),
query=parse.urlencode({"alert": str(incident.identifier)}),
query=parse.urlencode(title_link_params),
)

return {
Expand Down
9 changes: 8 additions & 1 deletion src/sentry/integrations/msteams/card_builder/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,15 @@ def build_incident_attachment(
incident: Incident,
new_status: IncidentStatus,
metric_value: int | None = None,
notification_uuid: str | None = None,
) -> Dict[str, Any]:
data = incident_attachment_info(incident, new_status, metric_value)
data = incident_attachment_info(
incident,
new_status,
metric_value,
notification_uuid=notification_uuid,
referrer="metric_alert_msteams",
)

colors = {"Resolved": "good", "Warning": "warning", "Critical": "attention"}

Expand Down
3 changes: 2 additions & 1 deletion src/sentry/integrations/msteams/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,14 @@ def send_incident_alert_notification(
incident: Incident,
metric_value: int | None,
new_status: IncidentStatus,
notification_uuid: str | None = None,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we be passing notification_uuid to build_incident_attachment here?

) -> None:
from .card_builder import build_incident_attachment

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

attachment = build_incident_attachment(incident, new_status, metric_value)
attachment = build_incident_attachment(incident, new_status, metric_value, notification_uuid)
integration_service.send_msteams_incident_alert_notification(
integration_id=action.integration_id,
channel=action.target_identifier,
Expand Down
Loading