Skip to content

Commit 338f454

Browse files
authored
fix(notifications): Convert slack notification title to rich text (#91637)
Converting the title for slack notifications to use rich text instead of markdown so the issue title link works for iOS. This is a [known issue](slackapi/bolt-js#2103) with Slack's iOS app. Visually, there should be no change here. Fixes #82961 <img width="447" alt="image" src="https://github.com/user-attachments/assets/86a29f7d-d0b1-4dbb-956a-3125aff2dc8b" /> <img width="422" alt="image" src="https://github.com/user-attachments/assets/d87428c7-766d-44a6-982f-abaaecdf7e05" />
1 parent 3403ed9 commit 338f454

File tree

19 files changed

+347
-180
lines changed

19 files changed

+347
-180
lines changed

src/sentry/integrations/slack/message_builder/base/block.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,31 @@ def get_markdown_block(text: str, emoji: str | None = None) -> SlackBlock:
3939
"text": {"type": "mrkdwn", "text": text},
4040
}
4141

42+
@staticmethod
43+
def get_rich_text_link(emojis: list[str], text: str, link: str | None = None) -> SlackBlock:
44+
elements: list[dict[str, Any]] = []
45+
for emoji in emojis:
46+
elements.append({"type": "emoji", "name": emoji})
47+
elements.append({"type": "text", "text": " "})
48+
49+
url: dict[str, Any] = {}
50+
if link:
51+
url = {
52+
"type": "link",
53+
"url": link,
54+
"text": text,
55+
"style": {"bold": True},
56+
}
57+
else:
58+
url = {"type": "text", "text": text}
59+
60+
elements.append(url)
61+
62+
return {
63+
"type": "rich_text",
64+
"elements": [{"type": "rich_text_section", "elements": elements}],
65+
}
66+
4267
@staticmethod
4368
def get_markdown_quote_block(text: str, max_block_text_length: int) -> SlackBlock:
4469
if len(text) > max_block_text_length:

src/sentry/integrations/slack/message_builder/issues.py

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -458,30 +458,28 @@ def get_title_block(
458458
) -> SlackBlock:
459459
summary_headline = self.get_issue_summary_headline(event_or_group)
460460
title = summary_headline or build_attachment_title(event_or_group)
461-
title_emoji = self.get_title_emoji(has_action)
461+
title_emojis = self.get_title_emoji(has_action)
462462

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

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

469-
title_emoji = None
468+
title_emojis: list[str] = []
470469
if has_action:
471470
# if issue is resolved, archived, or assigned, replace circle emojis with white circle
472-
title_emoji = (
471+
title_emojis = (
473472
ACTION_EMOJI
474473
if is_error_issue
475-
else ACTIONED_CATEGORY_TO_EMOJI.get(self.group.issue_category)
474+
else ACTIONED_CATEGORY_TO_EMOJI.get(self.group.issue_category, [])
476475
)
477476
elif is_error_issue:
478477
level_text = LOG_LEVELS[self.group.level]
479-
title_emoji = LEVEL_TO_EMOJI.get(level_text)
478+
title_emojis = LEVEL_TO_EMOJI.get(level_text, [])
480479
else:
481-
title_emoji = CATEGORY_TO_EMOJI.get(self.group.issue_category)
480+
title_emojis = CATEGORY_TO_EMOJI.get(self.group.issue_category, [])
482481

483-
title_emoji = title_emoji + " " if title_emoji else ""
484-
return title_emoji
482+
return title_emojis
485483

486484
def get_issue_summary_headline(self, event_or_group: Event | GroupEvent | Group) -> str | None:
487485
if self.issue_summary is None:

src/sentry/integrations/slack/message_builder/types.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,24 @@
1616
SLACK_URL_FORMAT = "<{url}|{text}>"
1717

1818
LEVEL_TO_EMOJI = {
19-
"_incident_resolved": ":green_circle:",
20-
"debug": ":bug:",
21-
"error": ":red_circle:",
22-
"fatal": ":red_circle:",
23-
"info": ":large_blue_circle:",
24-
"warning": ":large_yellow_circle:",
19+
"_incident_resolved": ["green_circle"],
20+
"debug": ["bug"],
21+
"error": ["red_circle"],
22+
"fatal": ["red_circle"],
23+
"info": ["large_blue_circle"],
24+
"warning": ["large_yellow_circle"],
2525
}
2626

27-
ACTION_EMOJI = ":white_circle:"
27+
ACTION_EMOJI = ["white_circle"]
2828

2929
CATEGORY_TO_EMOJI = {
30-
GroupCategory.PERFORMANCE: ":large_blue_circle: :chart_with_upwards_trend:",
31-
GroupCategory.FEEDBACK: ":large_blue_circle: :busts_in_silhouette:",
32-
GroupCategory.CRON: ":large_yellow_circle: :spiral_calendar_pad:",
30+
GroupCategory.PERFORMANCE: ["large_blue_circle", "chart_with_upwards_trend"],
31+
GroupCategory.FEEDBACK: ["large_blue_circle", "busts_in_silhouette"],
32+
GroupCategory.CRON: ["large_yellow_circle", "spiral_calendar_pad"],
3333
}
3434

35-
ACTIONED_CATEGORY_TO_EMOJI = {
36-
GroupCategory.PERFORMANCE: ACTION_EMOJI + " :chart_with_upwards_trend:",
37-
GroupCategory.FEEDBACK: ACTION_EMOJI + " :busts_in_silhouette:",
38-
GroupCategory.CRON: ACTION_EMOJI + " :spiral_calendar_pad:",
35+
ACTIONED_CATEGORY_TO_EMOJI: dict[GroupCategory, list[str]] = {
36+
GroupCategory.PERFORMANCE: [ACTION_EMOJI[0], "chart_with_upwards_trend"],
37+
GroupCategory.FEEDBACK: [ACTION_EMOJI[0], "busts_in_silhouette"],
38+
GroupCategory.CRON: [ACTION_EMOJI[0], "spiral_calendar_pad"],
3939
}

src/sentry/testutils/cases.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2822,14 +2822,17 @@ def assert_performance_issue_blocks_with_culprit_blocks(
28222822
alert_type: FineTuningAPIKey = FineTuningAPIKey.WORKFLOW,
28232823
issue_link_extra_params=None,
28242824
):
2825-
notification_uuid = self.get_notification_uuid(blocks[1]["text"]["text"])
2825+
notification_uuid = self.get_notification_uuid(
2826+
blocks[1]["elements"][0]["elements"][-1]["url"]
2827+
)
28262828
issue_link = f"http://testserver/organizations/{org.slug}/issues/{group.id}/?referrer={referrer}&notification_uuid={notification_uuid}"
28272829
if issue_link_extra_params is not None:
28282830
issue_link += issue_link_extra_params
2829-
assert (
2830-
blocks[1]["text"]["text"]
2831-
== f":large_blue_circle: :chart_with_upwards_trend: <{issue_link}|*N+1 Query*>"
2832-
)
2831+
emoji = "large_blue_circle"
2832+
text = "N+1 Query"
2833+
assert blocks[1]["elements"][0]["elements"][0]["name"] == emoji
2834+
assert blocks[1]["elements"][0]["elements"][-1]["url"] == issue_link
2835+
assert blocks[1]["elements"][0]["elements"][-1]["text"] == text
28332836
assert blocks[2]["elements"][0]["text"] == "/books/"
28342837
assert (
28352838
blocks[3]["text"]["text"]
@@ -2853,14 +2856,18 @@ def assert_generic_issue_blocks(
28532856
issue_link_extra_params=None,
28542857
with_culprit=False,
28552858
):
2856-
notification_uuid = self.get_notification_uuid(blocks[1]["text"]["text"])
2859+
notification_uuid = self.get_notification_uuid(
2860+
blocks[1]["elements"][0]["elements"][-1]["url"]
2861+
)
28572862
issue_link = f"http://testserver/organizations/{org.slug}/issues/{group.id}/?referrer={referrer}&notification_uuid={notification_uuid}"
28582863
if issue_link_extra_params is not None:
28592864
issue_link += issue_link_extra_params
2860-
assert (
2861-
blocks[1]["text"]["text"]
2862-
== f":red_circle: <{issue_link}|*{TEST_ISSUE_OCCURRENCE.issue_title}*>"
2863-
)
2865+
emoji = "red_circle"
2866+
text = f"{TEST_ISSUE_OCCURRENCE.issue_title}"
2867+
assert blocks[1]["elements"][0]["elements"][0]["name"] == emoji
2868+
assert blocks[1]["elements"][0]["elements"][-1]["url"] == issue_link
2869+
assert blocks[1]["elements"][0]["elements"][-1]["text"] == text
2870+
28642871
if with_culprit:
28652872
assert blocks[2]["elements"][0]["text"] == "raven.tasks.run_a_test"
28662873
evidence_index = 3

tests/sentry/integrations/slack/actions/notification/test_slack_notify_service_action.py

Lines changed: 57 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,12 @@ def test_after(self, mock_api_call, mock_post, mock_record):
9999
blocks = mock_post.call_args.kwargs["blocks"]
100100
blocks = orjson.loads(blocks)
101101

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

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

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

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

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

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

203-
assert (
204-
blocks[0]["text"]["text"]
205-
== 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*>"
206-
)
207+
emoji = "large_yellow_circle"
208+
url = f"http://testserver/organizations/{self.organization.slug}/issues/{self.event.group.id}/?referrer=slack&alert_rule_id={self.rule.id}&alert_type=issue"
209+
text = "Hello world"
210+
assert blocks[0]["elements"][0]["elements"][0]["name"] == emoji
211+
assert blocks[0]["elements"][0]["elements"][-1]["url"] == url
212+
assert blocks[0]["elements"][0]["elements"][-1]["text"] == text
207213

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

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

260-
assert (
261-
blocks[0]["text"]["text"]
262-
== 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*>"
263-
)
266+
emoji = "large_yellow_circle"
267+
url = f"http://testserver/organizations/{self.organization.slug}/issues/{self.event.group.id}/?referrer=slack&alert_rule_id={self.rule.id}&alert_type=issue"
268+
text = "Hello world"
269+
assert blocks[0]["elements"][0]["elements"][0]["name"] == emoji
270+
assert blocks[0]["elements"][0]["elements"][-1]["url"] == url
271+
assert blocks[0]["elements"][0]["elements"][-1]["text"] == text
264272

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

305-
assert (
306-
blocks[0]["text"]["text"]
307-
== 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*>"
308-
)
313+
emoji = "large_yellow_circle"
314+
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"
315+
text = "Hello world"
316+
317+
assert blocks[0]["elements"][0]["elements"][0]["name"] == emoji
318+
assert blocks[0]["elements"][0]["elements"][-1]["url"] == url
319+
assert blocks[0]["elements"][0]["elements"][-1]["text"] == text
309320

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

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

348-
assert (
349-
blocks[0]["text"]["text"]
350-
== 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*>"
351-
)
359+
emoji = "large_yellow_circle"
360+
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"
361+
text = "Hello world"
362+
363+
assert blocks[0]["elements"][0]["elements"][0]["name"] == emoji
364+
assert blocks[0]["elements"][0]["elements"][-1]["url"] == url
365+
assert blocks[0]["elements"][0]["elements"][-1]["text"] == text
352366

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

389-
assert (
390-
blocks[0]["text"]["text"]
391-
== 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*>"
392-
)
403+
emoji = "large_yellow_circle"
404+
url = f"http://testserver/organizations/{self.organization.slug}/issues/{self.event.group.id}/?referrer=slack&workflow_id={action_data['workflow_id']}&alert_type=issue"
405+
text = "Hello world"
406+
407+
assert blocks[0]["elements"][0]["elements"][0]["name"] == emoji
408+
assert blocks[0]["elements"][0]["elements"][-1]["url"] == url
409+
assert blocks[0]["elements"][0]["elements"][-1]["text"] == text
393410

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

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

432-
assert (
433-
blocks[0]["text"]["text"]
434-
== 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*>"
435-
)
449+
emoji = "large_yellow_circle"
450+
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"
451+
text = "Hello world"
452+
assert blocks[0]["elements"][0]["elements"][0]["name"] == emoji
453+
assert blocks[0]["elements"][0]["elements"][-1]["url"] == url
454+
assert blocks[0]["elements"][0]["elements"][-1]["text"] == text
436455

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

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

491-
assert (
492-
blocks[0]["text"]["text"]
493-
== 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*>"
494-
)
510+
emoji = "large_yellow_circle"
511+
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"
512+
text = "Hello world"
513+
assert blocks[0]["elements"][0]["elements"][0]["name"] == emoji
514+
assert blocks[0]["elements"][0]["elements"][-1]["url"] == url
515+
assert blocks[0]["elements"][0]["elements"][-1]["text"] == text
495516

496517
assert NotificationMessage.objects.all().count() == 2
497518
assert (

tests/sentry/integrations/slack/notifications/test_assigned.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,17 @@ def test_assignment_block(self):
9898
fallback_text = self.mock_post.call_args.kwargs["text"]
9999
assert fallback_text == f"Issue assigned to {self.name} by themselves"
100100
assert blocks[0]["text"]["text"] == fallback_text
101-
notification_uuid = self.get_notification_uuid(blocks[1]["text"]["text"])
102-
assert (
103-
blocks[1]["text"]["text"]
104-
== f":red_circle: <http://testserver/organizations/{self.organization.slug}/issues/{self.group.id}/?referrer=assigned_activity-slack&notification_uuid={notification_uuid}|*{self.group.title}*>"
101+
102+
notification_uuid = self.get_notification_uuid(
103+
blocks[1]["elements"][0]["elements"][-1]["url"]
105104
)
105+
emoji = "red_circle"
106+
url = f"http://testserver/organizations/{self.organization.slug}/issues/{self.group.id}/?referrer=assigned_activity-slack&notification_uuid={notification_uuid}"
107+
text = f"{self.group.title}"
108+
assert blocks[1]["elements"][0]["elements"][0]["name"] == emoji
109+
assert blocks[1]["elements"][0]["elements"][-1]["url"] == url
110+
assert blocks[1]["elements"][0]["elements"][-1]["text"] == text
111+
106112
assert (
107113
blocks[3]["elements"][0]["text"]
108114
== 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>"

0 commit comments

Comments
 (0)