Repository: getsentry/sentry

Issue:
Add check to integrations for rule snooze
We want to snooze integration notifications if they are muted for everyone since for integrations "mute for me" doesn't make much sense. "mute for me" won't be offered to the users as an option on the front end, but we should still check that the alert is being muted for everyone in these integration checks. Tests are very important here too to confirm that we are correctly muting the integration notifications. 

Similar PR from V1 of Mute Alerts: https://github.com/getsentry/sentry/pull/49375


Discussion:

Resolving patch:
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
@@ -21,6 +21,7 @@
 )
 from sentry.models.notificationsetting import NotificationSetting
 from sentry.models.options.user_option import UserOption
+from sentry.models.rulesnooze import RuleSnooze
 from sentry.models.user import User
 from sentry.services.hybrid_cloud.user import RpcUser
 from sentry.types.integrations import ExternalProviders
@@ -47,10 +48,16 @@ def resolve(self, metric_value: int | float, new_status: IncidentStatus):
 
 class DefaultActionHandler(ActionHandler):
     def fire(self, metric_value: int | float, new_status: IncidentStatus):
-        self.send_alert(metric_value, new_status)
+        if not RuleSnooze.objects.filter(
+            alert_rule=self.incident.alert_rule, user_id__isnull=True
+        ).exists():
+            self.send_alert(metric_value, new_status)
 
     def resolve(self, metric_value: int | float, new_status: IncidentStatus):
-        self.send_alert(metric_value, new_status)
+        if not RuleSnooze.objects.filter(
+            alert_rule=self.incident.alert_rule, user_id__isnull=True
+        ).exists():
+            self.send_alert(metric_value, new_status)
 
     @abc.abstractmethod
     def send_alert(self, metric_value: int | float, new_status: IncidentStatus):


Test patch:
diff --git a/tests/sentry/incidents/action_handlers/test_msteams.py b/tests/sentry/incidents/action_handlers/test_msteams.py
--- a/tests/sentry/incidents/action_handlers/test_msteams.py
+++ b/tests/sentry/incidents/action_handlers/test_msteams.py
@@ -6,6 +6,7 @@
 from sentry.incidents.action_handlers import MsTeamsActionHandler
 from sentry.incidents.models import AlertRuleTriggerAction, IncidentStatus
 from sentry.models import Integration
+from sentry.models.rulesnooze import RuleSnooze
 from sentry.testutils import TestCase
 from sentry.testutils.silo import exempt_from_silo_limits, region_silo_test
 from sentry.utils import json
@@ -17,9 +18,7 @@
 @freeze_time()
 class MsTeamsActionHandlerTest(FireTest, TestCase):
     @responses.activate
-    def run_test(self, incident, method):
-        from sentry.integrations.msteams.card_builder import build_incident_attachment
-
+    def setUp(self):
         with exempt_from_silo_limits():
             integration = Integration.objects.create(
                 provider="msteams",
@@ -43,13 +42,17 @@ def run_test(self, incident, method):
             json={"conversations": channels},
         )
 
-        action = self.create_alert_rule_trigger_action(
+        self.action = self.create_alert_rule_trigger_action(
             target_identifier=channel_name,
             type=AlertRuleTriggerAction.Type.MSTEAMS,
             target_type=AlertRuleTriggerAction.TargetType.SPECIFIC,
             integration=integration,
         )
 
+    @responses.activate
+    def run_test(self, incident, method):
+        from sentry.integrations.msteams.card_builder import build_incident_attachment
+
         responses.add(
             method=responses.POST,
             url="https://smba.trafficmanager.net/amer/v3/conversations/d_s/activities",
@@ -57,11 +60,11 @@ def run_test(self, incident, method):
             json={},
         )
 
-        handler = MsTeamsActionHandler(action, incident, self.project)
+        handler = MsTeamsActionHandler(self.action, incident, self.project)
         metric_value = 1000
         with self.tasks():
             getattr(handler, method)(metric_value, IncidentStatus(incident.status))
-        data = json.loads(responses.calls[1].request.body)
+        data = json.loads(responses.calls[0].request.body)
 
         assert data["attachments"][0]["content"] == build_incident_attachment(
             incident, IncidentStatus(incident.status), metric_value
@@ -69,3 +72,25 @@ def run_test(self, incident, method):
 
     def test_fire_metric_alert(self):
         self.run_fire_test()
+
+    def test_resolve_metric_alert(self):
+        self.run_fire_test("resolve")
+
+    @responses.activate
+    def test_rule_snoozed(self):
+        alert_rule = self.create_alert_rule()
+        incident = self.create_incident(alert_rule=alert_rule, status=IncidentStatus.CLOSED.value)
+        RuleSnooze.objects.create(alert_rule=alert_rule)
+        responses.add(
+            method=responses.POST,
+            url="https://smba.trafficmanager.net/amer/v3/conversations/d_s/activities",
+            status=200,
+            json={},
+        )
+
+        handler = MsTeamsActionHandler(self.action, incident, self.project)
+        metric_value = 1000
+        with self.tasks():
+            handler.fire(metric_value, IncidentStatus(incident.status))
+
+        assert len(responses.calls) == 0
diff --git a/tests/sentry/incidents/action_handlers/test_pagerduty.py b/tests/sentry/incidents/action_handlers/test_pagerduty.py
--- a/tests/sentry/incidents/action_handlers/test_pagerduty.py
+++ b/tests/sentry/incidents/action_handlers/test_pagerduty.py
@@ -5,6 +5,7 @@
 from sentry.incidents.logic import update_incident_status
 from sentry.incidents.models import AlertRuleTriggerAction, IncidentStatus, IncidentStatusMethod
 from sentry.models import Integration, PagerDutyService
+from sentry.models.rulesnooze import RuleSnooze
 from sentry.testutils import TestCase
 from sentry.utils import json
 
@@ -36,6 +37,12 @@ def setUp(self):
             integration_key=service[0]["integration_key"],
             organization_integration_id=self.integration.organizationintegration_set.first().id,
         )
+        self.action = self.create_alert_rule_trigger_action(
+            target_identifier=self.service.id,
+            type=AlertRuleTriggerAction.Type.PAGERDUTY,
+            target_type=AlertRuleTriggerAction.TargetType.SPECIFIC,
+            integration=self.integration,
+        )
 
     def test_build_incident_attachment(self):
         from sentry.integrations.pagerduty.utils import build_incident_attachment
@@ -75,13 +82,6 @@ def test_build_incident_attachment(self):
     def run_test(self, incident, method):
         from sentry.integrations.pagerduty.utils import build_incident_attachment
 
-        action = self.create_alert_rule_trigger_action(
-            target_identifier=self.service.id,
-            type=AlertRuleTriggerAction.Type.PAGERDUTY,
-            target_type=AlertRuleTriggerAction.TargetType.SPECIFIC,
-            integration=self.integration,
-        )
-
         responses.add(
             method=responses.POST,
             url="https://events.pagerduty.com/v2/enqueue/",
@@ -89,7 +89,7 @@ def run_test(self, incident, method):
             status=202,
             content_type="application/json",
         )
-        handler = PagerDutyActionHandler(action, incident, self.project)
+        handler = PagerDutyActionHandler(self.action, incident, self.project)
         metric_value = 1000
         with self.tasks():
             getattr(handler, method)(metric_value, IncidentStatus(incident.status))
@@ -120,3 +120,23 @@ def test_fire_metric_alert_multiple_services(self):
 
     def test_resolve_metric_alert(self):
         self.run_fire_test("resolve")
+
+    @responses.activate
+    def test_rule_snoozed(self):
+        alert_rule = self.create_alert_rule()
+        incident = self.create_incident(alert_rule=alert_rule, status=IncidentStatus.CLOSED.value)
+        RuleSnooze.objects.create(alert_rule=alert_rule)
+
+        responses.add(
+            method=responses.POST,
+            url="https://events.pagerduty.com/v2/enqueue/",
+            json={},
+            status=202,
+            content_type="application/json",
+        )
+        handler = PagerDutyActionHandler(self.action, incident, self.project)
+        metric_value = 1000
+        with self.tasks():
+            handler.fire(metric_value, IncidentStatus(incident.status))
+
+        assert len(responses.calls) == 0
diff --git a/tests/sentry/incidents/action_handlers/test_sentry_app.py b/tests/sentry/incidents/action_handlers/test_sentry_app.py
--- a/tests/sentry/incidents/action_handlers/test_sentry_app.py
+++ b/tests/sentry/incidents/action_handlers/test_sentry_app.py
@@ -3,6 +3,7 @@
 
 from sentry.incidents.action_handlers import SentryAppActionHandler
 from sentry.incidents.models import AlertRuleTriggerAction, IncidentStatus
+from sentry.models.rulesnooze import RuleSnooze
 from sentry.testutils import TestCase
 from sentry.testutils.silo import region_silo_test
 from sentry.utils import json
@@ -24,17 +25,17 @@ def setUp(self):
             slug=self.sentry_app.slug, organization=self.organization, user=self.user
         )
 
-    @responses.activate
-    def run_test(self, incident, method):
-        from sentry.rules.actions.notify_event_service import build_incident_attachment
-
-        action = self.create_alert_rule_trigger_action(
+        self.action = self.create_alert_rule_trigger_action(
             target_identifier=self.sentry_app.id,
             type=AlertRuleTriggerAction.Type.SENTRY_APP,
             target_type=AlertRuleTriggerAction.TargetType.SENTRY_APP,
             sentry_app=self.sentry_app,
         )
 
+    @responses.activate
+    def run_test(self, incident, method):
+        from sentry.rules.actions.notify_event_service import build_incident_attachment
+
         responses.add(
             method=responses.POST,
             url="https://example.com/webhook",
@@ -43,7 +44,7 @@ def run_test(self, incident, method):
             body=json.dumps({"ok": "true"}),
         )
 
-        handler = SentryAppActionHandler(action, incident, self.project)
+        handler = SentryAppActionHandler(self.action, incident, self.project)
         metric_value = 1000
         with self.tasks():
             getattr(handler, method)(metric_value, IncidentStatus(incident.status))
@@ -55,6 +56,27 @@ def run_test(self, incident, method):
             in data
         )
 
+    @responses.activate
+    def test_rule_snoozed(self):
+        alert_rule = self.create_alert_rule()
+        incident = self.create_incident(alert_rule=alert_rule, status=IncidentStatus.CLOSED.value)
+        RuleSnooze.objects.create(alert_rule=alert_rule)
+
+        responses.add(
+            method=responses.POST,
+            url="https://example.com/webhook",
+            status=200,
+            content_type="application/json",
+            body=json.dumps({"ok": "true"}),
+        )
+
+        handler = SentryAppActionHandler(self.action, incident, self.project)
+        metric_value = 1000
+        with self.tasks():
+            handler.fire(metric_value, IncidentStatus(incident.status))
+
+        assert len(responses.calls) == 0
+
     def test_fire_metric_alert(self):
         self.run_fire_test()
 
diff --git a/tests/sentry/incidents/action_handlers/test_slack.py b/tests/sentry/incidents/action_handlers/test_slack.py
--- a/tests/sentry/incidents/action_handlers/test_slack.py
+++ b/tests/sentry/incidents/action_handlers/test_slack.py
@@ -6,6 +6,7 @@
 from sentry.constants import ObjectStatus
 from sentry.incidents.action_handlers import SlackActionHandler
 from sentry.incidents.models import AlertRuleTriggerAction, IncidentStatus
+from sentry.models.rulesnooze import RuleSnooze
 from sentry.testutils import TestCase
 from sentry.testutils.silo import region_silo_test
 from sentry.utils import json
@@ -17,34 +18,36 @@
 @region_silo_test(stable=True)
 class SlackActionHandlerTest(FireTest, TestCase):
     @responses.activate
-    def run_test(self, incident, method, chart_url=None):
-        from sentry.integrations.slack.message_builder.incidents import SlackIncidentsMessageBuilder
-
+    def setUp(self):
         token = "xoxp-xxxxxxxxx-xxxxxxxxxx-xxxxxxxxxxxx"
-        integration = self.create_integration(
+        self.integration = self.create_integration(
             organization=self.organization,
             external_id="1",
             provider="slack",
             metadata={"access_token": token, "installation_type": "born_as_bot"},
         )
-        channel_id = "some_id"
-        channel_name = "#hello"
+        self.channel_id = "some_id"
+        self.channel_name = "#hello"
         responses.add(
             method=responses.GET,
             url="https://slack.com/api/conversations.list",
             status=200,
             content_type="application/json",
             body=json.dumps(
-                {"ok": "true", "channels": [{"name": channel_name[1:], "id": channel_id}]}
+                {"ok": "true", "channels": [{"name": self.channel_name[1:], "id": self.channel_id}]}
             ),
         )
-
-        action = self.create_alert_rule_trigger_action(
-            target_identifier=channel_name,
+        self.action = self.create_alert_rule_trigger_action(
+            target_identifier=self.channel_name,
             type=AlertRuleTriggerAction.Type.SLACK,
             target_type=AlertRuleTriggerAction.TargetType.SPECIFIC,
-            integration=integration,
+            integration=self.integration,
         )
+
+    @responses.activate
+    def run_test(self, incident, method, chart_url=None):
+        from sentry.integrations.slack.message_builder.incidents import SlackIncidentsMessageBuilder
+
         responses.add(
             method=responses.POST,
             url="https://slack.com/api/chat.postMessage",
@@ -52,12 +55,12 @@ def run_test(self, incident, method, chart_url=None):
             content_type="application/json",
             body='{"ok": true}',
         )
-        handler = SlackActionHandler(action, incident, self.project)
+        handler = SlackActionHandler(self.action, incident, self.project)
         metric_value = 1000
         with self.tasks():
             getattr(handler, method)(metric_value, IncidentStatus(incident.status))
-        data = parse_qs(responses.calls[1].request.body)
-        assert data["channel"] == [channel_id]
+        data = parse_qs(responses.calls[0].request.body)
+        assert data["channel"] == [self.channel_id]
         slack_body = SlackIncidentsMessageBuilder(
             incident, IncidentStatus(incident.status), metric_value, chart_url
         ).build()
@@ -80,7 +83,7 @@ def test_fire_metric_alert_with_missing_integration(self):
         incident = self.create_incident(alert_rule=alert_rule, status=IncidentStatus.CLOSED.value)
         integration = self.create_integration(
             organization=self.organization,
-            external_id="1",
+            external_id="2",
             provider="slack",
             status=ObjectStatus.DELETION_IN_PROGRESS,
         )
@@ -93,7 +96,51 @@ def test_fire_metric_alert_with_missing_integration(self):
             integration_id=integration.id,
             sentry_app_id=None,
         )
+
         handler = SlackActionHandler(action, incident, self.project)
         metric_value = 1000
         with self.tasks():
             handler.fire(metric_value, IncidentStatus(incident.status))
+
+    @responses.activate
+    def test_rule_snoozed(self):
+        alert_rule = self.create_alert_rule()
+        incident = self.create_incident(alert_rule=alert_rule, status=IncidentStatus.CLOSED.value)
+        RuleSnooze.objects.create(alert_rule=alert_rule)
+
+        responses.add(
+            method=responses.POST,
+            url="https://slack.com/api/chat.postMessage",
+            status=200,
+            content_type="application/json",
+            body='{"ok": true}',
+        )
+        handler = SlackActionHandler(self.action, incident, self.project)
+        metric_value = 1000
+        with self.tasks():
+            handler.fire(metric_value, IncidentStatus(incident.status))
+
+        assert len(responses.calls) == 0
+
+    @responses.activate
+    def test_rule_snoozed_by_user_still_sends(self):
+        """We shouldn't be able to get into this state from the UI, but this test ensures that if an alert whose action
+        is to notify an integration is muted for a specific user, that the alert still fires because it should only NOT
+        fire if it's muted for everyone"""
+        alert_rule = self.create_alert_rule()
+        incident = self.create_incident(alert_rule=alert_rule, status=IncidentStatus.CLOSED.value)
+        RuleSnooze.objects.create(alert_rule=alert_rule, user_id=self.user.id)
+
+        responses.add(
+            method=responses.POST,
+            url="https://slack.com/api/chat.postMessage",
+            status=200,
+            content_type="application/json",
+            body='{"ok": true}',
+        )
+        handler = SlackActionHandler(self.action, incident, self.project)
+        metric_value = 1000
+        with self.tasks():
+            handler.fire(metric_value, IncidentStatus(incident.status))
+
+        assert len(responses.calls) == 1