From c60e4c07f9ec910bef6d03eb51874fc31a9d5f26 Mon Sep 17 00:00:00 2001
From: s1gr1d <32902192+s1gr1d@users.noreply.github.com>
Date: Tue, 26 May 2026 15:46:30 +0200
Subject: [PATCH 1/4] only expand code snippets in markdown if explicitly
wanted
---
scripts/generate-md-exports.mjs | 2 +-
scripts/generate-md-exports.test.mjs | 370 ++++++++++++++++-----------
scripts/rehype-expand-code-tabs.mjs | 104 +++++---
src/remark-code-tabs.js | 8 +
4 files changed, 300 insertions(+), 184 deletions(-)
diff --git a/scripts/generate-md-exports.mjs b/scripts/generate-md-exports.mjs
index a99a21ec852ab..e07e71fc4ef6d 100644
--- a/scripts/generate-md-exports.mjs
+++ b/scripts/generate-md-exports.mjs
@@ -32,7 +32,7 @@ import {rehypeExpandCodeTabs} from './rehype-expand-code-tabs.mjs';
const DOCS_ORIGIN = process.env.NEXT_PUBLIC_DEVELOPER_DOCS
? 'https://develop.sentry.dev'
: 'https://docs.sentry.io';
-const CACHE_VERSION = 8;
+const CACHE_VERSION = 9;
const CACHE_COMPRESS_LEVEL = 4;
const R2_BUCKET = process.env.NEXT_PUBLIC_DEVELOPER_DOCS
? 'sentry-develop-docs'
diff --git a/scripts/generate-md-exports.test.mjs b/scripts/generate-md-exports.test.mjs
index bca320add3be4..c9dc307f97009 100644
--- a/scripts/generate-md-exports.test.mjs
+++ b/scripts/generate-md-exports.test.mjs
@@ -26,7 +26,7 @@ function htmlToMarkdown(html) {
);
}
-function buildCodeTabsHTML(tabs) {
+function buildCodeTabsHTML(tabs, {expand = false} = {}) {
const firstTab = tabs[0];
const codeTabsRendered =
@@ -49,169 +49,233 @@ function buildCodeTabsHTML(tabs) {
})
.join('');
- return `
${codeTabsRendered}${exportBlocks}
`;
+ const expandAttr = expand ? ' data-code-tab-md-expand-tabs' : '';
+ return `${codeTabsRendered}${exportBlocks}
`;
}
describe('rehypeExpandCodeTabs', () => {
- it('outputs one fenced code block per tab with "[Title] filename" headings', () => {
- const html = buildCodeTabsHTML([
- {
- title: 'Cloudflare Workers',
- filename: 'index.ts',
- lang: 'typescript',
- code: 'import { sentry } from "@sentry/hono/cloudflare";',
- },
- {
- title: 'Node.js',
- filename: 'app.ts',
- lang: 'typescript',
- code: 'import { sentry } from "@sentry/hono/node";',
- },
- {
- title: 'Bun',
- filename: 'index.ts',
- lang: 'typescript',
- code: 'import { sentry } from "@sentry/hono/bun";',
- },
- ]);
-
- const md = htmlToMarkdown(html);
-
- const codeBlocks = md.match(/```[\s\S]*?```/g);
- expect(codeBlocks).toHaveLength(3);
- expect(codeBlocks[0]).toContain('@sentry/hono/cloudflare');
- expect(codeBlocks[1]).toContain('@sentry/hono/node');
- expect(codeBlocks[2]).toContain('@sentry/hono/bun');
- expect(md).toContain('**\\[Cloudflare Workers] index.ts**');
- expect(md).toContain('**\\[Node.js] app.ts**');
- expect(md).toContain('**\\[Bun] index.ts**');
+ describe('default (collapsed)', () => {
+ it('outputs only the first tab code block', () => {
+ const html = buildCodeTabsHTML([
+ {title: 'npm', lang: 'bash', code: 'npm install @sentry/node'},
+ {title: 'yarn', lang: 'bash', code: 'yarn add @sentry/node'},
+ {title: 'pnpm', lang: 'bash', code: 'pnpm add @sentry/node'},
+ ]);
+
+ const md = htmlToMarkdown(html);
+
+ const codeBlocks = md.match(/```[\s\S]*?```/g);
+ expect(codeBlocks).toHaveLength(1);
+ expect(codeBlocks[0]).toContain('npm install @sentry/node');
+ });
+
+ it('appends "Also available for" note listing other tab titles', () => {
+ const html = buildCodeTabsHTML([
+ {title: 'npm', lang: 'bash', code: 'npm install @sentry/node'},
+ {title: 'yarn', lang: 'bash', code: 'yarn add @sentry/node'},
+ {title: 'pnpm', lang: 'bash', code: 'pnpm add @sentry/node'},
+ ]);
+
+ const md = htmlToMarkdown(html);
+
+ expect(md).toContain('*Also available for: yarn, pnpm*');
+ });
+
+ it('omits the note for a single-tab group', () => {
+ const html = buildCodeTabsHTML([
+ {title: 'typescript', lang: 'typescript', code: 'Sentry.init();'},
+ ]);
+
+ const md = htmlToMarkdown(html);
+
+ const codeBlocks = md.match(/```[\s\S]*?```/g);
+ expect(codeBlocks).toHaveLength(1);
+ expect(md).not.toContain('Also available for');
+ });
+
+ it('removes the CodeTabs-rendered active tab to avoid duplication', () => {
+ const html = buildCodeTabsHTML([
+ {title: 'ESM', filename: 'instrument.mjs', lang: 'javascript', code: 'import init'},
+ {title: 'CJS', filename: 'instrument.js', lang: 'javascript', code: 'require init'},
+ ]);
+
+ const md = htmlToMarkdown(html);
+
+ expect(md).not.toContain('`instrument.mjs`');
+ });
+
+ it('does not add bold headings to the code block', () => {
+ const html = buildCodeTabsHTML([
+ {title: 'ESM', filename: 'instrument.mjs', lang: 'javascript', code: 'import init'},
+ {title: 'CJS', filename: 'instrument.js', lang: 'javascript', code: 'require init'},
+ ]);
+
+ const md = htmlToMarkdown(html);
+
+ expect(md).not.toMatch(/\*\*.*\*\*/);
+ });
});
- it('removes the CodeTabs-rendered active tab to avoid duplication', () => {
- const html = buildCodeTabsHTML([
- {title: 'ESM', filename: 'instrument.mjs', lang: 'javascript', code: 'import init'},
- {title: 'CJS', filename: 'instrument.js', lang: 'javascript', code: 'require init'},
- ]);
-
- const md = htmlToMarkdown(html);
-
- expect(md).not.toContain('`instrument.mjs`');
- });
-
- it('uses tab title alone when filename is absent', () => {
- const html = buildCodeTabsHTML([
- {title: 'Cloudflare Workers', lang: 'javascript', code: 'workers();'},
- {title: 'Bun', lang: 'javascript', code: 'bun();'},
- ]);
-
- const md = htmlToMarkdown(html);
-
- const headings = md.match(/\*\*.*?\*\*/g);
- expect(headings).toHaveLength(2);
- expect(headings[0]).toBe('**Cloudflare Workers**');
- expect(headings[1]).toBe('**Bun**');
- });
-
- it('treats empty filename attribute the same as missing filename', () => {
- const html =
- '';
-
- const md = htmlToMarkdown(html);
-
- const headings = md.match(/\*\*.*?\*\*/g);
- expect(headings).toHaveLength(1);
- expect(headings[0]).toBe('**JavaScript**');
- });
-
- it('does not modify code blocks outside tab wrappers', () => {
- const html =
- 'curl -sL https://sentry.io/get-cli/ | bash
';
-
- const md = htmlToMarkdown(html);
+ describe('opt-in expanded ({mdExpandTabs})', () => {
+ it('outputs one fenced code block per tab with "[Title] filename" headings', () => {
+ const html = buildCodeTabsHTML(
+ [
+ {
+ title: 'Cloudflare Workers',
+ filename: 'index.ts',
+ lang: 'typescript',
+ code: 'import { sentry } from "@sentry/hono/cloudflare";',
+ },
+ {
+ title: 'Node.js',
+ filename: 'app.ts',
+ lang: 'typescript',
+ code: 'import { sentry } from "@sentry/hono/node";',
+ },
+ {
+ title: 'Bun',
+ filename: 'index.ts',
+ lang: 'typescript',
+ code: 'import { sentry } from "@sentry/hono/bun";',
+ },
+ ],
+ {expand: true}
+ );
- const codeBlocks = md.match(/```[\s\S]*?```/g);
- expect(codeBlocks).toHaveLength(1);
- expect(codeBlocks[0]).toContain('curl -sL');
- expect(md).not.toMatch(/\*\*.*\*\*\n/);
- });
+ const md = htmlToMarkdown(html);
+
+ const codeBlocks = md.match(/```[\s\S]*?```/g);
+ expect(codeBlocks).toHaveLength(3);
+ expect(codeBlocks[0]).toContain('@sentry/hono/cloudflare');
+ expect(codeBlocks[1]).toContain('@sentry/hono/node');
+ expect(codeBlocks[2]).toContain('@sentry/hono/bun');
+ expect(md).toContain('**\\[Cloudflare Workers] index.ts**');
+ expect(md).toContain('**\\[Node.js] app.ts**');
+ expect(md).toContain('**\\[Bun] index.ts**');
+ });
+
+ it('uses tab title alone when filename is absent', () => {
+ const html = buildCodeTabsHTML(
+ [
+ {title: 'Cloudflare Workers', lang: 'javascript', code: 'workers();'},
+ {title: 'Bun', lang: 'javascript', code: 'bun();'},
+ ],
+ {expand: true}
+ );
- it('preserves standalone code blocks when mixed with tab groups', () => {
- const standalone =
- 'npm install @sentry/node
';
- const tabs = buildCodeTabsHTML([
- {
- title: 'Node.js',
- filename: 'instrument.mjs',
- lang: 'javascript',
- code: 'Sentry.init();',
- },
- {title: 'Bun', lang: 'javascript', code: 'init();'},
- ]);
-
- const md = htmlToMarkdown(`${standalone}${tabs}
`);
-
- const codeBlocks = md.match(/```[\s\S]*?```/g);
- expect(codeBlocks).toHaveLength(3);
- expect(codeBlocks[0]).toContain('npm install');
- expect(md).toContain('**\\[Node.js] instrument.mjs**');
- expect(md).toContain('**Bun**');
+ const md = htmlToMarkdown(html);
+
+ const headings = md.match(/\*\*.*?\*\*/g);
+ expect(headings).toHaveLength(2);
+ expect(headings[0]).toBe('**Cloudflare Workers**');
+ expect(headings[1]).toBe('**Bun**');
+ });
+
+ it('treats empty filename attribute the same as missing filename', () => {
+ const html =
+ '';
+
+ const md = htmlToMarkdown(html);
+
+ const headings = md.match(/\*\*.*?\*\*/g);
+ expect(headings).toHaveLength(1);
+ expect(headings[0]).toBe('**JavaScript**');
+ });
+
+ it('drops export blocks that contain no pre element', () => {
+ const html =
+ '' +
+ '
' +
+ '
' +
+ '
' +
+ '
';
+
+ const md = htmlToMarkdown(html);
+
+ const codeBlocks = md.match(/```[\s\S]*?```/g);
+ expect(codeBlocks).toHaveLength(1);
+ expect(codeBlocks[0]).toContain('works()');
+ expect(md).toContain('**ok**');
+ expect(md).not.toContain('broken');
+ expect(md).not.toContain('active tab');
+ });
});
- it('expands multiple tab groups independently on the same page', () => {
- const group1 = buildCodeTabsHTML([
- {title: 'ESM', filename: 'instrument.mjs', lang: 'javascript', code: 'import init'},
- {title: 'CJS', filename: 'instrument.js', lang: 'javascript', code: 'require init'},
- ]);
- const group2 = buildCodeTabsHTML([
- {title: 'Python', filename: 'main.py', lang: 'python', code: 'import sentry_sdk'},
- {title: 'Ruby', filename: 'config.rb', lang: 'ruby', code: 'require "sentry-ruby"'},
- ]);
-
- const md = htmlToMarkdown(`${group1}${group2}
`);
-
- const codeBlocks = md.match(/```[\s\S]*?```/g);
- expect(codeBlocks).toHaveLength(4);
- expect(codeBlocks[0]).toContain('import init');
- expect(codeBlocks[1]).toContain('require init');
- expect(codeBlocks[2]).toContain('import sentry_sdk');
- expect(codeBlocks[3]).toContain('require "sentry-ruby"');
- });
+ describe('mixed page', () => {
+ it('collapses and expands groups independently on the same page', () => {
+ const collapsed = buildCodeTabsHTML([
+ {title: 'npm', lang: 'bash', code: 'npm install @sentry/node'},
+ {title: 'yarn', lang: 'bash', code: 'yarn add @sentry/node'},
+ ]);
+ const expanded = buildCodeTabsHTML(
+ [
+ {title: 'Cloudflare Workers', filename: 'index.ts', lang: 'typescript', code: 'cf()'},
+ {title: 'Node.js', filename: 'app.ts', lang: 'typescript', code: 'node()'},
+ ],
+ {expand: true}
+ );
- it('drops export blocks that contain no pre element', () => {
- const html =
- '' +
- '
' +
- '
' +
- '
' +
- '
';
-
- const md = htmlToMarkdown(html);
-
- const codeBlocks = md.match(/```[\s\S]*?```/g);
- expect(codeBlocks).toHaveLength(1);
- expect(codeBlocks[0]).toContain('works()');
- expect(md).toContain('**ok**');
- expect(md).not.toContain('broken');
- expect(md).not.toContain('active tab');
+ const md = htmlToMarkdown(`${collapsed}${expanded}
`);
+
+ const codeBlocks = md.match(/```[\s\S]*?```/g);
+ expect(codeBlocks).toHaveLength(3);
+ expect(codeBlocks[0]).toContain('npm install');
+ expect(codeBlocks[1]).toContain('cf()');
+ expect(codeBlocks[2]).toContain('node()');
+ expect(md).toContain('*Also available for: yarn*');
+ expect(md).toContain('**\\[Cloudflare Workers] index.ts**');
+ expect(md).toContain('**\\[Node.js] app.ts**');
+ });
+
+ it('preserves standalone code blocks alongside tab groups', () => {
+ const standalone =
+ 'npm install @sentry/node
';
+ const tabs = buildCodeTabsHTML([
+ {title: 'ESM', lang: 'javascript', code: 'import init'},
+ {title: 'CJS', lang: 'javascript', code: 'require init'},
+ ]);
+
+ const md = htmlToMarkdown(`${standalone}${tabs}
`);
+
+ const codeBlocks = md.match(/```[\s\S]*?```/g);
+ expect(codeBlocks).toHaveLength(2);
+ expect(codeBlocks[0]).toContain('npm install');
+ expect(codeBlocks[1]).toContain('import init');
+ expect(md).toContain('*Also available for: CJS*');
+ });
});
- it('leaves wrapper unchanged when it has no export blocks', () => {
- const html =
- '';
-
- const md = htmlToMarkdown(html);
-
- const codeBlocks = md.match(/```[\s\S]*?```/g);
- expect(codeBlocks).toHaveLength(1);
- expect(codeBlocks[0]).toContain('solo()');
+ describe('edge cases', () => {
+ it('does not modify code blocks outside tab wrappers', () => {
+ const html =
+ 'curl -sL https://sentry.io/get-cli/ | bash
';
+
+ const md = htmlToMarkdown(html);
+
+ const codeBlocks = md.match(/```[\s\S]*?```/g);
+ expect(codeBlocks).toHaveLength(1);
+ expect(codeBlocks[0]).toContain('curl -sL');
+ expect(md).not.toMatch(/\*\*.*\*\*\n/);
+ });
+
+ it('leaves wrapper unchanged when it has no export blocks', () => {
+ const html =
+ '';
+
+ const md = htmlToMarkdown(html);
+
+ const codeBlocks = md.match(/```[\s\S]*?```/g);
+ expect(codeBlocks).toHaveLength(1);
+ expect(codeBlocks[0]).toContain('solo()');
+ });
});
});
diff --git a/scripts/rehype-expand-code-tabs.mjs b/scripts/rehype-expand-code-tabs.mjs
index f6f0a528cffa3..29d603c3ea55a 100644
--- a/scripts/rehype-expand-code-tabs.mjs
+++ b/scripts/rehype-expand-code-tabs.mjs
@@ -1,7 +1,7 @@
import {visit} from 'unist-util-visit';
/**
- * Rehype plugin that expands CodeTabs for markdown export.
+ * Rehype plugin that converts CodeTabs export blocks for markdown export.
*
* The remark-code-tabs plugin injects hidden
* blocks alongside the interactive
component inside each
@@ -9,13 +9,12 @@ import {visit} from 'unist-util-visit';
* tab and are always present in the static HTML (unlike CodeTabs output,
* which may only include the active tab due to RSC serialization).
*
- * This plugin:
- * 1. Finds parent elements that contain [data-code-tab-title] children
- * 2. Replaces ALL children with expanded export blocks (removing the
- * CodeTabs-rendered content to avoid duplication)
- * 3. Each export block becomes a bold heading + fenced code block.
- * The heading format is "[Tab Title] filename" when both exist,
- * or just the tab title / filename when only one is present
+ * Behavior depends on whether the wrapper has data-code-tab-md-expand-tabs
+ * (set via {mdExpandTabs} in any code fence of the group):
+ *
+ * - With flag: all tabs are expanded with "[Title] filename" headings.
+ * - Without flag (default): only the first tab is kept, plus a note
+ * listing the other available tab titles.
*/
export function rehypeExpandCodeTabs() {
return tree => {
@@ -31,35 +30,80 @@ export function rehypeExpandCodeTabs() {
return;
}
- node.children = exportBlocks.flatMap(block => {
- const title = block.properties.dataCodeTabTitle;
- const filename = block.properties.dataCodeTabFilename;
- const label = filename && title ? `[${title}] ${filename}` : filename || title;
+ const expandAll = node.properties?.dataCodeTabMdExpandTabs != null;
+
+ if (expandAll) {
+ node.children = expandAllTabs(exportBlocks);
+ } else {
+ node.children = collapseToFirstTab(exportBlocks);
+ }
+ });
+ };
+}
+
+function expandAllTabs(exportBlocks) {
+ return exportBlocks.flatMap(block => {
+ const title = block.properties.dataCodeTabTitle;
+ const filename = block.properties.dataCodeTabFilename;
+ const label = filename && title ? `[${title}] ${filename}` : filename || title;
- const preElements = collectAll(block, el => el.tagName === 'pre');
- if (preElements.length === 0) {
- return [];
- }
+ const preElements = collectAll(block, el => el.tagName === 'pre');
+ if (preElements.length === 0) {
+ return [];
+ }
- return [
+ return [
+ {
+ type: 'element',
+ tagName: 'p',
+ properties: {},
+ children: [
{
type: 'element',
- tagName: 'p',
+ tagName: 'strong',
properties: {},
- children: [
- {
- type: 'element',
- tagName: 'strong',
- properties: {},
- children: [{type: 'text', value: label}],
- },
- ],
+ children: [{type: 'text', value: label}],
},
- ...preElements,
- ];
- });
+ ],
+ },
+ ...preElements,
+ ];
+ });
+}
+
+function collapseToFirstTab(exportBlocks) {
+ const first = exportBlocks[0];
+ const preElements = collectAll(first, el => el.tagName === 'pre');
+ if (preElements.length === 0) {
+ return [];
+ }
+
+ const result = [...preElements];
+
+ const otherTitles = exportBlocks
+ .slice(1)
+ .map(block => block.properties.dataCodeTabTitle)
+ .filter(Boolean);
+
+ if (otherTitles.length > 0) {
+ result.push({
+ type: 'element',
+ tagName: 'p',
+ properties: {},
+ children: [
+ {
+ type: 'element',
+ tagName: 'em',
+ properties: {},
+ children: [
+ {type: 'text', value: `Also available for: ${otherTitles.join(', ')}`},
+ ],
+ },
+ ],
});
- };
+ }
+
+ return result;
}
function collectAll(node, predicate) {
diff --git a/src/remark-code-tabs.js b/src/remark-code-tabs.js
index 2f1bdb654acca..ebc957a53edeb 100644
--- a/src/remark-code-tabs.js
+++ b/src/remark-code-tabs.js
@@ -32,6 +32,11 @@ function getTabTitle(node) {
return (match && match[1]) || '';
}
+function getMdExpandTabs(node) {
+ const meta = getFullMeta(node);
+ return /\{mdExpandTabs}/.test(meta || '');
+}
+
// TODO(dcramer): this should only operate on MDX
export default function remarkCodeTabs() {
return markdownAST => {
@@ -84,11 +89,14 @@ export default function remarkCodeTabs() {
};
});
+ const shouldExpand = pendingCode.some(([node]) => getMdExpandTabs(node));
+
rootNode.type = 'element';
rootNode.data = {
hName: 'div',
hProperties: {
className: 'code-tabs-wrapper',
+ ...(shouldExpand && {dataCodeTabMdExpandTabs: true}),
},
};
rootNode.children = [
From accb718f8da9c295a709859b119e464c19eee01d Mon Sep 17 00:00:00 2001
From: s1gr1d <32902192+s1gr1d@users.noreply.github.com>
Date: Tue, 26 May 2026 15:47:07 +0200
Subject: [PATCH 2/4] improve skill to include info about code tabs
---
.claude/skills/technical-docs/SKILL.md | 58 +++++++++++++++++++++++++-
1 file changed, 57 insertions(+), 1 deletion(-)
diff --git a/.claude/skills/technical-docs/SKILL.md b/.claude/skills/technical-docs/SKILL.md
index 66601af0f403b..da9c546573576 100644
--- a/.claude/skills/technical-docs/SKILL.md
+++ b/.claude/skills/technical-docs/SKILL.md
@@ -130,12 +130,68 @@ Link to related docs rather than repeating content:
For automatic tracing, see API Reference.
```
-### Code Block Filenames
+### Code Block Meta Flags
Always include filename when showing file-specific code:
```tsx {filename:app/error.tsx}
```
+Consecutive fenced code blocks are automatically grouped into tabbed code snippets.
+Each tab can have a title and filename:
+
+~~~
+```swift {tabTitle:Swift}
+SentrySDK.capture(error: error)
+```
+
+```objc {tabTitle:Objective-C}
+[SentrySDK captureError:error];
+```
+~~~
+
+#### Markdown Export and `{mdExpandTabs}`
+
+The `.md` export (mainly used by LLMs via the "Copy page" button) **collapses tab groups
+by default**: only the first tab is included, with a note listing the other tabs
+(e.g. *Also available for: yarn, pnpm*). This keeps context lean when tabs show
+trivial variations an LLM can infer on its own.
+
+Add `{mdExpandTabs}` to the first code fence in a group when the tabs contain code an LLM
+**cannot reliably derive** from seeing just one tab. This is rare — most times, adding only
+one tab to the produced `.md` is enough.
+
+~~~
+```swift {tabTitle:Swift} {mdExpandTabs}
+SentrySDK.start { options in
+ options.dsn = "..."
+}
+```
+
+```objc {tabTitle:Objective-C}
+[SentrySDK startWithConfigureOptions:^(SentryOptions *options) {
+ options.dsn = @"...";
+}];
+```
+~~~
+
+**Expand** — the code is too different for an LLM to infer:
+- Different languages: Swift / Objective-C, cross-language guides (JS/Python/PHP/Ruby/...)
+- Different setup flows: Hono guide init (Cloudflare vs Node.js `--import` vs Bun)
+- Different APIs or wrappers: GCP Cloud Functions (`wrapHttpFunction` vs `wrapCloudEventFunction`), serverless async/sync handlers
+- Different framework versions with distinct imports: Spring 5/6/7, Spring Boot 2/3/4, Svelte v5+ / v3
+- Client / Server splits: Next.js, Remix, React Router (Replay + browser tracing vs Node integrations)
+- Different platform tooling: KMP (`commonMain` / `iosApp` / `androidApp`), Flutter navigation (Navigator / GoRouter / AutoRoute)
+- Install methods with different patterns: npm (`import`) vs CDN (`