notification-platform
GitHubSentry通知平台开发指南,涵盖添加新通知、自定义渲染器及新增提供商。指导定义数据模板、注册组件并处理多端发送逻辑。
Trigger Scenarios
Install
npx skills add getsentry/sentry --skill notification-platform -g -y
SKILL.md
Frontmatter
{
"name": "notification-platform",
"description": "Guide for adding notifications, custom renderers, or new providers to Sentry's NotificationPlatform. Use when asked to \"add notification\", \"new notification\", \"notification platform\", \"send notification\", \"notification template\", \"notification renderer\", \"notification provider\", \"NotificationPlatform\", \"notify user\", \"send email notification\", \"send slack notification\"."
}
NotificationPlatform Guide
Sentry's NotificationPlatform is a provider-based system for sending notifications across Email, Slack, Discord, and MS Teams. You define data + template, register it, and the platform handles rendering and delivery per provider.
Glossary
| Concept | Role | Location |
|---|---|---|
NotificationData |
Protocol. Frozen dataclass carrying the payload for a single notification. Must declare a source class variable. |
types.py |
NotificationTemplate |
Abstract class. Converts NotificationData into a NotificationRenderedTemplate. Registered per NotificationSource. |
types.py |
NotificationRenderedTemplate |
Dataclass. Provider-agnostic output: subject (str or text blocks), body (sections containing text blocks), actions, chart, footer (str or text blocks), optional email paths. | types.py |
NotificationProvider |
Protocol. Knows how to validate a target, pick a renderer, and send the final renderable (Email, Slack, etc.). | provider.py |
NotificationRenderer |
Protocol. Converts a NotificationRenderedTemplate into a provider-specific renderable (HTML email, Slack blocks, etc.). |
renderer.py |
NotificationTarget |
Protocol. Identifies the recipient: email address, channel ID, or DM user ID. Two concrete classes: GenericNotificationTarget (email) and IntegrationNotificationTarget (Slack/Discord/MSTeams). |
target.py |
NotificationService |
Entry point. Orchestrates lookup, rendering, and delivery. Provides has_access(), notify_target(), notify_async(), notify_sync(). |
service.py |
All paths below are relative to src/sentry/notifications/platform/.
Step 1: Determine Your Operation
| I want to... | Go to |
|---|---|
| Add a new notification (most common) | Steps 2-5 |
| Add a custom renderer for an existing provider | Step 6 |
| Add an entirely new provider | Step 7 |
After any operation, continue to Step 8 (Test) and Step 9 (Verify).
Step 2: Define the Notification Source
Every notification needs a unique NotificationSource enum value and must be mapped to a NotificationCategory. A NotificationSource should represent the domain or feature that a given notification belongs to.
For examples, load
src/sentry/notifications/platform/types.py.
File: types.py
- Add the enum value under the appropriate category comment:
class NotificationSource(StrEnum):
# MY_CATEGORY
MY_NEW_SOURCE = "my-new-source"
- Add it to
NOTIFICATION_SOURCE_MAPunder the matching category key:
NOTIFICATION_SOURCE_MAP[NotificationCategory.MY_CATEGORY].append(
NotificationSource.MY_NEW_SOURCE
)
If no existing NotificationCategory fits, add a new one to the NotificationCategory enum first, then create its entry in NOTIFICATION_SOURCE_MAP.
All NotificationCategory options are defined in the src/sentry/notifications/platform/types.py file.
Step 3: Create the Notification Data
The data class is a frozen dataclass implementing the NotificationData protocol. It carries everything the template needs to render.
File: templates/<your_notification>.py (new file)
from dataclasses import dataclass
from sentry.notifications.platform.types import NotificationData, NotificationSource
@dataclass(frozen=True)
class MyNotificationData(NotificationData):
source = NotificationSource.MY_NEW_SOURCE # class variable, not a field
title: str
detail_url: str
Rules:
sourceis a class variable (no type annotation), not a dataclass field- Use
frozen=Truefor serialization safety - Only include fields needed by the template's
render()method - Avoid Django model instances; use primitive types or simple dataclasses for async serialization
For full examples (DataExportSuccess, DataExportFailure), load
references/data-and-templates.md.
Step 4: Create the Notification Template
The template converts your data into a provider-agnostic NotificationRenderedTemplate.
Same file as Step 3: templates/<your_notification>.py
from sentry.notifications.platform.registry import template_registry
from sentry.notifications.platform.types import (
NotificationCategory,
NotificationRenderedAction,
NotificationRenderedTemplate,
NotificationTemplate,
ParagraphSection,
PlainTextBlock,
)
@template_registry.register(MyNotificationData.source)
class MyNotificationTemplate(NotificationTemplate[MyNotificationData]):
category = NotificationCategory.MY_CATEGORY
example_data = MyNotificationData(
title="Example title",
detail_url="https://example.com",
)
def render(self, data: MyNotificationData) -> NotificationRenderedTemplate:
return NotificationRenderedTemplate(
subject=data.title,
body=[
ParagraphSection(blocks=[PlainTextBlock(text="Something happened.")])
],
actions=[
NotificationRenderedAction(label="View Details", link=data.detail_url)
],
)
Available types:
A notification is composed of sections and text blocks. Text blocks can appear in sections (for the body), and also directly in the subject and footer fields (as list[NotificationTextBlock] instead of plain str).
Sections (portions of a message, used in body):
| Section | Description |
|---|---|
ParagraphSection |
A block of text with a line break before. |
CodeSection |
A code block with a line break before. |
BlockQuoteSection |
A quoted block of text, rendered as a blockquote. |
Text blocks (parts of text — used inside sections, and also in subject and footer):
| Block | Description |
|---|---|
PlainTextBlock |
Unformatted text. |
BoldTextBlock |
Bold text. |
ItalicTextBlock |
Italic text. |
CodeTextBlock |
Inline code. |
LinkTextBlock |
A hyperlink with text and url fields. |
Refer to src/sentry/notifications/platform/types.py for the full definitions.
Register the import in templates/__init__.py:
from .my_notification import MyNotificationTemplate
This import is required so the @template_registry.register decorator executes at startup (via sentry/notifications/apps.py).
For the full rendered template field reference and more examples, load
references/data-and-templates.md.
Step 5: Register Rollout and Send
Rollout registration
The platform uses a tiered rollout system. Each notification source must be added to the appropriate rollout option before it will be delivered.
Rollout options are configured externally in sentry-options-automator (not this repo). The option keys are:
| Rollout stage | Option key |
|---|---|
| Internal testing | notifications.platform-rollout.internal-testing |
| Sentry orgs | notifications.platform-rollout.is-sentry |
| Early adopter | notifications.platform-rollout.early-adopter |
| General access | notifications.platform-rollout.general-access |
Each option is a Dict mapping source string to rollout rate (0.0-1.0). Example:
{"my-new-source": 1.0}
These options are registered in src/sentry/options/defaults.py (already done for the four stages above).
Sending pattern
from sentry.notifications.platform.service import NotificationService
from sentry.notifications.platform.target import GenericNotificationTarget
from sentry.notifications.platform.types import (
NotificationProviderKey,
NotificationTargetResourceType,
)
data = MyNotificationData(title="Export ready", detail_url="https://...")
# Guard with rollout check
if NotificationService.has_access(organization, data.source):
service = NotificationService(data=data)
target = GenericNotificationTarget(
provider_key=NotificationProviderKey.EMAIL,
resource_type=NotificationTargetResourceType.EMAIL,
resource_id=user.email,
)
service.notify_async(targets=[target])
For target types, async/sync decisions, and strategy patterns, load
references/targets-and-sending.md.
Step 6: Add a Custom Renderer
Custom renderers bypass the default template-to-renderable conversion for a specific provider + category combination. Use when the default section/block-based rendering is too limiting (e.g., interactive Slack buttons, rich card layouts).
When to use:
- The notification needs provider-specific interactive elements (buttons with action IDs, rich text blocks)
- The rendered output structure differs significantly from subject + body + actions
- You need to render different data types differently within the same provider
How it works: Override get_renderer() on the provider to return your custom renderer class for the relevant category:
# In the provider class
@classmethod
def get_renderer(
cls, *, data: NotificationData, category: NotificationCategory
) -> type[NotificationRenderer[MyRenderable]]:
if category == NotificationCategory.MY_CATEGORY:
return MyCustomRenderer
return cls.default_renderer
File placement: {provider}/renderers/{name}.py (e.g., slack/renderers/seer.py)
Links: Every Sentry URL in a notification must be decorated so clicks are tracked. The platform does this for default renderers. A custom renderer receives a link_decorator and must pass each Sentry URL it emits through link_decorator.decorate_url(url). Links taken straight from data are otherwise sent undecorated and untracked.
If a link points to a kind of Sentry page that classify_link in tracking.py doesn't recognize, add a NotificationLink value and match the page's path in classify_link. Otherwise the link is tagged other, and test_example_links_are_classified fails for the template's example_data.
For architecture details and the full Seer Slack renderer example, load
references/custom-renderers.md.
Step 7: Add a New Provider
Adding a new provider requires implementing the NotificationProvider protocol, a default NotificationRenderer, and registering both. This should only be done when onboarding a new integration provider.
High-level steps:
- Create
{provider_name}/provider.pywith provider + default renderer classes - Register with
@provider_registry.register(NotificationProviderKey.MY_PROVIDER) - Add
NotificationProviderKey.MY_PROVIDERto theNotificationProviderKeyenum intypes.py - Import the provider in
sentry/notifications/apps.py - Gate availability behind a feature flag in
is_available()
For the full provider scaffold and protocol requirements, load
references/provider-template.md.
Step 8: Test
Test directory: tests/sentry/notifications/platform/
Template test
class TestMyNotificationTemplate:
def test_render(self):
data = MyNotificationData(title="Test", detail_url="https://example.com")
template = MyNotificationTemplate()
rendered = template.render(data)
assert rendered.subject == "Test"
assert len(rendered.body) == 1
assert len(rendered.actions) == 1
assert rendered.actions[0].link == "https://example.com"
def test_render_example(self):
template = MyNotificationTemplate()
rendered = template.render_example()
assert rendered.subject # Verify example_data produces valid output
Service integration test
from unittest.mock import patch
from sentry.notifications.platform.service import NotificationService
class TestMyNotificationService:
@patch("sentry.notifications.platform.email.provider.EmailNotificationProvider.send")
def test_notify_target(self, mock_send):
data = MyNotificationData(title="Test", detail_url="https://example.com")
service = NotificationService(data=data)
target = GenericNotificationTarget(
provider_key=NotificationProviderKey.EMAIL,
resource_type=NotificationTargetResourceType.EMAIL,
resource_id="user@example.com",
)
service.notify_target(target=target)
assert mock_send.called
Custom renderer test
If you added a custom renderer, test that the provider dispatches to it:
def test_get_renderer_returns_custom():
data = MySpecialData(source=NotificationSource.MY_SOURCE, ...)
renderer = MyProvider.get_renderer(data=data, category=NotificationCategory.MY_CATEGORY)
assert renderer is MyCustomRenderer
Step 9: Verify
Pre-flight checklist before submitting:
-
NotificationSourceenum value added totypes.py - Source added to
NOTIFICATION_SOURCE_MAPunder correct category - Data class is
@dataclass(frozen=True)withsourceas class variable - Template registered with
@template_registry.register(DataClass.source) - Template imported in
templates/__init__.py -
example_dataon template produces valid output viarender_example() - Rollout option value configured (or ticket filed for
sentry-options-automator) - Sending code guarded with
NotificationService.has_access() - Custom renderers pass every Sentry URL through
link_decorator.decorate_url() - Every linked page type has a
NotificationLinkmatched inclassify_link - Tests pass:
pytest -svv --reuse-db tests/sentry/notifications/platform/ - Pre-commit passes on all modified files
Version History
-
9011df1
Current 2026-10-20 07:53
补充了自定义渲染器中链接追踪的处理说明,增加了链接装饰器使用示例及新页面链接类型要求。
- d3c9056 2026-08-20 20:33


