diff --git a/static/app/debug/notifications/components/notificationBodyRenderer.tsx b/static/app/debug/notifications/components/notificationBodyRenderer.tsx
new file mode 100644
index 00000000000000..cc8355853e8c2d
--- /dev/null
+++ b/static/app/debug/notifications/components/notificationBodyRenderer.tsx
@@ -0,0 +1,112 @@
+import {Fragment} from 'react';
+import styled from '@emotion/styled';
+
+// Match the Python types from notifications/platform/types.py
+enum NotificationBodyFormattingBlockType {
+ PARAGRAPH = 'paragraph',
+ CODE_BLOCK = 'code_block',
+}
+
+enum NotificationBodyTextBlockType {
+ PLAIN_TEXT = 'plain_text',
+ BOLD_TEXT = 'bold_text',
+ CODE = 'code',
+}
+
+interface NotificationBodyTextBlock {
+ text: string;
+ type: NotificationBodyTextBlockType;
+}
+
+export interface NotificationBodyFormattingBlock {
+ blocks: NotificationBodyTextBlock[];
+ type: NotificationBodyFormattingBlockType;
+}
+
+interface NotificationBodyRendererProps {
+ body: NotificationBodyFormattingBlock[];
+ codeBlockBackground?: string;
+ codeBlockBorder?: string;
+ codeBlockTextColor?: string;
+}
+
+function renderTextBlock(block: NotificationBodyTextBlock, index: number) {
+ switch (block.type) {
+ case NotificationBodyTextBlockType.PLAIN_TEXT:
+ return {block.text} ;
+ case NotificationBodyTextBlockType.BOLD_TEXT:
+ return {block.text} ;
+ case NotificationBodyTextBlockType.CODE:
+ return {block.text} ;
+ default:
+ return {block.text} ;
+ }
+}
+
+function renderFormattingBlock(
+ block: NotificationBodyFormattingBlock,
+ index: number,
+ codeBlockBg: string,
+ codeBlockBorder: string,
+ codeBlockTextColor: string
+) {
+ if (block.type === NotificationBodyFormattingBlockType.PARAGRAPH) {
+ return (
+
+ {block.blocks.map((textBlock, i) => renderTextBlock(textBlock, i))}
+
+ );
+ }
+ if (block.type === NotificationBodyFormattingBlockType.CODE_BLOCK) {
+ return (
+
+
+ {block.blocks.map((textBlock, i) => renderTextBlock(textBlock, i))}
+
+
+ );
+ }
+ return null;
+}
+
+const StyledCodeBlock = styled('code')<{
+ backgroundColor: string;
+ borderColor: string;
+ textColor: string;
+}>`
+ display: block;
+ padding: 12px;
+ background-color: ${p => p.backgroundColor};
+ border: 1px solid ${p => p.borderColor};
+ border-radius: 6px;
+ font-family: monospace;
+ font-size: 13px;
+ white-space: pre-wrap;
+ word-break: break-word;
+ color: ${p => p.textColor};
+`;
+
+export function NotificationBodyRenderer({
+ body,
+ codeBlockBackground = '#f6f8fa',
+ codeBlockBorder = '#e1e4e8',
+ codeBlockTextColor = '#24292e',
+}: NotificationBodyRendererProps) {
+ return (
+
+ {body.map((block, index) =>
+ renderFormattingBlock(
+ block,
+ index,
+ codeBlockBackground,
+ codeBlockBorder,
+ codeBlockTextColor
+ )
+ )}
+
+ );
+}
diff --git a/static/app/debug/notifications/previews/discordPreview.tsx b/static/app/debug/notifications/previews/discordPreview.tsx
index b8580f84f31f9f..ecbe0789a4b189 100644
--- a/static/app/debug/notifications/previews/discordPreview.tsx
+++ b/static/app/debug/notifications/previews/discordPreview.tsx
@@ -8,6 +8,7 @@ import {Image} from 'sentry/components/core/image';
import {Container, Flex, Grid} from 'sentry/components/core/layout';
import {Text} from 'sentry/components/core/text';
import {DebugNotificationsPreview} from 'sentry/debug/notifications/components/debugNotificationsPreview';
+import {NotificationBodyRenderer} from 'sentry/debug/notifications/components/notificationBodyRenderer';
import {
NotificationProviderKey,
type NotificationTemplateRegistration,
@@ -46,7 +47,14 @@ export function DiscordPreview({
{subject}
- {JSON.stringify(body)}
+
+
+
{chart && (
{subject}
- {JSON.stringify(body)}
+
+
+
{actions.map(action => (
diff --git a/static/app/debug/notifications/previews/teamsPreview.tsx b/static/app/debug/notifications/previews/teamsPreview.tsx
index 03ce198379f5df..077e189ce61f91 100644
--- a/static/app/debug/notifications/previews/teamsPreview.tsx
+++ b/static/app/debug/notifications/previews/teamsPreview.tsx
@@ -11,6 +11,7 @@ import {Image} from 'sentry/components/core/image/image';
import {Container, Flex, Grid} from 'sentry/components/core/layout';
import {Text} from 'sentry/components/core/text';
import {DebugNotificationsPreview} from 'sentry/debug/notifications/components/debugNotificationsPreview';
+import {NotificationBodyRenderer} from 'sentry/debug/notifications/components/notificationBodyRenderer';
import {
NotificationProviderKey,
type NotificationTemplateRegistration,
@@ -75,7 +76,13 @@ export function TeamsPreview({
{subject}
- {JSON.stringify(body)}
+
+
+
{actions.map(action => (
diff --git a/static/app/debug/notifications/types.ts b/static/app/debug/notifications/types.ts
index 3a20b98a658b56..57dd8697db5206 100644
--- a/static/app/debug/notifications/types.ts
+++ b/static/app/debug/notifications/types.ts
@@ -1,3 +1,5 @@
+import type {NotificationBodyFormattingBlock} from 'sentry/debug/notifications/components/notificationBodyRenderer';
+
export enum NotificationProviderKey {
EMAIL = 'email',
SLACK = 'slack',
@@ -9,7 +11,7 @@ export interface NotificationTemplateRegistration {
category: string;
example: {
actions: Array<{label: string; link: string}>;
- body: string;
+ body: NotificationBodyFormattingBlock[];
subject: string;
chart?: {alt_text: string; url: string};
footer?: string;