diff --git a/.agents/skills/notification-platform/SKILL.md b/.agents/skills/notification-platform/SKILL.md index c25580e265e8..ceb1c85de965 100644 --- a/.agents/skills/notification-platform/SKILL.md +++ b/.agents/skills/notification-platform/SKILL.md @@ -13,7 +13,7 @@ Sentry's NotificationPlatform is a provider-based system for sending notificatio | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | | `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` | +| `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` | @@ -98,7 +98,7 @@ from sentry.notifications.platform.types import ( NotificationRenderedAction, NotificationRenderedTemplate, NotificationTemplate, - ParagraphBlock, + ParagraphSection, PlainTextBlock, ) @@ -114,7 +114,7 @@ class MyNotificationTemplate(NotificationTemplate[MyNotificationData]): return NotificationRenderedTemplate( subject=data.title, body=[ - ParagraphBlock(blocks=[PlainTextBlock(text="Something happened.")]) + ParagraphSection(blocks=[PlainTextBlock(text="Something happened.")]) ], actions=[ NotificationRenderedAction(label="View Details", link=data.detail_url) @@ -122,9 +122,29 @@ class MyNotificationTemplate(NotificationTemplate[MyNotificationData]): ) ``` -**Available body block types:** +**Available types:** -Refer to `src/sentry/notifications/platform/types.py` for the latest available block 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`: @@ -186,7 +206,7 @@ if NotificationService.has_access(organization, data.source): ## 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). +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:** diff --git a/.agents/skills/notification-platform/references/custom-renderers.md b/.agents/skills/notification-platform/references/custom-renderers.md index b2fbbdf11b4c..ffec5e5df57f 100644 --- a/.agents/skills/notification-platform/references/custom-renderers.md +++ b/.agents/skills/notification-platform/references/custom-renderers.md @@ -4,7 +4,7 @@ The default flow is: `NotificationData` → `NotificationTemplate.render()` → `NotificationRenderedTemplate` → `NotificationRenderer.render()` → provider-specific renderable. -Custom renderers replace the last step. The provider's `get_renderer()` method dispatches to a custom renderer class based on category or data type, bypassing the default block-to-renderable conversion. +Custom renderers replace the last step. The provider's `get_renderer()` method dispatches to a custom renderer class based on category or data type, bypassing the default section/block-to-renderable conversion. ``` Template.render(data) → NotificationRenderedTemplate @@ -27,7 +27,7 @@ Use a custom renderer when: Do NOT use a custom renderer when: -- The default block types (`ParagraphBlock`, `CodeBlock`, `PlainTextBlock`, `BoldTextBlock`, `CodeTextBlock`) are sufficient +- The default section and block types (`ParagraphSection`, `CodeSection`, `BlockQuoteSection`, `PlainTextBlock`, `BoldTextBlock`, `ItalicTextBlock`, `CodeTextBlock`, `LinkTextBlock`) are sufficient - You only need to tweak styling — the default renderers establish common styles that the majority of notifications should abide by. ## File Placement @@ -139,6 +139,6 @@ class SeerAutofixUpdateTemplate(NotificationTemplate[SeerAutofixUpdate]): def render(self, data: SeerAutofixUpdate) -> NotificationRenderedTemplate: return NotificationRenderedTemplate( subject="Seer Autofix Update", - body=[ParagraphBlock(blocks=[PlainTextBlock(text="Update")])], + body=[ParagraphSection(blocks=[PlainTextBlock(text="Update")])], ) ``` diff --git a/.agents/skills/notification-platform/references/data-and-templates.md b/.agents/skills/notification-platform/references/data-and-templates.md index 44b507de923e..67865223b44e 100644 --- a/.agents/skills/notification-platform/references/data-and-templates.md +++ b/.agents/skills/notification-platform/references/data-and-templates.md @@ -18,7 +18,7 @@ from sentry.notifications.platform.types import ( NotificationRenderedTemplate, NotificationSource, NotificationTemplate, - ParagraphBlock, + ParagraphSection, PlainTextBlock, ) @@ -46,7 +46,7 @@ class DataExportSuccessTemplate(NotificationTemplate[DataExportSuccess]): return NotificationRenderedTemplate( subject="Your data is ready.", body=[ - ParagraphBlock( + ParagraphSection( blocks=[ PlainTextBlock( text="See, that wasn't so bad. We're all done assembling your download." @@ -59,7 +59,7 @@ class DataExportSuccessTemplate(NotificationTemplate[DataExportSuccess]): ) ``` -## Complete Example: DataExportFailure (with CodeBlock) +## Complete Example: DataExportFailure (with CodeSection) ```python @dataclass(frozen=True) @@ -83,20 +83,20 @@ class DataExportFailureTemplate(NotificationTemplate[DataExportFailure]): return NotificationRenderedTemplate( subject="We couldn't export your data.", body=[ - ParagraphBlock( + ParagraphSection( blocks=[ PlainTextBlock( text=f"The data export you created at {format_date(data.creation_date)} didn't work." ) ] ), - ParagraphBlock( + ParagraphSection( blocks=[ PlainTextBlock(text="It looks like there was an error: "), CodeTextBlock(text=data.error_message), ] ), - CodeBlock(blocks=[PlainTextBlock(text=orjson.dumps(data.error_payload).decode())]), + CodeSection(blocks=[PlainTextBlock(text=orjson.dumps(data.error_payload).decode())]), ], actions=[ NotificationRenderedAction(label="Documentation", link="https://docs.sentry.io/"), @@ -110,15 +110,15 @@ class DataExportFailureTemplate(NotificationTemplate[DataExportFailure]): ## NotificationRenderedTemplate Field Reference -| Field | Type | Required | Description | -| ----------------- | --------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------- | -| `subject` | `str` | Yes | Title/subject line. No formatting — displayed as-is. | -| `body` | `list[NotificationBodyFormattingBlock]` | Yes | Main content using block types below. | -| `actions` | `list[NotificationRenderedAction]` | No | Buttons/links. Each has `label` (str) and `link` (str). | -| `chart` | `NotificationRenderedImage` | No | Image with `url` and `alt_text` fields. | -| `footer` | `str` | No | Extra text after actions. No formatting. | -| `email_html_path` | `str` | No | Custom Django HTML template path. Data class passed as context. Default: `sentry/emails/platform/default.html`. | -| `email_text_path` | `str` | No | Custom Django text template path. Data class passed as context. | +| Field | Type | Required | Description | +| ----------------- | ------------------------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `subject` | `str \| list[NotificationTextBlock]` | Yes | Title/subject line. Can be plain text or a list of text blocks for rich formatting. | +| `body` | `list[NotificationSection]` | Yes | Main content. A list of sections (`ParagraphSection`, `CodeSection`, `BlockQuoteSection`), each containing text blocks (`PlainTextBlock`, `BoldTextBlock`, `ItalicTextBlock`, `CodeTextBlock`, `LinkTextBlock`). | +| `actions` | `list[NotificationRenderedAction]` | No | Buttons/links. Each has `label` (str) and `link` (str). | +| `chart` | `NotificationRenderedImage` | No | Image with `url` and `alt_text` fields. | +| `footer` | `str \| list[NotificationTextBlock]` | No | Extra text after actions. Can be plain text or a list of text blocks. | +| `email_html_path` | `str` | No | Custom Django HTML template path. Data class passed as context. Default: `sentry/emails/platform/default.html`. | +| `email_text_path` | `str` | No | Custom Django text template path. Data class passed as context. | ## Notes on Special Template Fields diff --git a/.agents/skills/notification-platform/references/provider-template.md b/.agents/skills/notification-platform/references/provider-template.md index bf7bc01db31a..fad167c93b16 100644 --- a/.agents/skills/notification-platform/references/provider-template.md +++ b/.agents/skills/notification-platform/references/provider-template.md @@ -30,15 +30,15 @@ from sentry.notifications.platform.target import ( PreparedIntegrationNotificationTarget, ) from sentry.notifications.platform.types import ( - NotificationBodyFormattingBlock, - NotificationBodyFormattingBlockType, - NotificationBodyTextBlock, - NotificationBodyTextBlockType, NotificationData, NotificationProviderKey, NotificationRenderedTemplate, + NotificationSection, + NotificationSectionType, NotificationTarget, NotificationTargetResourceType, + NotificationTextBlock, + NotificationTextBlockType, ) from sentry.organizations.services.organization.model import RpcOrganizationSummary @@ -53,31 +53,37 @@ class MyDefaultRenderer(NotificationRenderer[MyRenderable]): def render[DataT: NotificationData]( cls, *, data: DataT, rendered_template: NotificationRenderedTemplate ) -> MyRenderable: - # Convert rendered_template blocks into provider-specific format - body = cls.render_body_blocks(rendered_template.body) - # Build and return provider-specific renderable - return {"subject": rendered_template.subject, "body": body} + body = cls.render_sections(rendered_template.body) + subject = rendered_template.subject_text + footer = rendered_template.footer_text + return {"subject": subject, "body": body, "footer": footer} @classmethod - def render_body_blocks(cls, body: list[NotificationBodyFormattingBlock]) -> str: + def render_sections(cls, sections: list[NotificationSection]) -> str: parts = [] - for block in body: - if block.type == NotificationBodyFormattingBlockType.PARAGRAPH: - parts.append(cls.render_text_blocks(block.blocks)) - elif block.type == NotificationBodyFormattingBlockType.CODE_BLOCK: - parts.append(f"```{cls.render_text_blocks(block.blocks)}```") + for section in sections: + if section.type == NotificationSectionType.PARAGRAPH: + parts.append(cls.render_text_blocks(section.blocks)) + elif section.type == NotificationSectionType.CODE_BLOCK: + parts.append(f"```{cls.render_text_blocks(section.blocks)}```") + elif section.type == NotificationSectionType.BLOCK_QUOTE: + parts.append(f"> {cls.render_text_blocks(section.blocks)}") return "\n".join(parts) @classmethod - def render_text_blocks(cls, blocks: list[NotificationBodyTextBlock]) -> str: + def render_text_blocks(cls, blocks: list[NotificationTextBlock]) -> str: texts = [] for block in blocks: - if block.type == NotificationBodyTextBlockType.PLAIN_TEXT: + if block.type == NotificationTextBlockType.PLAIN_TEXT: texts.append(block.text) - elif block.type == NotificationBodyTextBlockType.BOLD_TEXT: + elif block.type == NotificationTextBlockType.BOLD_TEXT: texts.append(f"**{block.text}**") - elif block.type == NotificationBodyTextBlockType.CODE: + elif block.type == NotificationTextBlockType.ITALIC_TEXT: + texts.append(f"_{block.text}_") + elif block.type == NotificationTextBlockType.CODE: texts.append(f"`{block.text}`") + elif block.type == NotificationTextBlockType.LINK: + texts.append(f"[{block.text}]({block.url})") return " ".join(texts) diff --git a/src/sentry/notifications/platform/templates/data_export.py b/src/sentry/notifications/platform/templates/data_export.py index 68dfd18404ea..12565fe1bf5b 100644 --- a/src/sentry/notifications/platform/templates/data_export.py +++ b/src/sentry/notifications/platform/templates/data_export.py @@ -6,7 +6,7 @@ from sentry.notifications.platform.registry import template_registry from sentry.notifications.platform.types import ( - CodeBlock, + CodeSection, CodeTextBlock, NotificationCategory, NotificationData, @@ -14,7 +14,7 @@ NotificationRenderedTemplate, NotificationSource, NotificationTemplate, - ParagraphBlock, + ParagraphSection, PlainTextBlock, ) @@ -41,7 +41,7 @@ def render(self, data: DataExportSuccess) -> NotificationRenderedTemplate: return NotificationRenderedTemplate( subject="Your data is ready.", body=[ - ParagraphBlock( + ParagraphSection( blocks=[ PlainTextBlock( text="See, that wasn't so bad. We're all done assembling your download. Now have at it." @@ -78,27 +78,29 @@ def render(self, data: DataExportFailure) -> NotificationRenderedTemplate: return NotificationRenderedTemplate( subject="We couldn't export your data.", body=[ - ParagraphBlock( + ParagraphSection( blocks=[ PlainTextBlock( text=f"Well, this is a little awkward. The data export you created at {format_date(data.creation_date)} didn't work. Sorry about that." ) ] ), - ParagraphBlock( + ParagraphSection( blocks=[ PlainTextBlock(text="It looks like there was an error: "), CodeTextBlock(text=data.error_message), ] ), - ParagraphBlock( + ParagraphSection( blocks=[ PlainTextBlock( text="This is what you sent us. Maybe it'll help you sort this out: " ) ] ), - CodeBlock(blocks=[PlainTextBlock(text=orjson.dumps(data.error_payload).decode())]), + CodeSection( + blocks=[PlainTextBlock(text=orjson.dumps(data.error_payload).decode())] + ), ], actions=[ NotificationRenderedAction(label="Documentation", link="https://docs.sentry.io/"), diff --git a/src/sentry/notifications/platform/templates/repository.py b/src/sentry/notifications/platform/templates/repository.py index deb79ccd7b8a..c38e55913627 100644 --- a/src/sentry/notifications/platform/templates/repository.py +++ b/src/sentry/notifications/platform/templates/repository.py @@ -6,7 +6,7 @@ NotificationRenderedTemplate, NotificationSource, NotificationTemplate, - ParagraphBlock, + ParagraphSection, PlainTextBlock, ) @@ -31,7 +31,7 @@ def render(self, data: UnableToDeleteRepository) -> NotificationRenderedTemplate return NotificationRenderedTemplate( subject="Unable to Delete Repository Webhooks", body=[ - ParagraphBlock( + ParagraphSection( blocks=[ PlainTextBlock( text=f"We were unable to delete webhooks in {data.provider_name} for your repository " @@ -40,8 +40,8 @@ def render(self, data: UnableToDeleteRepository) -> NotificationRenderedTemplate PlainTextBlock(text=" due to the following error:"), ] ), - ParagraphBlock(blocks=[CodeTextBlock(text=data.error_message)]), - ParagraphBlock( + ParagraphSection(blocks=[CodeTextBlock(text=data.error_message)]), + ParagraphSection( blocks=[ PlainTextBlock( text=f"You will need to remove these webhooks manually in {data.provider_name} in order to stop sending commit data to Sentry." diff --git a/src/sentry/notifications/platform/templates/sample.py b/src/sentry/notifications/platform/templates/sample.py index ec95132a5261..e9d3c34a0ee6 100644 --- a/src/sentry/notifications/platform/templates/sample.py +++ b/src/sentry/notifications/platform/templates/sample.py @@ -3,7 +3,7 @@ from sentry.notifications.platform.registry import template_registry from sentry.notifications.platform.types import ( BoldTextBlock, - CodeBlock, + CodeSection, CodeTextBlock, LinkTextBlock, NotificationCategory, @@ -13,7 +13,7 @@ NotificationRenderedTemplate, NotificationSource, NotificationTemplate, - ParagraphBlock, + ParagraphSection, PlainTextBlock, ) @@ -54,7 +54,7 @@ def render(self, data: ErrorAlertData) -> NotificationRenderedTemplate: LinkTextBlock(text=data.project_name, url="https://example.com/project"), ], body=[ - ParagraphBlock( + ParagraphSection( blocks=[ PlainTextBlock(text="A new"), CodeTextBlock(text=data.error_type), @@ -64,9 +64,9 @@ def render(self, data: ErrorAlertData) -> NotificationRenderedTemplate: BoldTextBlock(text=f"{data.error_count} occurrences."), ], ), - ParagraphBlock(blocks=[PlainTextBlock(text="The error message is:")]), - CodeBlock(blocks=[PlainTextBlock(text=data.error_message)]), - ParagraphBlock( + ParagraphSection(blocks=[PlainTextBlock(text="The error message is:")]), + CodeSection(blocks=[PlainTextBlock(text=data.error_message)]), + ParagraphSection( blocks=[ PlainTextBlock( text=f"This error was first seen at {data.first_seen} and requires immediate attention.", @@ -120,21 +120,21 @@ def render(self, data: DeploymentData) -> NotificationRenderedTemplate: return NotificationRenderedTemplate( subject=f"Deployment to {data.environment}: {data.version}", body=[ - ParagraphBlock( + ParagraphSection( blocks=[ PlainTextBlock( text=f"Version {data.version} has been successfully deployed to {data.environment} for project {data.project_name}. ", ) ], ), - ParagraphBlock( + ParagraphSection( blocks=[ PlainTextBlock( text=f"The deployment was initiated by {data.deployer} with commit {data.commit_sha[:8]}: {data.commit_message}. ", ) ], ), - ParagraphBlock( + ParagraphSection( blocks=[ PlainTextBlock( text="Monitor the deployment status and be ready to rollback if any issues are detected.", @@ -186,7 +186,7 @@ def render(self, data: SlowLoadMetricAlertData) -> NotificationRenderedTemplate: return NotificationRenderedTemplate( subject=f"{data.severity.upper()}: {data.alert_type} in {data.project_name}", body=[ - ParagraphBlock( + ParagraphSection( blocks=[PlainTextBlock(text=f"{data.measurement} since {data.start_time}")], ), ], @@ -227,21 +227,21 @@ def render(self, data: PerformanceAlertData) -> NotificationRenderedTemplate: return NotificationRenderedTemplate( subject=f"Performance Alert: {data.metric_name} threshold exceeded", body=[ - ParagraphBlock( + ParagraphSection( blocks=[ PlainTextBlock( text=f"Performance alert triggered for {data.metric_name} in project {data.project_name}. ", ) ], ), - ParagraphBlock( + ParagraphSection( blocks=[ PlainTextBlock( text=f"The current value of {data.current_value} exceeds the threshold of {data.threshold}. ", ) ], ), - ParagraphBlock( + ParagraphSection( blocks=[ PlainTextBlock( text="Immediate investigation is recommended to identify and resolve the performance degradation.", @@ -285,15 +285,15 @@ def render(self, data: TeamUpdateData) -> NotificationRenderedTemplate: return NotificationRenderedTemplate( subject=f"Team Update: {data.update_type}", body=[ - ParagraphBlock( + ParagraphSection( blocks=[ PlainTextBlock( text=f"Team {data.team_name} has posted a {data.update_type} update. ", ) ], ), - ParagraphBlock(blocks=[PlainTextBlock(text=f"Message: {data.message} ")]), - ParagraphBlock( + ParagraphSection(blocks=[PlainTextBlock(text=f"Message: {data.message} ")]), + ParagraphSection( blocks=[PlainTextBlock(text=f"Posted by {data.author} at {data.timestamp}.")], ), ], diff --git a/src/sentry/notifications/platform/templates/seer.py b/src/sentry/notifications/platform/templates/seer.py index 53bd10900a43..56c4c7d349aa 100644 --- a/src/sentry/notifications/platform/templates/seer.py +++ b/src/sentry/notifications/platform/templates/seer.py @@ -12,7 +12,7 @@ NotificationRenderedTemplate, NotificationSource, NotificationTemplate, - ParagraphBlock, + ParagraphSection, PlainTextBlock, ) from sentry.organizations.services.organization.service import organization_service @@ -49,7 +49,7 @@ class SeerAutofixErrorTemplate(NotificationTemplate[SeerAutofixError]): def render(self, data: SeerAutofixError) -> NotificationRenderedTemplate: return NotificationRenderedTemplate( subject=data.error_title, - body=[ParagraphBlock(blocks=[PlainTextBlock(text=data.error_message)])], + body=[ParagraphSection(blocks=[PlainTextBlock(text=data.error_message)])], ) @@ -181,7 +181,7 @@ class SeerAgentErrorTemplate(NotificationTemplate[SeerAgentError]): def render(self, data: SeerAgentError) -> NotificationRenderedTemplate: return NotificationRenderedTemplate( subject=data.error_title, - body=[ParagraphBlock(blocks=[PlainTextBlock(text=data.error_message)])], + body=[ParagraphSection(blocks=[PlainTextBlock(text=data.error_message)])], ) diff --git a/src/sentry/notifications/platform/templates/sentry_app_webhook_disabled.py b/src/sentry/notifications/platform/templates/sentry_app_webhook_disabled.py index 7d929662f44f..c487f99ce8e6 100644 --- a/src/sentry/notifications/platform/templates/sentry_app_webhook_disabled.py +++ b/src/sentry/notifications/platform/templates/sentry_app_webhook_disabled.py @@ -7,7 +7,7 @@ NotificationRenderedTemplate, NotificationSource, NotificationTemplate, - ParagraphBlock, + ParagraphSection, PlainTextBlock, ) @@ -34,7 +34,7 @@ def render(self, data: SentryAppWebhookDisabled) -> NotificationRenderedTemplate return NotificationRenderedTemplate( subject=f"Webhook delivery paused for {data.sentry_app_name}", body=[ - ParagraphBlock( + ParagraphSection( blocks=[ PlainTextBlock(text="We're temporarily pausing webhook deliveries to "), CodeTextBlock(text=data.webhook_url), diff --git a/src/sentry/notifications/platform/templates/workflow_engine/activity/seer_base.py b/src/sentry/notifications/platform/templates/workflow_engine/activity/seer_base.py index 76ae5a320b0d..a9883ed88d8f 100644 --- a/src/sentry/notifications/platform/templates/workflow_engine/activity/seer_base.py +++ b/src/sentry/notifications/platform/templates/workflow_engine/activity/seer_base.py @@ -10,7 +10,7 @@ NotificationSection, NotificationSource, NotificationTextBlock, - ParagraphBlock, + ParagraphSection, PlainTextBlock, ) from sentry.types.activity import ActivityType @@ -54,7 +54,7 @@ def get_issue_description(group: Group) -> list[NotificationSection]: if blocks: blocks.append(PlainTextBlock(text="—")) blocks.append(CodeTextBlock(text=culprit)) - return [ParagraphBlock(blocks=blocks)] + return [ParagraphSection(blocks=blocks)] def get_subject(label: str, group: Group) -> list[NotificationTextBlock]: @@ -98,7 +98,7 @@ def build_template( def get_example_issue_description() -> list[NotificationSection]: return [ - ParagraphBlock( + ParagraphSection( blocks=[ PlainTextBlock(text="ExampleError: something went wrong"), PlainTextBlock(text="—"), diff --git a/src/sentry/notifications/platform/templates/workflow_engine/activity/seer_iteration_completed.py b/src/sentry/notifications/platform/templates/workflow_engine/activity/seer_iteration_completed.py index 9bf0fdd0f421..0f7e4e263142 100644 --- a/src/sentry/notifications/platform/templates/workflow_engine/activity/seer_iteration_completed.py +++ b/src/sentry/notifications/platform/templates/workflow_engine/activity/seer_iteration_completed.py @@ -15,7 +15,7 @@ NotificationSource, NotificationTemplate, NotificationTextBlock, - ParagraphBlock, + ParagraphSection, PlainTextBlock, ) from sentry.types.activity import ActivityType @@ -38,7 +38,7 @@ def render_example(self) -> NotificationRenderedTemplate: subject="Seer PR Iteration Completed for EXAMPLE-1", body=[ *get_example_issue_description(), - ParagraphBlock( + ParagraphSection( blocks=[ PlainTextBlock(text="Iteration #2: "), LinkTextBlock( @@ -84,7 +84,7 @@ def render(self, data: WorkflowEngineActivityAction) -> NotificationRenderedTemp if not detail_blocks: detail_blocks.append(PlainTextBlock(text=prefix)) - body.append(ParagraphBlock(blocks=detail_blocks)) + body.append(ParagraphSection(blocks=detail_blocks)) return build_template( data=data, diff --git a/src/sentry/notifications/platform/templates/workflow_engine/activity/seer_pr_created.py b/src/sentry/notifications/platform/templates/workflow_engine/activity/seer_pr_created.py index e4937920ab47..4094d856d9fc 100644 --- a/src/sentry/notifications/platform/templates/workflow_engine/activity/seer_pr_created.py +++ b/src/sentry/notifications/platform/templates/workflow_engine/activity/seer_pr_created.py @@ -15,7 +15,7 @@ NotificationSource, NotificationTemplate, NotificationTextBlock, - ParagraphBlock, + ParagraphSection, ) from sentry.types.activity import ActivityType @@ -37,7 +37,7 @@ def render_example(self) -> NotificationRenderedTemplate: subject="Seer PR Created for EXAMPLE-1", body=[ *get_example_issue_description(), - ParagraphBlock( + ParagraphSection( blocks=[ LinkTextBlock( text="getsentry/sentry (#1234)", @@ -69,7 +69,7 @@ def render(self, data: WorkflowEngineActivityAction) -> NotificationRenderedTemp body: list[NotificationSection] = [*get_issue_description(group)] if pr_links: - body.append(ParagraphBlock(blocks=pr_links)) + body.append(ParagraphSection(blocks=pr_links)) return build_template( data=data, subject=get_subject("Pull Request Created", group), body=body diff --git a/src/sentry/notifications/platform/templates/workflow_engine/activity/seer_rca_completed.py b/src/sentry/notifications/platform/templates/workflow_engine/activity/seer_rca_completed.py index 38c48df0bbcd..0bcf13d2cb71 100644 --- a/src/sentry/notifications/platform/templates/workflow_engine/activity/seer_rca_completed.py +++ b/src/sentry/notifications/platform/templates/workflow_engine/activity/seer_rca_completed.py @@ -8,7 +8,7 @@ get_subject, ) from sentry.notifications.platform.types import ( - CodeBlock, + CodeSection, NotificationCategory, NotificationRenderedTemplate, NotificationSection, @@ -36,7 +36,7 @@ def render_example(self) -> NotificationRenderedTemplate: subject="Seer RCA Completed for EXAMPLE-1", body=[ *get_example_issue_description(), - CodeBlock( + CodeSection( blocks=[ PlainTextBlock( text="The error is caused by a null pointer dereference in the user authentication flow." @@ -58,7 +58,7 @@ def render(self, data: WorkflowEngineActivityAction) -> NotificationRenderedTemp body: list[NotificationSection] = [*get_issue_description(group)] if activity.data: summary_block = PlainTextBlock(text=activity.data.get("summary", fallback)) - body.append(CodeBlock(blocks=[summary_block])) + body.append(CodeSection(blocks=[summary_block])) return build_template( data=data, subject=get_subject("Root Cause Analysis Completed", group), body=body ) diff --git a/src/sentry/notifications/platform/templates/workflow_engine/activity/seer_solution_completed.py b/src/sentry/notifications/platform/templates/workflow_engine/activity/seer_solution_completed.py index fb43fe8b930f..91cf09d3506e 100644 --- a/src/sentry/notifications/platform/templates/workflow_engine/activity/seer_solution_completed.py +++ b/src/sentry/notifications/platform/templates/workflow_engine/activity/seer_solution_completed.py @@ -8,7 +8,7 @@ get_subject, ) from sentry.notifications.platform.types import ( - CodeBlock, + CodeSection, NotificationCategory, NotificationRenderedTemplate, NotificationSection, @@ -36,7 +36,7 @@ def render_example(self) -> NotificationRenderedTemplate: subject="Seer Solution Completed for EXAMPLE-1", body=[ *get_example_issue_description(), - CodeBlock( + CodeSection( blocks=[ PlainTextBlock( text="Add a null check before accessing user.session in the authentication middleware." @@ -58,7 +58,7 @@ def render(self, data: WorkflowEngineActivityAction) -> NotificationRenderedTemp body: list[NotificationSection] = [*get_issue_description(group)] if activity.data: summary_block = PlainTextBlock(text=activity.data.get("summary", fallback)) - body.append(CodeBlock(blocks=[summary_block])) + body.append(CodeSection(blocks=[summary_block])) return build_template( data=data, subject=get_subject("Planning Completed", group), body=body ) diff --git a/src/sentry/notifications/platform/types.py b/src/sentry/notifications/platform/types.py index 4b1e6caf5690..54afde2e4bb2 100644 --- a/src/sentry/notifications/platform/types.py +++ b/src/sentry/notifications/platform/types.py @@ -309,7 +309,7 @@ def footer_text(self) -> str: class NotificationTextBlockType(StrEnum): """ - Represents a block of text to be rendered in the notification body. + Represents a block of text to be rendered in the notification. """ PLAIN_TEXT = "plain_text" @@ -355,42 +355,42 @@ class NotificationSectionType(StrEnum): class NotificationSection(Protocol): """ - A block that applies formatting such as a newline and encapsulates other text. + A section of text that applies formatting such as a newline and encapsulates other text. """ type: NotificationSectionType """ - The type of the block, such as ParagraphBlock, BoldTextBlock, etc. + The type of the section, such as ParagraphSection, CodeSection, etc. """ blocks: list[NotificationTextBlock] """ - Some blocks may want to contain other blocks, such as a ParagraphBlock containing a BoldTextBlock. + The text blocks contain actual content, such as BoldTextBlock, ItalicTextBlock, etc. """ class NotificationTextBlock(Protocol): """ - Represents a block of text to be rendered in the notification body. + Represents a block of text to be rendered in the notification. """ type: NotificationTextBlockType """ - The type of the block, such as BoldTextBlock, CodeBlock, etc. + The type of the block, such as BoldTextBlock, CodeTextBlock, etc. """ text: str """ - Text to be rendered in the body. + Text to be rendered in the notification. """ @dataclass -class ParagraphBlock(NotificationSection): +class ParagraphSection(NotificationSection): blocks: list[NotificationTextBlock] type: Literal[NotificationSectionType.PARAGRAPH] = NotificationSectionType.PARAGRAPH @dataclass -class CodeBlock(NotificationSection): +class CodeSection(NotificationSection): blocks: list[NotificationTextBlock] type: Literal[NotificationSectionType.CODE_BLOCK] = NotificationSectionType.CODE_BLOCK diff --git a/src/sentry/testutils/notifications/platform.py b/src/sentry/testutils/notifications/platform.py index f6de4e6fc8d5..acfc44b8d3cf 100644 --- a/src/sentry/testutils/notifications/platform.py +++ b/src/sentry/testutils/notifications/platform.py @@ -2,7 +2,7 @@ from sentry.notifications.platform.types import ( BlockQuoteSection, BoldTextBlock, - CodeBlock, + CodeSection, CodeTextBlock, ItalicTextBlock, LinkTextBlock, @@ -15,7 +15,7 @@ NotificationStrategy, NotificationTarget, NotificationTemplate, - ParagraphBlock, + ParagraphSection, PlainTextBlock, ) @@ -37,7 +37,7 @@ def render(self, data: MockNotification) -> NotificationRenderedTemplate: ItalicTextBlock(text="Mock Notification"), ], body=[ - ParagraphBlock( + ParagraphSection( blocks=[ PlainTextBlock(text=data.message), BoldTextBlock(text="important"), @@ -45,7 +45,7 @@ def render(self, data: MockNotification) -> NotificationRenderedTemplate: LinkTextBlock(text="View Issue", url="https://sentry.io/issue/1"), ] ), - CodeBlock(blocks=[PlainTextBlock(text="raise Exception('test')")]), + CodeSection(blocks=[PlainTextBlock(text="raise Exception('test')")]), BlockQuoteSection(blocks=[PlainTextBlock(text="This is a quoted message")]), ], actions=[ diff --git a/tests/sentry/notifications/platform/discord/test_provider.py b/tests/sentry/notifications/platform/discord/test_provider.py index 5a8c0e210bb5..843a4c8f96ec 100644 --- a/tests/sentry/notifications/platform/discord/test_provider.py +++ b/tests/sentry/notifications/platform/discord/test_provider.py @@ -24,7 +24,7 @@ NotificationRenderedImage, NotificationRenderedTemplate, NotificationTargetResourceType, - ParagraphBlock, + ParagraphSection, PlainTextBlock, ) from sentry.testutils.cases import TestCase @@ -97,7 +97,7 @@ def test_default_renderer(self) -> None: def test_renderer_without_chart(self) -> None: rendered_template = NotificationRenderedTemplate( subject="Test Without Chart", - body=[ParagraphBlock(blocks=[PlainTextBlock(text="test without chart")])], + body=[ParagraphSection(blocks=[PlainTextBlock(text="test without chart")])], actions=[ NotificationRenderedAction(label="Visit Sentry", link="https://www.sentry.io") ], @@ -118,7 +118,7 @@ def test_renderer_without_chart(self) -> None: def test_renderer_without_footer(self) -> None: rendered_template = NotificationRenderedTemplate( subject="Test Without Footer", - body=[ParagraphBlock(blocks=[PlainTextBlock(text="test without footer")])], + body=[ParagraphSection(blocks=[PlainTextBlock(text="test without footer")])], actions=[ NotificationRenderedAction(label="Visit Sentry", link="https://www.sentry.io") ], @@ -142,7 +142,7 @@ def test_renderer_without_footer(self) -> None: def test_renderer_without_actions(self) -> None: rendered_template = NotificationRenderedTemplate( subject="Test Without Actions", - body=[ParagraphBlock(blocks=[PlainTextBlock(text="test without actions")])], + body=[ParagraphSection(blocks=[PlainTextBlock(text="test without actions")])], actions=[], # No actions footer="Test footer", chart=NotificationRenderedImage( @@ -172,7 +172,7 @@ def test_renderer_multiple_actions(self) -> None: # Create a custom rendered template with multiple actions rendered_template = NotificationRenderedTemplate( subject="Test Multiple Actions", - body=[ParagraphBlock(blocks=[PlainTextBlock(text="test with multiple actions")])], + body=[ParagraphSection(blocks=[PlainTextBlock(text="test with multiple actions")])], actions=actions, footer="Test footer", chart=NotificationRenderedImage( diff --git a/tests/sentry/notifications/platform/email/test_provider.py b/tests/sentry/notifications/platform/email/test_provider.py index 443e50e2f1c9..29be72c67de3 100644 --- a/tests/sentry/notifications/platform/email/test_provider.py +++ b/tests/sentry/notifications/platform/email/test_provider.py @@ -15,7 +15,7 @@ NotificationTargetResourceType, NotificationTextBlock, NotificationTextBlockType, - ParagraphBlock, + ParagraphSection, PlainTextBlock, ) from sentry.testutils.cases import TestCase @@ -108,7 +108,7 @@ def test_xss_protection(self) -> None: xss_template = NotificationRenderedTemplate( subject="Test XSS", body=[ - ParagraphBlock( + ParagraphSection( blocks=[ PlainTextBlock(text=""), BoldTextBlock(text=""), @@ -137,7 +137,7 @@ def test_xss_protection_link_block(self) -> None: xss_template = NotificationRenderedTemplate( subject="Test XSS Link", body=[ - ParagraphBlock( + ParagraphSection( blocks=[ LinkTextBlock( text='', diff --git a/tests/sentry/notifications/platform/msteams/test_provider.py b/tests/sentry/notifications/platform/msteams/test_provider.py index 3e45ef2d8e51..e8725b1c2d7b 100644 --- a/tests/sentry/notifications/platform/msteams/test_provider.py +++ b/tests/sentry/notifications/platform/msteams/test_provider.py @@ -52,7 +52,7 @@ def test_default_renderer(self) -> None: assert title_block["size"] == TextSize.LARGE assert title_block["weight"] == TextWeight.BOLDER - # Verify body block 1 (ParagraphBlock) + # Verify body section 1 (ParagraphSection) body_block_1 = body_blocks[1] assert body_block_1["type"] == "TextBlock" assert ( @@ -60,12 +60,12 @@ def test_default_renderer(self) -> None: == "test **important** _urgent_ [View Issue](https://sentry.io/issue/1)" ) - # Verify body block 2 (CodeBlock) + # Verify body section 2 (CodeSection) body_block_2 = body_blocks[2] assert body_block_2["type"] == "CodeBlock" assert body_block_2["codeSnippet"] == "raise Exception('test')" - # Verify body block 3 (BlockQuoteSection) + # Verify body section 3 (BlockQuoteSection) body_block_3 = body_blocks[3] assert body_block_3["type"] == "TextBlock" assert "This is a quoted message" in body_block_3["text"] diff --git a/tests/sentry/notifications/platform/templates/test_seer.py b/tests/sentry/notifications/platform/templates/test_seer.py index e31833f1670d..5285106a09be 100644 --- a/tests/sentry/notifications/platform/templates/test_seer.py +++ b/tests/sentry/notifications/platform/templates/test_seer.py @@ -10,7 +10,7 @@ SeerAutofixUpdate, _get_next_stopping_point, ) -from sentry.notifications.platform.types import ParagraphBlock +from sentry.notifications.platform.types import ParagraphSection from sentry.seer.autofix.utils import AutofixStoppingPoint from sentry.testutils.cases import TestCase @@ -116,7 +116,7 @@ def test_render(self) -> None: assert rendered.subject == "Seer had some trouble..." assert len(rendered.body) == 1 - assert isinstance(rendered.body[0], ParagraphBlock) + assert isinstance(rendered.body[0], ParagraphSection) assert rendered.body[0].blocks[0].text == "Seer could not explore your organization." def test_render_custom_title(self) -> None: