Repository: getsentry/sentry
Pull request: ref(slack): Add suggested assignees
Adds suggested assignees as a context block to the new Slack issue alerts. Closes #62901

![image](https://github.com/getsentry/sentry/assets/45607721/f1facac1-207d-494f-89ab-364eb693a2be)
![image](https://github.com/getsentry/sentry/assets/45607721/089ee665-4df9-46d8-aaaf-efd8b3adb697)


Problem statement:
Display issue assignee if available, suggested assignees if not
Add the issue assignee to issue alerts if available, or show the suggested assignees (if available). If there are no suggested assignees, just show "unassigned". Render the user's email / team name rather than doing `@colleen` for example, so as to not notify multiple suggested assignees in Slack.

Hints from issue discussion:

Commit message: remove duplicate func from merge

Patch to review:
diff --git a/src/sentry/api/endpoints/group_details.py b/src/sentry/api/endpoints/group_details.py
index 085ec1ba82dece..14b80c46e49601 100644
--- a/src/sentry/api/endpoints/group_details.py
+++ b/src/sentry/api/endpoints/group_details.py
@@ -47,6 +47,11 @@
 delete_logger = logging.getLogger("sentry.deletions.api")
 
 
+def get_group_global_count(group: Group) -> str:
+    fetch_buffered_group_stats(group)
+    return str(group.times_seen_with_pending)
+
+
 @region_silo_endpoint
 class GroupDetailsEndpoint(GroupEndpoint, EnvironmentMixin):
     publish_status = {
@@ -119,11 +124,6 @@ def __group_hourly_daily_stats(group: Group, environment_ids: Sequence[int]):
 
         return hourly_stats, daily_stats
 
-    @staticmethod
-    def __get_group_global_count(group: Group) -> str:
-        fetch_buffered_group_stats(group)
-        return str(group.times_seen_with_pending)
-
     def get(self, request: Request, group) -> Response:
         """
         Retrieve an Issue
@@ -235,7 +235,7 @@ def get(self, request: Request, group) -> Response:
                     "pluginContexts": self._get_context_plugins(request, group),
                     "userReportCount": user_reports.count(),
                     "stats": {"24h": hourly_stats, "30d": daily_stats},
-                    "count": self.__get_group_global_count(group),
+                    "count": get_group_global_count(group),
                 }
             )
 
diff --git a/src/sentry/conf/server.py b/src/sentry/conf/server.py
index b6c4f31bc99e4f..305938614a7866 100644
--- a/src/sentry/conf/server.py
+++ b/src/sentry/conf/server.py
@@ -1888,8 +1888,6 @@ def custom_parameter_sort(parameter: dict) -> tuple[str, int]:
     "organizations:spike-protection-decay-heuristic": False,
     # Enable Slack messages using Block Kit
     "organizations:slack-block-kit": False,
-    # Enable new Slack message formatting
-    "organizations:slack-formatting-update": False,
     # Enable basic SSO functionality, providing configurable single sign on
     # using services like GitHub / Google. This is *not* the same as the signup
     # and login with Github / Azure DevOps that sentry.io provides.
diff --git a/src/sentry/features/__init__.py b/src/sentry/features/__init__.py
index 210f5f6df7b80c..0a2ef807c24c92 100644
--- a/src/sentry/features/__init__.py
+++ b/src/sentry/features/__init__.py
@@ -261,7 +261,6 @@
 default_manager.add("organizations:session-replay-weekly-email", OrganizationFeature, FeatureHandlerStrategy.INTERNAL)
 default_manager.add("organizations:set-grouping-config", OrganizationFeature, FeatureHandlerStrategy.INTERNAL)
 default_manager.add("organizations:slack-block-kit", OrganizationFeature, FeatureHandlerStrategy.INTERNAL)
-default_manager.add("organizations:slack-formatting-update", OrganizationFeature, FeatureHandlerStrategy.INTERNAL)
 default_manager.add("organizations:slack-overage-notifications", OrganizationFeature, FeatureHandlerStrategy.REMOTE)
 default_manager.add("organizations:source-maps-debugger-blue-thunder-edition", OrganizationFeature, FeatureHandlerStrategy.REMOTE)
 default_manager.add("organizations:sourcemaps-bundle-flat-file-indexing", OrganizationFeature, FeatureHandlerStrategy.REMOTE)
diff --git a/src/sentry/integrations/message_builder.py b/src/sentry/integrations/message_builder.py
index 9b3db722a515d5..6f8ab20f820feb 100644
--- a/src/sentry/integrations/message_builder.py
+++ b/src/sentry/integrations/message_builder.py
@@ -192,6 +192,9 @@ def build_footer(
         text = rules[0].label if rules[0].label else "Test Alert"
         footer += f" via {url_format.format(text=text, url=rule_url)}"
 
+        if features.has("organizations:slack-block-kit", project.organization):
+            footer = url_format.format(text=text, url=rule_url)
+
         if len(rules) > 1:
             footer += f" (+{len(rules) - 1} other)"
 
diff --git a/src/sentry/integrations/slack/actions/notification.py b/src/sentry/integrations/slack/actions/notification.py
index e96d4573dba4c7..d97fefe0e6eeaf 100644
--- a/src/sentry/integrations/slack/actions/notification.py
+++ b/src/sentry/integrations/slack/actions/notification.py
@@ -30,7 +30,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
         super().__init__(*args, **kwargs)
         # XXX(CEO): when removing the feature flag, put `label` back up as a class var
         self.label = "Send a notification to the {workspace} Slack workspace to {channel} (optionally, an ID: {channel_id}) and show tags {tags} in notification"  # type: ignore
-        if features.has("organizations:slack-formatting-update", self.project.organization):
+        if features.has("organizations:slack-block-kit", self.project.organization):
             self.label = "Send a notification to the {workspace} Slack workspace to {channel} (optionally, an ID: {channel_id}) and show tags {tags} and mentions {mentions} in notification"  # type: ignore
         self.form_fields = {
             "workspace": {
@@ -41,7 +41,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
             "channel_id": {"type": "string", "placeholder": "e.g., CA2FRA079 or UA1J9RTE1"},
             "tags": {"type": "string", "placeholder": "e.g., environment,user,my_tag"},
         }
-        if features.has("organizations:slack-formatting-update", self.project.organization):
+        if features.has("organizations:slack-block-kit", self.project.organization):
             self.form_fields["mentions"] = {
                 "type": "string",
                 "placeholder": "e.g. @jane, @on-call-team",
@@ -136,7 +136,7 @@ def send_notification(event: GroupEvent, futures: Sequence[RuleFuture]) -> None:
     def render_label(self) -> str:
         tags = self.get_tags_list()
 
-        if features.has("organizations:slack-formatting-update", self.project.organization):
+        if features.has("organizations:slack-block-kit", self.project.organization):
             return self.label.format(
                 workspace=self.get_integration_name(),
                 channel=self.get_option("channel"),
diff --git a/src/sentry/integrations/slack/message_builder/__init__.py b/src/sentry/integrations/slack/message_builder/__init__.py
index 4d037dbb1e604e..3411d1a1cd2e9f 100644
--- a/src/sentry/integrations/slack/message_builder/__init__.py
+++ b/src/sentry/integrations/slack/message_builder/__init__.py
@@ -1,5 +1,7 @@
 from typing import Any, List, Mapping, Union
 
+from sentry.issues.grouptype import GroupCategory
+
 # TODO(mgaeta): Continue fleshing out these types.
 SlackAttachment = Mapping[str, Any]
 SlackBlock = Mapping[str, Any]
@@ -27,9 +29,15 @@
 LEVEL_TO_EMOJI = {
     "_actioned_issue": ":white_check_mark:",
     "_incident_resolved": ":green_circle:",
-    "debug": ":yellow_circle:",
-    "error": ":red_circle:",
-    "fatal": ":red_circle:",
-    "info": ":large_blue_circle:",
-    "warning": ":yellow_circle:",
+    "debug": ":bug:",
+    "error": ":exclamation:",
+    "fatal": ":skull_and_crossbones:",
+    "info": ":information_source:",
+    "warning": ":warning:",
+}
+
+CATEGORY_TO_EMOJI = {
+    GroupCategory.PERFORMANCE: ":chart_with_downwards_trend:",
+    GroupCategory.FEEDBACK: ":busts_in_silhouette:",
+    GroupCategory.CRON: ":spiral_calendar_pad:",
 }
diff --git a/src/sentry/integrations/slack/message_builder/base/block.py b/src/sentry/integrations/slack/message_builder/base/block.py
index ef29aaa84e0e3b..bfe523af33ed5c 100644
--- a/src/sentry/integrations/slack/message_builder/base/block.py
+++ b/src/sentry/integrations/slack/message_builder/base/block.py
@@ -40,12 +40,15 @@ def get_markdown_block(text: str, emoji: Optional[str] = None) -> SlackBlock:
 
     @staticmethod
     def get_tags_block(tags) -> SlackBlock:
-        fields = []
+        text = ""
         for tag in tags:
             title = tag["title"]
             value = tag["value"]
-            fields.append({"type": "mrkdwn", "text": f"*{title}:*\n{value}"})
-        return {"type": "section", "fields": fields}
+            text += f"`{title}: {value}`  "
+        return {
+            "type": "section",
+            "text": {"type": "mrkdwn", "text": text},
+        }
 
     @staticmethod
     def get_divider() -> SlackBlock:
diff --git a/src/sentry/integrations/slack/message_builder/issues.py b/src/sentry/integrations/slack/message_builder/issues.py
index 1076feec464cb6..2acd6bb3432557 100644
--- a/src/sentry/integrations/slack/message_builder/issues.py
+++ b/src/sentry/integrations/slack/message_builder/issues.py
@@ -1,8 +1,16 @@
 from __future__ import annotations
 
+import logging
+from datetime import datetime, timedelta
 from typing import Any, Mapping, Sequence, Union
 
+from django.utils import timezone
+from django.utils.timesince import timesince
+from django.utils.translation import gettext as _
+
 from sentry import features, tagstore
+from sentry.api.endpoints.group_details import get_group_global_count
+from sentry.constants import LOG_LEVELS_MAP
 from sentry.eventstore.models import GroupEvent
 from sentry.integrations.message_builder import (
     build_attachment_replay_link,
@@ -15,24 +23,55 @@
     get_timestamp,
     get_title_link,
 )
-from sentry.integrations.slack.message_builder import SLACK_URL_FORMAT, SlackAttachment, SlackBlock
+from sentry.integrations.slack.message_builder import (
+    CATEGORY_TO_EMOJI,
+    LEVEL_TO_EMOJI,
+    SLACK_URL_FORMAT,
+    SlackAttachment,
+    SlackBlock,
+)
 from sentry.integrations.slack.message_builder.base.block import BlockSlackMessageBuilder
 from sentry.integrations.slack.utils.escape import escape_slack_text
 from sentry.issues.grouptype import GroupCategory
 from sentry.models.actor import ActorTuple
+from sentry.models.commit import Commit
 from sentry.models.group import Group, GroupStatus
 from sentry.models.project import Project
+from sentry.models.projectownership import ProjectOwnership
+from sentry.models.release import Release
 from sentry.models.rule import Rule
 from sentry.models.team import Team
 from sentry.models.user import User
 from sentry.notifications.notifications.base import ProjectNotification
 from sentry.notifications.utils.actions import MessageAction
+from sentry.notifications.utils.participants import (
+    dedupe_suggested_assignees,
+    get_suspect_commit_users,
+)
 from sentry.services.hybrid_cloud.actor import ActorType, RpcActor
 from sentry.services.hybrid_cloud.identity import RpcIdentity, identity_service
-from sentry.types.integrations import ExternalProviders
+from sentry.services.hybrid_cloud.user.model import RpcUser
+from sentry.types.group import SUBSTATUS_TO_STR
+from sentry.types.integrations import ExternalProviderEnum, ExternalProviders
 from sentry.utils import json
 
 STATUSES = {"resolved": "resolved", "ignored": "ignored", "unresolved": "re-opened"}
+logger = logging.getLogger(__name__)
+
+
+def time_since(value: datetime):
+    """
+    Display the relative time
+    """
+    now = timezone.now()
+    if value < (now - timedelta(days=5)):
+        return value.date()
+    diff = timesince(value, now)
+    if diff == timesince(now, now):
+        return "Just now"
+    if diff == "1 day":
+        return _("Yesterday")
+    return f"{diff} ago"
 
 
 def build_assigned_text(identity: RpcIdentity, assignee: str) -> str | None:
@@ -109,6 +148,15 @@ def get_tags(
 ) -> Sequence[Mapping[str, str | bool]]:
     """Get tag keys and values for block kit"""
     fields = []
+    if not tags:
+        tags = set()
+
+    # XXX(CEO): context is passing tags as a list of tuples from self.event.tags
+    # we should standardize but it might break other notifications
+    if tags and type(tags) is list:
+        tags = set(tags[0])
+
+    tags = tags | {"level", "release"}
     if tags:
         event_tags = event_for_tags.tags if event_for_tags else []
         for key, value in event_tags:
@@ -180,18 +228,68 @@ def get_group_assignees(group: Group) -> Sequence[Mapping[str, Any]]:
     return option_groups
 
 
+def get_suggested_assignees(
+    project: Project, event: GroupEvent, current_assignee: RpcUser | Team | None
+) -> list[str]:
+    """Get suggested assignees as a list of formatted strings"""
+    suggested_assignees = []
+    issue_owners, _ = ProjectOwnership.get_owners(project.id, event.data)
+    if (
+        issue_owners != ProjectOwnership.Everyone
+    ):  # we don't want every user in the project to be a suggested assignee
+        resolved_owners = ActorTuple.resolve_many(issue_owners)
+        suggested_assignees = RpcActor.many_from_object(resolved_owners)
+    if features.has("organizations:streamline-targeting-context", project.organization):
+        try:
+            suspect_commit_users = RpcActor.many_from_object(
+                get_suspect_commit_users(project, event)
+            )
+            suggested_assignees.extend(suspect_commit_users)
+        except (Release.DoesNotExist, Commit.DoesNotExist):
+            logger.info("Skipping suspect committers because release does not exist.")
+        except Exception:
+            logger.exception("Could not get suspect committers. Continuing execution.")
+    if suggested_assignees:
+        suggested_assignees = dedupe_suggested_assignees(suggested_assignees)
+        assignee_texts = []
+        for assignee in suggested_assignees:
+            # skip over any suggested assignees that are the current assignee of the issue, if there is any
+            if assignee.actor_type == ActorType.USER and not (
+                isinstance(current_assignee, RpcUser) and assignee.id == current_assignee.id
+            ):
+                # for user assignees, we first try to get their Slack identity; if it's not linked,
+                # we use their display name linked with their email
+                assignee_identity = None
+                assignee_identities = identity_service.get_user_identities_by_provider_type(
+                    user_id=assignee.id, provider_type=ExternalProviderEnum.SLACK.value
+                )
+                if len(assignee_identities) > 0:
+                    assignee_identity = assignee_identities[0]
+                if assignee_identity is None:
+                    assignee_as_user = assignee.resolve()
+                    assignee_text = (
+                        f"<mailto:{assignee_as_user.email}|{assignee_as_user.get_display_name()}>"
+                    )
+                else:
+                    assignee_text = f"<@{assignee_identity.external_id}>"
+                assignee_texts.append(assignee_text)
+            elif assignee.actor_type == ActorType.TEAM and not (
+                isinstance(current_assignee, Team) and assignee.id == current_assignee.id
+            ):
+                assignee_texts.append(f"#{assignee.slug}")
+        return assignee_texts
+    return []
+
+
 def get_action_text(text: str, actions: Sequence[Any], identity: RpcIdentity) -> str:
-    return (
-        text
-        + "\n"
-        + "\n".join(
-            [
-                action_text
-                for action_text in [build_action_text(identity, action) for action in actions]
-                if action_text
-            ]
-        )
+    action_text = "\n".join(
+        [
+            action_text
+            for action_text in [build_action_text(identity, action) for action in actions]
+            if action_text
+        ]
     )
+    return action_text
 
 
 def build_actions(
@@ -327,7 +425,6 @@ def escape_text(self) -> bool:
     def build(self, notification_uuid: str | None = None) -> Union[SlackBlock, SlackAttachment]:
         # XXX(dcramer): options are limited to 100 choices, even when nested
         text = build_attachment_text(self.group, self.event) or ""
-
         if self.escape_text:
             text = escape_slack_text(text)
             # XXX(scefali): Not sure why we actually need to do this just for unfurled messages.
@@ -337,7 +434,6 @@ def build(self, notification_uuid: str | None = None) -> Union[SlackBlock, Slack
 
         # This link does not contain user input (it's a static label and a url), must not escape it.
         text += build_attachment_replay_link(self.group, self.event) or ""
-
         project = Project.objects.get_from_cache(id=self.group.project_id)
 
         # If an event is unspecified, use the tags of the latest event (if one exists).
@@ -350,10 +446,11 @@ def build(self, notification_uuid: str | None = None) -> Union[SlackBlock, Slack
             else build_footer(self.group, project, self.rules, SLACK_URL_FORMAT)
         )
         obj = self.event if self.event is not None else self.group
+        action_text = ""
         if not self.issue_details or (
             self.recipient and self.recipient.actor_type == ActorType.TEAM
         ):
-            payload_actions, text, color = build_actions(
+            payload_actions, action_text, color = build_actions(
                 self.group, project, text, color, self.actions, self.identity
             )
         else:
@@ -363,7 +460,22 @@ def build(self, notification_uuid: str | None = None) -> Union[SlackBlock, Slack
         if self.rules:
             rule_id = self.rules[0].id
 
+        title_link = get_title_link(
+            self.group,
+            self.event,
+            self.link_to_event,
+            self.issue_details,
+            self.notification,
+            ExternalProviders.SLACK,
+            rule_id,
+            notification_uuid=notification_uuid,
+        )
+        title = build_attachment_title(obj)
+
         if not features.has("organizations:slack-block-kit", self.group.project.organization):
+            if action_text and self.identity:
+                text += "\n" + action_text
+
             return self._build(
                 actions=payload_actions,
                 callback_id=json.dumps({"issue": self.group.id}),
@@ -372,59 +484,57 @@ def build(self, notification_uuid: str | None = None) -> Union[SlackBlock, Slack
                 fields=fields,
                 footer=footer,
                 text=text,
-                title=build_attachment_title(obj),
-                title_link=get_title_link(
-                    self.group,
-                    self.event,
-                    self.link_to_event,
-                    self.issue_details,
-                    self.notification,
-                    ExternalProviders.SLACK,
-                    rule_id,
-                    notification_uuid=notification_uuid,
-                ),
+                title=title,
+                title_link=title_link,
                 ts=get_timestamp(self.group, self.event) if not self.issue_details else None,
             )
 
         # build up the blocks for newer issue alert formatting #
-        tags = get_tags(event_for_tags, self.tags)
+
         # build title block
-        title_link = get_title_link(
-            self.group,
-            self.event,
-            self.link_to_event,
-            self.issue_details,
-            self.notification,
-            ExternalProviders.SLACK,
-            rule_id,
-            notification_uuid=notification_uuid,
-        )
-        blocks = [
-            self.get_markdown_block(
-                text=f"<{title_link}|*{escape_slack_text(build_attachment_title(obj))}*>  \n{text}",
-            )
-        ]
+        if text:
+            text = f"```{text.lstrip(' ')}```"
+            if self.actions:
+                text += "\n" + action_text
+        if not text:
+            text = action_text
+        title_text = f"<{title_link}|*{escape_slack_text(title)}*>  \n{text}"
+
+        if self.group.issue_category == GroupCategory.ERROR:
+            level_text = None
+            for k, v in LOG_LEVELS_MAP.items():
+                if self.group.level == v:
+                    level_text = k
+
+            title_emoji = LEVEL_TO_EMOJI.get(level_text)
+        else:
+            title_emoji = CATEGORY_TO_EMOJI.get(self.group.issue_category)
+
+        if title_emoji:
+            title_text = f"{title_emoji} {title_text}"
+
+        blocks = [self.get_markdown_block(title_text)]
         # build tags block
+        tags = get_tags(event_for_tags, self.tags)
         if tags:
             blocks.append(self.get_tags_block(tags))
 
-        # add mentions
-        if (
-            features.has("organizations:slack-formatting-update", self.group.project.organization)
-            and self.mentions
-        ):
-            mentions_text = f"Mentions: {self.mentions}"
-            blocks.append(self.get_markdown_block(mentions_text))
-
-        # build footer block
-        timestamp = None
-        if not self.issue_details:
-            ts = self.group.last_seen
-            timestamp = max(ts, self.event.datetime) if self.event else ts
-        blocks.append(self.get_context_block(text=footer, timestamp=timestamp))
+        # add event count, user count, substate, first seen
+        context = {
+            "Events": get_group_global_count(self.group),
+            "Users Affected": self.group.count_users_seen(),
+            "State": SUBSTATUS_TO_STR.get(self.group.substatus, "").replace("_", " ").title(),
+            "First Seen": time_since(self.group.first_seen),
+        }
+        context_text = ""
+        for k, v in context.items():
+            if k and v:
+                context_text += f"{k}: *{v}*   "
+        blocks.append(self.get_markdown_block(context_text[:-3]))
 
         # build actions
         actions = []
+        assignee = self.group.get_assignee()
         for action in payload_actions:
             if action.label in (
                 "Archive",
@@ -437,7 +547,6 @@ def build(self, notification_uuid: str | None = None) -> Union[SlackBlock, Slack
             ):
                 actions.append(self.get_button_action(action))
             elif action.name == "assign":
-                assignee = self.group.get_assignee()
                 actions.append(
                     self.get_external_select_action(
                         action, format_actor_option(assignee, True) if assignee else None
@@ -448,10 +557,48 @@ def build(self, notification_uuid: str | None = None) -> Union[SlackBlock, Slack
             action_block = {"type": "actions", "elements": [action for action in actions]}
             blocks.append(action_block)
 
+        # suggested assignees
+        suggested_assignees = []
+        if event_for_tags:
+            suggested_assignees = get_suggested_assignees(
+                self.group.project, event_for_tags, assignee
+            )
+        if len(suggested_assignees) > 0:
+            suggested_assignee_text = "Suggested Assignees: "
+            for idx, assignee in enumerate(suggested_assignees):
+                if idx != 0:
+                    suggested_assignee_text += ", "
+                suggested_assignee_text += assignee
+            blocks.append(self.get_context_block(suggested_assignee_text))
+
+        # add mentions
+        if self.mentions:
+            mentions_text = f"Mentions: {self.mentions}"
+            blocks.append(self.get_markdown_block(mentions_text))
+
+        # build footer block
+        timestamp = None
+        if not self.issue_details:
+            ts = self.group.last_seen
+            timestamp = max(ts, self.event.datetime) if self.event else ts
+
+        if not self.notification:
+            # the footer content differs if it's a workflow notification, so we must check for that
+            footer = f"Project: <{project.get_absolute_url()}|{escape_slack_text(project.slug)}>    Alert: {footer}"
+            blocks.append(self.get_context_block(text=footer))
+        else:
+            blocks.append(self.get_context_block(text=footer, timestamp=timestamp))
+
+        blocks.append(self.get_divider())
+
+        block_id = {"issue": self.group.id}
+        if rule_id:
+            block_id["rule"] = rule_id
+
         return self._build_blocks(
             *blocks,
             fallback_text=self.build_fallback_text(obj, project.slug),
-            block_id=json.dumps({"issue": self.group.id}),
+            block_id=json.dumps(block_id),
             skip_fallback=self.skip_fallback,
         )
 
diff --git a/src/sentry/integrations/slack/webhooks/action.py b/src/sentry/integrations/slack/webhooks/action.py
index 0ca1539df7a0c1..250cfbbeeee24b 100644
--- a/src/sentry/integrations/slack/webhooks/action.py
+++ b/src/sentry/integrations/slack/webhooks/action.py
@@ -29,6 +29,7 @@
 from sentry.models.activity import ActivityIntegration
 from sentry.models.group import Group
 from sentry.models.organizationmember import InviteStatus, OrganizationMember
+from sentry.models.rule import Rule
 from sentry.notifications.utils.actions import BlockKitMessageAction, MessageAction
 from sentry.services.hybrid_cloud.integration import integration_service
 from sentry.services.hybrid_cloud.notifications import notifications_service
@@ -106,6 +107,14 @@ def update_group(
     )
 
 
+def get_rule(slack_request: SlackActionRequest) -> Group | None:
+    """Get the rule that fired"""
+    rule_id = slack_request.callback_data.get("rule")
+    if not rule_id:
+        return None
+    return Rule.objects.get(id=rule_id)
+
+
 def get_group(slack_request: SlackActionRequest) -> Group | None:
     """Determine the issue group on which an action is being taken."""
     group_id = slack_request.callback_data["issue"]
@@ -328,7 +337,7 @@ def open_resolve_dialog(self, slack_request: SlackActionRequest, group: Group) -
         }
         if use_block_kit and slack_request.data.get("channel"):
             callback_id["channel_id"] = slack_request.data["channel"]["id"]
-
+            callback_id["rule"] = slack_request.callback_data.get("rule")
         callback_id = json.dumps(callback_id)
 
         dialog = {
@@ -389,6 +398,11 @@ def _handle_group_actions(
         if not group:
             return self.respond(status=403)
 
+        use_block_kit = features.has("organizations:slack-block-kit", group.project.organization)
+        rule = None
+        if use_block_kit:
+            rule = get_rule(slack_request)
+
         identity = slack_request.get_identity()
         # Determine the acting user by Slack identity.
         identity_user = slack_request.get_identity_user()
@@ -404,7 +418,6 @@ def _handle_group_actions(
 
         original_tags_from_request = slack_request.get_tags()
 
-        use_block_kit = features.has("organizations:slack-block-kit", group.project.organization)
         if use_block_kit and slack_request.type == "view_submission":
             # TODO: if we use modals for something other than resolve, this will need to be more specific
 
@@ -437,6 +450,7 @@ def _handle_group_actions(
                 identity=identity,
                 actions=[action],
                 tags=original_tags_from_request,
+                rules=[rule] if rule else None,
                 skip_fallback=True,
             ).build()
             body = self.construct_reply(
@@ -468,7 +482,11 @@ def _handle_group_actions(
                 return self.api_error(slack_request, group, identity_user, error, "status_dialog")
 
             attachment = SlackIssuesMessageBuilder(
-                group, identity=identity, actions=[action], tags=original_tags_from_request
+                group,
+                identity=identity,
+                actions=[action],
+                tags=original_tags_from_request,
+                rules=[rule] if rule else None,
             ).build()
             body = self.construct_reply(
                 attachment, is_message=slack_request.callback_data["is_message"]
@@ -520,7 +538,11 @@ def _handle_group_actions(
 
         if use_block_kit:
             response = SlackIssuesMessageBuilder(
-                group, identity=identity, actions=action_list, tags=original_tags_from_request
+                group,
+                identity=identity,
+                actions=action_list,
+                tags=original_tags_from_request,
+                rules=[rule] if rule else None,
             ).build()
             # XXX(isabella): for actions on link unfurls, we omit the fallback text from the
             # response so the unfurling endpoint understands the payload
@@ -543,7 +565,11 @@ def _handle_group_actions(
             return self.respond(response)
 
         attachment = SlackIssuesMessageBuilder(
-            group, identity=identity, actions=action_list, tags=original_tags_from_request
+            group,
+            identity=identity,
+            actions=action_list,
+            tags=original_tags_from_request,
+            rules=[rule] if rule else None,
         ).build()
         body = self.construct_reply(attachment, is_message=_is_message(slack_request.data))
 
diff --git a/src/sentry/testutils/cases.py b/src/sentry/testutils/cases.py
index 15a6dad74671d7..daccca0e72eaf6 100644
--- a/src/sentry/testutils/cases.py
+++ b/src/sentry/testutils/cases.py
@@ -2793,10 +2793,10 @@ def assert_performance_issue_blocks(
             issue_link += issue_link_extra_params
         assert (
             blocks[1]["text"]["text"]
-            == f"<{issue_link}|*N+1 Query*>  \ndb - SELECT `books_author`.`id`, `books_author`.`name` FROM `books_author` WHERE `books_author`.`id` = %s LIMIT 21"
+            == f":chart_with_downwards_trend: <{issue_link}|*N+1 Query*>  \n```db - SELECT `books_author`.`id`, `books_author`.`name` FROM `books_author` WHERE `books_author`.`id` = %s LIMIT 21```"
         )
         assert (
-            blocks[2]["elements"][0]["text"]
+            blocks[4]["elements"][0]["text"]
             == f"{project_slug} | production | <http://testserver/settings/account/notifications/{alert_type}/?referrer={referrer}-user&notification_uuid={notification_uuid}|Notification Settings>"
         )
 
@@ -2827,10 +2827,10 @@ def assert_generic_issue_blocks(
             issue_link += issue_link_extra_params
         assert (
             blocks[1]["text"]["text"]
-            == f"<{issue_link}|*{TEST_ISSUE_OCCURRENCE.issue_title}*>  \n{TEST_ISSUE_OCCURRENCE.evidence_display[0].value}"
+            == f":exclamation: <{issue_link}|*{TEST_ISSUE_OCCURRENCE.issue_title}*>  \n```{TEST_ISSUE_OCCURRENCE.evidence_display[0].value}```"
         )
         assert (
-            blocks[2]["elements"][0]["text"]
+            blocks[4]["elements"][0]["text"]
             == f"{project_slug} | <http://testserver/settings/account/notifications/{alert_type}/?referrer={referrer}-user&notification_uuid={notification_uuid}|Notification Settings>"
         )
 
diff --git a/tests/sentry/integrations/slack/notifications/test_assigned.py b/tests/sentry/integrations/slack/notifications/test_assigned.py
index 5ad81834d650ec..3d6f0bb8897c09 100644
--- a/tests/sentry/integrations/slack/notifications/test_assigned.py
+++ b/tests/sentry/integrations/slack/notifications/test_assigned.py
@@ -149,10 +149,10 @@ def test_assignment_block(self):
         notification_uuid = self.get_notification_uuid(blocks[1]["text"]["text"])
         assert (
             blocks[1]["text"]["text"]
-            == f"<http://testserver/organizations/{self.organization.slug}/issues/{self.group.id}/?referrer=assigned_activity-slack&notification_uuid={notification_uuid}|*{self.group.title}*>  \n"
+            == f":exclamation: <http://testserver/organizations/{self.organization.slug}/issues/{self.group.id}/?referrer=assigned_activity-slack&notification_uuid={notification_uuid}|*{self.group.title}*>  \n"
         )
         assert (
-            blocks[2]["elements"][0]["text"]
+            blocks[3]["elements"][0]["text"]
             == f"{self.project.slug} | <http://testserver/settings/account/notifications/workflow/?referrer=assigned_activity-slack-user&notification_uuid={notification_uuid}|Notification Settings>"
         )
 
diff --git a/tests/sentry/integrations/slack/notifications/test_issue_alert.py b/tests/sentry/integrations/slack/notifications/test_issue_alert.py
index 69cf793132ea3c..17fee9196dd23d 100644
--- a/tests/sentry/integrations/slack/notifications/test_issue_alert.py
+++ b/tests/sentry/integrations/slack/notifications/test_issue_alert.py
@@ -115,10 +115,10 @@ def test_issue_alert_user_block(self):
         assert event.group
         assert (
             blocks[1]["text"]["text"]
-            == f"<http://testserver/organizations/{event.organization.slug}/issues/{event.group.id}/?referrer=issue_alert-slack&notification_uuid={notification_uuid}&alert_rule_id={self.rule.id}&alert_type=issue|*Hello world*>  \n"
+            == f":exclamation: <http://testserver/organizations/{event.organization.slug}/issues/{event.group.id}/?referrer=issue_alert-slack&notification_uuid={notification_uuid}&alert_rule_id={self.rule.id}&alert_type=issue|*Hello world*>  \n"
         )
         assert (
-            blocks[2]["elements"][0]["text"]
+            blocks[4]["elements"][0]["text"]
             == f"{event.project.slug} | <http://testserver/settings/account/notifications/alerts/?referrer=issue_alert-slack-user&notification_uuid={notification_uuid}|Notification Settings>"
         )
 
@@ -350,10 +350,10 @@ def test_issue_alert_issue_owners_block(self):
         assert event.group
         assert (
             blocks[1]["text"]["text"]
-            == f"<http://testserver/organizations/{event.organization.slug}/issues/{event.group.id}/?referrer=issue_alert-slack&notification_uuid={notification_uuid}&alert_rule_id={rule.id}&alert_type=issue|*Hello world*>  \n"
+            == f":exclamation: <http://testserver/organizations/{event.organization.slug}/issues/{event.group.id}/?referrer=issue_alert-slack&notification_uuid={notification_uuid}&alert_rule_id={rule.id}&alert_type=issue|*Hello world*>  \n"
         )
         assert (
-            blocks[2]["elements"][0]["text"]
+            blocks[4]["elements"][0]["text"]
             == f"{event.project.slug} | <http://testserver/settings/account/notifications/alerts/?referrer=issue_alert-slack-user&notification_uuid={notification_uuid}|Notification Settings>"
         )
 
@@ -458,10 +458,10 @@ def test_issue_alert_issue_owners_environment_block(self):
         assert event.group
         assert (
             blocks[1]["text"]["text"]
-            == f"<http://testserver/organizations/{event.organization.slug}/issues/{event.group.id}/?referrer=issue_alert-slack&notification_uuid={notification_uuid}&environment={environment.name}&alert_rule_id={rule.id}&alert_type=issue|*Hello world*>  \n"
+            == f":exclamation: <http://testserver/organizations/{event.organization.slug}/issues/{event.group.id}/?referrer=issue_alert-slack&notification_uuid={notification_uuid}&environment={environment.name}&alert_rule_id={rule.id}&alert_type=issue|*Hello world*>  \n"
         )
         assert (
-            blocks[2]["elements"][0]["text"]
+            blocks[4]["elements"][0]["text"]
             == f"{event.project.slug} | {environment.name} | <http://testserver/settings/account/notifications/alerts/?referrer=issue_alert-slack-user&notification_uuid={notification_uuid}|Notification Settings>"
         )
 
@@ -651,10 +651,11 @@ def test_issue_alert_team_issue_owners_block(self):
         assert event.group
         assert (
             blocks[1]["text"]["text"]
-            == f"<http://testserver/organizations/{event.organization.slug}/issues/{event.group.id}/?referrer=issue_alert-slack&notification_uuid={notification_uuid}&alert_rule_id={rule.id}&alert_type=issue|*Hello world*>  \n"
+            == f":exclamation: <http://testserver/organizations/{event.organization.slug}/issues/{event.group.id}/?referrer=issue_alert-slack&notification_uuid={notification_uuid}&alert_rule_id={rule.id}&alert_type=issue|*Hello world*>  \n"
         )
+        assert blocks[5]["elements"][0]["text"] == f"Suggested Assignees: #{self.team.slug}"
         assert (
-            blocks[2]["elements"][0]["text"]
+            blocks[6]["elements"][0]["text"]
             == f"{event.project.slug} | <http://testserver/settings/{event.organization.slug}/teams/{self.team.slug}/notifications/?referrer=issue_alert-slack-team&notification_uuid={notification_uuid}|Notification Settings>"
         )
 
@@ -982,10 +983,10 @@ def test_issue_alert_team_block(self):
         assert event.group
         assert (
             blocks[1]["text"]["text"]
-            == f"<http://example.com/organizations/{event.organization.slug}/issues/{event.group.id}/?referrer=issue_alert-slack&notification_uuid={notification_uuid}&alert_rule_id={rule.id}&alert_type=issue|*Hello world*>  \n"
+            == f":exclamation: <http://example.com/organizations/{event.organization.slug}/issues/{event.group.id}/?referrer=issue_alert-slack&notification_uuid={notification_uuid}&alert_rule_id={rule.id}&alert_type=issue|*Hello world*>  \n"
         )
         assert (
-            blocks[2]["elements"][0]["text"]
+            blocks[5]["elements"][0]["text"]
             == f"{event.project.slug} | <http://example.com/settings/{event.organization.slug}/teams/{self.team.slug}/notifications/?referrer=issue_alert-slack-team&notification_uuid={notification_uuid}|Notification Settings>"
         )
 
@@ -1244,9 +1245,9 @@ def test_digest_enabled_block(self, digests):
         assert event.group
         assert (
             blocks[1]["text"]["text"]
-            == f"<http://testserver/organizations/{event.organization.slug}/issues/{event.group.id}/?referrer=issue_alert-slack&notification_uuid={notification_uuid}&alert_rule_id={rule.id}&alert_type=issue|*Hello world*>  \n"
+            == f":exclamation: <http://testserver/organizations/{event.organization.slug}/issues/{event.group.id}/?referrer=issue_alert-slack&notification_uuid={notification_uuid}&alert_rule_id={rule.id}&alert_type=issue|*Hello world*>  \n"
         )
         assert (
-            blocks[2]["elements"][0]["text"]
+            blocks[4]["elements"][0]["text"]
             == f"{event.project.slug} | <http://testserver/settings/account/notifications/alerts/?referrer=issue_alert-slack-user&notification_uuid={notification_uuid}|Notification Settings>"
         )
diff --git a/tests/sentry/integrations/slack/notifications/test_regression.py b/tests/sentry/integrations/slack/notifications/test_regression.py
index fb24932120fcce..8d9a074c695264 100644
--- a/tests/sentry/integrations/slack/notifications/test_regression.py
+++ b/tests/sentry/integrations/slack/notifications/test_regression.py
@@ -59,9 +59,9 @@ def test_regression_block(self):
         assert blocks[0]["text"]["text"] == fallback_text
         notification_uuid = self.get_notification_uuid(blocks[1]["text"]["text"])
         assert blocks[1]["text"]["text"] == (
-            f"<http://testserver/organizations/{self.organization.slug}/issues/{self.group.id}/?referrer=regression_activity-slack&notification_uuid={notification_uuid}|*{self.group.title}*>  \n"
+            f":exclamation: <http://testserver/organizations/{self.organization.slug}/issues/{self.group.id}/?referrer=regression_activity-slack&notification_uuid={notification_uuid}|*{self.group.title}*>  \n"
         )
-        assert blocks[2]["elements"][0]["text"] == (
+        assert blocks[3]["elements"][0]["text"] == (
             f"{self.project.slug} | <http://testserver/settings/account/notifications/workflow/?referrer=regression_activity-slack-user&notification_uuid={notification_uuid}|Notification Settings>"
         )
 
@@ -105,9 +105,9 @@ def test_regression_with_release_block(self):
         )
         assert blocks[0]["text"]["text"] == fallback_text
         assert blocks[1]["text"]["text"] == (
-            f"<http://testserver/organizations/{self.organization.slug}/issues/{self.group.id}/?referrer=regression_activity-slack&notification_uuid={notification_uuid}|*{self.group.title}*>  \n"
+            f":exclamation: <http://testserver/organizations/{self.organization.slug}/issues/{self.group.id}/?referrer=regression_activity-slack&notification_uuid={notification_uuid}|*{self.group.title}*>  \n"
         )
-        assert blocks[2]["elements"][0]["text"] == (
+        assert blocks[3]["elements"][0]["text"] == (
             f"{self.project.slug} | <http://testserver/settings/account/notifications/workflow/?referrer=regression_activity-slack-user&notification_uuid={notification_uuid}|Notification Settings>"
         )
 
diff --git a/tests/sentry/integrations/slack/notifications/test_resolved.py b/tests/sentry/integrations/slack/notifications/test_resolved.py
index 51a2e74bcf2577..a2cd670fa14dce 100644
--- a/tests/sentry/integrations/slack/notifications/test_resolved.py
+++ b/tests/sentry/integrations/slack/notifications/test_resolved.py
@@ -67,10 +67,11 @@ def test_resolved_block(self):
         assert blocks[0]["text"]["text"] == fallback_text
         assert (
             blocks[1]["text"]["text"]
-            == f"<{issue_link}/?referrer=resolved_activity-slack&notification_uuid={notification_uuid}|*{self.group.title}*>  \n"
+            == f":exclamation: <{issue_link}/?referrer=resolved_activity-slack&notification_uuid={notification_uuid}|*{self.group.title}*>  \n"
         )
+        # import pdb; pdb.set_trace()
         assert (
-            blocks[2]["elements"][0]["text"]
+            blocks[3]["elements"][0]["text"]
             == f"{self.project.slug} | <http://testserver/settings/account/notifications/workflow/?referrer=resolved_activity-slack-user&notification_uuid={notification_uuid}|Notification Settings>"
         )
 
diff --git a/tests/sentry/integrations/slack/notifications/test_resolved_in_release.py b/tests/sentry/integrations/slack/notifications/test_resolved_in_release.py
index 93302cea197324..d6e57d0973825f 100644
--- a/tests/sentry/integrations/slack/notifications/test_resolved_in_release.py
+++ b/tests/sentry/integrations/slack/notifications/test_resolved_in_release.py
@@ -64,10 +64,10 @@ def test_resolved_in_release_block(self):
         notification_uuid = self.get_notification_uuid(blocks[1]["text"]["text"])
         assert (
             blocks[1]["text"]["text"]
-            == f"<http://testserver/organizations/{self.organization.slug}/issues/{self.group.id}/?referrer=resolved_in_release_activity-slack&notification_uuid={notification_uuid}|*{self.group.title}*>  \n"
+            == f":exclamation: <http://testserver/organizations/{self.organization.slug}/issues/{self.group.id}/?referrer=resolved_in_release_activity-slack&notification_uuid={notification_uuid}|*{self.group.title}*>  \n"
         )
         assert (
-            blocks[2]["elements"][0]["text"]
+            blocks[3]["elements"][0]["text"]
             == f"{self.project.slug} | <http://testserver/settings/account/notifications/workflow/?referrer=resolved_in_release_activity-slack-user&notification_uuid={notification_uuid}|Notification Settings>"
         )
 
@@ -211,6 +211,6 @@ def test_resolved_in_release_parsed_version_block(self):
         assert blocks[0]["text"]["text"] == fallback_text
         notification_uuid = self.get_notification_uuid(blocks[1]["text"]["text"])
         assert (
-            blocks[2]["elements"][0]["text"]
+            blocks[3]["elements"][0]["text"]
             == f"{self.project.slug} | <http://testserver/settings/account/notifications/workflow/?referrer=resolved_in_release_activity-slack-user&notification_uuid={notification_uuid}|Notification Settings>"
         )
diff --git a/tests/sentry/integrations/slack/notifications/test_unassigned.py b/tests/sentry/integrations/slack/notifications/test_unassigned.py
index 08b6e709f07be4..64c5fad7fd0c21 100644
--- a/tests/sentry/integrations/slack/notifications/test_unassigned.py
+++ b/tests/sentry/integrations/slack/notifications/test_unassigned.py
@@ -58,10 +58,10 @@ def test_unassignment_block(self):
         assert blocks[0]["text"]["text"] == fallback_text
         notification_uuid = self.get_notification_uuid(blocks[1]["text"]["text"])
         assert blocks[1]["text"]["text"] == (
-            f"<http://testserver/organizations/{self.organization.slug}/issues/{self.group.id}/?referrer=unassigned_activity-slack&notification_uuid={notification_uuid}|*{self.group.title}*>  \n"
+            f":exclamation: <http://testserver/organizations/{self.organization.slug}/issues/{self.group.id}/?referrer=unassigned_activity-slack&notification_uuid={notification_uuid}|*{self.group.title}*>  \n"
         )
         assert (
-            blocks[2]["elements"][0]["text"]
+            blocks[3]["elements"][0]["text"]
             == f"{self.project.slug} | <http://testserver/settings/account/notifications/workflow/?referrer=unassigned_activity-slack-user&notification_uuid={notification_uuid}|Notification Settings>"
         )
 
diff --git a/tests/sentry/integrations/slack/test_message_builder.py b/tests/sentry/integrations/slack/test_message_builder.py
index b40b74c4fc1b7e..ed7a44c41a7d3a 100644
--- a/tests/sentry/integrations/slack/test_message_builder.py
+++ b/tests/sentry/integrations/slack/test_message_builder.py
@@ -16,6 +16,7 @@
     build_actions,
     get_option_groups,
     get_option_groups_block_kit,
+    time_since,
 )
 from sentry.integrations.slack.message_builder.metric_alerts import SlackMetricAlertMessageBuilder
 from sentry.issues.grouptype import (
@@ -25,14 +26,20 @@
 )
 from sentry.models.group import Group, GroupStatus
 from sentry.models.groupassignee import GroupAssignee
+from sentry.models.groupowner import GroupOwner, GroupOwnerType
+from sentry.models.identity import Identity, IdentityStatus
+from sentry.models.projectownership import ProjectOwnership
+from sentry.models.repository import Repository
 from sentry.models.team import Team
 from sentry.models.user import User
 from sentry.notifications.utils.actions import MessageAction
+from sentry.ownership.grammar import Matcher, Owner, Rule, dump_schema
 from sentry.services.hybrid_cloud.actor import RpcActor
+from sentry.silo.base import SiloMode
 from sentry.testutils.cases import PerformanceIssueTestCase, TestCase
 from sentry.testutils.helpers.datetime import before_now, iso_format
 from sentry.testutils.helpers.features import with_feature
-from sentry.testutils.silo import region_silo_test
+from sentry.testutils.silo import assume_test_silo_mode, region_silo_test
 from sentry.testutils.skips import requires_snuba
 from sentry.utils.dates import to_timestamp
 from sentry.utils.http import absolute_uri
@@ -44,11 +51,11 @@
 def build_test_message_blocks(
     teams: set[Team],
     users: set[User],
-    timestamp: datetime,
     group: Group,
     event: Event | None = None,
     link_to_event: bool = False,
     tags: dict[str, str] | None = None,
+    suggested_assignees: str | None = None,
     mentions: str | None = None,
 ) -> dict[str, Any]:
     project = group.project
@@ -63,40 +70,36 @@ def build_test_message_blocks(
         if link_to_event:
             title_link += f"/events/{event.event_id}"
     title_link += "/?referrer=slack"
-    ts = group.last_seen
-    timestamp = max(ts, event.datetime) if event else ts
-    event_date = "<!date^{:.0f}^{} at {} | Sentry Issue>".format(
-        to_timestamp(timestamp), "{date_pretty}", "{time}"
-    )
+    title_text = f":exclamation: <{title_link}|*{formatted_title}*>  \n"
+
     blocks: list[dict[str, Any]] = [
         {
             "type": "section",
-            "text": {"type": "mrkdwn", "text": f"<{title_link}|*{formatted_title}*>  \n"},
+            "text": {"type": "mrkdwn", "text": title_text},
             "block_id": f'{{"issue":{group.id}}}',
         },
     ]
-    if tags:
-        tags_section = {
-            "type": "section",
-            "fields": [
-                {"type": "mrkdwn", "text": f"*{key}:*\n{value}"} for key, value in tags.items()
-            ],
-        }
-        blocks.append(tags_section)
 
-    if mentions:
-        mentions_text = f"Mentions: {mentions}"
-        mentions_section = {
-            "type": "section",
-            "text": {"type": "mrkdwn", "text": mentions_text},
-        }
-        blocks.append(mentions_section)
+    tags_text = ""
+    if not tags:
+        tags = {}
+    tags["level"] = "error"
 
-    context = {
-        "type": "context",
-        "elements": [{"type": "mrkdwn", "text": f"BAR-{group.short_id} | {event_date}"}],
+    for k, v in tags.items():
+        tags_text += f"`{k}: {v}`  "
+
+    tags_section = {"type": "section", "text": {"type": "mrkdwn", "text": tags_text}}
+    blocks.append(tags_section)
+
+    # add event and user count, state, first seen
+    counts_section = {
+        "type": "section",
+        "text": {
+            "type": "mrkdwn",
+            "text": f"Events: *1*   State: *Ongoing*   First Seen: *{time_since(group.first_seen)}*",
+        },
     }
-    blocks.append(context)
+    blocks.append(counts_section)
 
     actions = {
         "type": "actions",
@@ -126,6 +129,31 @@ def build_test_message_blocks(
     }
     blocks.append(actions)
 
+    if suggested_assignees:
+        suggested_assignees_text = f"Suggested Assignees: {suggested_assignees}"
+        suggested_assignees_section = {
+            "type": "context",
+            "elements": [{"type": "mrkdwn", "text": suggested_assignees_text}],
+        }
+        blocks.append(suggested_assignees_section)
+
+    if mentions:
+        mentions_text = f"Mentions: {mentions}"
+        mentions_section = {
+            "type": "section",
+            "text": {"type": "mrkdwn", "text": mentions_text},
+        }
+        blocks.append(mentions_section)
+
+    context_text = f"Project: <http://testserver/organizations/{project.organization.slug}/issues/?project={project.id}|{project.slug}>    Alert: BAR-{group.short_id}"
+    context = {
+        "type": "context",
+        "elements": [{"type": "mrkdwn", "text": context_text}],
+    }
+    blocks.append(context)
+
+    blocks.append({"type": "divider"})
+
     return {
         "blocks": blocks,
         "text": f"[{project.slug}] {title}",
@@ -256,6 +284,7 @@ def test_build_group_block(self):
         event = self.store_event(
             data={
                 "event_id": "a" * 32,
+                "tags": {"foo": "bar"},
                 "timestamp": iso_format(before_now(minutes=1)),
                 "logentry": {"formatted": "bar"},
                 "_meta": {"logentry": {"formatted": {"": {"err": ["some error"]}}}},
@@ -267,58 +296,53 @@ def test_build_group_block(self):
         assert group
         self.project.flags.has_releases = True
         self.project.save(update_fields=["flags"])
-        tags = {"level": "error"}
+        tags = {"foo": "bar"}
         mentions = "hey @colleen fix it"
 
         assert SlackIssuesMessageBuilder(group).build() == build_test_message_blocks(
             teams={self.team},
             users={self.user},
-            timestamp=group.last_seen,
             group=group,
         )
-        # add tags to message
+
+        # add extra tag to message
         assert SlackIssuesMessageBuilder(
-            group, event.for_group(group), tags={"level"}
+            group, event.for_group(group), tags={"foo"}
         ).build() == build_test_message_blocks(
             teams={self.team},
             users={self.user},
-            timestamp=group.last_seen,
             group=group,
             tags=tags,
             event=event,
         )
+
         # add mentions to message
-        with self.feature("organizations:slack-formatting-update"):
-            assert SlackIssuesMessageBuilder(
-                group, event.for_group(group), mentions=mentions
-            ).build() == build_test_message_blocks(
-                teams={self.team},
-                users={self.user},
-                timestamp=group.last_seen,
-                group=group,
-                mentions=mentions,
-                event=event,
-            )
-        # add tags and mentions to message
-        with self.feature("organizations:slack-formatting-update"):
-            assert SlackIssuesMessageBuilder(
-                group, event.for_group(group), tags={"level"}, mentions=mentions
-            ).build() == build_test_message_blocks(
-                teams={self.team},
-                users={self.user},
-                timestamp=group.last_seen,
-                group=group,
-                tags=tags,
-                mentions=mentions,
-                event=event,
-            )
+        assert SlackIssuesMessageBuilder(
+            group, event.for_group(group), mentions=mentions
+        ).build() == build_test_message_blocks(
+            teams={self.team},
+            users={self.user},
+            group=group,
+            mentions=mentions,
+            event=event,
+        )
+        # add extra tag and mentions to message
+        assert SlackIssuesMessageBuilder(
+            group, event.for_group(group), tags={"foo"}, mentions=mentions
+        ).build() == build_test_message_blocks(
+            teams={self.team},
+            users={self.user},
+            group=group,
+            tags=tags,
+            mentions=mentions,
+            event=event,
+        )
 
         assert SlackIssuesMessageBuilder(
             group, event.for_group(group)
         ).build() == build_test_message_blocks(
             teams={self.team},
             users={self.user},
-            timestamp=event.datetime,
             group=group,
             event=event,
         )
@@ -328,7 +352,6 @@ def test_build_group_block(self):
         ).build() == build_test_message_blocks(
             teams={self.team},
             users={self.user},
-            timestamp=event.datetime,
             group=group,
             event=event,
             link_to_event=True,
@@ -337,7 +360,6 @@ def test_build_group_block(self):
         test_message = build_test_message_blocks(
             teams={self.team},
             users={self.user},
-            timestamp=group.last_seen,
             group=group,
         )
 
@@ -402,6 +424,121 @@ def test_build_group_attachment_issue_alert_block_kit(self):
         for section in ret["blocks"]:
             assert section["type"] != "actions"
 
+    @with_feature("organizations:slack-block-kit")
+    @with_feature("organizations:streamline-targeting-context")
+    def test_issue_alert_with_suggested_assignees(self):
+        self.project.flags.has_releases = True
+        self.project.save(update_fields=["flags"])
+        event = self.store_event(
+            data={
+                "message": "Hello world",
+                "level": "error",
+                "stacktrace": {"frames": [{"filename": "foo.py"}]},
+            },
+            project_id=self.project.id,
+        )
+        assert event.group
+        group = event.group
+
+        # create codeowner; user with no slack identity linked
+        self.code_mapping = self.create_code_mapping(project=self.project)
+        g_rule1 = Rule(Matcher("path", "*"), [Owner("team", self.team.slug)])
+        self.create_codeowners(self.project, self.code_mapping, schema=dump_schema([g_rule1]))
+        GroupOwner.objects.create(
+            group=group,
+            type=GroupOwnerType.CODEOWNERS.value,
+            user_id=None,
+            team_id=self.team.id,
+            project=self.project,
+            organization=self.organization,
+            context={"rule": str(g_rule1)},
+        )
+
+        # create ownership rule
+        g_rule2 = Rule(Matcher("level", "error"), [Owner("user", self.user.email)])
+        GroupOwner.objects.create(
+            group=group,
+            type=GroupOwnerType.OWNERSHIP_RULE.value,
+            user_id=self.user.id,
+            team_id=None,
+            project=self.project,
+            organization=self.organization,
+            context={"rule": str(g_rule2)},
+        )
+
+        # create suspect commit
+        repo = Repository.objects.create(
+            organization_id=self.organization.id,
+            name="home-repo",
+            integration_id=self.integration.id,
+        )
+        user2 = self.create_user()
+        self.create_member(teams=[self.team], user=user2, organization=self.organization)
+        commit = self.create_commit(
+            project=self.project,
+            repo=repo,
+            author=self.create_commit_author(project=self.project, user=user2),
+            key="qwertyuiopiuytrewq",
+            message="This is a suspect commit!",
+        )
+        GroupOwner.objects.create(
+            group=group,
+            user_id=user2.id,
+            project=self.project,
+            organization=self.organization,
+            type=GroupOwnerType.SUSPECT_COMMIT.value,
+            context={"commitId": commit.id},
+        )
+
+        # auto assign group
+        ProjectOwnership.handle_auto_assignment(self.project.id, event)
+
+        expected_blocks = build_test_message_blocks(
+            teams={self.team},
+            users={self.user},
+            group=group,
+            event=event,
+            suggested_assignees=f"#{self.team.slug}, <mailto:{user2.email}|{user2.email}>",  # auto-assignee is not included in suggested
+        )
+        expected_blocks["blocks"][3]["elements"][2]["initial_option"] = {
+            "text": {"type": "plain_text", "text": f"{self.user.email}"},
+            "value": f"user:{self.user.id}",
+        }
+        assert (
+            SlackIssuesMessageBuilder(group, event.for_group(group), tags={"foo"}).build()
+            == expected_blocks
+        )
+
+        # suggested user without slack identity linked, with display name
+        user2.name = "Scooby Doo"
+        with assume_test_silo_mode(SiloMode.CONTROL):
+            user2.save()
+        expected_blocks["blocks"][4]["elements"][0][
+            "text"
+        ] = f"Suggested Assignees: #{self.team.slug}, <mailto:{user2.email}|{user2.name}>"
+        assert (
+            SlackIssuesMessageBuilder(group, event.for_group(group), tags={"foo"}).build()
+            == expected_blocks
+        )
+
+        # suggested user with slack identity linked
+        with assume_test_silo_mode(SiloMode.CONTROL):
+            self.idp = self.create_identity_provider(type="slack", external_id="TXXXXXXX2")
+            self.identity = Identity.objects.create(
+                external_id="UXXXXXXX2",
+                idp=self.idp,
+                user=user2,
+                status=IdentityStatus.VALID,
+                scopes=[],
+            )
+        expected_blocks["blocks"][4]["elements"][0][
+            "text"
+        ] = f"Suggested Assignees: #{self.team.slug}, <@{self.identity.external_id}>"
+        assert (
+            SlackIssuesMessageBuilder(group, event.for_group(group), tags={"foo"}).build()
+            == expected_blocks
+        )
+
     def test_team_recipient(self):
         issue_alert_group = self.create_group(project=self.project)
         ret = SlackIssuesMessageBuilder(
@@ -922,15 +1059,14 @@ class ActionsTest(TestCase):
     def test_identity_and_action(self):
         group = self.create_group(project=self.project)
         MOCKIDENTITY = Mock()
-
         assert build_actions(
             group, self.project, "test txt", "red", [MessageAction(name="TEST")], MOCKIDENTITY
-        ) == ([], "test txt\n", "_actioned_issue")
+        ) == ([], "", "_actioned_issue")
 
         with self.feature("organizations:slack-block-kit"):
             assert build_actions(
                 group, self.project, "test txt", "red", [MessageAction(name="TEST")], MOCKIDENTITY
-            ) == ([], "test txt\n", "_actioned_issue")
+            ) == ([], "", "_actioned_issue")
 
     def _assert_message_actions_list(self, actions, expected):
         actions_dict = [
diff --git a/tests/sentry/integrations/slack/test_notify_action.py b/tests/sentry/integrations/slack/test_notify_action.py
index c1bbf38bf28698..406b20334c2e1b 100644
--- a/tests/sentry/integrations/slack/test_notify_action.py
+++ b/tests/sentry/integrations/slack/test_notify_action.py
@@ -76,7 +76,7 @@ def test_render_label(self):
             == "Send a notification to the Awesome Team Slack workspace to #my-channel (optionally, an ID: ) and show tags [one, two] in notification"
         )
 
-    @with_feature("organizations:slack-formatting-update")
+    @with_feature("organizations:slack-block-kit")
     def test_render_label_with_mentions(self):
         rule = self.get_rule(
             data={
diff --git a/tests/sentry/integrations/slack/webhooks/actions/__init__.py b/tests/sentry/integrations/slack/webhooks/actions/__init__.py
index 0db05fe6ab4b14..3a205710af4365 100644
--- a/tests/sentry/integrations/slack/webhooks/actions/__init__.py
+++ b/tests/sentry/integrations/slack/webhooks/actions/__init__.py
@@ -16,6 +16,8 @@ def setUp(self):
         self.response_url = (
             "https://hooks.slack.com/actions/T47563693/6204672533/x7ZLaiVMoECAW50Gw1ZYAXEM"
         )
+        self.project = self.create_project()
+        self.rule = self.create_project_rule(project=self.project)
 
     @patch(
         "sentry.integrations.slack.requests.base.SlackRequest._check_signing_secret",
@@ -37,7 +39,7 @@ def post_webhook(
             slack_user = {"id": self.external_id, "domain": "example"}
 
         if callback_id is None:
-            callback_id = json.dumps({"issue": self.group.id})
+            callback_id = json.dumps({"issue": self.group.id, "rule": self.rule.id})
 
         if original_message is None:
             original_message = {}
diff --git a/tests/sentry/integrations/slack/webhooks/actions/test_status.py b/tests/sentry/integrations/slack/webhooks/actions/test_status.py
index 776f5cd5536a22..f15aee4957323d 100644
--- a/tests/sentry/integrations/slack/webhooks/actions/test_status.py
+++ b/tests/sentry/integrations/slack/webhooks/actions/test_status.py
@@ -174,6 +174,7 @@ def test_ignore_issue(self):
             assert resp.status_code == 200, resp.content
             assert self.group.get_status() == GroupStatus.IGNORED
             assert self.group.substatus == GroupSubStatus.FOREVER
+            expect_status = f"```Identity not found.```\n*Issue archived by <@{self.external_id}>*"
             assert resp.data["blocks"][0]["text"]["text"].endswith(expect_status)
 
     def test_ignore_issue_block_kit(self):
@@ -196,7 +197,7 @@ def test_ignore_issue_block_kit(self):
             assert resp.status_code == 200, resp.content
             assert self.group.get_status() == GroupStatus.IGNORED
             assert self.group.substatus == GroupSubStatus.FOREVER
-            expect_status = f"Identity not found.\n*Issue archived by <@{self.external_id}>*"
+            expect_status = f"```Identity not found.```\n*Issue archived by <@{self.external_id}>*"
             assert resp.data["blocks"][0]["text"]["text"].endswith(expect_status)
 
     def test_ignore_issue_block_kit_through_unfurl(self):
@@ -218,7 +219,7 @@ def test_ignore_issue_block_kit_through_unfurl(self):
             assert resp.status_code == 200, resp.content
             assert self.group.get_status() == GroupStatus.IGNORED
             assert self.group.substatus == GroupSubStatus.FOREVER
-            expect_status = f"Identity not found.\n*Issue archived by <@{self.external_id}>*"
+            expect_status = f"```Identity not found.```\n*Issue archived by <@{self.external_id}>*"
             assert resp.data["blocks"][0]["text"]["text"].endswith(expect_status)
 
     def test_archive_issue(self):
@@ -256,6 +257,8 @@ def test_archive_issue(self):
             assert resp.status_code == 200, resp.content
             assert self.group.get_status() == GroupStatus.IGNORED
             assert self.group.substatus == GroupSubStatus.UNTIL_ESCALATING
+            # XXX(CEO): it's kind of odd to code format this but would be tricky to avoid
+            expect_status = f"```Identity not found.```\n*Issue archived by <@{self.external_id}>*"
             assert resp.data["blocks"][0]["text"]["text"].endswith(expect_status)
 
     def test_archive_issue_block_kit(self):
@@ -277,7 +280,7 @@ def test_archive_issue_block_kit(self):
         assert self.group.get_status() == GroupStatus.IGNORED
         assert self.group.substatus == GroupSubStatus.UNTIL_ESCALATING
 
-        expect_status = f"Identity not found.\n*Issue archived by <@{self.external_id}>*"
+        expect_status = f"```Identity not found.```\n*Issue archived by <@{self.external_id}>*"
         assert resp.data["blocks"][0]["text"]["text"].endswith(expect_status)
 
     def test_archive_issue_block_kit_through_unfurl(self):
@@ -300,7 +303,7 @@ def test_archive_issue_block_kit_through_unfurl(self):
         assert self.group.get_status() == GroupStatus.IGNORED
         assert self.group.substatus == GroupSubStatus.UNTIL_ESCALATING
 
-        expect_status = f"Identity not found.\n*Issue archived by <@{self.external_id}>*"
+        expect_status = f"```Identity not found.```\n*Issue archived by <@{self.external_id}>*"
         assert resp.data["blocks"][0]["text"]["text"].endswith(expect_status)
 
     def test_ignore_issue_with_additional_user_auth(self):
@@ -446,7 +449,7 @@ def test_assign_issue(self):
                 "assigneeType": "user",
                 "integration": ActivityIntegration.SLACK.value,
             }
-
+            expect_status = f"*Issue assigned to #{self.team.slug} by <@{self.external_id}>*"
             assert resp.data["blocks"][0]["text"]["text"].endswith(expect_status), resp.data["text"]
 
     def test_assign_issue_block_kit(self):
diff --git a/tests/sentry/notifications/notifications/test_digests.py b/tests/sentry/notifications/notifications/test_digests.py
index 8d9a3ff5d86bb9..5ac61e3f7c04ec 100644
--- a/tests/sentry/notifications/notifications/test_digests.py
+++ b/tests/sentry/notifications/notifications/test_digests.py
@@ -279,28 +279,28 @@ def test_slack_digest_notification_block(self, digests):
             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 len(blocks) == 11
         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"
+        event1_alert_title = f":exclamation: <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"
+        event2_alert_title = f":exclamation: <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
+            assert blocks[6]["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[6]["text"]["text"] == event1_alert_title
 
         assert (
-            blocks[2]["elements"][0]["text"]
+            blocks[4]["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"]
+            blocks[9]["elements"][0]["text"]
             == f"{self.project.slug} | <http://testserver/settings/account/notifications/?referrer=digest-slack-user&notification_uuid={notification_uuid}|Notification Settings>"
         )


Code review comments:
diff_hunk: @@ -409,6 +424,121 @@ def test_build_group_attachment_issue_alert_block_kit(self):
         for section in ret["blocks"]:
             assert section["type"] != "actions"
 
+    @with_feature("organizations:slack-block-kit")
+    @with_feature("organizations:streamline-targeting-context")
+    def test_issue_alert_with_suggested_assignees(self):
+        self.project.flags.has_releases = True
+        self.project.save(update_fields=["flags"])
+        event = self.store_event(
+            data={
+                "message": "Hello world",
+                "level": "error",
+                "stacktrace": {"frames": [{"filename": "foo.py"}]},
+            },
+            project_id=self.project.id,
+        )
+        assert event.group
+        group = event.group
+
+        # create codeowner; user with no slack identity linked
+        self.code_mapping = self.create_code_mapping(project=self.project)
+        g_rule1 = Rule(Matcher("path", "*"), [Owner("team", self.team.slug)])
+        self.create_codeowners(self.project, self.code_mapping, schema=dump_schema([g_rule1]))
+        GroupOwner.objects.create(
+            group=group,
+            type=GroupOwnerType.CODEOWNERS.value,
+            user_id=None,
+            team_id=self.team.id,
+            project=self.project,
+            organization=self.organization,
+            context={"rule": str(g_rule1)},
+        )
+
+        # create ownership rule
+        g_rule2 = Rule(Matcher("level", "error"), [Owner("user", self.user.email)])
+        GroupOwner.objects.create(
+            group=group,
+            type=GroupOwnerType.OWNERSHIP_RULE.value,
+            user_id=self.user.id,
+            team_id=None,
+            project=self.project,
+            organization=self.organization,
+            context={"rule": str(g_rule2)},
+        )
+
+        # create suspect commit
+        repo = Repository.objects.create(
+            organization_id=self.organization.id,
+            name="home-repo",
+            integration_id=self.integration.id,
+        )
+        user2 = self.create_user()
+        self.create_member(teams=[self.team], user=user2, organization=self.organization)
+        commit = self.create_commit(
+            project=self.project,
+            repo=repo,
+            author=self.create_commit_author(project=self.project, user=user2),
+            key="qwertyuiopiuytrewq",
+            message="This is a suspect commit!",
+        )
+        GroupOwner.objects.create(
+            group=group,
+            user_id=user2.id,
+            project=self.project,
+            organization=self.organization,
+            type=GroupOwnerType.SUSPECT_COMMIT.value,
+            context={"commitId": commit.id},
+        )
+
+        # auto assign group
+        ProjectOwnership.handle_auto_assignment(self.project.id, event)
+
+        expected_blocks = build_test_message_blocks(
+            teams={self.team},
+            users={self.user},
+            group=group,
+            event=event,
+            suggested_assignees=f"#{self.team.slug}, <mailto:{user2.email}|{user2.email}>",  # auto-assignee is not included in suggested
+        )
+        expected_blocks["blocks"][3]["elements"][2]["initial_option"] = {
path: tests/sentry/integrations/slack/test_message_builder.py
text: @user1:
is this changing the default assignee? 

@author:
yeah based on the auto assignment that happens but realizing i can just move this to `build_test_message_blocks` with an optional arg instead of having a whole separate line to set the default assignee

diff_hunk: @@ -494,6 +557,20 @@ def build(self, notification_uuid: str | None = None) -> Union[SlackBlock, Slack
             action_block = {"type": "actions", "elements": [action for action in actions]}
             blocks.append(action_block)
 
+        # suggested assignees
+        suggested_assignees = []
+        if event_for_tags:
+            suggested_assignees = get_suggested_assignees(
+                self.group.project, event_for_tags, assignee
+            )
+        if len(suggested_assignees) > 0:
+            suggested_assignee_text = "Suggested Assignees: "
+            for idx, assignee in enumerate(suggested_assignees):
+                if idx != 0:
path: src/sentry/integrations/slack/message_builder/issues.py
text: @user1:
nit: I believe `enumerate` can optionally take a `start` position, so you can just start at 1 and not have to check this