Repository: getsentry/sentry

Issue:
Expand workflow notifications to send a notification for an issue when it escalates to subscribed users.
Expand workflow notifications to send a notification for an issue when it escalates to subscribed users.
    1. When a user Archives an issue, subscribe them to that issue to ensure they get notified when it escalates


Discussion:

Resolving patch:
diff --git a/src/sentry/integrations/msteams/notifications.py b/src/sentry/integrations/msteams/notifications.py
--- a/src/sentry/integrations/msteams/notifications.py
+++ b/src/sentry/integrations/msteams/notifications.py
@@ -11,6 +11,7 @@
 from sentry.models import Team, User
 from sentry.notifications.notifications.activity import (
     AssignedActivityNotification,
+    EscalatingActivityNotification,
     NoteActivityNotification,
     RegressionActivityNotification,
     ReleaseActivityNotification,
@@ -42,6 +43,7 @@
     ResolvedInReleaseActivityNotification,
     ReleaseActivityNotification,
     RegressionActivityNotification,
+    EscalatingActivityNotification,
 ]
 MESSAGE_BUILDERS = {
     "SlackNotificationsMessageBuilder": MSTeamsNotificationsMessageBuilder,
diff --git a/src/sentry/notifications/notifications/activity/__init__.py b/src/sentry/notifications/notifications/activity/__init__.py
--- a/src/sentry/notifications/notifications/activity/__init__.py
+++ b/src/sentry/notifications/notifications/activity/__init__.py
@@ -1,4 +1,5 @@
 from sentry.notifications.notifications.activity.assigned import AssignedActivityNotification
+from sentry.notifications.notifications.activity.escalating import EscalatingActivityNotification
 from sentry.notifications.notifications.activity.new_processing_issues import (
     NewProcessingIssuesActivityNotification,
 )
@@ -21,4 +22,5 @@
     ActivityType.SET_RESOLVED.value: ResolvedActivityNotification,
     ActivityType.SET_RESOLVED_IN_RELEASE.value: ResolvedInReleaseActivityNotification,
     ActivityType.UNASSIGNED.value: UnassignedActivityNotification,
+    ActivityType.SET_ESCALATING.value: EscalatingActivityNotification,
 }
diff --git a/src/sentry/notifications/notifications/activity/escalating.py b/src/sentry/notifications/notifications/activity/escalating.py
new file mode 100644
--- /dev/null
+++ b/src/sentry/notifications/notifications/activity/escalating.py
@@ -0,0 +1,32 @@
+from __future__ import annotations
+
+from typing import Any, Mapping
+
+from sentry.services.hybrid_cloud.actor import RpcActor
+from sentry.types.integrations import ExternalProviders
+
+from .base import GroupActivityNotification
+
+
+class EscalatingActivityNotification(GroupActivityNotification):
+    message_builder = "SlackNotificationsMessageBuilder"
+    metrics_key = "escalating_activity"
+    title = "Escalating"
+    template_path = "sentry/emails/activity/note"
+
+    def get_notification_title(
+        self, provider: ExternalProviders, context: Mapping[str, Any] | None = None
+    ) -> str:
+
+        return "Issue marked as escalating"
+
+    def get_description(self) -> tuple[str, Mapping[str, Any], Mapping[str, Any]]:
+        forecast = int(self.activity.data["forecast"])
+        return (
+            "Sentry flagged this issue as escalating because over {forecast} {event} happened in an hour",
+            {"forecast": forecast, "event": "event" if forecast == 1 else "events"},
+            {},
+        )
+
+    def get_message_description(self, recipient: RpcActor, provider: ExternalProviders) -> Any:
+        return self.get_context()["text_description"]


Test patch:
diff --git a/tests/sentry/integrations/msteams/notifications/test_escalating.py b/tests/sentry/integrations/msteams/notifications/test_escalating.py
new file mode 100644
--- /dev/null
+++ b/tests/sentry/integrations/msteams/notifications/test_escalating.py
@@ -0,0 +1,52 @@
+from unittest.mock import MagicMock, Mock, patch
+
+from sentry.models import Activity
+from sentry.notifications.notifications.activity import EscalatingActivityNotification
+from sentry.testutils.cases import MSTeamsActivityNotificationTest
+from sentry.types.activity import ActivityType
+
+
+@patch(
+    "sentry.integrations.msteams.MsTeamsAbstractClient.get_user_conversation_id",
+    Mock(return_value="some_conversation_id"),
+)
+@patch("sentry.integrations.msteams.MsTeamsAbstractClient.send_card")
+class MSTeamsEscalatingNotificationTest(MSTeamsActivityNotificationTest):
+    def test_note(self, mock_send_card: MagicMock):
+        """
+        Test that the card for MS Teams notification is generated correctly when a comment is made on an issue.
+        """
+        notification = EscalatingActivityNotification(
+            Activity(
+                project=self.project,
+                group=self.group,
+                user_id=self.user.id,
+                type=ActivityType.SET_ESCALATING,
+                data={"forecast": 100},
+            )
+        )
+        with self.tasks():
+            notification.send()
+
+        mock_send_card.assert_called_once()
+
+        args, kwargs = mock_send_card.call_args
+
+        assert args[0] == "some_conversation_id"
+
+        body = args[1]["body"]
+        assert 4 == len(body)
+
+        assert body[0]["text"] == "Issue marked as escalating"
+        assert (
+            f"[{self.group.title}](http://testserver/organizations/{self.organization.slug}/issues/{self.group.id}/?referrer=escalating\\_activity-msteams)"
+            == body[1]["text"]
+        )
+        assert (
+            body[2]["text"]
+            == "Sentry flagged this issue as escalating because over 100 events happened in an hour"
+        )
+        assert (
+            body[3]["columns"][1]["items"][0]["text"]
+            == f"{self.project.slug} | [Notification Settings](http://testserver/settings/account/notifications/workflow/?referrer=escalating\\_activity-msteams-user)"
+        )
diff --git a/tests/sentry/integrations/slack/notifications/test_escalating.py b/tests/sentry/integrations/slack/notifications/test_escalating.py
new file mode 100644
--- /dev/null
+++ b/tests/sentry/integrations/slack/notifications/test_escalating.py
@@ -0,0 +1,110 @@
+from unittest import mock
+
+import responses
+
+from sentry.models import Activity
+from sentry.notifications.notifications.activity import EscalatingActivityNotification
+from sentry.testutils.cases import PerformanceIssueTestCase, SlackActivityNotificationTest
+from sentry.testutils.helpers.notifications import TEST_ISSUE_OCCURRENCE, TEST_PERF_ISSUE_OCCURRENCE
+from sentry.testutils.helpers.slack import get_attachment, send_notification
+from sentry.types.activity import ActivityType
+
+
+class SlackRegressionNotificationTest(SlackActivityNotificationTest, PerformanceIssueTestCase):
+    def create_notification(self, group):
+        return EscalatingActivityNotification(
+            Activity(
+                project=self.project,
+                group=group,
+                user_id=self.user.id,
+                type=ActivityType.SET_ESCALATING,
+                data={"forecast": 100},
+            )
+        )
+
+    @responses.activate
+    @mock.patch("sentry.notifications.notify.notify", side_effect=send_notification)
+    def test_escalating(self, mock_func):
+        """
+        Test that a Slack message is sent with the expected payload when an issue escalates
+        """
+        with self.tasks():
+            self.create_notification(self.group).send()
+
+        attachment, text = get_attachment()
+        assert text == "Issue marked as escalating"
+        assert attachment["title"] == "こんにちは"
+        assert (
+            attachment["text"]
+            == "Sentry flagged this issue as escalating because over 100 events happened in an hour"
+        )
+        assert (
+            attachment["footer"]
+            == f"{self.project.slug} | <http://testserver/settings/account/notifications/workflow/?referrer=escalating_activity-slack-user|Notification Settings>"
+        )
+
+    @responses.activate
+    @mock.patch(
+        "sentry.eventstore.models.GroupEvent.occurrence",
+        return_value=TEST_PERF_ISSUE_OCCURRENCE,
+        new_callable=mock.PropertyMock,
+    )
+    @mock.patch("sentry.notifications.notify.notify", side_effect=send_notification)
+    def test_escalating_performance_issue(self, mock_func, occurrence):
+        """
+        Test that a Slack message is sent with the expected payload when a performance issue escalates
+        """
+        event = self.create_performance_issue()
+        with self.tasks():
+            self.create_notification(event.group).send()
+
+        attachment, text = get_attachment()
+        assert text == "Issue marked as escalating"
+        assert attachment["title"] == "N+1 Query"
+        assert (
+            attachment["title_link"]
+            == f"http://testserver/organizations/{self.organization.slug}/issues/{event.group.id}/?referrer=escalating_activity-slack"
+        )
+        assert (
+            attachment["text"]
+            == "Sentry flagged this issue as escalating because over 100 events happened in an hour"
+        )
+        assert (
+            attachment["footer"]
+            == f"{self.project.slug} | production | <http://testserver/settings/account/notifications/workflow/?referrer=escalating_activity-slack-user|Notification Settings>"
+        )
+
+    @responses.activate
+    @mock.patch(
+        "sentry.eventstore.models.GroupEvent.occurrence",
+        return_value=TEST_ISSUE_OCCURRENCE,
+        new_callable=mock.PropertyMock,
+    )
+    @mock.patch("sentry.notifications.notify.notify", side_effect=send_notification)
+    def test_escalating_generic_issue(self, mock_func, occurrence):
+        """
+        Test that a Slack message is sent with the expected payload when a generic issue type escalates
+        """
+        event = self.store_event(
+            data={"message": "Hellboy's world", "level": "error"}, project_id=self.project.id
+        )
+        event = event.for_group(event.groups[0])
+
+        with self.tasks():
+            self.create_notification(event.group).send()
+
+        attachment, text = get_attachment()
+        assert text == "Issue marked as escalating"
+        assert attachment["title"] == TEST_ISSUE_OCCURRENCE.issue_title
+        assert (
+            attachment["title_link"]
+            == f"http://testserver/organizations/{self.organization.slug}/issues/{event.group.id}/?referrer=escalating_activity-slack"
+        )
+        assert (
+            attachment["text"]
+            == "Sentry flagged this issue as escalating because over 100 events happened in an hour"
+        )
+        assert (
+            attachment["footer"]
+            == f"{self.project.slug} | <http://testserver/settings/account/notifications/workflow/?referrer=escalating_activity-slack-user|Notification Settings>"
+        )