notification-platform
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, body blocks, actions, chart, footer, 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,
ParagraphBlock,
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=[
ParagraphBlock(blocks=[PlainTextBlock(text="Something happened.")])
],
actions=[
NotificationRenderedAction(label="View Details", link=data.detail_url)
],
)
Available body block types:
Refer to src/sentry/notifications/platform/types.py for the latest available block types.
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 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)
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() - Tests pass:
pytest -svv --reuse-db tests/sentry/notifications/platform/ - Pre-commit passes on all modified files