Skip to content

Commit 9342ad3

Browse files
feat(notif platform): Impl basic Slack renderer (#97623)
1 parent 784c5e5 commit 9342ad3

File tree

4 files changed

+100
-14
lines changed

4 files changed

+100
-14
lines changed

src/sentry/notifications/platform/slack/provider.py

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
1-
from typing import Any
1+
from typing import TypedDict
2+
3+
from slack_sdk.models.blocks import (
4+
ActionsBlock,
5+
Block,
6+
ButtonElement,
7+
HeaderBlock,
8+
ImageBlock,
9+
MarkdownTextObject,
10+
PlainTextObject,
11+
SectionBlock,
12+
)
213

314
from sentry.notifications.platform.provider import NotificationProvider
415
from sentry.notifications.platform.registry import provider_registry
@@ -13,18 +24,39 @@
1324
)
1425
from sentry.organizations.services.organization.model import RpcOrganizationSummary
1526

16-
# TODO(ecosystem): Figure out a way to use 'SlackBlock' type
17-
type SlackRenderable = Any
27+
28+
class SlackRenderable(TypedDict):
29+
blocks: list[Block]
1830

1931

2032
class SlackRenderer(NotificationRenderer[SlackRenderable]):
21-
provider_key = NotificationProviderKey.DISCORD
33+
provider_key = NotificationProviderKey.SLACK
2234

2335
@classmethod
2436
def render[DataT: NotificationData](
2537
cls, *, data: DataT, rendered_template: NotificationRenderedTemplate
2638
) -> SlackRenderable:
27-
return {}
39+
subject = HeaderBlock(text=PlainTextObject(text=rendered_template.subject))
40+
body = SectionBlock(text=MarkdownTextObject(text=rendered_template.body))
41+
42+
blocks = [subject, body]
43+
44+
if len(rendered_template.actions) > 0:
45+
actions_block = ActionsBlock(elements=[])
46+
for action in rendered_template.actions:
47+
actions_block.elements.append(ButtonElement(text=action.label, url=action.link))
48+
blocks.append(actions_block)
49+
50+
if rendered_template.footer:
51+
footer = SectionBlock(text=MarkdownTextObject(text=rendered_template.footer))
52+
blocks.append(footer)
53+
if rendered_template.chart:
54+
chart = ImageBlock(
55+
image_url=rendered_template.chart.url, alt_text=rendered_template.chart.alt_text
56+
)
57+
blocks.append(chart)
58+
59+
return SlackRenderable(blocks=blocks)
2860

2961

3062
@provider_registry.register(NotificationProviderKey.SLACK)

src/sentry/notifications/platform/types.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
from __future__ import annotations
22

33
import abc
4-
from dataclasses import dataclass
4+
from dataclasses import dataclass, field
55
from enum import StrEnum
6-
from typing import Any, Protocol, TypedDict
6+
from typing import Any, Protocol
77

88
from sentry.integrations.types import ExternalProviderEnum
99

@@ -80,7 +80,8 @@ class NotificationData(Protocol):
8080
"""
8181

8282

83-
class NotificationRenderedAction(TypedDict):
83+
@dataclass(frozen=True)
84+
class NotificationRenderedAction:
8485
"""
8586
A rendered action for an integration.
8687
"""
@@ -96,6 +97,22 @@ class NotificationRenderedAction(TypedDict):
9697
"""
9798

9899

100+
@dataclass(frozen=True)
101+
class NotificationRenderedImage:
102+
"""
103+
An image that will be displayed in the notification.
104+
"""
105+
106+
url: str
107+
"""
108+
The URL of the image.
109+
"""
110+
alt_text: str
111+
"""
112+
The alt text of the image.
113+
"""
114+
115+
99116
@dataclass(frozen=True)
100117
class NotificationRenderedTemplate:
101118
subject: str
@@ -110,13 +127,13 @@ class NotificationRenderedTemplate:
110127
keeping it concise and useful to the receiver. This string should not contain any formatting,
111128
and will be displayed as is.
112129
"""
113-
actions: list[NotificationRenderedAction]
130+
actions: list[NotificationRenderedAction] = field(default_factory=list)
114131
"""
115132
The list of actions that a receiver may take after having received the notification.
116133
"""
117-
chart: str | None = None
134+
chart: NotificationRenderedImage | None = None
118135
"""
119-
The accessible URL of a chart that will be displayed in the notification.
136+
The image that will be displayed in the notification.
120137
"""
121138
footer: str | None = None
122139
"""

src/sentry/testutils/notifications/platform.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from sentry.notifications.platform.types import (
55
NotificationCategory,
66
NotificationData,
7+
NotificationRenderedAction,
8+
NotificationRenderedImage,
79
NotificationRenderedTemplate,
810
NotificationStrategy,
911
NotificationTarget,
@@ -25,16 +27,28 @@ def render(self, data: MockNotification) -> NotificationRenderedTemplate:
2527
return NotificationRenderedTemplate(
2628
subject="Mock Notification",
2729
body=data.message,
28-
actions=[{"label": "Visit Sentry", "link": "https://www.sentry.io"}],
30+
actions=[
31+
NotificationRenderedAction(label="Visit Sentry", link="https://www.sentry.io")
32+
],
2933
footer="This is a mock footer",
34+
chart=NotificationRenderedImage(
35+
url="https://raw.githubusercontent.com/knobiknows/all-the-bufo/main/all-the-bufo/bufo-pog.png",
36+
alt_text="Bufo Pog",
37+
),
3038
)
3139

3240
def render_example(self) -> NotificationRenderedTemplate:
3341
return NotificationRenderedTemplate(
3442
subject="Mock Notification",
3543
body="This is a mock notification",
36-
actions=[{"label": "Visit Sentry", "link": "https://www.sentry.io"}],
44+
actions=[
45+
NotificationRenderedAction(label="Visit Sentry", link="https://www.sentry.io")
46+
],
3747
footer="This is a mock footer",
48+
chart=NotificationRenderedImage(
49+
url="https://raw.githubusercontent.com/knobiknows/all-the-bufo/main/all-the-bufo/bufo-pog.png",
50+
alt_text="Bufo Pog",
51+
),
3852
)
3953

4054

tests/sentry/notifications/platform/slack/test_provider.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,30 @@ def test_default_renderer(self) -> None:
1717
renderer = SlackNotificationProvider.get_renderer(
1818
data=data, category=NotificationCategory.DEBUG
1919
)
20-
assert renderer.render(data=data, rendered_template=rendered_template) == {}
20+
21+
rendererable = renderer.render(data=data, rendered_template=rendered_template)
22+
rendererable_dict = [block.to_dict() for block in rendererable.get("blocks", [])]
23+
24+
assert rendererable_dict == [
25+
{"text": {"text": "Mock Notification", "type": "plain_text"}, "type": "header"},
26+
{"text": {"text": "test", "type": "mrkdwn"}, "type": "section"},
27+
{
28+
"elements": [
29+
{
30+
"text": {"emoji": True, "text": "Visit Sentry", "type": "plain_text"},
31+
"type": "button",
32+
"url": "https://www.sentry.io",
33+
}
34+
],
35+
"type": "actions",
36+
},
37+
{"text": {"text": "This is a mock footer", "type": "mrkdwn"}, "type": "section"},
38+
{
39+
"image_url": "https://raw.githubusercontent.com/knobiknows/all-the-bufo/main/all-the-bufo/bufo-pog.png",
40+
"alt_text": "Bufo Pog",
41+
"type": "image",
42+
},
43+
]
2144

2245

2346
class SlackNotificationProviderTest(TestCase):

0 commit comments

Comments
 (0)