Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions src/sentry/notifications/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ class Config(AppConfig):
def ready(self) -> None:
# Imports to populate registries
import sentry.notifications.platform.discord.provider # noqa: F401
import sentry.notifications.platform.discord.renderers.issue # noqa: F401
import sentry.notifications.platform.discord.renderers.metric_alert # noqa: F401
import sentry.notifications.platform.email.provider # noqa: F401
import sentry.notifications.platform.msteams.provider # noqa: F401
import sentry.notifications.platform.slack.provider # noqa: F401
import sentry.notifications.platform.slack.renderers.issue # noqa: F401
import sentry.notifications.platform.slack.renderers.metric_alert # noqa: F401
import sentry.notifications.platform.slack.renderers.seer # noqa: F401
import sentry.notifications.platform.slack.renderers.seer_agent_write_approval # noqa: F401
import sentry.notifications.platform.templates # noqa: F401
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ def serialize_slack_preview[T: NotificationData](
) -> dict[str, Any]:
data = template.example_data
rendered_template = template.render_example()
renderer = SlackNotificationProvider.get_renderer(data=data, category=template.category)
renderer = SlackNotificationProvider.get_renderer(data=data)
message = renderer.render(data=data, rendered_template=rendered_template)

serialized_blocks = []
Expand Down
18 changes: 0 additions & 18 deletions src/sentry/notifications/platform/discord/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
from sentry.notifications.platform.threading import ThreadContext
from sentry.notifications.platform.types import (
LinkTextBlock,
NotificationCategory,
NotificationData,
NotificationProviderKey,
NotificationRenderedTemplate,
Expand All @@ -40,8 +39,6 @@


class DiscordRenderer(NotificationRenderer[DiscordRenderable]):
provider_key = NotificationProviderKey.DISCORD

@classmethod
def render[DataT: NotificationData](
cls, *, data: DataT, rendered_template: NotificationRenderedTemplate
Expand Down Expand Up @@ -152,21 +149,6 @@ def is_available(cls, *, organization: RpcOrganizationSummary | None = None) ->
# TODO(ecosystem): Check for the integration, maybe a feature as well
return False

@classmethod
def get_renderer(
cls, *, data: NotificationData, category: NotificationCategory
) -> type[NotificationRenderer[DiscordRenderable]]:
from sentry.notifications.platform.discord.renderers.issue import IssueDiscordRenderer
from sentry.notifications.platform.discord.renderers.metric_alert import (
DiscordMetricAlertRenderer,
)

if category == NotificationCategory.ISSUE:
return IssueDiscordRenderer
if category == NotificationCategory.METRIC_ALERT:
return DiscordMetricAlertRenderer
return cls.default_renderer

@classmethod
def send(
cls,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,21 @@
from sentry.integrations.discord.message_builder.issues import DiscordIssuesMessageBuilder
from sentry.models.group import Group
from sentry.notifications.platform.discord.provider import DiscordRenderable
from sentry.notifications.platform.registry import renderer_registry
from sentry.notifications.platform.renderer import NotificationRenderer
from sentry.notifications.platform.service import NotificationRenderError
from sentry.notifications.platform.templates.issue import IssueNotificationData
from sentry.notifications.platform.types import (
NotificationData,
NotificationProviderKey,
NotificationRenderedTemplate,
NotificationSource,
)
from sentry.services.eventstore.models import Event


@renderer_registry.register(NotificationProviderKey.DISCORD, NotificationSource.ISSUE)
class IssueDiscordRenderer(NotificationRenderer[DiscordRenderable]):
provider_key = NotificationProviderKey.DISCORD

@classmethod
def render[DataT: NotificationData](
cls, *, data: DataT, rendered_template: NotificationRenderedTemplate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,19 @@
from sentry.integrations.discord.message_builder.metric_alerts import get_started_at
from sentry.integrations.metric_alerts import get_status_text
from sentry.notifications.platform.discord.provider import DiscordRenderable
from sentry.notifications.platform.registry import renderer_registry
from sentry.notifications.platform.renderer import NotificationRenderer
from sentry.notifications.platform.templates.metric_alert import MetricAlertNotificationData
from sentry.notifications.platform.types import (
NotificationData,
NotificationProviderKey,
NotificationRenderedTemplate,
NotificationSource,
)


@renderer_registry.register(NotificationProviderKey.DISCORD, NotificationSource.METRIC_ALERT)
class DiscordMetricAlertRenderer(NotificationRenderer[DiscordRenderable]):
provider_key = NotificationProviderKey.DISCORD

@classmethod
def render[DataT: NotificationData](
cls, *, data: DataT, rendered_template: NotificationRenderedTemplate
Expand Down
2 changes: 0 additions & 2 deletions src/sentry/notifications/platform/email/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@


class EmailRenderer(NotificationRenderer[EmailRenderable]):
provider_key = NotificationProviderKey.EMAIL

@classmethod
def render[DataT: NotificationData](
cls, *, data: DataT, rendered_template: NotificationRenderedTemplate
Expand Down
2 changes: 0 additions & 2 deletions src/sentry/notifications/platform/msteams/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@


class MSTeamsRenderer(NotificationRenderer[MSTeamsRenderable]):
provider_key = NotificationProviderKey.MSTEAMS

@classmethod
def render[DataT: NotificationData](
cls, *, data: DataT, rendered_template: NotificationRenderedTemplate
Expand Down
11 changes: 6 additions & 5 deletions src/sentry/notifications/platform/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from sentry.notifications.platform.target import IntegrationNotificationTarget
from sentry.notifications.platform.threading import ThreadContext
from sentry.notifications.platform.types import (
NotificationCategory,
NotificationData,
NotificationProviderKey,
NotificationTarget,
Expand Down Expand Up @@ -133,15 +132,17 @@
return

@classmethod
def get_renderer(
cls, *, data: NotificationData, category: NotificationCategory
) -> type[NotificationRenderer[RenderableT]]:
def get_renderer(cls, *, data: NotificationData) -> type[NotificationRenderer[RenderableT]]:
"""
Returns an instance of a renderer for a given notification, falling back to the default renderer.
Override this to method to permit different renderers for the provider, though keep in mind
that this may produce inconsistencies between notifications.
"""
return cls.default_renderer
# Imported here since the registry imports this module to type its registrations.
from sentry.notifications.platform.registry import renderer_registry

renderer = renderer_registry.get(provider_key=cls.key, source=data.source)
return renderer or cls.default_renderer

Check warning on line 145 in src/sentry/notifications/platform/provider.py

View check run for this annotation

@sentry/warden / warden: sentry-backend-bugs

SLACK_STAGING never resolves registered Slack renderers

`get_renderer` looks up by `cls.key`, but specialized Slack renderers only register under `NotificationProviderKey.SLACK`, so `SlackStagingNotificationProvider` always falls back to the default renderer for issue/metric/seer sources. Register those renderers for `SLACK_STAGING` as well, or fall back to the `SLACK` key when `cls.key` is staging.
Comment on lines +144 to +145

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SLACK_STAGING never resolves registered Slack renderers

get_renderer looks up by cls.key, but specialized Slack renderers only register under NotificationProviderKey.SLACK, so SlackStagingNotificationProvider always falls back to the default renderer for issue/metric/seer sources. Register those renderers for SLACK_STAGING as well, or fall back to the SLACK key when cls.key is staging.

Evidence
  • get_renderer calls renderer_registry.get(provider_key=cls.key, source=data.source) and returns cls.default_renderer when that miss returns None.
  • SlackStagingNotificationProvider sets key = NotificationProviderKey.SLACK_STAGING and inherits this base get_renderer.
  • All Slack specialized renderers register only under NotificationProviderKey.SLACK (issue, metric_alert, seer, seer_agent_write_approval).
  • Prior category-based Slack overrides were inherited by staging and returned the specialized renderers; source+provider_key lookup no longer does.

Identified by Warden · sentry-backend-bugs · R99-8QC


@classmethod
def is_available(cls, *, organization: RpcOrganizationSummary | None = None) -> bool:
Expand Down
54 changes: 52 additions & 2 deletions src/sentry/notifications/platform/registry.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
from __future__ import annotations

from collections.abc import Callable
from typing import Any

from sentry.notifications.platform.provider import NotificationProvider
from sentry.notifications.platform.types import NotificationTemplate
from sentry.notifications.platform.renderer import NotificationRenderer
from sentry.notifications.platform.types import (
NotificationProviderKey,
NotificationSource,
NotificationTemplate,
)
from sentry.organizations.services.organization.model import RpcOrganizationSummary
from sentry.utils.registry import Registry
from sentry.utils.registry import AlreadyRegisteredError, Registry


class NotificationProviderRegistry(Registry[type[NotificationProvider[Any]]]):
Expand Down Expand Up @@ -33,5 +39,49 @@ def get_available(
]


class NotificationRendererRegistry:
"""
A registry for renderers which override a provider's default renderer. Renderers are keyed by
the provider they produce output for, and the notification sources they render.
"""

def __init__(self) -> None:
self.registrations: dict[
tuple[NotificationProviderKey, NotificationSource], type[NotificationRenderer[Any]]
] = {}

def register[RenderableT](
self, provider_key: NotificationProviderKey, *sources: NotificationSource
) -> Callable[
[type[NotificationRenderer[RenderableT]]], type[NotificationRenderer[RenderableT]]
]:
if not sources:
raise ValueError("At least one notification source is required to register a renderer")

def inner(
renderer: type[NotificationRenderer[RenderableT]],
) -> type[NotificationRenderer[RenderableT]]:
for source in sources:
key = (provider_key, source)
if key in self.registrations:
raise AlreadyRegisteredError(
f"A registration already exists for {key}: {self.registrations[key]}"
)
self.registrations[key] = renderer
return renderer

return inner

def get(
self, *, provider_key: NotificationProviderKey, source: NotificationSource
) -> type[NotificationRenderer[Any]] | None:
"""
Returns the registered renderer for the provider/source pair, or `None` if the provider has
no override and should fall back to its default renderer.
"""
return self.registrations.get((provider_key, source))


provider_registry = NotificationProviderRegistry()
template_registry = Registry[type[NotificationTemplate[Any]]]()
renderer_registry = NotificationRendererRegistry()
3 changes: 0 additions & 3 deletions src/sentry/notifications/platform/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from sentry.notifications.platform.types import (
NotificationData,
NotificationProviderKey,
NotificationRenderedTemplate,
)

Expand All @@ -17,8 +16,6 @@ class NotificationRenderer[RenderableT](Protocol):
RenderableT is a type that matches the connected provider.
"""

provider_key: NotificationProviderKey

@classmethod
def render[DataT: NotificationData](
cls, *, data: DataT, rendered_template: NotificationRenderedTemplate
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/notifications/platform/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ def render_template[RenderableT](
provider: type[NotificationProvider[RenderableT]],
) -> RenderableT:
rendered_template = template.render(data=data)
renderer = provider.get_renderer(data=data, category=template.category)
renderer = provider.get_renderer(data=data)
return renderer.render(data=data, rendered_template=rendered_template)

@staticmethod
Expand Down
29 changes: 0 additions & 29 deletions src/sentry/notifications/platform/slack/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,11 @@
from sentry.notifications.platform.threading import ThreadContext
from sentry.notifications.platform.types import (
LinkTextBlock,
NotificationCategory,
NotificationData,
NotificationProviderKey,
NotificationRenderedTemplate,
NotificationSection,
NotificationSectionType,
NotificationSource,
NotificationTarget,
NotificationTargetResourceType,
NotificationTextBlock,
Expand All @@ -65,8 +63,6 @@ class SlackRenderable(TypedDict):


class SlackRenderer(NotificationRenderer[SlackRenderable]):
provider_key = NotificationProviderKey.SLACK

@classmethod
def render[DataT: NotificationData](
cls, *, data: DataT, rendered_template: NotificationRenderedTemplate
Expand Down Expand Up @@ -144,31 +140,6 @@ def is_available(cls, *, organization: RpcOrganizationSummary | None = None) ->
# I currently view this as akin to a rollout or feature flag for the registry
return False

@classmethod
def get_renderer(
cls, *, data: NotificationData, category: NotificationCategory
) -> type[NotificationRenderer[SlackRenderable]]:
from sentry.notifications.platform.slack.renderers.issue import (
IssueSlackRenderer,
)
from sentry.notifications.platform.slack.renderers.metric_alert import (
SlackMetricAlertRenderer,
)
from sentry.notifications.platform.slack.renderers.seer import SeerSlackRenderer
from sentry.notifications.platform.slack.renderers.seer_agent_write_approval import (
SeerAgentWriteApprovalSlackRenderer,
)

if category == NotificationCategory.SEER:
if data.source == NotificationSource.SEER_AGENT_WRITE_APPROVAL:
return SeerAgentWriteApprovalSlackRenderer
return SeerSlackRenderer
if category == NotificationCategory.ISSUE:
return IssueSlackRenderer
if category == NotificationCategory.METRIC_ALERT:
return SlackMetricAlertRenderer
return cls.default_renderer

@classmethod
def send(
cls,
Expand Down
5 changes: 3 additions & 2 deletions src/sentry/notifications/platform/slack/renderers/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,20 @@

from sentry import eventstore
from sentry.models.group import Group
from sentry.notifications.platform.registry import renderer_registry
from sentry.notifications.platform.renderer import NotificationRenderer
from sentry.notifications.platform.slack.provider import SlackRenderable
from sentry.notifications.platform.templates.issue import IssueNotificationData
from sentry.notifications.platform.types import (
NotificationData,
NotificationProviderKey,
NotificationRenderedTemplate,
NotificationSource,
)


@renderer_registry.register(NotificationProviderKey.SLACK, NotificationSource.ISSUE)
class IssueSlackRenderer(NotificationRenderer[SlackRenderable]):
provider_key = NotificationProviderKey.SLACK

@classmethod
def render[DataT: NotificationData](
cls, *, data: DataT, rendered_template: NotificationRenderedTemplate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,20 @@
from sentry.integrations.slack.message_builder.incidents import get_started_at
from sentry.integrations.slack.message_builder.types import INCIDENT_COLOR_MAPPING
from sentry.integrations.slack.utils.escape import escape_slack_text
from sentry.notifications.platform.registry import renderer_registry
from sentry.notifications.platform.renderer import NotificationRenderer
from sentry.notifications.platform.slack.provider import SlackRenderable
from sentry.notifications.platform.templates.metric_alert import MetricAlertNotificationData
from sentry.notifications.platform.types import (
NotificationData,
NotificationProviderKey,
NotificationRenderedTemplate,
NotificationSource,
)


@renderer_registry.register(NotificationProviderKey.SLACK, NotificationSource.METRIC_ALERT)
class SlackMetricAlertRenderer(NotificationRenderer[SlackRenderable]):
provider_key = NotificationProviderKey.SLACK

@classmethod
def render[DataT: NotificationData](
cls, *, data: DataT, rendered_template: NotificationRenderedTemplate
Expand Down
12 changes: 12 additions & 0 deletions src/sentry/notifications/platform/slack/renderers/seer.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
SectionBlock,
)

from sentry.notifications.platform.registry import renderer_registry
from sentry.notifications.platform.renderer import NotificationRenderer
from sentry.notifications.platform.slack.provider import SlackRenderable
from sentry.notifications.platform.templates.seer import (
Expand All @@ -32,7 +33,9 @@
)
from sentry.notifications.platform.types import (
NotificationData,
NotificationProviderKey,
NotificationRenderedTemplate,
NotificationSource,
)
from sentry.seer.autofix.utils import AutofixStoppingPoint, CodingAgentProviderType

Expand Down Expand Up @@ -85,6 +88,15 @@ class AutofixStageConfig(TypedDict):
}


@renderer_registry.register(
NotificationProviderKey.SLACK,
NotificationSource.SEER_AUTOFIX_TRIGGER,
NotificationSource.SEER_AUTOFIX_ERROR,
NotificationSource.SEER_AUTOFIX_SUCCESS,
NotificationSource.SEER_AUTOFIX_UPDATE,
NotificationSource.SEER_AGENT_RESPONSE,
NotificationSource.SEER_AGENT_ERROR,
)
class SeerSlackRenderer(NotificationRenderer[SlackRenderable]):
@classmethod
def render[DataT: NotificationData](
Expand Down
Loading
Loading