We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Add check to default notifications for rule snooze
We will want to check to see if the alert is being snoozed + how it is being snoozed to determine if we send out the default notification for the metric alert. Tests will be very important here to make sure we are correctly muting the alert. 

Similar PR from V1 of Mute Alerts: https://github.com/getsentry/sentry/commit/0e779826e56f69c1e247943fc46a5754bee6039d

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `src/sentry/incidents/action_handlers.py`
Content:
```
1 from __future__ import annotations
2 
3 import abc
4 import logging
5 from typing import Sequence, Set, Tuple
6 
7 from django.conf import settings
8 from django.template.defaultfilters import pluralize
9 from django.urls import reverse
10 
11 from sentry import features
12 from sentry.charts.types import ChartSize
13 from sentry.constants import CRASH_RATE_ALERT_AGGREGATE_ALIAS
14 from sentry.incidents.charts import build_metric_alert_chart
15 from sentry.incidents.models import (
16     INCIDENT_STATUS,
17     AlertRuleThresholdType,
18     AlertRuleTriggerAction,
19     IncidentStatus,
20     TriggerStatus,
21 )
22 from sentry.models.notificationsetting import NotificationSetting
23 from sentry.models.options.user_option import UserOption
24 from sentry.models.user import User
25 from sentry.services.hybrid_cloud.user import RpcUser
26 from sentry.types.integrations import ExternalProviders
27 from sentry.utils import json
28 from sentry.utils.email import MessageBuilder, get_email_addresses
29 
30 
31 class ActionHandler(metaclass=abc.ABCMeta):
32     status_display = {TriggerStatus.ACTIVE: "Fired", TriggerStatus.RESOLVED: "Resolved"}
33 
34     def __init__(self, action, incident, project):
35         self.action = action
36         self.incident = incident
37         self.project = project
38 
39     @abc.abstractmethod
40     def fire(self, metric_value: int | float, new_status: IncidentStatus):
41         pass
42 
43     @abc.abstractmethod
44     def resolve(self, metric_value: int | float, new_status: IncidentStatus):
45         pass
46 
47 
48 class DefaultActionHandler(ActionHandler):
49     def fire(self, metric_value: int | float, new_status: IncidentStatus):
50         self.send_alert(metric_value, new_status)
51 
52     def resolve(self, metric_value: int | float, new_status: IncidentStatus):
53         self.send_alert(metric_value, new_status)
54 
55     @abc.abstractmethod
56     def send_alert(self, metric_value: int | float, new_status: IncidentStatus):
57         pass
58 
59 
60 @AlertRuleTriggerAction.register_type(
61     "email",
62     AlertRuleTriggerAction.Type.EMAIL,
63     [AlertRuleTriggerAction.TargetType.USER, AlertRuleTriggerAction.TargetType.TEAM],
64 )
65 class EmailActionHandler(ActionHandler):
66     def _get_targets(self) -> Set[int]:
67         target = self.action.target
68         if not target:
69             return set()
70 
71         if self.action.target_type == AlertRuleTriggerAction.TargetType.USER.value:
72             return {target.id}
73 
74         elif self.action.target_type == AlertRuleTriggerAction.TargetType.TEAM.value:
75             users = NotificationSetting.objects.filter_to_accepting_recipients(
76                 self.project,
77                 {RpcUser(id=member.user_id) for member in target.member_set},
78             )[ExternalProviders.EMAIL]
79             return {user.id for user in users}
80 
81         return set()
82 
83     def get_targets(self) -> Sequence[Tuple[int, str]]:
84         return list(get_email_addresses(self._get_targets(), project=self.project).items())
85 
86     def fire(self, metric_value: int | float, new_status: IncidentStatus):
87         self.email_users(TriggerStatus.ACTIVE, new_status)
88 
89     def resolve(self, metric_value: int | float, new_status: IncidentStatus):
90         self.email_users(TriggerStatus.RESOLVED, new_status)
91 
92     def email_users(self, trigger_status: TriggerStatus, incident_status: IncidentStatus) -> None:
93         for user_id, email in self.get_targets():
94             user = User.objects.get_from_cache(id=user_id)
95             email_context = generate_incident_trigger_email_context(
96                 self.project,
97                 self.incident,
98                 self.action.alert_rule_trigger,
99                 trigger_status,
100                 incident_status,
101                 user,
102             )
103             self.build_message(email_context, trigger_status, user_id).send_async(to=[email])
104 
105     def build_message(self, context, status, user_id):
106         display = self.status_display[status]
107         return MessageBuilder(
108             subject="[{}] {} - {}".format(
109                 context["status"], context["incident_name"], self.project.slug
110             ),
111             template="sentry/emails/incidents/trigger.txt",
112             html_template="sentry/emails/incidents/trigger.html",
113             type=f"incident.alert_rule_{display.lower()}",
114             context=context,
115             headers={"X-SMTPAPI": json.dumps({"category": "metric_alert_email"})},
116         )
117 
118 
119 @AlertRuleTriggerAction.register_type(
120     "slack",
121     AlertRuleTriggerAction.Type.SLACK,
122     [AlertRuleTriggerAction.TargetType.SPECIFIC],
123     integration_provider="slack",
124 )
125 class SlackActionHandler(DefaultActionHandler):
126     def send_alert(self, metric_value: int | float, new_status: IncidentStatus):
127         from sentry.integrations.slack.utils import send_incident_alert_notification
128 
129         send_incident_alert_notification(self.action, self.incident, metric_value, new_status)
130 
131 
132 @AlertRuleTriggerAction.register_type(
133     "msteams",
134     AlertRuleTriggerAction.Type.MSTEAMS,
135     [AlertRuleTriggerAction.TargetType.SPECIFIC],
136     integration_provider="msteams",
137 )
138 class MsTeamsActionHandler(DefaultActionHandler):
139     def send_alert(self, metric_value: int | float, new_status: IncidentStatus):
140         from sentry.integrations.msteams.utils import send_incident_alert_notification
141 
142         send_incident_alert_notification(self.action, self.incident, metric_value, new_status)
143 
144 
145 @AlertRuleTriggerAction.register_type(
146     "pagerduty",
147     AlertRuleTriggerAction.Type.PAGERDUTY,
148     [AlertRuleTriggerAction.TargetType.SPECIFIC],
149     integration_provider="pagerduty",
150 )
151 class PagerDutyActionHandler(DefaultActionHandler):
152     def send_alert(self, metric_value: int | float, new_status: IncidentStatus):
153         from sentry.integrations.pagerduty.utils import send_incident_alert_notification
154 
155         send_incident_alert_notification(self.action, self.incident, metric_value, new_status)
156 
157 
158 @AlertRuleTriggerAction.register_type(
159     "sentry_app",
160     AlertRuleTriggerAction.Type.SENTRY_APP,
161     [AlertRuleTriggerAction.TargetType.SENTRY_APP],
162 )
163 class SentryAppActionHandler(DefaultActionHandler):
164     def send_alert(self, metric_value: int | float, new_status: IncidentStatus):
165         from sentry.rules.actions.notify_event_service import send_incident_alert_notification
166 
167         send_incident_alert_notification(self.action, self.incident, new_status, metric_value)
168 
169 
170 def format_duration(minutes):
171     """
172     Format minutes into a duration string
173     """
174 
175     if minutes >= 1440:
176         days = int(minutes // 1440)
177         return f"{days:d} day{pluralize(days)}"
178 
179     if minutes >= 60:
180         hours = int(minutes // 60)
181         return f"{hours:d} hour{pluralize(hours)}"
182 
183     if minutes >= 1:
184         minutes = int(minutes)
185         return f"{minutes:d} minute{pluralize(minutes)}"
186 
187     seconds = int(minutes // 60)
188     return f"{seconds:d} second{pluralize(seconds)}"
189 
190 
191 def generate_incident_trigger_email_context(
192     project,
193     incident,
194     alert_rule_trigger,
195     trigger_status,
196     incident_status,
197     user=None,
198 ):
199     trigger = alert_rule_trigger
200     alert_rule = trigger.alert_rule
201     snuba_query = alert_rule.snuba_query
202     is_active = trigger_status == TriggerStatus.ACTIVE
203     is_threshold_type_above = alert_rule.threshold_type == AlertRuleThresholdType.ABOVE.value
204 
205     # if alert threshold and threshold type is above then show '>'
206     # if resolve threshold and threshold type is *BELOW* then show '>'
207     # we can simplify this to be the below statement
208     show_greater_than_string = is_active == is_threshold_type_above
209     environment_string = snuba_query.environment.name if snuba_query.environment else "All"
210 
211     aggregate = alert_rule.snuba_query.aggregate
212     if CRASH_RATE_ALERT_AGGREGATE_ALIAS in aggregate:
213         aggregate = aggregate.split(f"AS {CRASH_RATE_ALERT_AGGREGATE_ALIAS}")[0].strip()
214 
215     threshold = trigger.alert_threshold if is_active else alert_rule.resolve_threshold
216     if threshold is None:
217         # Setting this to trigger threshold because in the case of a resolve if no resolve
218         # threshold is specified this will be None. Since we add a comparison sign to the
219         # string it makes sense to set this to the trigger alert threshold if no threshold is
220         # specified
221         threshold = trigger.alert_threshold
222 
223     chart_url = None
224     if features.has("organizations:metric-alert-chartcuterie", incident.organization):
225         try:
226             chart_url = build_metric_alert_chart(
227                 organization=incident.organization,
228                 alert_rule=incident.alert_rule,
229                 selected_incident=incident,
230                 size=ChartSize({"width": 600, "height": 200}),
231             )
232         except Exception:
233             logging.exception("Error while attempting to build_metric_alert_chart")
234 
235     tz = settings.SENTRY_DEFAULT_TIME_ZONE
236     if user is not None:
237         user_option_tz = UserOption.objects.get_value(user=user, key="timezone")
238         if user_option_tz is not None:
239             tz = user_option_tz
240 
241     organization = incident.organization
242     return {
243         "link": organization.absolute_url(
244             reverse(
245                 "sentry-metric-alert",
246                 kwargs={
247                     "organization_slug": organization.slug,
248                     "incident_id": incident.identifier,
249                 },
250             ),
251             query="referrer=alert_email",
252         ),
253         "rule_link": organization.absolute_url(
254             reverse(
255                 "sentry-alert-rule",
256                 kwargs={
257                     "organization_slug": organization.slug,
258                     "project_slug": project.slug,
259                     "alert_rule_id": trigger.alert_rule_id,
260                 },
261             )
262         ),
263         "project_slug": project.slug,
264         "incident_name": incident.title,
265         "environment": environment_string,
266         "time_window": format_duration(snuba_query.time_window / 60),
267         "triggered_at": incident.date_added,
268         "aggregate": aggregate,
269         "query": snuba_query.query,
270         "threshold": threshold,
271         # if alert threshold and threshold type is above then show '>'
272         # if resolve threshold and threshold type is *BELOW* then show '>'
273         "threshold_direction_string": ">" if show_greater_than_string else "<",
274         "status": INCIDENT_STATUS[incident_status],
275         "status_key": INCIDENT_STATUS[incident_status].lower(),
276         "is_critical": incident_status == IncidentStatus.CRITICAL,
277         "is_warning": incident_status == IncidentStatus.WARNING,
278         "unsubscribe_link": None,
279         "chart_url": chart_url,
280         "timezone": tz,
281     }
282
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/src/sentry/incidents/action_handlers.py b/src/sentry/incidents/action_handlers.py
--- a/src/sentry/incidents/action_handlers.py
+++ b/src/sentry/incidents/action_handlers.py
@@ -19,6 +19,7 @@
     IncidentStatus,
     TriggerStatus,
 )
+from sentry.models import RuleSnooze
 from sentry.models.notificationsetting import NotificationSetting
 from sentry.models.options.user_option import UserOption
 from sentry.models.user import User
@@ -68,7 +69,17 @@
         if not target:
             return set()
 
+        if RuleSnooze.objects.filter(
+            alert_rule=self.incident.alert_rule, user_id__isnull=True
+        ).exists():
+            return set()
+
         if self.action.target_type == AlertRuleTriggerAction.TargetType.USER.value:
+            if RuleSnooze.objects.filter(
+                alert_rule=self.incident.alert_rule, user_id=target.id
+            ).exists():
+                return set()
+
             return {target.id}
 
         elif self.action.target_type == AlertRuleTriggerAction.TargetType.TEAM.value:
@@ -76,7 +87,10 @@
                 self.project,
                 {RpcUser(id=member.user_id) for member in target.member_set},
             )[ExternalProviders.EMAIL]
-            return {user.id for user in users}
+            snoozed_users = RuleSnooze.objects.filter(
+                alert_rule=self.incident.alert_rule, user_id__in=[user.id for user in users]
+            ).values_list("user_id", flat=True)
+            return {user.id for user in users if user.id not in snoozed_users}
 
         return set()

{"golden_diff": "diff --git a/src/sentry/incidents/action_handlers.py b/src/sentry/incidents/action_handlers.py\n--- a/src/sentry/incidents/action_handlers.py\n+++ b/src/sentry/incidents/action_handlers.py\n@@ -19,6 +19,7 @@\n     IncidentStatus,\n     TriggerStatus,\n )\n+from sentry.models import RuleSnooze\n from sentry.models.notificationsetting import NotificationSetting\n from sentry.models.options.user_option import UserOption\n from sentry.models.user import User\n@@ -68,7 +69,17 @@\n         if not target:\n             return set()\n \n+        if RuleSnooze.objects.filter(\n+            alert_rule=self.incident.alert_rule, user_id__isnull=True\n+        ).exists():\n+            return set()\n+\n         if self.action.target_type == AlertRuleTriggerAction.TargetType.USER.value:\n+            if RuleSnooze.objects.filter(\n+                alert_rule=self.incident.alert_rule, user_id=target.id\n+            ).exists():\n+                return set()\n+\n             return {target.id}\n \n         elif self.action.target_type == AlertRuleTriggerAction.TargetType.TEAM.value:\n@@ -76,7 +87,10 @@\n                 self.project,\n                 {RpcUser(id=member.user_id) for member in target.member_set},\n             )[ExternalProviders.EMAIL]\n-            return {user.id for user in users}\n+            snoozed_users = RuleSnooze.objects.filter(\n+                alert_rule=self.incident.alert_rule, user_id__in=[user.id for user in users]\n+            ).values_list(\"user_id\", flat=True)\n+            return {user.id for user in users if user.id not in snoozed_users}\n \n         return set()\n", "issue": "Add check to default notifications for rule snooze\nWe will want to check to see if the alert is being snoozed + how it is being snoozed to determine if we send out the default notification for the metric alert. Tests will be very important here to make sure we are correctly muting the alert. \r\n\r\nSimilar PR from V1 of Mute Alerts: https://github.com/getsentry/sentry/commit/0e779826e56f69c1e247943fc46a5754bee6039d\n", "before_files": [{"content": "from __future__ import annotations\n\nimport abc\nimport logging\nfrom typing import Sequence, Set, Tuple\n\nfrom django.conf import settings\nfrom django.template.defaultfilters import pluralize\nfrom django.urls import reverse\n\nfrom sentry import features\nfrom sentry.charts.types import ChartSize\nfrom sentry.constants import CRASH_RATE_ALERT_AGGREGATE_ALIAS\nfrom sentry.incidents.charts import build_metric_alert_chart\nfrom sentry.incidents.models import (\n    INCIDENT_STATUS,\n    AlertRuleThresholdType,\n    AlertRuleTriggerAction,\n    IncidentStatus,\n    TriggerStatus,\n)\nfrom sentry.models.notificationsetting import NotificationSetting\nfrom sentry.models.options.user_option import UserOption\nfrom sentry.models.user import User\nfrom sentry.services.hybrid_cloud.user import RpcUser\nfrom sentry.types.integrations import ExternalProviders\nfrom sentry.utils import json\nfrom sentry.utils.email import MessageBuilder, get_email_addresses\n\n\nclass ActionHandler(metaclass=abc.ABCMeta):\n    status_display = {TriggerStatus.ACTIVE: \"Fired\", TriggerStatus.RESOLVED: \"Resolved\"}\n\n    def __init__(self, action, incident, project):\n        self.action = action\n        self.incident = incident\n        self.project = project\n\n    @abc.abstractmethod\n    def fire(self, metric_value: int | float, new_status: IncidentStatus):\n        pass\n\n    @abc.abstractmethod\n    def resolve(self, metric_value: int | float, new_status: IncidentStatus):\n        pass\n\n\nclass DefaultActionHandler(ActionHandler):\n    def fire(self, metric_value: int | float, new_status: IncidentStatus):\n        self.send_alert(metric_value, new_status)\n\n    def resolve(self, metric_value: int | float, new_status: IncidentStatus):\n        self.send_alert(metric_value, new_status)\n\n    @abc.abstractmethod\n    def send_alert(self, metric_value: int | float, new_status: IncidentStatus):\n        pass\n\n\n@AlertRuleTriggerAction.register_type(\n    \"email\",\n    AlertRuleTriggerAction.Type.EMAIL,\n    [AlertRuleTriggerAction.TargetType.USER, AlertRuleTriggerAction.TargetType.TEAM],\n)\nclass EmailActionHandler(ActionHandler):\n    def _get_targets(self) -> Set[int]:\n        target = self.action.target\n        if not target:\n            return set()\n\n        if self.action.target_type == AlertRuleTriggerAction.TargetType.USER.value:\n            return {target.id}\n\n        elif self.action.target_type == AlertRuleTriggerAction.TargetType.TEAM.value:\n            users = NotificationSetting.objects.filter_to_accepting_recipients(\n                self.project,\n                {RpcUser(id=member.user_id) for member in target.member_set},\n            )[ExternalProviders.EMAIL]\n            return {user.id for user in users}\n\n        return set()\n\n    def get_targets(self) -> Sequence[Tuple[int, str]]:\n        return list(get_email_addresses(self._get_targets(), project=self.project).items())\n\n    def fire(self, metric_value: int | float, new_status: IncidentStatus):\n        self.email_users(TriggerStatus.ACTIVE, new_status)\n\n    def resolve(self, metric_value: int | float, new_status: IncidentStatus):\n        self.email_users(TriggerStatus.RESOLVED, new_status)\n\n    def email_users(self, trigger_status: TriggerStatus, incident_status: IncidentStatus) -> None:\n        for user_id, email in self.get_targets():\n            user = User.objects.get_from_cache(id=user_id)\n            email_context = generate_incident_trigger_email_context(\n                self.project,\n                self.incident,\n                self.action.alert_rule_trigger,\n                trigger_status,\n                incident_status,\n                user,\n            )\n            self.build_message(email_context, trigger_status, user_id).send_async(to=[email])\n\n    def build_message(self, context, status, user_id):\n        display = self.status_display[status]\n        return MessageBuilder(\n            subject=\"[{}] {} - {}\".format(\n                context[\"status\"], context[\"incident_name\"], self.project.slug\n            ),\n            template=\"sentry/emails/incidents/trigger.txt\",\n            html_template=\"sentry/emails/incidents/trigger.html\",\n            type=f\"incident.alert_rule_{display.lower()}\",\n            context=context,\n            headers={\"X-SMTPAPI\": json.dumps({\"category\": \"metric_alert_email\"})},\n        )\n\n\n@AlertRuleTriggerAction.register_type(\n    \"slack\",\n    AlertRuleTriggerAction.Type.SLACK,\n    [AlertRuleTriggerAction.TargetType.SPECIFIC],\n    integration_provider=\"slack\",\n)\nclass SlackActionHandler(DefaultActionHandler):\n    def send_alert(self, metric_value: int | float, new_status: IncidentStatus):\n        from sentry.integrations.slack.utils import send_incident_alert_notification\n\n        send_incident_alert_notification(self.action, self.incident, metric_value, new_status)\n\n\n@AlertRuleTriggerAction.register_type(\n    \"msteams\",\n    AlertRuleTriggerAction.Type.MSTEAMS,\n    [AlertRuleTriggerAction.TargetType.SPECIFIC],\n    integration_provider=\"msteams\",\n)\nclass MsTeamsActionHandler(DefaultActionHandler):\n    def send_alert(self, metric_value: int | float, new_status: IncidentStatus):\n        from sentry.integrations.msteams.utils import send_incident_alert_notification\n\n        send_incident_alert_notification(self.action, self.incident, metric_value, new_status)\n\n\n@AlertRuleTriggerAction.register_type(\n    \"pagerduty\",\n    AlertRuleTriggerAction.Type.PAGERDUTY,\n    [AlertRuleTriggerAction.TargetType.SPECIFIC],\n    integration_provider=\"pagerduty\",\n)\nclass PagerDutyActionHandler(DefaultActionHandler):\n    def send_alert(self, metric_value: int | float, new_status: IncidentStatus):\n        from sentry.integrations.pagerduty.utils import send_incident_alert_notification\n\n        send_incident_alert_notification(self.action, self.incident, metric_value, new_status)\n\n\n@AlertRuleTriggerAction.register_type(\n    \"sentry_app\",\n    AlertRuleTriggerAction.Type.SENTRY_APP,\n    [AlertRuleTriggerAction.TargetType.SENTRY_APP],\n)\nclass SentryAppActionHandler(DefaultActionHandler):\n    def send_alert(self, metric_value: int | float, new_status: IncidentStatus):\n        from sentry.rules.actions.notify_event_service import send_incident_alert_notification\n\n        send_incident_alert_notification(self.action, self.incident, new_status, metric_value)\n\n\ndef format_duration(minutes):\n    \"\"\"\n    Format minutes into a duration string\n    \"\"\"\n\n    if minutes >= 1440:\n        days = int(minutes // 1440)\n        return f\"{days:d} day{pluralize(days)}\"\n\n    if minutes >= 60:\n        hours = int(minutes // 60)\n        return f\"{hours:d} hour{pluralize(hours)}\"\n\n    if minutes >= 1:\n        minutes = int(minutes)\n        return f\"{minutes:d} minute{pluralize(minutes)}\"\n\n    seconds = int(minutes // 60)\n    return f\"{seconds:d} second{pluralize(seconds)}\"\n\n\ndef generate_incident_trigger_email_context(\n    project,\n    incident,\n    alert_rule_trigger,\n    trigger_status,\n    incident_status,\n    user=None,\n):\n    trigger = alert_rule_trigger\n    alert_rule = trigger.alert_rule\n    snuba_query = alert_rule.snuba_query\n    is_active = trigger_status == TriggerStatus.ACTIVE\n    is_threshold_type_above = alert_rule.threshold_type == AlertRuleThresholdType.ABOVE.value\n\n    # if alert threshold and threshold type is above then show '>'\n    # if resolve threshold and threshold type is *BELOW* then show '>'\n    # we can simplify this to be the below statement\n    show_greater_than_string = is_active == is_threshold_type_above\n    environment_string = snuba_query.environment.name if snuba_query.environment else \"All\"\n\n    aggregate = alert_rule.snuba_query.aggregate\n    if CRASH_RATE_ALERT_AGGREGATE_ALIAS in aggregate:\n        aggregate = aggregate.split(f\"AS {CRASH_RATE_ALERT_AGGREGATE_ALIAS}\")[0].strip()\n\n    threshold = trigger.alert_threshold if is_active else alert_rule.resolve_threshold\n    if threshold is None:\n        # Setting this to trigger threshold because in the case of a resolve if no resolve\n        # threshold is specified this will be None. Since we add a comparison sign to the\n        # string it makes sense to set this to the trigger alert threshold if no threshold is\n        # specified\n        threshold = trigger.alert_threshold\n\n    chart_url = None\n    if features.has(\"organizations:metric-alert-chartcuterie\", incident.organization):\n        try:\n            chart_url = build_metric_alert_chart(\n                organization=incident.organization,\n                alert_rule=incident.alert_rule,\n                selected_incident=incident,\n                size=ChartSize({\"width\": 600, \"height\": 200}),\n            )\n        except Exception:\n            logging.exception(\"Error while attempting to build_metric_alert_chart\")\n\n    tz = settings.SENTRY_DEFAULT_TIME_ZONE\n    if user is not None:\n        user_option_tz = UserOption.objects.get_value(user=user, key=\"timezone\")\n        if user_option_tz is not None:\n            tz = user_option_tz\n\n    organization = incident.organization\n    return {\n        \"link\": organization.absolute_url(\n            reverse(\n                \"sentry-metric-alert\",\n                kwargs={\n                    \"organization_slug\": organization.slug,\n                    \"incident_id\": incident.identifier,\n                },\n            ),\n            query=\"referrer=alert_email\",\n        ),\n        \"rule_link\": organization.absolute_url(\n            reverse(\n                \"sentry-alert-rule\",\n                kwargs={\n                    \"organization_slug\": organization.slug,\n                    \"project_slug\": project.slug,\n                    \"alert_rule_id\": trigger.alert_rule_id,\n                },\n            )\n        ),\n        \"project_slug\": project.slug,\n        \"incident_name\": incident.title,\n        \"environment\": environment_string,\n        \"time_window\": format_duration(snuba_query.time_window / 60),\n        \"triggered_at\": incident.date_added,\n        \"aggregate\": aggregate,\n        \"query\": snuba_query.query,\n        \"threshold\": threshold,\n        # if alert threshold and threshold type is above then show '>'\n        # if resolve threshold and threshold type is *BELOW* then show '>'\n        \"threshold_direction_string\": \">\" if show_greater_than_string else \"<\",\n        \"status\": INCIDENT_STATUS[incident_status],\n        \"status_key\": INCIDENT_STATUS[incident_status].lower(),\n        \"is_critical\": incident_status == IncidentStatus.CRITICAL,\n        \"is_warning\": incident_status == IncidentStatus.WARNING,\n        \"unsubscribe_link\": None,\n        \"chart_url\": chart_url,\n        \"timezone\": tz,\n    }\n", "path": "src/sentry/incidents/action_handlers.py"}], "after_files": [{"content": "from __future__ import annotations\n\nimport abc\nimport logging\nfrom typing import Sequence, Set, Tuple\n\nfrom django.conf import settings\nfrom django.template.defaultfilters import pluralize\nfrom django.urls import reverse\n\nfrom sentry import features\nfrom sentry.charts.types import ChartSize\nfrom sentry.constants import CRASH_RATE_ALERT_AGGREGATE_ALIAS\nfrom sentry.incidents.charts import build_metric_alert_chart\nfrom sentry.incidents.models import (\n    INCIDENT_STATUS,\n    AlertRuleThresholdType,\n    AlertRuleTriggerAction,\n    IncidentStatus,\n    TriggerStatus,\n)\nfrom sentry.models import RuleSnooze\nfrom sentry.models.notificationsetting import NotificationSetting\nfrom sentry.models.options.user_option import UserOption\nfrom sentry.models.user import User\nfrom sentry.services.hybrid_cloud.user import RpcUser\nfrom sentry.types.integrations import ExternalProviders\nfrom sentry.utils import json\nfrom sentry.utils.email import MessageBuilder, get_email_addresses\n\n\nclass ActionHandler(metaclass=abc.ABCMeta):\n    status_display = {TriggerStatus.ACTIVE: \"Fired\", TriggerStatus.RESOLVED: \"Resolved\"}\n\n    def __init__(self, action, incident, project):\n        self.action = action\n        self.incident = incident\n        self.project = project\n\n    @abc.abstractmethod\n    def fire(self, metric_value: int | float, new_status: IncidentStatus):\n        pass\n\n    @abc.abstractmethod\n    def resolve(self, metric_value: int | float, new_status: IncidentStatus):\n        pass\n\n\nclass DefaultActionHandler(ActionHandler):\n    def fire(self, metric_value: int | float, new_status: IncidentStatus):\n        self.send_alert(metric_value, new_status)\n\n    def resolve(self, metric_value: int | float, new_status: IncidentStatus):\n        self.send_alert(metric_value, new_status)\n\n    @abc.abstractmethod\n    def send_alert(self, metric_value: int | float, new_status: IncidentStatus):\n        pass\n\n\n@AlertRuleTriggerAction.register_type(\n    \"email\",\n    AlertRuleTriggerAction.Type.EMAIL,\n    [AlertRuleTriggerAction.TargetType.USER, AlertRuleTriggerAction.TargetType.TEAM],\n)\nclass EmailActionHandler(ActionHandler):\n    def _get_targets(self) -> Set[int]:\n        target = self.action.target\n        if not target:\n            return set()\n\n        if RuleSnooze.objects.filter(\n            alert_rule=self.incident.alert_rule, user_id__isnull=True\n        ).exists():\n            return set()\n\n        if self.action.target_type == AlertRuleTriggerAction.TargetType.USER.value:\n            if RuleSnooze.objects.filter(\n                alert_rule=self.incident.alert_rule, user_id=target.id\n            ).exists():\n                return set()\n\n            return {target.id}\n\n        elif self.action.target_type == AlertRuleTriggerAction.TargetType.TEAM.value:\n            users = NotificationSetting.objects.filter_to_accepting_recipients(\n                self.project,\n                {RpcUser(id=member.user_id) for member in target.member_set},\n            )[ExternalProviders.EMAIL]\n            snoozed_users = RuleSnooze.objects.filter(\n                alert_rule=self.incident.alert_rule, user_id__in=[user.id for user in users]\n            ).values_list(\"user_id\", flat=True)\n            return {user.id for user in users if user.id not in snoozed_users}\n\n        return set()\n\n    def get_targets(self) -> Sequence[Tuple[int, str]]:\n        return list(get_email_addresses(self._get_targets(), project=self.project).items())\n\n    def fire(self, metric_value: int | float, new_status: IncidentStatus):\n        self.email_users(TriggerStatus.ACTIVE, new_status)\n\n    def resolve(self, metric_value: int | float, new_status: IncidentStatus):\n        self.email_users(TriggerStatus.RESOLVED, new_status)\n\n    def email_users(self, trigger_status: TriggerStatus, incident_status: IncidentStatus) -> None:\n        for user_id, email in self.get_targets():\n            user = User.objects.get_from_cache(id=user_id)\n            email_context = generate_incident_trigger_email_context(\n                self.project,\n                self.incident,\n                self.action.alert_rule_trigger,\n                trigger_status,\n                incident_status,\n                user,\n            )\n            self.build_message(email_context, trigger_status, user_id).send_async(to=[email])\n\n    def build_message(self, context, status, user_id):\n        display = self.status_display[status]\n        return MessageBuilder(\n            subject=\"[{}] {} - {}\".format(\n                context[\"status\"], context[\"incident_name\"], self.project.slug\n            ),\n            template=\"sentry/emails/incidents/trigger.txt\",\n            html_template=\"sentry/emails/incidents/trigger.html\",\n            type=f\"incident.alert_rule_{display.lower()}\",\n            context=context,\n            headers={\"X-SMTPAPI\": json.dumps({\"category\": \"metric_alert_email\"})},\n        )\n\n\n@AlertRuleTriggerAction.register_type(\n    \"slack\",\n    AlertRuleTriggerAction.Type.SLACK,\n    [AlertRuleTriggerAction.TargetType.SPECIFIC],\n    integration_provider=\"slack\",\n)\nclass SlackActionHandler(DefaultActionHandler):\n    def send_alert(self, metric_value: int | float, new_status: IncidentStatus):\n        from sentry.integrations.slack.utils import send_incident_alert_notification\n\n        send_incident_alert_notification(self.action, self.incident, metric_value, new_status)\n\n\n@AlertRuleTriggerAction.register_type(\n    \"msteams\",\n    AlertRuleTriggerAction.Type.MSTEAMS,\n    [AlertRuleTriggerAction.TargetType.SPECIFIC],\n    integration_provider=\"msteams\",\n)\nclass MsTeamsActionHandler(DefaultActionHandler):\n    def send_alert(self, metric_value: int | float, new_status: IncidentStatus):\n        from sentry.integrations.msteams.utils import send_incident_alert_notification\n\n        send_incident_alert_notification(self.action, self.incident, metric_value, new_status)\n\n\n@AlertRuleTriggerAction.register_type(\n    \"pagerduty\",\n    AlertRuleTriggerAction.Type.PAGERDUTY,\n    [AlertRuleTriggerAction.TargetType.SPECIFIC],\n    integration_provider=\"pagerduty\",\n)\nclass PagerDutyActionHandler(DefaultActionHandler):\n    def send_alert(self, metric_value: int | float, new_status: IncidentStatus):\n        from sentry.integrations.pagerduty.utils import send_incident_alert_notification\n\n        send_incident_alert_notification(self.action, self.incident, metric_value, new_status)\n\n\n@AlertRuleTriggerAction.register_type(\n    \"sentry_app\",\n    AlertRuleTriggerAction.Type.SENTRY_APP,\n    [AlertRuleTriggerAction.TargetType.SENTRY_APP],\n)\nclass SentryAppActionHandler(DefaultActionHandler):\n    def send_alert(self, metric_value: int | float, new_status: IncidentStatus):\n        from sentry.rules.actions.notify_event_service import send_incident_alert_notification\n\n        send_incident_alert_notification(self.action, self.incident, new_status, metric_value)\n\n\ndef format_duration(minutes):\n    \"\"\"\n    Format minutes into a duration string\n    \"\"\"\n\n    if minutes >= 1440:\n        days = int(minutes // 1440)\n        return f\"{days:d} day{pluralize(days)}\"\n\n    if minutes >= 60:\n        hours = int(minutes // 60)\n        return f\"{hours:d} hour{pluralize(hours)}\"\n\n    if minutes >= 1:\n        minutes = int(minutes)\n        return f\"{minutes:d} minute{pluralize(minutes)}\"\n\n    seconds = int(minutes // 60)\n    return f\"{seconds:d} second{pluralize(seconds)}\"\n\n\ndef generate_incident_trigger_email_context(\n    project,\n    incident,\n    alert_rule_trigger,\n    trigger_status,\n    incident_status,\n    user=None,\n):\n    trigger = alert_rule_trigger\n    alert_rule = trigger.alert_rule\n    snuba_query = alert_rule.snuba_query\n    is_active = trigger_status == TriggerStatus.ACTIVE\n    is_threshold_type_above = alert_rule.threshold_type == AlertRuleThresholdType.ABOVE.value\n\n    # if alert threshold and threshold type is above then show '>'\n    # if resolve threshold and threshold type is *BELOW* then show '>'\n    # we can simplify this to be the below statement\n    show_greater_than_string = is_active == is_threshold_type_above\n    environment_string = snuba_query.environment.name if snuba_query.environment else \"All\"\n\n    aggregate = alert_rule.snuba_query.aggregate\n    if CRASH_RATE_ALERT_AGGREGATE_ALIAS in aggregate:\n        aggregate = aggregate.split(f\"AS {CRASH_RATE_ALERT_AGGREGATE_ALIAS}\")[0].strip()\n\n    threshold = trigger.alert_threshold if is_active else alert_rule.resolve_threshold\n    if threshold is None:\n        # Setting this to trigger threshold because in the case of a resolve if no resolve\n        # threshold is specified this will be None. Since we add a comparison sign to the\n        # string it makes sense to set this to the trigger alert threshold if no threshold is\n        # specified\n        threshold = trigger.alert_threshold\n\n    chart_url = None\n    if features.has(\"organizations:metric-alert-chartcuterie\", incident.organization):\n        try:\n            chart_url = build_metric_alert_chart(\n                organization=incident.organization,\n                alert_rule=incident.alert_rule,\n                selected_incident=incident,\n                size=ChartSize({\"width\": 600, \"height\": 200}),\n            )\n        except Exception:\n            logging.exception(\"Error while attempting to build_metric_alert_chart\")\n\n    tz = settings.SENTRY_DEFAULT_TIME_ZONE\n    if user is not None:\n        user_option_tz = UserOption.objects.get_value(user=user, key=\"timezone\")\n        if user_option_tz is not None:\n            tz = user_option_tz\n\n    organization = incident.organization\n    return {\n        \"link\": organization.absolute_url(\n            reverse(\n                \"sentry-metric-alert\",\n                kwargs={\n                    \"organization_slug\": organization.slug,\n                    \"incident_id\": incident.identifier,\n                },\n            ),\n            query=\"referrer=alert_email\",\n        ),\n        \"rule_link\": organization.absolute_url(\n            reverse(\n                \"sentry-alert-rule\",\n                kwargs={\n                    \"organization_slug\": organization.slug,\n                    \"project_slug\": project.slug,\n                    \"alert_rule_id\": trigger.alert_rule_id,\n                },\n            )\n        ),\n        \"project_slug\": project.slug,\n        \"incident_name\": incident.title,\n        \"environment\": environment_string,\n        \"time_window\": format_duration(snuba_query.time_window / 60),\n        \"triggered_at\": incident.date_added,\n        \"aggregate\": aggregate,\n        \"query\": snuba_query.query,\n        \"threshold\": threshold,\n        # if alert threshold and threshold type is above then show '>'\n        # if resolve threshold and threshold type is *BELOW* then show '>'\n        \"threshold_direction_string\": \">\" if show_greater_than_string else \"<\",\n        \"status\": INCIDENT_STATUS[incident_status],\n        \"status_key\": INCIDENT_STATUS[incident_status].lower(),\n        \"is_critical\": incident_status == IncidentStatus.CRITICAL,\n        \"is_warning\": incident_status == IncidentStatus.WARNING,\n        \"unsubscribe_link\": None,\n        \"chart_url\": chart_url,\n        \"timezone\": tz,\n    }\n", "path": "src/sentry/incidents/action_handlers.py"}]}