static/app/components/core/markdown/markdown.mdx
import {Markdown} from '@sentry/scraps/markdown';
import * as Storybook from 'sentry/stories';
import {CustomComponentsDemo, StreamingDemo, TagDemo} from './stories/components';
export const documentation = import('!!type-loader!@sentry/scraps/markdown/markdown');
The <Markdown> component renders markdown strings as native React elements using a token-based pipeline. Instead of dangerouslySetInnerHTML, it lexes the input with marked and renders each token as a React component. This enables:
components prop (replace any element with a custom React component)<Markdown raw="**Hello**, world!" />
Pass a markdown string to the raw prop.
<Storybook.Demo minHeight="max-content"> <Markdown raw={`# Issue Grouping
Sentry uses fingerprints to group similar events into issues. Each issue tracks frequency, error count impact, and affected users. Use `sentry.set_fingerprint()` to customize grouping.
Visit the Sentry docs for more on issue triage.
```python import sentry_sdk
sentry_sdk.set_tag("region", "us-east") sentry_sdk.capture_exception(err) ```
Custom fingerprints are useful when:
See the grouping best practices guide for advanced configuration.
Events are grouped by stack trace similarity by default. Custom fingerprints override this behavior.
| Status | Description |
|---|---|
| Unresolved | Active and needs triage |
| Resolved | Fixed in a release |
| Regressed | Reappeared after being resolved |
Hard line break:\ New line here.
<mark>Note:</mark> Reprocessing events requires uploading new debug files. `} />
</Storybook.Demo>
<Markdown raw={markdownString} />
The components prop lets you replace any rendered element with a custom React component. Keys are named after design system primitives: Heading, Paragraph, Link, InlineCode, CodeBlock, Text, etc.
Every override receives a Default prop — a reference to the built-in component for that element. This lets you conditionally customize rendering without importing internal defaults.
Use the Default prop to apply custom behavior for specific cases and fall through to the built-in rendering for everything else.
<Markdown
raw={content}
components={{
Heading: ({children, level, Default}) => {
if (level > 3) return <strong>{children}</strong>;
return <Default level={level}>{children}</Default>;
},
}}
/>
Use components.Link to control how links render (e.g., client-side navigation). Use components.Text to intercept plain text and inject patterns like issue linkification.
<Storybook.Demo> <CustomComponentsDemo /> </Storybook.Demo>
<Markdown
raw="The issue SENTRY-1234 was caused by a race condition..."
components={{
Link: ({href, children}) => <RouterLink to={href}>{children}</RouterLink>,
Text: ({children}) => linkifyIssueShortIds(children),
}}
/>
Override Heading to render headings as bold text instead of <h1>-<h6>.
<Storybook.Demo>
<Markdown
raw={# Heading becomes bold\n\nParagraph stays normal.}
components={{
Heading: ({children}) => <strong>{children}</strong>,
}}
/>
</Storybook.Demo>
<Markdown
raw={content}
components={{
Heading: ({children}) => <strong>{children}</strong>,
}}
/>
Set variant="streaming" to enable animation for new content. As the raw string grows, new text appears to be decoded progressively. The consumer accumulates chunks into a string and passes it as raw—the component handles the animation imperatively.
prefers-reduced-motion (animation is skipped entirely)<Storybook.Demo minHeight="520px" align="flex-start"> <StreamingDemo /> </Storybook.Demo>
function StreamingExample() {
const [text, setText] = useState('');
useEffect(() => {
// Accumulate chunks into a string
let buffer = '';
const reader = getStreamReader();
reader.onChunk = chunk => {
buffer += chunk;
setText(buffer);
};
}, []);
return <Markdown raw={text} variant="streaming" />;
}
Tags are a minimal Markdown syntax extension for custom embedded components. Inspired by Markdoc tag syntax, they let consumers wire up their own integration surface — for example, Seer uses tags to embed entity refs and generated artifacts inline in streamed markdown.
Two syntax forms are supported:
Self-closing — inline within text, no body:
The crash is caused by {% ref type="issue" id="PROJ-ABC" /%} in the auth middleware.
Block — with a JSON body between opening and closing tags:
{% artifact type="root-cause" %}
{"description":"Race condition in session refresh","severity":"high"}
{% /artifact %}
The Tag component receives four props:
name — the tag name (e.g. "ref", "artifact", or domain-specific conventions)level — "block" or "inline", reflecting where the parser encountered the tag (not the syntax form)attrs — key="value" pairs from the opening tag, parsed as Record<string, string>data — the parsed JSON body (undefined for self-closing tags)By default, Tag renders nothing. Provide a Tag component override to handle tags:
<Storybook.Demo> <TagDemo /> </Storybook.Demo>
<Markdown
raw={seerOutput}
components={{
Tag: ({name, attrs, data}) => {
if (name === 'ref' && attrs.type === 'issue') {
return <IssueBadge shortId={attrs.id} snapshot={data} />;
}
return null;
},
}}
>
During streaming, partial tag syntax (e.g. {% ref type="issue" before the closing /%} arrives) is automatically suppressed to avoid flashing raw source text.
The component applies the same security model as the existing MarkedText:
https:, mailto: only). Unsafe protocols like javascript: render as plain text.Image component to opt in.dangerouslySetInnerHTML for markdown tokens.Every override receives a Default prop in addition to the props listed below (except Image, which has no built-in default).
| Key | Default | Props |
|---|---|---|
Paragraph | <Text as="p"> | children |
Heading | <Heading as="h1-h6"> | children, level (1-6) |
CodeBlock | <CodeBlock> | children (string), lang |
InlineCode | <InlineCode> | children (string) |
Link | <Link> / <ExternalLink> | children, href, title |
Blockquote | <Quote> | children |
Strong | <strong> | children |
Emphasis | <em> | children |
Strikethrough | <Text strikethrough> | children |
Image | stripped | src, alt, title |
Text | passthrough | children (string) |
OrderedList | <Stack as="ol"> | children |
UnorderedList | <Stack as="ul"> | children |
ListItem | <Container as="li"> | children |
TaskList | <Stack as="ul"> (no bullets) | children |
TaskListItem | <Container as="li"> | children, checked (bool) |
Table | styled <table> | children |
Tag | null (silent) | name, level, attrs, data |
HorizontalRule | <Separator> | - |
LineBreak | ` | |
| ` | - | |
Html | DOMPurify sanitized | html (string) |