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 2 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
14 changes: 14 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,20 @@ 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) -> SlackBlock:
elements = []
for emoji in emojis:
elements.append({"type": "emoji", "name": emoji})
elements.append({"type": "text", "text": " "})

elements.append({"type": "link", "url": link, "text": text, "style": {"bold": True}})

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
18 changes: 8 additions & 10 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 = []
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)
)
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
26 changes: 13 additions & 13 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:",
GroupCategory.PERFORMANCE: [ACTION_EMOJI, "chart_with_upwards_trend"],
GroupCategory.FEEDBACK: [ACTION_EMOJI, "busts_in_silhouette"],
GroupCategory.CRON: [ACTION_EMOJI, "spiral_calendar_pad"],
}
42 changes: 29 additions & 13 deletions tests/sentry/integrations/slack/test_message_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,6 @@ def build_test_message_blocks(
else:
title_link += f"&alert_rule_id={rule.id}&alert_type=issue"

title_text = f":red_circle: <{title_link}|*{formatted_title}*>"

if rule:
if legacy_rule_id:
block_id = f'{{"issue":{group.id},"rule":{legacy_rule_id}}}'
Expand All @@ -115,8 +113,22 @@ def build_test_message_blocks(

blocks: list[dict[str, Any]] = [
{
"type": "section",
"text": {"type": "mrkdwn", "text": title_text},
"type": "rich_text",
"elements": [
{
"type": "rich_text_section",
"elements": [
{"type": "emoji", "name": "red_circle"},
{"type": "text", "text": " "},
{
"type": "link",
"url": title_link,
"text": formatted_title,
"style": {"bold": True},
},
],
}
],
"block_id": block_id,
},
]
Expand Down Expand Up @@ -848,7 +860,8 @@ def test_build_performance_issue(self):
with self.feature("organizations:performance-issues"):
blocks = SlackIssuesMessageBuilder(event.group, event).build()
assert isinstance(blocks, dict)
assert "N+1 Query" in blocks["blocks"][0]["text"]["text"]
title_text = blocks["blocks"][0]["elements"][0]["elements"][-1]["text"]
assert "N+1 Query" in title_text
assert (
"db - SELECT `books_author`.`id`, `books_author`.`name` FROM `books_author` WHERE `books_author`.`id` = %s LIMIT 21"
in blocks["blocks"][2]["text"]["text"]
Expand Down Expand Up @@ -996,8 +1009,9 @@ def test_build_group_block_with_ai_summary_with_feature_flag(
mock_get_summary.assert_called_once_with(group, source="alert")

# Verify that the original title is \\ present
assert "IntegrationError" in blocks["blocks"][0]["text"]["text"]
assert "Identity not found" in blocks["blocks"][0]["text"]["text"]
title_text = blocks["blocks"][0]["elements"][0]["elements"][-1]["text"]
assert "IntegrationError" in title_text
assert "Identity not found" in title_text

# Verify that the AI content is used in the context block
content_block = blocks["blocks"][1]["elements"][0]["text"]
Expand Down Expand Up @@ -1042,7 +1056,8 @@ def test_build_group_block_with_ai_summary_without_feature_flag(
with patch(patch_path) as mock_get_summary:
mock_get_summary.assert_not_called()
blocks = SlackIssuesMessageBuilder(group).build()
assert "IntegrationError" in blocks["blocks"][0]["text"]["text"]
title_text = blocks["blocks"][0]["elements"][0]["elements"][-1]["text"]
assert "IntegrationError" in title_text

@override_options({"alerts.issue_summary_timeout": 5})
@with_feature({"organizations:gen-ai-features", "projects:trigger-issue-summary-on-alerts"})
Expand Down Expand Up @@ -1126,7 +1141,7 @@ def test_build_group_block_with_ai_summary_text_truncation(
):
mock_get_summary.return_value = (mock_summary, 200)
blocks = SlackIssuesMessageBuilder(group1, event1.for_group(group1)).build()
title_text = blocks["blocks"][0]["text"]["text"]
title_text = blocks["blocks"][0]["elements"][0]["elements"][-1]["text"]

assert "First line of text..." in title_text
assert "Second line" not in title_text
Expand All @@ -1138,7 +1153,7 @@ def test_build_group_block_with_ai_summary_text_truncation(
):
mock_get_summary.return_value = (mock_summary, 200)
blocks = SlackIssuesMessageBuilder(group2, event2.for_group(group2)).build()
title_text = blocks["blocks"][0]["text"]["text"]
title_text = blocks["blocks"][0]["elements"][0]["elements"][-1]["text"]

expected_truncated = long_text[:MAX_SUMMARY_HEADLINE_LENGTH] + "..."
assert expected_truncated in title_text
Expand Down Expand Up @@ -1181,8 +1196,8 @@ def test_build_group_block_with_ai_summary_text_truncation(
):
mock_get_summary.return_value = (mock_summary, 200)
blocks = SlackIssuesMessageBuilder(group_lb, event_lb.for_group(group_lb)).build()
title_block = blocks["blocks"][0]["text"]["text"]
assert f": {expected_headline_part}*>" in title_block, f"Failed for {name}"
title_block = blocks["blocks"][0]["elements"][0]["elements"][-1]["text"]
assert f": {expected_headline_part}" in title_block, f"Failed for {name}"

@override_options({"alerts.issue_summary_timeout": 5})
@patch(
Expand Down Expand Up @@ -1214,7 +1229,8 @@ def test_build_group_block_with_ai_summary_without_org_acknowledgement(
mock_get_issue_summary.assert_not_called()

blocks = SlackIssuesMessageBuilder(group).build()
assert "IntegrationError" in blocks["blocks"][0]["text"]["text"]
title_text = blocks["blocks"][0]["elements"][0]["elements"][-1]["text"]
assert "IntegrationError" in title_text


class BuildGroupAttachmentReplaysTest(TestCase):
Expand Down
Loading