Repository: getsentry/sentry

Issue:
Convert digest notifications to use block kit
Convert the `DigestNotificationMessageBuilder` (code [here](https://github.com/getsentry/sentry/blob/master/src/sentry/integrations/slack/message_builder/notifications/digest.py)) to use block kit. This may be harder to test as I have personally never received one.


Discussion:

Resolving patch:
diff --git a/src/sentry/integrations/slack/message_builder/notifications/digest.py b/src/sentry/integrations/slack/message_builder/notifications/digest.py
--- a/src/sentry/integrations/slack/message_builder/notifications/digest.py
+++ b/src/sentry/integrations/slack/message_builder/notifications/digest.py
@@ -2,9 +2,10 @@
 
 from typing import Any, Mapping
 
+from sentry import features
 from sentry.digests import Digest
 from sentry.digests.utils import get_groups
-from sentry.integrations.slack.message_builder import SlackBody
+from sentry.integrations.slack.message_builder import SlackAttachment, SlackBlock
 from sentry.integrations.slack.message_builder.issues import SlackIssuesMessageBuilder
 from sentry.notifications.notifications.digest import DigestNotification
 from sentry.services.hybrid_cloud.actor import RpcActor
@@ -22,14 +23,28 @@ def __init__(
         super().__init__(notification, context, recipient)
         self.notification: DigestNotification = notification
 
-    def build(self) -> SlackBody:
+    def build(self) -> SlackAttachment | SlackBlock:
         """
         It's currently impossible in mypy to have recursive types so we need a
         hack to get this to return a SlackBody.
         """
         digest: Digest = self.context.get("digest", {})
-        return [
-            SlackIssuesMessageBuilder(
+        digest_groups = get_groups(digest)
+        if not features.has("organizations:slack-block-kit", self.notification.organization):
+            return [
+                SlackIssuesMessageBuilder(
+                    group=group,
+                    event=event,
+                    rules=[rule],
+                    issue_details=True,
+                    notification=self.notification,
+                    recipient=self.recipient,
+                ).build()
+                for rule, group, event in digest_groups
+            ]
+        blocks = []
+        for rule, group, event in digest_groups:
+            alert_as_blocks = SlackIssuesMessageBuilder(
                 group=group,
                 event=event,
                 rules=[rule],
@@ -37,5 +52,8 @@ def build(self) -> SlackBody:
                 notification=self.notification,
                 recipient=self.recipient,
             ).build()
-            for rule, group, event in get_groups(digest)
-        ]
+            # we iterate through the list of blocks created for each alert in the digest and add
+            # each block to the list of blocks which is used for the entire digest notification
+            for block in alert_as_blocks.get("blocks"):
+                blocks.append(block)
+        return self._build_blocks(*blocks)


Test patch:
diff --git a/tests/sentry/notifications/notifications/test_digests.py b/tests/sentry/notifications/notifications/test_digests.py
--- a/tests/sentry/notifications/notifications/test_digests.py
+++ b/tests/sentry/notifications/notifications/test_digests.py
@@ -17,6 +17,8 @@
 from sentry.tasks.digests import deliver_digest
 from sentry.testutils.cases import PerformanceIssueTestCase, SlackActivityNotificationTest, TestCase
 from sentry.testutils.helpers.datetime import before_now, iso_format
+from sentry.testutils.helpers.features import with_feature
+from sentry.testutils.helpers.slack import get_blocks_and_fallback_text
 from sentry.testutils.skips import requires_snuba
 from sentry.utils import json
 from tests.sentry.issues.test_utils import OccurrenceTestMixin
@@ -223,3 +225,87 @@ def test_slack_digest_notification(self, digests):
         assert len(attachments) == 2
         assert "notification_uuid" in attachments[0]["title_link"]
         assert "notification_uuid" in attachments[1]["title_link"]
+
+    @responses.activate
+    @with_feature("organizations:slack-block-kit")
+    @mock.patch.object(sentry, "digests")
+    def test_slack_digest_notification_block(self, digests):
+        """
+        Test that with digests and block kkit enabled, but Slack notification settings
+        (and not email settings), we send a properly formatted Slack notification
+        """
+        ProjectOwnership.objects.create(project_id=self.project.id, fallthrough=True)
+        backend = RedisBackend()
+        digests.digest = backend.digest
+        digests.enabled.return_value = True
+        timestamp_raw = before_now(days=1)
+        timestamp_secs = int(timestamp_raw.timestamp())
+        timestamp = iso_format(timestamp_raw)
+        key = f"slack:p:{self.project.id}"
+        rule = Rule.objects.create(project=self.project, label="my rule")
+        event1 = self.store_event(
+            data={
+                "timestamp": timestamp,
+                "message": "Hello world",
+                "level": "error",
+                "fingerprint": ["group-1"],
+            },
+            project_id=self.project.id,
+        )
+
+        event2 = self.store_event(
+            data={
+                "timestamp": timestamp,
+                "message": "Goodbye world",
+                "level": "error",
+                "fingerprint": ["group-2"],
+            },
+            project_id=self.project.id,
+        )
+        notification_uuid = str(uuid.uuid4())
+        backend.add(
+            key,
+            event_to_record(event1, [rule], notification_uuid),
+            increment_delay=0,
+            maximum_delay=0,
+        )
+        backend.add(
+            key,
+            event_to_record(event2, [rule], notification_uuid),
+            increment_delay=0,
+            maximum_delay=0,
+        )
+        with self.tasks():
+            deliver_digest(key)
+
+        assert len(responses.calls) >= 1
+        blocks, fallback_text = get_blocks_and_fallback_text()
+        assert (
+            fallback_text
+            == f"<!date^{timestamp_secs}^2 issues detected {{date_pretty}} in| Digest Report for> <http://testserver/organizations/{self.organization.slug}/projects/{self.project.slug}/|{self.project.name}>"
+        )
+        assert len(blocks) == 5
+        assert blocks[0]["text"]["text"] == fallback_text
+
+        assert event1.group
+        event1_alert_title = f"<http://testserver/organizations/{self.organization.slug}/issues/{event1.group.id}/?referrer=digest-slack&notification_uuid={notification_uuid}&alert_rule_id={rule.id}&alert_type=issue|*{event1.group.title}*>  \n"
+
+        assert event2.group
+        event2_alert_title = f"<http://testserver/organizations/{self.organization.slug}/issues/{event2.group.id}/?referrer=digest-slack&notification_uuid={notification_uuid}&alert_rule_id={rule.id}&alert_type=issue|*{event2.group.title}*>  \n"
+
+        # digest order not definitive
+        try:
+            assert blocks[1]["text"]["text"] == event1_alert_title
+            assert blocks[3]["text"]["text"] == event2_alert_title
+        except AssertionError:
+            assert blocks[1]["text"]["text"] == event2_alert_title
+            assert blocks[3]["text"]["text"] == event1_alert_title
+
+        assert (
+            blocks[2]["elements"][0]["text"]
+            == f"{self.project.slug} | <http://testserver/settings/account/notifications/?referrer=digest-slack-user&notification_uuid={notification_uuid}|Notification Settings>"
+        )
+        assert (
+            blocks[4]["elements"][0]["text"]
+            == f"{self.project.slug} | <http://testserver/settings/account/notifications/?referrer=digest-slack-user&notification_uuid={notification_uuid}|Notification Settings>"
+        )