Skip to content

fix(notifications): Convert slack notification title to rich text #91637

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/sentry/integrations/slack/message_builder/base/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,31 @@ def get_markdown_block(text: str, emoji: str | None = None) -> SlackBlock:
"text": {"type": "mrkdwn", "text": text},
}

@staticmethod
def get_rich_text_link(emojis: list[str], text: str, link: str | None = None) -> SlackBlock:
elements: list[dict[str, Any]] = []
for emoji in emojis:
elements.append({"type": "emoji", "name": emoji})
elements.append({"type": "text", "text": " "})

url: dict[str, Any] = {}
if link:
url = {
"type": "link",
"url": link,
"text": text,
"style": {"bold": True},
}
else:
url = {"type": "text", "text": text}

elements.append(url)

return {
"type": "rich_text",
"elements": [{"type": "rich_text_section", "elements": elements}],
}

@staticmethod
def get_markdown_quote_block(text: str, max_block_text_length: int) -> SlackBlock:
if len(text) > max_block_text_length:
Expand Down
20 changes: 9 additions & 11 deletions src/sentry/integrations/slack/message_builder/issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -458,30 +458,28 @@ def get_title_block(
) -> SlackBlock:
summary_headline = self.get_issue_summary_headline(event_or_group)
title = summary_headline or build_attachment_title(event_or_group)
title_emoji = self.get_title_emoji(has_action)
title_emojis = self.get_title_emoji(has_action)

title_text = f"{title_emoji}<{title_link}|*{escape_slack_text(title)}*>"
return self.get_markdown_block(title_text)
return self.get_rich_text_link(title_emojis, title, title_link)

def get_title_emoji(self, has_action: bool) -> str | None:
def get_title_emoji(self, has_action: bool) -> list[str]:
is_error_issue = self.group.issue_category == GroupCategory.ERROR

title_emoji = None
title_emojis: list[str] = []
if has_action:
# if issue is resolved, archived, or assigned, replace circle emojis with white circle
title_emoji = (
title_emojis = (
ACTION_EMOJI
if is_error_issue
else ACTIONED_CATEGORY_TO_EMOJI.get(self.group.issue_category)
else ACTIONED_CATEGORY_TO_EMOJI.get(self.group.issue_category, [])
)
elif is_error_issue:
level_text = LOG_LEVELS[self.group.level]
title_emoji = LEVEL_TO_EMOJI.get(level_text)
title_emojis = LEVEL_TO_EMOJI.get(level_text, [])
else:
title_emoji = CATEGORY_TO_EMOJI.get(self.group.issue_category)
title_emojis = CATEGORY_TO_EMOJI.get(self.group.issue_category, [])

title_emoji = title_emoji + " " if title_emoji else ""
return title_emoji
return title_emojis

def get_issue_summary_headline(self, event_or_group: Event | GroupEvent | Group) -> str | None:
if self.issue_summary is None:
Expand Down
28 changes: 14 additions & 14 deletions src/sentry/integrations/slack/message_builder/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,24 @@
SLACK_URL_FORMAT = "<{url}|{text}>"

LEVEL_TO_EMOJI = {
"_incident_resolved": ":green_circle:",
"debug": ":bug:",
"error": ":red_circle:",
"fatal": ":red_circle:",
"info": ":large_blue_circle:",
"warning": ":large_yellow_circle:",
"_incident_resolved": ["green_circle"],
"debug": ["bug"],
"error": ["red_circle"],
"fatal": ["red_circle"],
"info": ["large_blue_circle"],
"warning": ["large_yellow_circle"],
}

ACTION_EMOJI = ":white_circle:"
ACTION_EMOJI = ["white_circle"]

CATEGORY_TO_EMOJI = {
GroupCategory.PERFORMANCE: ":large_blue_circle: :chart_with_upwards_trend:",
GroupCategory.FEEDBACK: ":large_blue_circle: :busts_in_silhouette:",
GroupCategory.CRON: ":large_yellow_circle: :spiral_calendar_pad:",
GroupCategory.PERFORMANCE: ["large_blue_circle", "chart_with_upwards_trend"],
GroupCategory.FEEDBACK: ["large_blue_circle", "busts_in_silhouette"],
GroupCategory.CRON: ["large_yellow_circle", "spiral_calendar_pad"],
}

ACTIONED_CATEGORY_TO_EMOJI = {
GroupCategory.PERFORMANCE: ACTION_EMOJI + " :chart_with_upwards_trend:",
GroupCategory.FEEDBACK: ACTION_EMOJI + " :busts_in_silhouette:",
GroupCategory.CRON: ACTION_EMOJI + " :spiral_calendar_pad:",
ACTIONED_CATEGORY_TO_EMOJI: dict[GroupCategory, list[str]] = {
GroupCategory.PERFORMANCE: [ACTION_EMOJI[0], "chart_with_upwards_trend"],
GroupCategory.FEEDBACK: [ACTION_EMOJI[0], "busts_in_silhouette"],
GroupCategory.CRON: [ACTION_EMOJI[0], "spiral_calendar_pad"],
}
27 changes: 17 additions & 10 deletions src/sentry/testutils/cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -2822,14 +2822,17 @@ def assert_performance_issue_blocks_with_culprit_blocks(
alert_type: FineTuningAPIKey = FineTuningAPIKey.WORKFLOW,
issue_link_extra_params=None,
):
notification_uuid = self.get_notification_uuid(blocks[1]["text"]["text"])
notification_uuid = self.get_notification_uuid(
blocks[1]["elements"][0]["elements"][-1]["url"]
)
issue_link = f"http://testserver/organizations/{org.slug}/issues/{group.id}/?referrer={referrer}&notification_uuid={notification_uuid}"
if issue_link_extra_params is not None:
issue_link += issue_link_extra_params
assert (
blocks[1]["text"]["text"]
== f":large_blue_circle: :chart_with_upwards_trend: <{issue_link}|*N+1 Query*>"
)
emoji = "large_blue_circle"
text = "N+1 Query"
assert blocks[1]["elements"][0]["elements"][0]["name"] == emoji
assert blocks[1]["elements"][0]["elements"][-1]["url"] == issue_link
assert blocks[1]["elements"][0]["elements"][-1]["text"] == text
assert blocks[2]["elements"][0]["text"] == "/books/"
assert (
blocks[3]["text"]["text"]
Expand All @@ -2853,14 +2856,18 @@ def assert_generic_issue_blocks(
issue_link_extra_params=None,
with_culprit=False,
):
notification_uuid = self.get_notification_uuid(blocks[1]["text"]["text"])
notification_uuid = self.get_notification_uuid(
blocks[1]["elements"][0]["elements"][-1]["url"]
)
issue_link = f"http://testserver/organizations/{org.slug}/issues/{group.id}/?referrer={referrer}&notification_uuid={notification_uuid}"
if issue_link_extra_params is not None:
issue_link += issue_link_extra_params
assert (
blocks[1]["text"]["text"]
== f":red_circle: <{issue_link}|*{TEST_ISSUE_OCCURRENCE.issue_title}*>"
)
emoji = "red_circle"
text = f"{TEST_ISSUE_OCCURRENCE.issue_title}"
assert blocks[1]["elements"][0]["elements"][0]["name"] == emoji
assert blocks[1]["elements"][0]["elements"][-1]["url"] == issue_link
assert blocks[1]["elements"][0]["elements"][-1]["text"] == text

if with_culprit:
assert blocks[2]["elements"][0]["text"] == "raven.tasks.run_a_test"
evidence_index = 3
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,12 @@ def test_after(self, mock_api_call, mock_post, mock_record):
blocks = mock_post.call_args.kwargs["blocks"]
blocks = orjson.loads(blocks)

assert (
blocks[0]["text"]["text"]
== f":large_yellow_circle: <http://testserver/organizations/{self.organization.slug}/issues/{self.event.group.id}/?referrer=slack|*Hello world*>"
)
emoji = "large_yellow_circle"
url = f"http://testserver/organizations/{self.organization.slug}/issues/{self.event.group.id}/?referrer=slack"
text = "Hello world"
assert blocks[0]["elements"][0]["elements"][0]["name"] == emoji
assert blocks[0]["elements"][0]["elements"][-1]["url"] == url
assert blocks[0]["elements"][0]["elements"][-1]["text"] == text

assert NotificationMessage.objects.all().count() == 0

Expand Down Expand Up @@ -139,10 +141,12 @@ def test_after_slo_halt(self, mock_post, mock_record):
blocks = mock_post.call_args.kwargs["blocks"]
blocks = orjson.loads(blocks)

assert (
blocks[0]["text"]["text"]
== f":large_yellow_circle: <http://testserver/organizations/{self.organization.slug}/issues/{self.event.group.id}/?referrer=slack|*Hello world*>"
)
emoji = "large_yellow_circle"
url = f"http://testserver/organizations/{self.organization.slug}/issues/{self.event.group.id}/?referrer=slack"
text = "Hello world"
assert blocks[0]["elements"][0]["elements"][0]["name"] == emoji
assert blocks[0]["elements"][0]["elements"][-1]["url"] == url
assert blocks[0]["elements"][0]["elements"][-1]["text"] == text

assert NotificationMessage.objects.all().count() == 0

Expand Down Expand Up @@ -200,10 +204,12 @@ def test_after_with_threads(self, mock_api_call, mock_post, mock_record):
blocks = mock_post.call_args.kwargs["blocks"]
blocks = orjson.loads(blocks)

assert (
blocks[0]["text"]["text"]
== f":large_yellow_circle: <http://testserver/organizations/{self.organization.slug}/issues/{self.event.group.id}/?referrer=slack&alert_rule_id={self.rule.id}&alert_type=issue|*Hello world*>"
)
emoji = "large_yellow_circle"
url = f"http://testserver/organizations/{self.organization.slug}/issues/{self.event.group.id}/?referrer=slack&alert_rule_id={self.rule.id}&alert_type=issue"
text = "Hello world"
assert blocks[0]["elements"][0]["elements"][0]["name"] == emoji
assert blocks[0]["elements"][0]["elements"][-1]["url"] == url
assert blocks[0]["elements"][0]["elements"][-1]["text"] == text

assert NotificationMessage.objects.all().count() == 1

Expand Down Expand Up @@ -257,10 +263,12 @@ def test_after_reply_in_thread(self, mock_api_call, mock_post, mock_record):
blocks = mock_post.call_args.kwargs["blocks"]
blocks = orjson.loads(blocks)

assert (
blocks[0]["text"]["text"]
== f":large_yellow_circle: <http://testserver/organizations/{self.organization.slug}/issues/{self.event.group.id}/?referrer=slack&alert_rule_id={self.rule.id}&alert_type=issue|*Hello world*>"
)
emoji = "large_yellow_circle"
url = f"http://testserver/organizations/{self.organization.slug}/issues/{self.event.group.id}/?referrer=slack&alert_rule_id={self.rule.id}&alert_type=issue"
text = "Hello world"
assert blocks[0]["elements"][0]["elements"][0]["name"] == emoji
assert blocks[0]["elements"][0]["elements"][-1]["url"] == url
assert blocks[0]["elements"][0]["elements"][-1]["text"] == text

assert NotificationMessage.objects.all().count() == 2
assert (
Expand Down Expand Up @@ -302,10 +310,13 @@ def test_after_noa(self, mock_api_call, mock_post, mock_record):
blocks = mock_post.call_args.kwargs["blocks"]
blocks = orjson.loads(blocks)

assert (
blocks[0]["text"]["text"]
== f":large_yellow_circle: <http://testserver/organizations/{self.organization.slug}/issues/{self.event.group.id}/?referrer=slack&alert_rule_id={action_data['legacy_rule_id']}&alert_type=issue|*Hello world*>"
)
emoji = "large_yellow_circle"
url = f"http://testserver/organizations/{self.organization.slug}/issues/{self.event.group.id}/?referrer=slack&alert_rule_id={action_data['legacy_rule_id']}&alert_type=issue"
text = "Hello world"

assert blocks[0]["elements"][0]["elements"][0]["name"] == emoji
assert blocks[0]["elements"][0]["elements"][-1]["url"] == url
assert blocks[0]["elements"][0]["elements"][-1]["text"] == text

assert NotificationMessage.objects.all().count() == 1

Expand Down Expand Up @@ -345,10 +356,13 @@ def test_after_noa_test_action(self, mock_api_call, mock_post, mock_record):
blocks = mock_post.call_args.kwargs["blocks"]
blocks = orjson.loads(blocks)

assert (
blocks[0]["text"]["text"]
== f":large_yellow_circle: <http://testserver/organizations/{self.organization.slug}/issues/{self.event.group.id}/?referrer=slack&alert_rule_id={action_data['legacy_rule_id']}&alert_type=issue|*Hello world*>"
)
emoji = "large_yellow_circle"
url = f"http://testserver/organizations/{self.organization.slug}/issues/{self.event.group.id}/?referrer=slack&alert_rule_id={action_data['legacy_rule_id']}&alert_type=issue"
text = "Hello world"

assert blocks[0]["elements"][0]["elements"][0]["name"] == emoji
assert blocks[0]["elements"][0]["elements"][-1]["url"] == url
assert blocks[0]["elements"][0]["elements"][-1]["text"] == text

# Test action should not create a notification message
assert NotificationMessage.objects.all().count() == 0
Expand Down Expand Up @@ -386,10 +400,13 @@ def test_after_noa_new_ui(self, mock_api_call, mock_post, mock_record):
blocks = mock_post.call_args.kwargs["blocks"]
blocks = orjson.loads(blocks)

assert (
blocks[0]["text"]["text"]
== f":large_yellow_circle: <http://testserver/organizations/{self.organization.slug}/issues/{self.event.group.id}/?referrer=slack&workflow_id={action_data['workflow_id']}&alert_type=issue|*Hello world*>"
)
emoji = "large_yellow_circle"
url = f"http://testserver/organizations/{self.organization.slug}/issues/{self.event.group.id}/?referrer=slack&workflow_id={action_data['workflow_id']}&alert_type=issue"
text = "Hello world"

assert blocks[0]["elements"][0]["elements"][0]["name"] == emoji
assert blocks[0]["elements"][0]["elements"][-1]["url"] == url
assert blocks[0]["elements"][0]["elements"][-1]["text"] == text

assert NotificationMessage.objects.all().count() == 1

Expand Down Expand Up @@ -429,10 +446,12 @@ def test_after_with_threads_noa(self, mock_api_call, mock_post, mock_record):
blocks = mock_post.call_args.kwargs["blocks"]
blocks = orjson.loads(blocks)

assert (
blocks[0]["text"]["text"]
== f":large_yellow_circle: <http://testserver/organizations/{self.organization.slug}/issues/{self.event.group.id}/?referrer=slack&alert_rule_id={action_data['legacy_rule_id']}&alert_type=issue|*Hello world*>"
)
emoji = "large_yellow_circle"
url = f"http://testserver/organizations/{self.organization.slug}/issues/{self.event.group.id}/?referrer=slack&alert_rule_id={action_data['legacy_rule_id']}&alert_type=issue"
text = "Hello world"
assert blocks[0]["elements"][0]["elements"][0]["name"] == emoji
assert blocks[0]["elements"][0]["elements"][-1]["url"] == url
assert blocks[0]["elements"][0]["elements"][-1]["text"] == text

assert NotificationMessage.objects.all().count() == 1

Expand Down Expand Up @@ -488,10 +507,12 @@ def test_after_reply_in_thread_noa(self, mock_api_call, mock_post, mock_record):
blocks = mock_post.call_args.kwargs["blocks"]
blocks = orjson.loads(blocks)

assert (
blocks[0]["text"]["text"]
== f":large_yellow_circle: <http://testserver/organizations/{self.organization.slug}/issues/{self.event.group.id}/?referrer=slack&alert_rule_id={action_data['legacy_rule_id']}&alert_type=issue|*Hello world*>"
)
emoji = "large_yellow_circle"
url = f"http://testserver/organizations/{self.organization.slug}/issues/{self.event.group.id}/?referrer=slack&alert_rule_id={action_data['legacy_rule_id']}&alert_type=issue"
text = "Hello world"
assert blocks[0]["elements"][0]["elements"][0]["name"] == emoji
assert blocks[0]["elements"][0]["elements"][-1]["url"] == url
assert blocks[0]["elements"][0]["elements"][-1]["text"] == text

assert NotificationMessage.objects.all().count() == 2
assert (
Expand Down
14 changes: 10 additions & 4 deletions tests/sentry/integrations/slack/notifications/test_assigned.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,17 @@ def test_assignment_block(self):
fallback_text = self.mock_post.call_args.kwargs["text"]
assert fallback_text == f"Issue assigned to {self.name} by themselves"
assert blocks[0]["text"]["text"] == fallback_text
notification_uuid = self.get_notification_uuid(blocks[1]["text"]["text"])
assert (
blocks[1]["text"]["text"]
== f":red_circle: <http://testserver/organizations/{self.organization.slug}/issues/{self.group.id}/?referrer=assigned_activity-slack&notification_uuid={notification_uuid}|*{self.group.title}*>"

notification_uuid = self.get_notification_uuid(
blocks[1]["elements"][0]["elements"][-1]["url"]
)
emoji = "red_circle"
url = f"http://testserver/organizations/{self.organization.slug}/issues/{self.group.id}/?referrer=assigned_activity-slack&notification_uuid={notification_uuid}"
text = f"{self.group.title}"
assert blocks[1]["elements"][0]["elements"][0]["name"] == emoji
assert blocks[1]["elements"][0]["elements"][-1]["url"] == url
assert blocks[1]["elements"][0]["elements"][-1]["text"] == text

assert (
blocks[3]["elements"][0]["text"]
== f"{self.project.slug} | <http://testserver/settings/account/notifications/workflow/?referrer=assigned_activity-slack-user&notification_uuid={notification_uuid}&organizationId={self.organization.id}|Notification Settings>"
Expand Down
Loading
Loading