diff --git a/docs/platforms/javascript/common/migration/v10-to-v11.mdx b/docs/platforms/javascript/common/migration/v10-to-v11.mdx
new file mode 100644
index 0000000000000..7958c277f0cef
--- /dev/null
+++ b/docs/platforms/javascript/common/migration/v10-to-v11.mdx
@@ -0,0 +1,45 @@
+---
+title: Migrate from 10.x to 11.x
+sidebar_order: 8870
+description: "Learn about migrating from Sentry JavaScript SDK 10.x to 11.x, with an interactive checklist of the changes that apply to your setup."
+notSupported:
+ - javascript.capacitor
+ - javascript.cordova
+ - javascript.electron
+---
+
+
+
+
+This page shows the changes that apply to browser JavaScript. To see the steps for your setup instead, switch to the guide for it with the platform dropdown in the top-left corner, or go straight to [Next.js](/platforms/javascript/guides/nextjs/migration/v10-to-v11/), [React](/platforms/javascript/guides/react/migration/v10-to-v11/), [Node.js](/platforms/javascript/guides/node/migration/v10-to-v11/), or [any other JavaScript platform](/platforms/javascript/).
+
+
+
+
+Version 11 of the Sentry JavaScript SDK focuses on better OpenTelemetry interoperability, more flexible instrumentation, and better out-of-the-box defaults. It raises the minimum versions of Node.js, TypeScript, and several frameworks. For many of these breaking changes, you can migrate automatically:
+
+```bash
+npx sentry@latest migrate
+```
+
+We recommend upgrading to the most recent 10.x release first, since most of what v11 removes is already deprecated there.
+
+Version 11 is compatible with Sentry self-hosted 24.4.2 or higher, unchanged from v10. Lower versions may continue to work, but may not support all features.
+
+
+
+TypeScript and other tooling won't catch all of these changes. Read through the steps below before upgrading, rather than upgrading and fixing what breaks.
+
+
+
+## Your Migration Steps
+
+
+
+## No Version Support Timeline
+
+Version support timelines are stressful for everybody using the SDK, so we won't be defining one. Instead, we will be applying bug fixes and features to older versions as long as there is demand.
+
+Additionally, we hold ourselves accountable to any security issues, meaning that if any vulnerabilities are found, we will in almost all cases backport them.
+
+It's decided on a case-by-case basis what gets backported. If you need a fix or feature in a previous version of the SDK, please reach out via a [GitHub issue](https://github.com/getsentry/sentry-javascript/issues).
diff --git a/docs/platforms/javascript/guides/effect/index.mdx b/docs/platforms/javascript/guides/effect/index.mdx
index e77b39975489f..5085426aadae4 100644
--- a/docs/platforms/javascript/guides/effect/index.mdx
+++ b/docs/platforms/javascript/guides/effect/index.mdx
@@ -2,6 +2,10 @@
title: Effect
sdk: sentry.javascript.effect
description: Learn how to set up and configure Sentry in your Effect application, capture your first errors, and view them in Sentry.
+categories:
+ - javascript
+ - server
+ - server-node
---
diff --git a/includes/migration/javascript-v11/ai-exports-moved.mdx b/includes/migration/javascript-v11/ai-exports-moved.mdx
new file mode 100644
index 0000000000000..e23b9858bca6b
--- /dev/null
+++ b/includes/migration/javascript-v11/ai-exports-moved.mdx
@@ -0,0 +1,17 @@
+---
+id: ai-exports-moved
+title: "AI instrumentation moved out of `@sentry/core`"
+phase: code-changes
+category: removed-api
+severity: action-required
+frameworks: all
+features: [ai-agents]
+platformCategory: server
+order: 270
+---
+
+AI instrumentation moved from `@sentry/core` to `@sentry/server-utils`, and is no longer available in the browser SDK. The `enableTruncation` and `streamGenAiSpans` flags were removed, along with the internal `sentry.sdk_meta.gen_ai.input.messages.original_length` attribute.
+
+Importing a helper from `@sentry/node` or another platform SDK still works. If you imported one directly from `@sentry/core`, import it from `@sentry/server-utils` instead: `instrumentOpenAiClient`, `instrumentAnthropicAiClient`, `instrumentGoogleGenAIClient`, `instrumentWorkersAiClient`, `createLangChainCallbackHandler`, `instrumentLangChainEmbeddings`, `instrumentStateGraph`, `instrumentStateGraphCompile`, `instrumentCreateReactAgent`, `addVercelAiProcessors`.
+
+Remove `enableTruncation` and `streamGenAiSpans` from your config, and remove AI integrations from browser setups.
diff --git a/includes/migration/javascript-v11/ai-internal-exports-removed.mdx b/includes/migration/javascript-v11/ai-internal-exports-removed.mdx
new file mode 100644
index 0000000000000..94ee509f04928
--- /dev/null
+++ b/includes/migration/javascript-v11/ai-internal-exports-removed.mdx
@@ -0,0 +1,17 @@
+---
+id: ai-internal-exports-removed
+title: "Low-level AI exports are no longer public API"
+phase: code-changes
+category: removed-api
+severity: informational
+frameworks: all
+features: [ai-agents]
+platformCategory: server
+order: 280
+---
+
+These provider-instrumentation internals are no longer exported from `@sentry/core`:
+
+- **Helpers:** `extractOpenAiRequestAttributes`, `addOpenAiRequestAttributes`, `addOpenAiResponseAttributes`, `extractOpenAiRequestParameters`, `instrumentOpenAiStream`, `extractAnthropicRequestAttributes`, `addAnthropicRequestAttributes`, `addAnthropicResponseAttributes`, `instrumentAsyncIterableStream`, `instrumentMessageStream`, `extractGoogleGenAIRequestAttributes`, `addGoogleGenAIRequestAttributes`, `addGoogleGenAIResponseAttributes`, `instrumentGoogleGenAIStream`, `getProviderMetadataAttributes`, `getTruncatedJsonString`, `shouldEnableTruncation`, `resolveAIRecordingOptions`, `wrapToolsWithSpans`, `extractLLMFromParams`, `extractAgentNameFromParams`, `instrumentCompiledGraphInvoke`
+- **Integration-name constants:** `OPENAI_INTEGRATION_NAME`, `ANTHROPIC_AI_INTEGRATION_NAME`, `GOOGLE_GENAI_INTEGRATION_NAME`, `LANGCHAIN_INTEGRATION_NAME`, `LANGGRAPH_INTEGRATION_NAME`
+- **Types:** `OpenAiClient`, `OpenAiOptions`, `InstrumentedMethod`, `AnthropicAiClient`, `AnthropicAiOptions`, `AnthropicAiResponse`, `AnthropicAiInstrumentedMethod`, `GoogleGenAIClient`, `GoogleGenAIChat`, `GoogleGenAIOptions`, `GoogleGenAIResponse`, `GoogleGenAIInstrumentedMethod`, `GoogleGenAIIstrumentedMethod`, `WorkersAiClient`, `WorkersAiOptions`, `LangChainOptions`, `LangChainIntegration`, `LangGraphOptions`, `LangGraphIntegration`, `CompiledGraph`
diff --git a/includes/migration/javascript-v11/ai-non-inference-spans.mdx b/includes/migration/javascript-v11/ai-non-inference-spans.mdx
new file mode 100644
index 0000000000000..faa42da30dc5c
--- /dev/null
+++ b/includes/migration/javascript-v11/ai-non-inference-spans.mdx
@@ -0,0 +1,15 @@
+---
+id: ai-non-inference-spans
+title: "AI integrations no longer trace non-inference operations"
+phase: verify
+category: behavior
+severity: behavior-change
+frameworks: all
+features: [ai-agents]
+platformCategory: server
+order: 90
+---
+
+AI integrations only trace model invocations, tool calls, and agent invocations now. Anthropic `messages.countTokens`, `models.retrieve` and `models.get`, and LangGraph `gen_ai.create_agent` on graph compilation, no longer produce spans.
+
+Update dashboards and alerts that reference these spans.
diff --git a/includes/migration/javascript-v11/astro-min-version.mdx b/includes/migration/javascript-v11/astro-min-version.mdx
new file mode 100644
index 0000000000000..769468c2da2fd
--- /dev/null
+++ b/includes/migration/javascript-v11/astro-min-version.mdx
@@ -0,0 +1,15 @@
+---
+id: astro-min-version
+title: "Astro 3 support was dropped"
+phase: prerequisites
+category: version-support
+severity: action-required
+frameworks: [astro]
+features: []
+platformCategory: all
+order: 70
+---
+
+`@sentry/astro` now requires Astro 4.
+
+Upgrade Astro before upgrading the SDK.
diff --git a/includes/migration/javascript-v11/astro-runtime-options.mdx b/includes/migration/javascript-v11/astro-runtime-options.mdx
new file mode 100644
index 0000000000000..82180c944a18c
--- /dev/null
+++ b/includes/migration/javascript-v11/astro-runtime-options.mdx
@@ -0,0 +1,44 @@
+---
+id: astro-runtime-options
+title: "Runtime SDK options moved out of `sentryAstro()`"
+phase: code-changes
+category: removed-api
+severity: action-required
+frameworks: [astro]
+features: []
+platformCategory: all
+order: 400
+---
+
+Runtime SDK options (`dsn`, `environment`, `release` as a string, `sampleRate`, `tracesSampleRate`, `replaysSessionSampleRate`, `replaysOnErrorSampleRate`) can no longer be passed to `sentryAstro()`. `release` and `debug` are build-time options now, and the generated client snippet always includes the `Replay` integration.
+
+Keep build-time options in `astro.config.mjs`:
+
+```ts
+export default defineConfig({
+ integrations: [
+ sentry({
+ org: 'my-org',
+ project: 'my-project',
+ authToken: process.env.SENTRY_AUTH_TOKEN,
+ release: { name: '1.0.0' },
+ debug: true,
+ }),
+ ],
+});
+```
+
+Move runtime options to `sentry.client.config.ts` or `sentry.server.config.ts`:
+
+```ts
+import * as Sentry from '@sentry/astro';
+
+Sentry.init({
+ dsn: 'https://example@sentry.io/123',
+ release: '1.0.0',
+ environment: 'production',
+ tracesSampleRate: 0.5,
+});
+```
+
+To customize or remove Replay, create a `sentry.client.config.ts`.
diff --git a/includes/migration/javascript-v11/attach-stacktrace-default.mdx b/includes/migration/javascript-v11/attach-stacktrace-default.mdx
new file mode 100644
index 0000000000000..1673241b24b84
--- /dev/null
+++ b/includes/migration/javascript-v11/attach-stacktrace-default.mdx
@@ -0,0 +1,15 @@
+---
+id: attach-stacktrace-default
+title: "`attachStacktrace` now defaults to `true`"
+phase: verify
+category: behavior
+severity: behavior-change
+frameworks: all
+features: []
+platformCategory: all
+order: 40
+---
+
+`attachStacktrace` now defaults to `true`, so `Sentry.captureMessage` events and non-`Error` values passed to `Sentry.captureException` get a synthetic stack trace. Expect new issue groups, and expect these events to mark sessions as errored (crash-free rate is unaffected).
+
+Set `attachStacktrace: false` to restore the previous behavior. For purely informational output, use Sentry Logs instead.
diff --git a/includes/migration/javascript-v11/aws-lambda-layer.mdx b/includes/migration/javascript-v11/aws-lambda-layer.mdx
new file mode 100644
index 0000000000000..ccab301a0fdc0
--- /dev/null
+++ b/includes/migration/javascript-v11/aws-lambda-layer.mdx
@@ -0,0 +1,15 @@
+---
+id: aws-lambda-layer
+title: "A new AWS Lambda Layer is published for v11"
+phase: packages
+category: package
+severity: action-required
+frameworks: [aws-lambda]
+features: []
+platformCategory: serverless
+order: 50
+---
+
+The v11 AWS Lambda Layer is published as `SentryNodeServerlessSDKv11`. Version 10 updates continue as `SentryNodeServerlessSDKv10`.
+
+Point your functions at the new layer ARN, listed in the [Lambda layer docs](https://docs.sentry.io/platforms/javascript/guides/aws-lambda/install/cjs-layer/). The layer supports the `nodejs20.x`, `nodejs22.x`, and `nodejs24.x` runtimes, so functions on `nodejs18.x` need a newer runtime first.
diff --git a/includes/migration/javascript-v11/before-send-span-format.mdx b/includes/migration/javascript-v11/before-send-span-format.mdx
new file mode 100644
index 0000000000000..0e5cc6563c990
--- /dev/null
+++ b/includes/migration/javascript-v11/before-send-span-format.mdx
@@ -0,0 +1,49 @@
+---
+id: before-send-span-format
+title: "`beforeSendSpan` receives the streamed span format"
+phase: code-changes
+category: behavior
+severity: action-required
+frameworks: all
+features: [tracing]
+platformCategory: all
+order: 50
+---
+
+`beforeSendSpan` now receives a `StreamedSpanJSON` object as each span finishes, rather than every span of a transaction just before it is sent. The fields were renamed:
+
+| Before (`SpanJSON`) | After (`StreamedSpanJSON`) |
+| ------------------- | ------------------------------ |
+| `description` | `name` |
+| `data` | `attributes` |
+| `op` | `attributes['sentry.op']` |
+| `timestamp` | `end_timestamp` |
+| `status` (`string`) | `status` (`'ok'` or `'error'`) |
+
+Update the callback:
+
+```js
+// Before
+Sentry.init({
+ beforeSendSpan: span => {
+ if (span.op === 'db.query') {
+ span.description = scrub(span.description);
+ span.data['db.statement'] = scrub(span.data['db.statement']);
+ }
+ return span;
+ },
+});
+
+// After
+Sentry.init({
+ beforeSendSpan: span => {
+ if (span.attributes?.['sentry.op'] === 'db.query') {
+ span.name = scrub(span.name);
+ span.attributes['db.statement'] = scrub(span.attributes['db.statement']);
+ }
+ return span;
+ },
+});
+```
+
+Use `ignoreSpans` to drop spans; returning `null` is a no-op. A callback that does not match your `traceLifecycle` is never invoked: wrap it in `Sentry.withStaticSpan()` for `'static'`, leave it unwrapped for `'stream'`.
diff --git a/includes/migration/javascript-v11/before-send-transaction-removed.mdx b/includes/migration/javascript-v11/before-send-transaction-removed.mdx
new file mode 100644
index 0000000000000..c1fa2dad801fb
--- /dev/null
+++ b/includes/migration/javascript-v11/before-send-transaction-removed.mdx
@@ -0,0 +1,41 @@
+---
+id: before-send-transaction-removed
+title: "`beforeSendTransaction` no longer runs"
+phase: code-changes
+category: behavior
+severity: action-required
+frameworks: all
+features: [tracing]
+platformCategory: all
+order: 60
+---
+
+`beforeSendTransaction` no-ops, because span streaming produces no transaction events.
+
+Move scrubbing to `beforeSendSpan` and guard on `is_segment`, and move dropping to `ignoreSpans`:
+
+```js
+// Before
+Sentry.init({
+ beforeSendTransaction: event => {
+ if (event.transaction === 'GET /health') {
+ return null;
+ }
+ event.transaction = scrubIds(event.transaction);
+ return event;
+ },
+});
+
+// After
+Sentry.init({
+ ignoreSpans: ['GET /health'],
+ beforeSendSpan: span => {
+ if (span.is_segment) {
+ span.name = scrubIds(span.name);
+ }
+ return span;
+ },
+});
+```
+
+Scope `tags` and `extra` are not carried over to streamed spans. Use `Sentry.setAttribute()` or `Sentry.setAttributes()`.
diff --git a/includes/migration/javascript-v11/browser-http-headers-data-collection.mdx b/includes/migration/javascript-v11/browser-http-headers-data-collection.mdx
new file mode 100644
index 0000000000000..df8298d43cc48
--- /dev/null
+++ b/includes/migration/javascript-v11/browser-http-headers-data-collection.mdx
@@ -0,0 +1,15 @@
+---
+id: browser-http-headers-data-collection
+title: "Browser request headers follow `dataCollection`"
+phase: verify
+category: behavior
+severity: behavior-change
+frameworks: all
+features: []
+platformCategory: browser
+order: 140
+---
+
+`httpContextIntegration` attached `Referer` and `User-Agent` regardless of your configuration. Both now filter through `dataCollection.httpHeaders.request`, matching the server SDKs. Headers you set yourself are left alone.
+
+Nothing to do, unless you set `httpHeaders: { request: false }` and relied on those two headers still arriving from the browser.
diff --git a/includes/migration/javascript-v11/browser-session-lifecycle-page.mdx b/includes/migration/javascript-v11/browser-session-lifecycle-page.mdx
new file mode 100644
index 0000000000000..ee865226c0977
--- /dev/null
+++ b/includes/migration/javascript-v11/browser-session-lifecycle-page.mdx
@@ -0,0 +1,21 @@
+---
+id: browser-session-lifecycle-page
+title: "`page` is the default browser session lifecycle mode"
+phase: verify
+category: behavior
+severity: behavior-change
+frameworks: all
+features: []
+platformCategory: browser
+order: 30
+---
+
+`browserSessionIntegration` now defaults to `lifecycle: 'page'`, so a session is created on page load and not renewed on navigation. Expect session counts to drop for single-page apps.
+
+To keep the previous behavior:
+
+```js
+Sentry.init({
+ integrations: [Sentry.browserSessionIntegration({ lifecycle: 'route' })],
+});
+```
diff --git a/includes/migration/javascript-v11/browser-session-unhandled.mdx b/includes/migration/javascript-v11/browser-session-unhandled.mdx
new file mode 100644
index 0000000000000..92e3983251eeb
--- /dev/null
+++ b/includes/migration/javascript-v11/browser-session-unhandled.mdx
@@ -0,0 +1,15 @@
+---
+id: browser-session-unhandled
+title: "Browser sessions use `unhandled` instead of `crashed`"
+phase: verify
+category: behavior
+severity: behavior-change
+frameworks: all
+features: []
+platformCategory: browser
+order: 20
+---
+
+Browser sessions affected by an uncaught error are recorded as `unhandled` instead of `crashed`, so crash-free session rates will shift.
+
+Review Release Health alerts built on crash-free rate before rolling out.
diff --git a/includes/migration/javascript-v11/browser-support.mdx b/includes/migration/javascript-v11/browser-support.mdx
new file mode 100644
index 0000000000000..4265689e37e36
--- /dev/null
+++ b/includes/migration/javascript-v11/browser-support.mdx
@@ -0,0 +1,15 @@
+---
+id: browser-support
+title: "Safari 14 support was dropped"
+phase: prerequisites
+category: version-support
+severity: behavior-change
+frameworks: all
+features: []
+platformCategory: browser
+order: 30
+---
+
+The SDK now supports Safari 15 and higher. Safari 14 was dropped.
+
+Some SDK features rely on newer browser APIs and don't work on Safari 15.
diff --git a/includes/migration/javascript-v11/browser-user-timing.mdx b/includes/migration/javascript-v11/browser-user-timing.mdx
new file mode 100644
index 0000000000000..bac955d069a10
--- /dev/null
+++ b/includes/migration/javascript-v11/browser-user-timing.mdx
@@ -0,0 +1,32 @@
+---
+id: browser-user-timing
+title: "`performance.mark()` spans moved to `userTimingIntegration`"
+phase: code-changes
+category: removed-api
+severity: action-required
+frameworks: all
+features: [tracing]
+platformCategory: browser
+order: 210
+---
+
+`browserTracingIntegration` no longer captures `performance.mark()` and `performance.measure()` spans. The `ignorePerformanceApiSpans` option moved to `userTimingIntegration` as `ignore`.
+
+Add `userTimingIntegration()` to keep capturing them:
+
+```js
+// Before
+Sentry.init({
+ integrations: [
+ Sentry.browserTracingIntegration({ ignorePerformanceApiSpans: ['third-party-mark'] }),
+ ],
+});
+
+// After
+Sentry.init({
+ integrations: [
+ Sentry.browserTracingIntegration(),
+ Sentry.userTimingIntegration({ ignore: ['third-party-mark'] }),
+ ],
+});
+```
diff --git a/includes/migration/javascript-v11/browser-web-vital-options.mdx b/includes/migration/javascript-v11/browser-web-vital-options.mdx
new file mode 100644
index 0000000000000..54d76bd0c1a23
--- /dev/null
+++ b/includes/migration/javascript-v11/browser-web-vital-options.mdx
@@ -0,0 +1,15 @@
+---
+id: browser-web-vital-options
+title: "Standalone CLS and LCP span options were removed"
+phase: code-changes
+category: removed-api
+severity: action-required
+frameworks: all
+features: [tracing]
+platformCategory: browser
+order: 220
+---
+
+The experimental `_experiments.enableStandaloneClsSpans` and `_experiments.enableStandaloneLcpSpans` options were removed from `browserTracingIntegration` and `webVitalsIntegration`. With span streaming on by default, CLS and LCP are sent as dedicated spans. They are only recorded as measurements on the pageload span if you opt out with `traceLifecycle: 'static'`.
+
+Remove both options, and update dashboards or alerts that read CLS or LCP as pageload span measurements.
diff --git a/includes/migration/javascript-v11/channel-based-instrumentation.mdx b/includes/migration/javascript-v11/channel-based-instrumentation.mdx
new file mode 100644
index 0000000000000..b0ff19507c78e
--- /dev/null
+++ b/includes/migration/javascript-v11/channel-based-instrumentation.mdx
@@ -0,0 +1,15 @@
+---
+id: channel-based-instrumentation
+title: "Channel-based instrumentation is the default"
+phase: verify
+category: behavior
+severity: behavior-change
+frameworks: all
+features: []
+platformCategory: server
+order: 10
+---
+
+Channel-based instrumentation, which uses `orchestrion` instead of `import-in-the-middle`, is now the default. It was opt-in in v10.
+
+After upgrading, confirm spans still appear for the libraries you care about, especially if you had a custom instrumentation setup or relied on ESM loader hooks.
diff --git a/includes/migration/javascript-v11/child-process-worker-split.mdx b/includes/migration/javascript-v11/child-process-worker-split.mdx
new file mode 100644
index 0000000000000..466bcb5da318b
--- /dev/null
+++ b/includes/migration/javascript-v11/child-process-worker-split.mdx
@@ -0,0 +1,15 @@
+---
+id: child-process-worker-split
+title: "`childProcessIntegration` was split in two"
+phase: code-changes
+category: rename
+severity: action-required
+frameworks: all
+features: []
+platformCategory: server
+order: 320
+---
+
+`childProcessIntegration` was split into `childProcessIntegration` for `child_process` and `workerIntegration` for `worker_threads`.
+
+If you customized worker thread behavior, move that configuration to `workerIntegration`.
diff --git a/includes/migration/javascript-v11/cloudflare-instrument-d1.mdx b/includes/migration/javascript-v11/cloudflare-instrument-d1.mdx
new file mode 100644
index 0000000000000..46329f790dacc
--- /dev/null
+++ b/includes/migration/javascript-v11/cloudflare-instrument-d1.mdx
@@ -0,0 +1,28 @@
+---
+id: cloudflare-instrument-d1
+title: "`instrumentD1WithSentry` was removed"
+phase: code-changes
+category: removed-api
+severity: action-required
+frameworks: [cloudflare]
+features: [tracing]
+platformCategory: serverless
+order: 430
+---
+
+The deprecated `instrumentD1WithSentry` export was removed. `withSentry()` instruments all D1 bindings on `env`.
+
+Use the binding directly:
+
+```diff
+ export default withSentry(
+ (env) => ({ dsn: env.SENTRY_DSN }),
+ {
+ async fetch(request, env, ctx) {
+- const db = Sentry.instrumentD1WithSentry(env.DB);
+- const result = await db.prepare('SELECT * FROM users').all();
++ const result = await env.DB.prepare('SELECT * FROM users').all();
+ },
+ },
+ );
+```
diff --git a/includes/migration/javascript-v11/cloudflare-instrument-prototype-methods.mdx b/includes/migration/javascript-v11/cloudflare-instrument-prototype-methods.mdx
new file mode 100644
index 0000000000000..9f5dae8e98cd1
--- /dev/null
+++ b/includes/migration/javascript-v11/cloudflare-instrument-prototype-methods.mdx
@@ -0,0 +1,26 @@
+---
+id: cloudflare-instrument-prototype-methods
+title: "`instrumentPrototypeMethods` was removed"
+phase: code-changes
+category: removed-api
+severity: action-required
+frameworks: [cloudflare]
+features: [tracing]
+platformCategory: serverless
+order: 440
+---
+
+The `instrumentPrototypeMethods` option of `instrumentDurableObjectWithSentry` was removed.
+
+Use `enableRpcTracePropagation` instead:
+
+```diff
+ export const MyDO = Sentry.instrumentDurableObjectWithSentry(
+ (env) => ({
+ dsn: env.SENTRY_DSN,
+- instrumentPrototypeMethods: true,
++ enableRpcTracePropagation: true,
+ }),
+ MyDOBase,
+ );
+```
diff --git a/includes/migration/javascript-v11/cloudflare-nodejs-compat-subpath.mdx b/includes/migration/javascript-v11/cloudflare-nodejs-compat-subpath.mdx
new file mode 100644
index 0000000000000..b84c554f58423
--- /dev/null
+++ b/includes/migration/javascript-v11/cloudflare-nodejs-compat-subpath.mdx
@@ -0,0 +1,18 @@
+---
+id: cloudflare-nodejs-compat-subpath
+title: "The `@sentry/cloudflare/nodejs_compat` subpath was removed"
+phase: packages
+category: removed-api
+severity: action-required
+frameworks: [cloudflare]
+features: []
+platformCategory: serverless
+order: 60
+---
+
+The `@sentry/cloudflare/nodejs_compat` subpath was removed. The main entry point now includes everything.
+
+```diff
+- import * as Sentry from '@sentry/cloudflare/nodejs_compat';
++ import * as Sentry from '@sentry/cloudflare';
+```
diff --git a/includes/migration/javascript-v11/cloudflare-nodejs-compat.mdx b/includes/migration/javascript-v11/cloudflare-nodejs-compat.mdx
new file mode 100644
index 0000000000000..a7a1cf8def477
--- /dev/null
+++ b/includes/migration/javascript-v11/cloudflare-nodejs-compat.mdx
@@ -0,0 +1,20 @@
+---
+id: cloudflare-nodejs-compat
+title: "The `nodejs_compat` compatibility flag is now required"
+phase: prerequisites
+category: version-support
+severity: action-required
+frameworks: [cloudflare]
+features: []
+platformCategory: serverless
+order: 110
+---
+
+`@sentry/cloudflare` now requires the `nodejs_compat` compatibility flag instead of `nodejs_als`.
+
+Update your `wrangler.toml` or `wrangler.jsonc`:
+
+```diff
+- compatibility_flags = ["nodejs_als"]
++ compatibility_flags = ["nodejs_compat"]
+```
diff --git a/includes/migration/javascript-v11/cloudflare-rpc-trace-propagation.mdx b/includes/migration/javascript-v11/cloudflare-rpc-trace-propagation.mdx
new file mode 100644
index 0000000000000..df8c88cfb6b8c
--- /dev/null
+++ b/includes/migration/javascript-v11/cloudflare-rpc-trace-propagation.mdx
@@ -0,0 +1,15 @@
+---
+id: cloudflare-rpc-trace-propagation
+title: "`enableRpcTracePropagation` now defaults to `true`"
+phase: verify
+category: behavior
+severity: behavior-change
+frameworks: [cloudflare]
+features: [tracing]
+platformCategory: serverless
+order: 110
+---
+
+Trace context is now propagated across RPC calls (service bindings, Durable Objects, and WorkerEntrypoints) by default.
+
+Set `enableRpcTracePropagation: false` to opt out.
diff --git a/includes/migration/javascript-v11/cloudflare-type-changes.mdx b/includes/migration/javascript-v11/cloudflare-type-changes.mdx
new file mode 100644
index 0000000000000..0fb864ef5531f
--- /dev/null
+++ b/includes/migration/javascript-v11/cloudflare-type-changes.mdx
@@ -0,0 +1,33 @@
+---
+id: cloudflare-type-changes
+title: "Cloudflare `env` types and generics were reworked"
+phase: cleanup
+category: type
+severity: informational
+frameworks: [cloudflare]
+features: []
+platformCategory: serverless
+order: 20
+---
+
+The `env` types and the generics on `withSentry` and `instrumentDurableObjectWithSentry` were reworked. Types are now inferred.
+
+Drop explicit generic type parameters:
+
+```diff
+- export default withSentry(
++ export default withSentry(
+ (env) => ({ dsn: env.SENTRY_DSN }),
+ {
+ async fetch(request, env, ctx) {},
+ } satisfies ExportedHandler,
+ );
+```
+
+```diff
+- export const MyDO = Sentry.instrumentDurableObjectWithSentry(
++ export const MyDO = Sentry.instrumentDurableObjectWithSentry(
+ (env) => ({ dsn: env.SENTRY_DSN }),
+ MyDOBase,
+ );
+```
diff --git a/includes/migration/javascript-v11/cloudflare-vite-auto-instrumentation.mdx b/includes/migration/javascript-v11/cloudflare-vite-auto-instrumentation.mdx
new file mode 100644
index 0000000000000..b650bc751a13b
--- /dev/null
+++ b/includes/migration/javascript-v11/cloudflare-vite-auto-instrumentation.mdx
@@ -0,0 +1,22 @@
+---
+id: cloudflare-vite-auto-instrumentation
+title: "The Cloudflare Vite plugin auto-instruments your Worker"
+phase: verify
+category: behavior
+severity: behavior-change
+frameworks: [cloudflare]
+features: []
+platformCategory: serverless
+order: 120
+---
+
+`sentryCloudflareVitePlugin()` now wraps your Worker entry, and any Durable Object, Workflow or WorkerEntrypoint class in your wrangler config, at build time. Entries you wrapped yourself are left untouched.
+
+Remove the experimental opt-in if you set it:
+
+```diff
+- sentryCloudflareVitePlugin({ _experimental: { autoInstrumentation: true } });
++ sentryCloudflareVitePlugin();
+```
+
+To opt out entirely, use `sentryCloudflareVitePlugin({ autoInstrumentation: false })`.
diff --git a/includes/migration/javascript-v11/cloudflare-wrap-request-handler.mdx b/includes/migration/javascript-v11/cloudflare-wrap-request-handler.mdx
new file mode 100644
index 0000000000000..1ebefaed663a1
--- /dev/null
+++ b/includes/migration/javascript-v11/cloudflare-wrap-request-handler.mdx
@@ -0,0 +1,18 @@
+---
+id: cloudflare-wrap-request-handler
+title: "`wrapRequestHandler` moved to `@sentry/cloudflare/request`"
+phase: code-changes
+category: removed-api
+severity: action-required
+frameworks: [cloudflare]
+features: []
+platformCategory: serverless
+order: 420
+---
+
+`wrapRequestHandler` moved out of the main `@sentry/cloudflare` entry point.
+
+```diff
+- import { wrapRequestHandler } from '@sentry/cloudflare';
++ import { wrapRequestHandler } from '@sentry/cloudflare/request';
+```
diff --git a/includes/migration/javascript-v11/console-breadcrumbs.mdx b/includes/migration/javascript-v11/console-breadcrumbs.mdx
new file mode 100644
index 0000000000000..4e2a7af216962
--- /dev/null
+++ b/includes/migration/javascript-v11/console-breadcrumbs.mdx
@@ -0,0 +1,15 @@
+---
+id: console-breadcrumbs
+title: "Console breadcrumbs are handled by `consoleIntegration`"
+phase: code-changes
+category: removed-api
+severity: action-required
+frameworks: all
+features: []
+platformCategory: browser
+order: 240
+---
+
+The `console` option of `breadcrumbsIntegration` was removed from `@sentry/browser` and `@sentry/deno`.
+
+Use `consoleIntegration` from `@sentry/core` to capture console breadcrumbs.
diff --git a/includes/migration/javascript-v11/core-removed-apis.mdx b/includes/migration/javascript-v11/core-removed-apis.mdx
new file mode 100644
index 0000000000000..bc086033cbf62
--- /dev/null
+++ b/includes/migration/javascript-v11/core-removed-apis.mdx
@@ -0,0 +1,23 @@
+---
+id: core-removed-apis
+title: "`@sentry/core` API removals"
+phase: code-changes
+category: removed-api
+severity: action-required
+frameworks: all
+features: []
+platformCategory: all
+order: 170
+---
+
+These exports and options were removed from `@sentry/core` and every SDK that re-exported them:
+
+- `addAutoIpAddressToUser`
+- `Scope.clear()`. Use `withScope` or `withIsolationScope` for a fresh scope.
+- The positional `spanOrigin` argument of `instrumentFetchRequest`. Pass `{ spanOrigin }` as the last argument.
+- `createSpanEnvelope`, and the `SpanEnvelope` and `SpanItem` types.
+- `disableInstrumentationWarnings` and the `MissingInstrumentationContext` type.
+- `isStreamedBeforeSendSpanCallback()`
+- The `semanticAttributes` re-export. Import span attribute constants from `@sentry/core` directly.
+
+TypeScript catches all of these except `disableInstrumentationWarnings` in plain-JavaScript configs.
diff --git a/includes/migration/javascript-v11/custom-otel-setup.mdx b/includes/migration/javascript-v11/custom-otel-setup.mdx
new file mode 100644
index 0000000000000..489e1cf645a2f
--- /dev/null
+++ b/includes/migration/javascript-v11/custom-otel-setup.mdx
@@ -0,0 +1,50 @@
+---
+id: custom-otel-setup
+title: "Custom OpenTelemetry setups must export over OTLP"
+phase: code-changes
+category: removed-api
+severity: action-required
+frameworks: all
+features: [custom-otel]
+platformCategory: server
+order: 140
+---
+
+`SentryContextManager`, `SentrySampler`, and `SentrySpanProcessor` were removed, so you can no longer route spans from your own OpenTelemetry provider into Sentry as Sentry spans.
+
+Export them over OTLP instead. `Sentry.getOtlpTracesEndpoint()` turns your DSN into the endpoint URL and auth headers:
+
+```js
+import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
+import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
+import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
+import * as Sentry from '@sentry/node';
+
+const provider = new NodeTracerProvider({
+ spanProcessors: [new BatchSpanProcessor(new OTLPTraceExporter(Sentry.getOtlpTracesEndpoint('__DSN__')))],
+});
+
+provider.register();
+
+Sentry.init({
+ dsn: '__DSN__',
+ // no tracesSampleRate: OpenTelemetry owns spans, Sentry owns errors and logs
+ integrations: [Sentry.otlpIntegration()],
+});
+```
+
+Leave `tracesSampleRate` unset to avoid duplicate spans, since Sentry instruments many of the same libraries. Sentry still isolates requests with tracing off.
+
+If you do want Sentry spans alongside your own, keep `tracesSampleRate` set and drop the overlapping integrations. Turn off HTTP and fetch spans rather than the integrations, which also provide request isolation and session tracking:
+
+```js
+Sentry.init({
+ dsn: '__DSN__',
+ tracesSampleRate: 1.0,
+ integrations: integrations => [
+ ...integrations.filter(integration => integration.name !== 'Postgres'),
+ Sentry.httpIntegration({ spans: false }),
+ Sentry.nativeNodeFetchIntegration({ spans: false }),
+ ],
+});
+```
diff --git a/includes/migration/javascript-v11/data-collection.mdx b/includes/migration/javascript-v11/data-collection.mdx
new file mode 100644
index 0000000000000..c1c2ad5291a79
--- /dev/null
+++ b/includes/migration/javascript-v11/data-collection.mdx
@@ -0,0 +1,49 @@
+---
+id: data-collection
+title: "`sendDefaultPii` is replaced by `dataCollection`"
+phase: code-changes
+category: behavior
+severity: action-required
+frameworks: all
+features: []
+platformCategory: all
+order: 10
+---
+
+`sendDefaultPii` was replaced by `dataCollection`, which controls each category individually. Leaving `dataCollection` unset collects most categories by default, where the unset `sendDefaultPii` was restrictive.
+
+| Category | v10 default (`sendDefaultPii` off) | v11 default |
+| --------------------- | ---------------------------------- | -------------------- |
+| `userInfo` | `false` | `true` |
+| `cookies` | not collected | `true` |
+| `httpHeaders` | request + response, PII scrubbed | request + response |
+| `httpBodies` | not collected (size only) | all request/response |
+| `urlQueryParams` | `true` | `true` |
+| `genAI` | inputs + outputs not collected | inputs + outputs |
+| `databaseQueryData` | `false` | `true` |
+| `stackFrameVariables` | `true` | `true` |
+| `frameContextLines` | `7` | `5` |
+
+If you set `sendDefaultPii: true`, remove it. The v11 default matches that behavior.
+
+If you relied on the v10 default, set the baseline explicitly:
+
+```js
+Sentry.init({
+ dataCollection: {
+ userInfo: false,
+ cookies: false,
+ httpHeaders: {
+ request: { deny: ['forwarded', '-ip', 'remote-', 'via', '-user'] },
+ response: { deny: ['forwarded', '-ip', 'remote-', 'via', '-user'] },
+ },
+ httpBodies: [],
+ urlQueryParams: { deny: ['forwarded', '-ip', 'remote-', 'via', '-user'] },
+ genAI: { inputs: false, outputs: false },
+ databaseQueryData: false,
+ graphQL: { document: false, variables: false },
+ },
+});
+```
+
+Review your data-scrubbing config, especially for request and response bodies. `cookies`, `urlQueryParams`, and the `httpHeaders` fields also accept `{ allow: string[] }` or `{ deny: string[] }`. See the [`dataCollection` docs](https://docs.sentry.io/platforms/javascript/configuration/options/#dataCollection).
diff --git a/includes/migration/javascript-v11/deno-integrations-rename.mdx b/includes/migration/javascript-v11/deno-integrations-rename.mdx
new file mode 100644
index 0000000000000..d6360a18cc012
--- /dev/null
+++ b/includes/migration/javascript-v11/deno-integrations-rename.mdx
@@ -0,0 +1,24 @@
+---
+id: deno-integrations-rename
+title: "Deno default integrations were renamed"
+phase: code-changes
+category: rename
+severity: behavior-change
+frameworks: [deno]
+features: []
+platformCategory: server
+order: 310
+---
+
+Default integrations in `@sentry/deno` were renamed to match the other SDKs. The old `deno*Integration` exports remain as deprecated aliases.
+
+| Before | After |
+| -------------- | ---------- |
+| `DenoAmqplib` | `Amqplib` |
+| `DenoKoa` | `Koa` |
+| `DenoMongodb` | `Mongodb` |
+| `DenoMongoose` | `Mongoose` |
+| `DenoMysql` | `Mysql` |
+| `DenoPostgres` | `Postgres` |
+
+Update any reference to the old names, such as disabling an integration by name.
diff --git a/includes/migration/javascript-v11/deno-version.mdx b/includes/migration/javascript-v11/deno-version.mdx
new file mode 100644
index 0000000000000..b0a5866774d15
--- /dev/null
+++ b/includes/migration/javascript-v11/deno-version.mdx
@@ -0,0 +1,15 @@
+---
+id: deno-version
+title: "Deno 2.8.3 is the new minimum"
+phase: prerequisites
+category: version-support
+severity: action-required
+frameworks: [deno]
+features: []
+platformCategory: server
+order: 20
+---
+
+The minimum supported Deno version is now 2.8.3.
+
+Upgrade Deno before upgrading the SDK.
diff --git a/includes/migration/javascript-v11/ember-app-instance-required.mdx b/includes/migration/javascript-v11/ember-app-instance-required.mdx
new file mode 100644
index 0000000000000..66eab612e7984
--- /dev/null
+++ b/includes/migration/javascript-v11/ember-app-instance-required.mdx
@@ -0,0 +1,15 @@
+---
+id: ember-app-instance-required
+title: "`appInstance` is required in the Ember browser tracing options"
+phase: code-changes
+category: behavior
+severity: action-required
+frameworks: [ember]
+features: [tracing]
+platformCategory: browser
+order: 460
+---
+
+`appInstance` is now required in the Ember browser tracing integration options.
+
+Pass the application instance explicitly. If you set up performance through `instrumentAppInstancePerformance()`, it is already threaded through for you.
diff --git a/includes/migration/javascript-v11/ember-v2-addon.mdx b/includes/migration/javascript-v11/ember-v2-addon.mdx
new file mode 100644
index 0000000000000..189672d97172f
--- /dev/null
+++ b/includes/migration/javascript-v11/ember-v2-addon.mdx
@@ -0,0 +1,55 @@
+---
+id: ember-v2-addon
+title: "`@sentry/ember` is now a v2 addon with manual setup"
+phase: code-changes
+category: behavior
+severity: action-required
+frameworks: [ember]
+features: []
+platformCategory: browser
+order: 450
+---
+
+`@sentry/ember` is now a [v2 (Embroider) addon](https://rfcs.emberjs.com/id/0507-embroider-v2-package-format/), so it no longer configures itself from `config/environment.js` or registers an initializer.
+
+Remove the `'@sentry/ember'` block from `config/environment.js` and call `init()` in `app/app.ts`:
+
+```ts
+import Application from '@ember/application';
+import Resolver from 'ember-resolver';
+import loadInitializers from 'ember-load-initializers';
+import config from 'my-app/config/environment';
+import * as Sentry from '@sentry/ember';
+
+Sentry.init({
+ dsn: config.sentryDsn,
+ tracesSampleRate: 1.0,
+});
+
+export default class App extends Application {
+ modulePrefix = config.modulePrefix;
+ podModulePrefix = config.podModulePrefix;
+ Resolver = Resolver;
+}
+
+loadInitializers(App, config.modulePrefix);
+```
+
+Opt into performance instrumentation with an instance-initializer, where the former `disable*` flags now live:
+
+```ts
+// app/instance-initializers/sentry-performance.ts
+import type ApplicationInstance from '@ember/application/instance';
+import { instrumentAppInstancePerformance } from '@sentry/ember';
+
+export function initialize(appInstance: ApplicationInstance): void {
+ instrumentAppInstancePerformance(appInstance, {
+ // disableRunloopPerformance: false,
+ // disableInstrumentComponents: false,
+ });
+}
+
+export default { initialize };
+```
+
+Omit that initializer to disable performance instrumentation; `disablePerformance` no longer exists. `instrumentRoutePerformance` is unchanged, and FastBoot is detected automatically. Full walkthrough: [`packages/ember/UPGRADE.md`](https://github.com/getsentry/sentry-javascript/blob/master/packages/ember/UPGRADE.md).
diff --git a/includes/migration/javascript-v11/enable-logs-removed.mdx b/includes/migration/javascript-v11/enable-logs-removed.mdx
new file mode 100644
index 0000000000000..d5d12e3bd7e5f
--- /dev/null
+++ b/includes/migration/javascript-v11/enable-logs-removed.mdx
@@ -0,0 +1,23 @@
+---
+id: enable-logs-removed
+title: "The `enableLogs` option was removed"
+phase: code-changes
+category: removed-api
+severity: action-required
+frameworks: all
+features: [logs]
+platformCategory: all
+order: 90
+---
+
+`enableLogs` and `_experiments.enableLogs` were removed. Logs are captured whenever you call `Sentry.logger.*` or add a logging integration such as `consoleLoggingIntegration()`.
+
+Remove the option:
+
+```js
+// Before
+Sentry.init({ enableLogs: true });
+
+// After
+Sentry.init({});
+```
diff --git a/includes/migration/javascript-v11/enable-metrics-removed.mdx b/includes/migration/javascript-v11/enable-metrics-removed.mdx
new file mode 100644
index 0000000000000..f7b5cceeef84b
--- /dev/null
+++ b/includes/migration/javascript-v11/enable-metrics-removed.mdx
@@ -0,0 +1,28 @@
+---
+id: enable-metrics-removed
+title: "The `enableMetrics` option was removed"
+phase: code-changes
+category: removed-api
+severity: action-required
+frameworks: all
+features: [metrics]
+platformCategory: all
+order: 100
+---
+
+`enableMetrics` and `_experiments.enableMetrics` were removed. Metrics are captured whenever you use `Sentry.metrics.*`. The `_experiments.beforeSendMetric` callback moved to the top level.
+
+```js
+// Before
+Sentry.init({
+ _experiments: {
+ enableMetrics: true,
+ beforeSendMetric: metric => metric,
+ },
+});
+
+// After
+Sentry.init({
+ beforeSendMetric: metric => metric,
+});
+```
diff --git a/includes/migration/javascript-v11/enable-opentelemetry-setup.mdx b/includes/migration/javascript-v11/enable-opentelemetry-setup.mdx
new file mode 100644
index 0000000000000..ef83f5248eef0
--- /dev/null
+++ b/includes/migration/javascript-v11/enable-opentelemetry-setup.mdx
@@ -0,0 +1,27 @@
+---
+id: enable-opentelemetry-setup
+title: "`skipOpenTelemetrySetup` is replaced by `enableOpenTelemetrySetup`"
+phase: code-changes
+category: rename
+severity: action-required
+frameworks: all
+features: [custom-otel]
+platformCategory: server
+order: 130
+---
+
+`skipOpenTelemetrySetup` was replaced by `enableOpenTelemetrySetup`, with inverted meaning. It defaults to `false` for most server SDKs, and `true` for `@sentry/nextjs` and `@sentry/sveltekit`.
+
+```js
+// Before
+Sentry.init({ skipOpenTelemetrySetup: true });
+
+// After
+Sentry.init({ enableOpenTelemetrySetup: false });
+```
+
+Pick the setup you want:
+
+1. **Sentry only** (default): nothing to set.
+2. **OpenTelemetry-compatible, everything to Sentry**: set `enableOpenTelemetrySetup: true`. Spans created through `@opentelemetry/api` become Sentry spans. There is no exporter and no OTLP output.
+3. **Your own OpenTelemetry, Sentry linked to it**: leave it unset or `false`, turn Sentry tracing off, and add `Sentry.otlpIntegration()`. On `@sentry/nextjs` and `@sentry/sveltekit` you must set it to `false` explicitly.
diff --git a/includes/migration/javascript-v11/event-filters-rename.mdx b/includes/migration/javascript-v11/event-filters-rename.mdx
new file mode 100644
index 0000000000000..dee2f57456a50
--- /dev/null
+++ b/includes/migration/javascript-v11/event-filters-rename.mdx
@@ -0,0 +1,29 @@
+---
+id: event-filters-rename
+title: "`InboundFilters` was renamed to `EventFilters`"
+phase: code-changes
+category: rename
+severity: action-required
+frameworks: all
+features: []
+platformCategory: all
+order: 290
+---
+
+`inboundFiltersIntegration` was renamed to `eventFiltersIntegration`, and the integration reports itself as `EventFilters`.
+
+Update the import and any reference by name:
+
+```js
+// Before
+import { inboundFiltersIntegration } from '@sentry/browser';
+Sentry.init({
+ integrations: integrations => integrations.filter(integration => integration.name !== 'InboundFilters'),
+});
+
+// After
+import { eventFiltersIntegration } from '@sentry/browser';
+Sentry.init({
+ integrations: integrations => integrations.filter(integration => integration.name !== 'EventFilters'),
+});
+```
diff --git a/includes/migration/javascript-v11/fastify-min-version.mdx b/includes/migration/javascript-v11/fastify-min-version.mdx
new file mode 100644
index 0000000000000..f872d8671b89d
--- /dev/null
+++ b/includes/migration/javascript-v11/fastify-min-version.mdx
@@ -0,0 +1,15 @@
+---
+id: fastify-min-version
+title: "Fastify 3.21 is the new minimum"
+phase: prerequisites
+category: version-support
+severity: action-required
+frameworks: [fastify]
+features: []
+platformCategory: server
+order: 100
+---
+
+`@sentry/node` now requires Fastify 3.21. Fastify 3.0 to 3.20 were dropped.
+
+Upgrade Fastify before upgrading the SDK.
diff --git a/includes/migration/javascript-v11/hono-integration-removed.mdx b/includes/migration/javascript-v11/hono-integration-removed.mdx
new file mode 100644
index 0000000000000..0e2c834c6db92
--- /dev/null
+++ b/includes/migration/javascript-v11/hono-integration-removed.mdx
@@ -0,0 +1,23 @@
+---
+id: hono-integration-removed
+title: "`honoIntegration` was removed"
+phase: code-changes
+category: removed-api
+severity: action-required
+frameworks: [hono, cloudflare]
+features: []
+platformCategory: server
+order: 200
+---
+
+The deprecated `honoIntegration` was removed from `@sentry/node` and `@sentry/cloudflare`.
+
+Use the [`@sentry/hono`](https://www.npmjs.com/package/@sentry/hono) middleware:
+
+```diff
+- import * as Sentry from '@sentry/cloudflare';
++ import { sentry } from '@sentry/hono/cloudflare';
+
+ const app = new Hono();
++ app.use(sentry());
+```
diff --git a/includes/migration/javascript-v11/hydrogen-legacy-dropped.mdx b/includes/migration/javascript-v11/hydrogen-legacy-dropped.mdx
new file mode 100644
index 0000000000000..abb906e429e81
--- /dev/null
+++ b/includes/migration/javascript-v11/hydrogen-legacy-dropped.mdx
@@ -0,0 +1,15 @@
+---
+id: hydrogen-legacy-dropped
+title: "Legacy Hydrogen on Oxygen is no longer supported"
+phase: prerequisites
+category: version-support
+severity: action-required
+frameworks: [cloudflare]
+features: []
+platformCategory: serverless
+order: 140
+---
+
+Legacy Hydrogen versions are no longer supported on Oxygen, and local development needs `@shopify/mini-oxygen` 4 or higher, which ships the miniflare version that supports all compatibility flags.
+
+Upgrade Hydrogen and `@shopify/mini-oxygen` before upgrading the SDK. See the [Hydrogen guide](https://docs.sentry.io/platforms/javascript/guides/cloudflare/frameworks/hydrogen-remix/).
diff --git a/includes/migration/javascript-v11/ignore-transactions-removed.mdx b/includes/migration/javascript-v11/ignore-transactions-removed.mdx
new file mode 100644
index 0000000000000..1d17505771ee5
--- /dev/null
+++ b/includes/migration/javascript-v11/ignore-transactions-removed.mdx
@@ -0,0 +1,29 @@
+---
+id: ignore-transactions-removed
+title: "`ignoreTransactions` is replaced by `ignoreSpans`"
+phase: code-changes
+category: behavior
+severity: action-required
+frameworks: all
+features: [tracing]
+platformCategory: all
+order: 70
+---
+
+`ignoreTransactions` no-ops. `ignoreSpans` now takes effect when a span starts, and dropping a segment span drops its children with it.
+
+```js
+// Before
+Sentry.init({ ignoreTransactions: ['GET /health'] });
+
+// After
+Sentry.init({ ignoreSpans: ['GET /health'] });
+```
+
+`ignoreSpans` matches on span `name` and applies to every span, so narrow the filter if child spans share a name:
+
+```js
+Sentry.init({
+ ignoreSpans: [{ name: 'GET /health', attributes: { 'sentry.op': 'http.server' } }],
+});
+```
diff --git a/includes/migration/javascript-v11/inp-web-vital-span.mdx b/includes/migration/javascript-v11/inp-web-vital-span.mdx
new file mode 100644
index 0000000000000..52f872749da9d
--- /dev/null
+++ b/includes/migration/javascript-v11/inp-web-vital-span.mdx
@@ -0,0 +1,15 @@
+---
+id: inp-web-vital-span
+title: "INP is always sent as a web vital span"
+phase: verify
+category: behavior
+severity: behavior-change
+frameworks: all
+features: [tracing]
+platformCategory: browser
+order: 80
+---
+
+INP is always sent as a web vital span now, carrying its value in the `browser.web_vital.inp.value` attribute instead of as a span measurement.
+
+Update dashboards and alerts that read INP as a span measurement.
diff --git a/includes/migration/javascript-v11/instrument-state-graph-rename.mdx b/includes/migration/javascript-v11/instrument-state-graph-rename.mdx
new file mode 100644
index 0000000000000..c56b2c5073144
--- /dev/null
+++ b/includes/migration/javascript-v11/instrument-state-graph-rename.mdx
@@ -0,0 +1,21 @@
+---
+id: instrument-state-graph-rename
+title: "`instrumentLangGraph` was renamed to `instrumentStateGraph`"
+phase: code-changes
+category: rename
+severity: action-required
+frameworks: all
+features: [ai-agents]
+platformCategory: server
+order: 300
+---
+
+`instrumentLangGraph` was renamed to `instrumentStateGraph`.
+
+```js
+// Before
+import { instrumentLangGraph } from '@sentry/node';
+
+// After
+import { instrumentStateGraph } from '@sentry/node';
+```
diff --git a/includes/migration/javascript-v11/interaction-spans-integration.mdx b/includes/migration/javascript-v11/interaction-spans-integration.mdx
new file mode 100644
index 0000000000000..c69f061a63f5e
--- /dev/null
+++ b/includes/migration/javascript-v11/interaction-spans-integration.mdx
@@ -0,0 +1,27 @@
+---
+id: interaction-spans-integration
+title: "Interaction spans moved to `interactionsIntegration`"
+phase: code-changes
+category: removed-api
+severity: action-required
+frameworks: all
+features: [tracing]
+platformCategory: browser
+order: 215
+---
+
+The experimental `_experiments.enableInteractions` option of `browserTracingIntegration` was removed. Interaction spans now come from a dedicated opt-in integration.
+
+```js
+// Before
+Sentry.init({
+ integrations: [
+ Sentry.browserTracingIntegration({ _experiments: { enableInteractions: true } }),
+ ],
+});
+
+// After
+Sentry.init({
+ integrations: [Sentry.browserTracingIntegration(), Sentry.interactionsIntegration()],
+});
+```
diff --git a/includes/migration/javascript-v11/metrics-cdn-bundle.mdx b/includes/migration/javascript-v11/metrics-cdn-bundle.mdx
new file mode 100644
index 0000000000000..3ec173191e5ab
--- /dev/null
+++ b/includes/migration/javascript-v11/metrics-cdn-bundle.mdx
@@ -0,0 +1,15 @@
+---
+id: metrics-cdn-bundle
+title: "Metrics moved out of the base CDN bundle"
+phase: packages
+category: package
+severity: action-required
+frameworks: all
+features: [metrics]
+platformCategory: browser
+order: 40
+---
+
+Metrics ship only in the `*.logs.metrics` CDN bundles. On other bundles, `Sentry.metrics.*` is a no-op shim.
+
+If you use metrics via the CDN, switch your script tag to a `*.logs.metrics` bundle.
diff --git a/includes/migration/javascript-v11/nextjs-min-version.mdx b/includes/migration/javascript-v11/nextjs-min-version.mdx
new file mode 100644
index 0000000000000..73f1c87f2c40d
--- /dev/null
+++ b/includes/migration/javascript-v11/nextjs-min-version.mdx
@@ -0,0 +1,15 @@
+---
+id: nextjs-min-version
+title: "Next.js 13 support was dropped"
+phase: prerequisites
+category: version-support
+severity: action-required
+frameworks: [nextjs]
+features: []
+platformCategory: all
+order: 50
+---
+
+`@sentry/nextjs` now requires Next.js 14.
+
+Upgrade Next.js before upgrading the SDK.
diff --git a/includes/migration/javascript-v11/nextjs-react-component-annotation.mdx b/includes/migration/javascript-v11/nextjs-react-component-annotation.mdx
new file mode 100644
index 0000000000000..7ae204ef4cf70
--- /dev/null
+++ b/includes/migration/javascript-v11/nextjs-react-component-annotation.mdx
@@ -0,0 +1,24 @@
+---
+id: nextjs-react-component-annotation
+title: "`reactComponentAnnotation` is a single top-level option again"
+phase: code-changes
+category: behavior
+severity: action-required
+frameworks: [nextjs]
+features: []
+platformCategory: all
+order: 340
+---
+
+React component annotation is configured through a single top-level `reactComponentAnnotation` option that covers webpack and Turbopack:
+
+```js
+export default withSentryConfig(nextConfig, {
+ reactComponentAnnotation: {
+ enabled: true,
+ ignoredComponents: ['MyComponent'],
+ },
+});
+```
+
+If you moved to `webpack.reactComponentAnnotation` for v10, move back to the top level. The bundler-specific options still work but are deprecated, and win over the top-level one for their bundler. On Turbopack, annotation requires Next.js 16 or higher, and the SDK now warns at build time on older versions.
diff --git a/includes/migration/javascript-v11/nextjs-removed-options.mdx b/includes/migration/javascript-v11/nextjs-removed-options.mdx
new file mode 100644
index 0000000000000..60cdf86157eed
--- /dev/null
+++ b/includes/migration/javascript-v11/nextjs-removed-options.mdx
@@ -0,0 +1,29 @@
+---
+id: nextjs-removed-options
+title: "Long-deprecated `withSentryConfig` options were removed"
+phase: code-changes
+category: removed-api
+severity: action-required
+frameworks: [nextjs]
+features: []
+platformCategory: all
+order: 330
+---
+
+These deprecated top-level options in `withSentryConfig` were removed:
+
+| Removed option | Replacement |
+| --------------------------------------- | -------------------------------------------------- |
+| `autoInstrumentServerFunctions` | `webpack.autoInstrumentServerFunctions` |
+| `autoInstrumentMiddleware` | `webpack.autoInstrumentMiddleware` |
+| `autoInstrumentAppDirectory` | `webpack.autoInstrumentAppDirectory` |
+| `automaticVercelMonitors` | `webpack.automaticVercelMonitors` |
+| `excludeServerRoutes` | `webpack.excludeServerRoutes` |
+| `reactComponentAnnotation` | `webpack.reactComponentAnnotation` |
+| `unstable_sentryWebpackPluginOptions` | removed, see the `unstable_` options item |
+| `disableSentryWebpackConfig` | `webpack.disableSentryConfig` |
+| `disableLogger` | `webpack.treeshake.removeDebugLogging` |
+| `disableManifestInjection` | `routeManifestInjection: false` |
+| `_experimental.turbopackApplicationKey` | `applicationKey` |
+
+Move each option you use to its replacement. `next.config.js` is often plain JavaScript, so check it by hand.
diff --git a/includes/migration/javascript-v11/nextjs-template-tracing.mdx b/includes/migration/javascript-v11/nextjs-template-tracing.mdx
new file mode 100644
index 0000000000000..5a0ca7144022f
--- /dev/null
+++ b/includes/migration/javascript-v11/nextjs-template-tracing.mdx
@@ -0,0 +1,15 @@
+---
+id: nextjs-template-tracing
+title: "Tracing was removed from generated Next.js templates"
+phase: verify
+category: behavior
+severity: behavior-change
+frameworks: [nextjs]
+features: [tracing]
+platformCategory: all
+order: 100
+---
+
+Tracing was removed from the generated Pages Router API handler, Edge API handler, and Middleware wrapper templates.
+
+Route handlers and middleware are still instrumented automatically. After upgrading, confirm you still see the spans you expect.
diff --git a/includes/migration/javascript-v11/node-core-merged.mdx b/includes/migration/javascript-v11/node-core-merged.mdx
new file mode 100644
index 0000000000000..0a8e637ab66fd
--- /dev/null
+++ b/includes/migration/javascript-v11/node-core-merged.mdx
@@ -0,0 +1,23 @@
+---
+id: node-core-merged
+title: "`@sentry/node-core` was merged back into `@sentry/node`"
+phase: packages
+category: package
+severity: action-required
+frameworks: all
+features: []
+platformCategory: server
+order: 20
+---
+
+`@sentry/node-core` was removed.
+
+Remove it from your dependencies and import from `@sentry/node`:
+
+```js
+// Before
+import { init } from '@sentry/node-core';
+
+// After
+import { init } from '@sentry/node';
+```
diff --git a/includes/migration/javascript-v11/node-loader-entry-points.mdx b/includes/migration/javascript-v11/node-loader-entry-points.mdx
new file mode 100644
index 0000000000000..0ffb2beb36567
--- /dev/null
+++ b/includes/migration/javascript-v11/node-loader-entry-points.mdx
@@ -0,0 +1,29 @@
+---
+id: node-loader-entry-points
+title: "The `/loader`, `/init`, and `/preload` entry points were removed"
+phase: packages
+category: removed-api
+severity: action-required
+frameworks: all
+features: []
+platformCategory: server
+order: 80
+---
+
+The loader and preload entry points were removed:
+
+- `@sentry/node/init` and `@sentry/node/preload`
+- `@sentry/node/loader`, `@sentry/astro/loader`, `@sentry/aws-serverless/loader`, `@sentry/google-cloud-serverless/loader`, `@sentry/nextjs/loader`, `@sentry/remix/loader`, `@sentry/tanstackstart-react/loader`
+- `preloadOpenTelemetry()`
+
+Replace `/loader` imports with `/import`:
+
+```bash
+# Before
+node --import @sentry/node/loader app.js
+
+# After
+node --import @sentry/node/import app.js
+```
+
+If you used `/init` or `/preload`, create your own instrument file that calls `Sentry.init()` and preload it with `node --import ./instrument.mjs app.js`. Remove `preloadOpenTelemetry()` calls.
diff --git a/includes/migration/javascript-v11/node-removed-apis.mdx b/includes/migration/javascript-v11/node-removed-apis.mdx
new file mode 100644
index 0000000000000..d9345aca27da3
--- /dev/null
+++ b/includes/migration/javascript-v11/node-removed-apis.mdx
@@ -0,0 +1,24 @@
+---
+id: node-removed-apis
+title: "`@sentry/node` and server SDK API removals"
+phase: code-changes
+category: removed-api
+severity: action-required
+frameworks: all
+features: []
+platformCategory: server
+order: 180
+---
+
+These were removed from `@sentry/node` and the server-side SDKs that re-exported them:
+
+- `SentryContextManager`
+- The `OpenTelemetryServerRuntimeOptions` type. `enableOpenTelemetrySetup` is part of `NodeOptions` and the other SDK option types.
+- The `connect` instrumentation.
+- The `prismaInstrumentation` option. Prisma works out of the box.
+- The `registerEsmLoaderHooks` option.
+- `SentryHttpInstrumentation` and `SentryNodeFetchInstrumentation`. Use `instrumentHttpOutgoingRequests()` and `nativeNodeFetchIntegration`.
+- `generateInstrumentOnce`
+- (Fastify) `setShouldHandleError`
+- (AWS Lambda) `disableAwsContextPropagation`, `startTrace`, and `tryPatchHandler`. To disable tracing, set `tracesSampleRate` to `0`.
+- (Express) The `patchExpressModule(options)` signature. Use `patchExpressModule(moduleExports, getOptions)`.
diff --git a/includes/migration/javascript-v11/node-version.mdx b/includes/migration/javascript-v11/node-version.mdx
new file mode 100644
index 0000000000000..084e27f9023a1
--- /dev/null
+++ b/includes/migration/javascript-v11/node-version.mdx
@@ -0,0 +1,24 @@
+---
+id: node-version
+title: "Node.js 20.19.0 is the new minimum"
+phase: prerequisites
+category: version-support
+severity: action-required
+frameworks: all
+features: []
+platformCategory: all
+order: 10
+---
+
+The minimum supported Node.js version is now 20.19.0, and the supported range has gaps:
+
+| Node.js version | Supported |
+| --------------- | --------- |
+| < 20.19.0 | No |
+| 20.19.0 to 21.x | Yes |
+| 22.0 to 22.11 | No |
+| 22.12 to 22.x | Yes |
+| 23.0 to 23.1 | No |
+| >= 23.2.0 | Yes |
+
+Move to a supported version everywhere the SDK is installed or your app is built: Dockerfiles, CI images, `engines`, `.nvmrc`, and serverless runtime settings. This applies to browser projects too, if their build steps run on Node.js.
diff --git a/includes/migration/javascript-v11/nuxt-source-maps-options.mdx b/includes/migration/javascript-v11/nuxt-source-maps-options.mdx
new file mode 100644
index 0000000000000..0a9907b4d6c63
--- /dev/null
+++ b/includes/migration/javascript-v11/nuxt-source-maps-options.mdx
@@ -0,0 +1,31 @@
+---
+id: nuxt-source-maps-options
+title: "`sourceMapsUploadOptions` was removed from the Nuxt module"
+phase: code-changes
+category: removed-api
+severity: action-required
+frameworks: [nuxt]
+features: []
+platformCategory: all
+order: 360
+---
+
+The deprecated `sourceMapsUploadOptions` module option was removed. `url` was renamed to `sentryUrl`, and `enabled: false` became `sourcemaps: { disable: true }`.
+
+Move its fields to the root of the `sentry` module options:
+
+```ts
+// nuxt.config.ts
+export default defineNuxtConfig({
+ modules: ['@sentry/nuxt/module'],
+ sentry: {
+ org: 'my-org',
+ project: 'my-project',
+ authToken: process.env.SENTRY_AUTH_TOKEN,
+ sentryUrl: 'https://my-sentry.example.com',
+ sourcemaps: {
+ assets: ['./dist/**/*'],
+ },
+ },
+});
+```
diff --git a/includes/migration/javascript-v11/opentelemetry-interop.mdx b/includes/migration/javascript-v11/opentelemetry-interop.mdx
new file mode 100644
index 0000000000000..44f7b763cfe15
--- /dev/null
+++ b/includes/migration/javascript-v11/opentelemetry-interop.mdx
@@ -0,0 +1,19 @@
+---
+id: opentelemetry-interop
+title: "Sentry no longer sets up OpenTelemetry for you"
+phase: code-changes
+category: behavior
+severity: behavior-change
+frameworks: all
+features: [tracing]
+platformCategory: server
+order: 120
+---
+
+Most server SDKs no longer set up an OpenTelemetry tracer provider and produce native Sentry spans instead. Only `@sentry/nextjs` and `@sentry/sveltekit` still register a lightweight OpenTelemetry-compatible provider. `@opentelemetry/api` is the only remaining OpenTelemetry dependency.
+
+Nothing to do if you only use the Sentry SDK. If you run your own OpenTelemetry setup, read these:
+
+- [`skipOpenTelemetrySetup` is replaced by `enableOpenTelemetrySetup`](#enable-opentelemetry-setup)
+- [Custom OpenTelemetry setups must export over OTLP](#custom-otel-setup)
+- [`otlpIntegration` moved and lost its exporter options](#otlp-integration-changes)
diff --git a/includes/migration/javascript-v11/opentelemetry-removed-apis.mdx b/includes/migration/javascript-v11/opentelemetry-removed-apis.mdx
new file mode 100644
index 0000000000000..7c16e6a3ca6c0
--- /dev/null
+++ b/includes/migration/javascript-v11/opentelemetry-removed-apis.mdx
@@ -0,0 +1,15 @@
+---
+id: opentelemetry-removed-apis
+title: "`@sentry/opentelemetry` removals"
+phase: code-changes
+category: removed-api
+severity: action-required
+frameworks: all
+features: [custom-otel]
+platformCategory: server
+order: 160
+---
+
+`getTraceContextForScope` and `getSentryResource` were removed, the `@opentelemetry/core` peer dependency is gone, and OpenTelemetry resources are no longer collected. `contexts.otel.resource` was dropped from events, and `OTEL_SERVICE_NAME` and `OTEL_RESOURCE_ATTRIBUTES` are no longer read.
+
+Remove any use of the two functions. If you set those environment variables, set the equivalent values on the SDK directly, and update dashboards or alerts that read `contexts.otel.resource`.
diff --git a/includes/migration/javascript-v11/otlp-integration-changes.mdx b/includes/migration/javascript-v11/otlp-integration-changes.mdx
new file mode 100644
index 0000000000000..d2a8ecb772aaa
--- /dev/null
+++ b/includes/migration/javascript-v11/otlp-integration-changes.mdx
@@ -0,0 +1,27 @@
+---
+id: otlp-integration-changes
+title: "`otlpIntegration` moved and lost its exporter options"
+phase: code-changes
+category: removed-api
+severity: action-required
+frameworks: all
+features: [custom-otel]
+platformCategory: server
+order: 150
+---
+
+`Sentry.otlpIntegration()` is exported from every server-side SDK now, and no longer sets up an exporter, span processor, or tracer provider. The `@sentry/node-core/light/otlp` entry point, the `setupOtlpTracesExporter` and `collectorUrl` options, and the `OtlpIntegration` name were removed.
+
+Import it from your platform SDK and configure your own exporter, pointing it at `Sentry.getOtlpTracesEndpoint(dsn)` or at your collector:
+
+```diff
+- import { otlpIntegration } from '@sentry/node-core/light/otlp';
++ import { otlpIntegration } from '@sentry/node';
+```
+
+If you reference the integration by name, update it:
+
+```diff
+- integrations.filter(integration => integration.name !== 'OtlpIntegration')
++ integrations.filter(integration => integration.name !== 'Otlp')
+```
diff --git a/includes/migration/javascript-v11/profiling-legacy-options.mdx b/includes/migration/javascript-v11/profiling-legacy-options.mdx
new file mode 100644
index 0000000000000..e71b5040dc1b4
--- /dev/null
+++ b/includes/migration/javascript-v11/profiling-legacy-options.mdx
@@ -0,0 +1,26 @@
+---
+id: profiling-legacy-options
+title: "`profilesSampleRate` and `profilesSampler` were removed"
+phase: code-changes
+category: removed-api
+severity: action-required
+frameworks: all
+features: [profiling]
+platformCategory: all
+order: 250
+---
+
+The `profilesSampleRate` and `profilesSampler` options were removed from browser and Node.js profiling.
+
+Use session-based profiling with `profileSessionSampleRate` and a `profileLifecycle` of `'trace'` (starts and stops with active traces) or `'manual'`:
+
+```js
+// Before
+Sentry.init({ profilesSampleRate: 1.0 });
+
+// After
+Sentry.init({
+ profileSessionSampleRate: 1.0,
+ profileLifecycle: 'trace',
+});
+```
diff --git a/includes/migration/javascript-v11/profiling-node-prune-script.mdx b/includes/migration/javascript-v11/profiling-node-prune-script.mdx
new file mode 100644
index 0000000000000..bb5e5ece29c48
--- /dev/null
+++ b/includes/migration/javascript-v11/profiling-node-prune-script.mdx
@@ -0,0 +1,15 @@
+---
+id: profiling-node-prune-script
+title: "The `prune-profiler-binaries` script was removed"
+phase: code-changes
+category: removed-api
+severity: informational
+frameworks: all
+features: [profiling]
+platformCategory: server
+order: 260
+---
+
+The `prune-profiler-binaries` script was removed from `@sentry/profiling-node`.
+
+Nothing to do, unless you call it in a build or packaging step. Remove that call.
diff --git a/includes/migration/javascript-v11/react-error-boundary-type.mdx b/includes/migration/javascript-v11/react-error-boundary-type.mdx
new file mode 100644
index 0000000000000..a8ff73868fe67
--- /dev/null
+++ b/includes/migration/javascript-v11/react-error-boundary-type.mdx
@@ -0,0 +1,15 @@
+---
+id: react-error-boundary-type
+title: "`withErrorBoundary` returns a memoized component type"
+phase: cleanup
+category: type
+severity: informational
+frameworks: [react, nextjs, remix, react-router, tanstackstart-react, gatsby]
+features: []
+platformCategory: browser
+order: 40
+---
+
+`withErrorBoundary` wraps its component in `memo`, so its return type is `React.MemoExoticComponent` instead of a plain function component.
+
+Nothing to do, unless you annotated the result yourself. Update that annotation.
diff --git a/includes/migration/javascript-v11/react-min-version.mdx b/includes/migration/javascript-v11/react-min-version.mdx
new file mode 100644
index 0000000000000..4326d247f4499
--- /dev/null
+++ b/includes/migration/javascript-v11/react-min-version.mdx
@@ -0,0 +1,15 @@
+---
+id: react-min-version
+title: "React 16 support was dropped"
+phase: prerequisites
+category: version-support
+severity: action-required
+frameworks: [react, nextjs, remix, react-router, tanstackstart-react, gatsby]
+features: []
+platformCategory: browser
+order: 60
+---
+
+`@sentry/react` now requires React 17.
+
+Upgrade React before upgrading the SDK.
diff --git a/includes/migration/javascript-v11/react-router-min-version.mdx b/includes/migration/javascript-v11/react-router-min-version.mdx
new file mode 100644
index 0000000000000..186b9134fd32a
--- /dev/null
+++ b/includes/migration/javascript-v11/react-router-min-version.mdx
@@ -0,0 +1,15 @@
+---
+id: react-router-min-version
+title: "React Router 7.15 is the new minimum"
+phase: prerequisites
+category: version-support
+severity: action-required
+frameworks: [react-router]
+features: []
+platformCategory: all
+order: 80
+---
+
+`@sentry/react-router` now requires React Router 7.15 in framework mode.
+
+Upgrade React Router before upgrading the SDK.
diff --git a/includes/migration/javascript-v11/react-router-server-wrappers.mdx b/includes/migration/javascript-v11/react-router-server-wrappers.mdx
new file mode 100644
index 0000000000000..36ed856c46e6f
--- /dev/null
+++ b/includes/migration/javascript-v11/react-router-server-wrappers.mdx
@@ -0,0 +1,19 @@
+---
+id: react-router-server-wrappers
+title: "`wrapServerLoader` and `wrapServerAction` were removed"
+phase: code-changes
+category: removed-api
+severity: action-required
+frameworks: [react-router]
+features: [tracing]
+platformCategory: server
+order: 410
+---
+
+The deprecated `wrapServerLoader`, `wrapServerAction`, and `sentryHandleRequest` exports were removed.
+
+Loaders and actions are instrumented automatically. Export the instrumentation from your `entry.server.tsx`, and replace `sentryHandleRequest` with `wrapSentryHandleRequest`:
+
+```ts
+export const instrumentations = [Sentry.createSentryServerInstrumentation()];
+```
diff --git a/includes/migration/javascript-v11/react-router-source-maps-options.mdx b/includes/migration/javascript-v11/react-router-source-maps-options.mdx
new file mode 100644
index 0000000000000..6f7c6be6d8c05
--- /dev/null
+++ b/includes/migration/javascript-v11/react-router-source-maps-options.mdx
@@ -0,0 +1,15 @@
+---
+id: react-router-source-maps-options
+title: "`sourceMapsUploadOptions` was removed from `sentryReactRouter()`"
+phase: code-changes
+category: removed-api
+severity: action-required
+frameworks: [react-router]
+features: []
+platformCategory: all
+order: 380
+---
+
+The deprecated `sourceMapsUploadOptions` option was removed from `sentryReactRouter()`. `enabled: false` became `sourcemaps: { disable: true }`.
+
+Move its fields to the root of the `sentryConfig` passed to `sentryReactRouter()`.
diff --git a/includes/migration/javascript-v11/remix-action-form-data.mdx b/includes/migration/javascript-v11/remix-action-form-data.mdx
new file mode 100644
index 0000000000000..f479bdd990411
--- /dev/null
+++ b/includes/migration/javascript-v11/remix-action-form-data.mdx
@@ -0,0 +1,28 @@
+---
+id: remix-action-form-data
+title: "Remix action form data is reported under a new attribute name"
+phase: code-changes
+category: behavior
+severity: action-required
+frameworks: [remix]
+features: [tracing]
+platformCategory: all
+order: 30
+---
+
+`captureActionFormDataKeys` is an integration-level override now, so it no longer requires `dataCollection.httpBodies` to include `'incomingRequest'`. Captured fields are reported as `remix.action_form_data.` span attributes on every runtime, where Node.js previously used `formData.`.
+
+```js
+// v10: both were required
+Sentry.init({
+ captureActionFormDataKeys: { username: true },
+ dataCollection: { httpBodies: ['incomingRequest'] },
+});
+
+// v11
+Sentry.init({
+ captureActionFormDataKeys: { username: true },
+});
+```
+
+Update dashboards, alerts, and saved searches that query `formData.*`. If you do not set `captureActionFormDataKeys`, all form fields are captured when `dataCollection.httpBodies` includes `'incomingRequest'`, with sensitive-looking names replaced by `[Filtered]`.
diff --git a/includes/migration/javascript-v11/remix-min-version.mdx b/includes/migration/javascript-v11/remix-min-version.mdx
new file mode 100644
index 0000000000000..d1c53050993b7
--- /dev/null
+++ b/includes/migration/javascript-v11/remix-min-version.mdx
@@ -0,0 +1,15 @@
+---
+id: remix-min-version
+title: "`@remix-run/node` v1 support was dropped"
+phase: prerequisites
+category: version-support
+severity: action-required
+frameworks: [remix]
+features: []
+platformCategory: all
+order: 90
+---
+
+`@sentry/remix` now requires `@remix-run/node` v2.
+
+Upgrade `@remix-run/node` before upgrading the SDK.
diff --git a/includes/migration/javascript-v11/request-data-integration.mdx b/includes/migration/javascript-v11/request-data-integration.mdx
new file mode 100644
index 0000000000000..7dd1126d23cc2
--- /dev/null
+++ b/includes/migration/javascript-v11/request-data-integration.mdx
@@ -0,0 +1,15 @@
+---
+id: request-data-integration
+title: "`requestDataIntegration` overrides now interact with `dataCollection`"
+phase: code-changes
+category: behavior
+severity: behavior-change
+frameworks: all
+features: []
+platformCategory: server
+order: 20
+---
+
+The `include` options of `requestDataIntegration` stay an integration-level override on top of `dataCollection`, and user IP inference is now controlled by `dataCollection.userInfo`.
+
+Nothing to do, unless you set `sendDefaultPii: false` and relied on `requestDataIntegration({ include: { ip: true } })`. That override still wins, so verify the combination matches your intent under the new defaults.
diff --git a/includes/migration/javascript-v11/require-to-import.mdx b/includes/migration/javascript-v11/require-to-import.mdx
new file mode 100644
index 0000000000000..262a9027b550f
--- /dev/null
+++ b/includes/migration/javascript-v11/require-to-import.mdx
@@ -0,0 +1,25 @@
+---
+id: require-to-import
+title: "Initializing via `--require` is no longer supported"
+phase: code-changes
+category: behavior
+severity: action-required
+frameworks: all
+features: []
+platformCategory: server
+order: 110
+---
+
+The SDK now skips initialization on the Node.js internal module loader thread, and warns when it detects that it was loaded through `--require`.
+
+Use [`--import`](https://nodejs.org/api/cli.html#--importmodule) instead:
+
+```bash
+# Before
+node --require ./instrument.js app.js
+
+# After
+node --import ./instrument.js app.js
+```
+
+`--import` works for CommonJS apps too. The instrument file's extension decides how it loads.
diff --git a/includes/migration/javascript-v11/sentry-cli-v4.mdx b/includes/migration/javascript-v11/sentry-cli-v4.mdx
new file mode 100644
index 0000000000000..e15fe3fd41dff
--- /dev/null
+++ b/includes/migration/javascript-v11/sentry-cli-v4.mdx
@@ -0,0 +1,15 @@
+---
+id: sentry-cli-v4
+title: "The SDK and bundler plugins now use Sentry CLI v4"
+phase: prerequisites
+category: version-support
+severity: behavior-change
+frameworks: all
+features: []
+platformCategory: all
+order: 120
+---
+
+The SDK and the Sentry bundler plugins now depend on Sentry CLI v4.
+
+Nothing to do, unless you pin `@sentry/cli` yourself or call it in scripts or CI. Upgrade those to v4.
diff --git a/includes/migration/javascript-v11/sentry-types-removed.mdx b/includes/migration/javascript-v11/sentry-types-removed.mdx
new file mode 100644
index 0000000000000..9e11416f8b8c6
--- /dev/null
+++ b/includes/migration/javascript-v11/sentry-types-removed.mdx
@@ -0,0 +1,23 @@
+---
+id: sentry-types-removed
+title: "`@sentry/types` is no longer published"
+phase: packages
+category: package
+severity: action-required
+frameworks: all
+features: []
+platformCategory: all
+order: 10
+---
+
+The `@sentry/types` package is no longer published.
+
+Remove it from your dependencies and import types from `@sentry/core`:
+
+```js
+// Before
+import type { Event } from '@sentry/types';
+
+// After
+import type { Event } from '@sentry/core';
+```
diff --git a/includes/migration/javascript-v11/server-utils-removed-exports.mdx b/includes/migration/javascript-v11/server-utils-removed-exports.mdx
new file mode 100644
index 0000000000000..1fbab5be9b125
--- /dev/null
+++ b/includes/migration/javascript-v11/server-utils-removed-exports.mdx
@@ -0,0 +1,15 @@
+---
+id: server-utils-removed-exports
+title: "`@sentry/server-utils` internal exports were removed"
+phase: code-changes
+category: removed-api
+severity: informational
+frameworks: all
+features: []
+platformCategory: server
+order: 190
+---
+
+These internal exports were removed from `@sentry/server-utils`: `instrumentPrisma`, `defaultDbStatementSerializer`, and the types `PrismaInstrumentationConfig`, `PrismaOptions`, `RedisDiagnosticChannelsOptions`, `SentryTracingChannel`, `TracingChannelLifeCycleOptions`, `TracingChannelBindingHandle`.
+
+Nothing to do, unless you imported from `@sentry/server-utils` directly, which was never a supported path. Prisma is instrumented via `prismaIntegration`.
diff --git a/includes/migration/javascript-v11/solidstart-source-maps-options.mdx b/includes/migration/javascript-v11/solidstart-source-maps-options.mdx
new file mode 100644
index 0000000000000..39d6188d82e5c
--- /dev/null
+++ b/includes/migration/javascript-v11/solidstart-source-maps-options.mdx
@@ -0,0 +1,33 @@
+---
+id: solidstart-source-maps-options
+title: "`sourceMapsUploadOptions` was removed from the SolidStart build options"
+phase: code-changes
+category: removed-api
+severity: action-required
+frameworks: [solidstart]
+features: []
+platformCategory: all
+order: 390
+---
+
+The `sourceMapsUploadOptions` build option was removed from SolidStart 1 setups using `withSentry()` or `sentrySolidStartVite()`. `enabled: false` became `sourcemaps: { disable: true }`.
+
+Move its fields to the top level:
+
+```ts
+// app.config.ts
+export default defineConfig(
+ withSentry(
+ {},
+ {
+ telemetry: false,
+ sourcemaps: {
+ disable: false,
+ filesToDeleteAfterUpload: ['./dist/**/*.map'],
+ },
+ },
+ ),
+);
+```
+
+The build options also accept `applicationKey`, `sentryUrl`, `headers`, `silent`, `errorHandler`, `release`, and `moduleMetadata` now.
diff --git a/includes/migration/javascript-v11/span-attribute-changes.mdx b/includes/migration/javascript-v11/span-attribute-changes.mdx
new file mode 100644
index 0000000000000..28425d340a396
--- /dev/null
+++ b/includes/migration/javascript-v11/span-attribute-changes.mdx
@@ -0,0 +1,50 @@
+---
+id: span-attribute-changes
+title: "Span attributes were renamed to match semantic conventions"
+phase: verify
+category: behavior
+severity: behavior-change
+frameworks: all
+features: [tracing]
+platformCategory: all
+order: 60
+---
+
+Span attributes follow the shared `@sentry/conventions` package now, and many were renamed.
+
+**HTTP and network**
+
+- `http.query` and `http.fragment` to `url.query` and `url.fragment`
+- `net.peer.name` and `net.peer.port` on database and messaging client spans to `server.address` and `server.port`
+- `net.transport` to `network.transport`
+
+**Database**
+
+- `db.system` to `db.system.name`
+- `db.name` to `db.namespace`
+- `db.operation` to `db.operation.name`
+- `db.statement` to `db.query.text`
+- `db.mongodb.collection` to `db.collection.name`
+- Legacy `messaging.*` attributes on the AMQP instrumentation now use their semantic-convention equivalents
+
+**Gen AI**
+
+- `gen_ai.system` to `gen_ai.provider.name`
+- `gen_ai.request.available_tools` to `gen_ai.tool.definitions`
+- `gen_ai.tool.input` to `gen_ai.tool.call.arguments`
+- `gen_ai.tool.output` to `gen_ai.tool.call.result`
+- `gen_ai.usage.cache_creation_input_tokens` to `gen_ai.usage.cache_creation.input_tokens`
+- `gen_ai.usage.cache_read_input_tokens` to `gen_ai.usage.cache_read.input_tokens`
+- (Vercel AI) `gen_ai.usage.input_tokens.cached` to `gen_ai.usage.cache_read.input_tokens`
+- (Vercel AI) `gen_ai.usage.input_tokens.cache_write` to `gen_ai.usage.cache_creation.input_tokens`
+- (Vercel AI) `gen_ai.usage.output_tokens.reasoning` to `gen_ai.usage.reasoning.output_tokens`
+- (Vercel AI) `ai.pipeline.name` and `ai.streaming` to `gen_ai.pipeline.name` and `gen_ai.response.streaming`
+- `gen_ai.tool.type` is no longer set
+- (Anthropic) `gen_ai.prompt` is no longer set. The legacy Completions API's `prompt` is reported as a user message on `gen_ai.input.messages`.
+
+**Browser and filesystem**
+
+- `code.filepath` and `code.function` on `ui.long_animation_frame` spans to `code.file.path` and `code.function.name`
+- `fs_error` on `file` spans to `error.type`, carrying the syscall error code (such as `ENOENT`) instead of the full message
+
+Update any custom instrumentation, `beforeSendSpan` callback, dashboard or alert that references these. TypeScript does not catch them.
diff --git a/includes/migration/javascript-v11/span-op-changes.mdx b/includes/migration/javascript-v11/span-op-changes.mdx
new file mode 100644
index 0000000000000..ef2b6ee3eebd5
--- /dev/null
+++ b/includes/migration/javascript-v11/span-op-changes.mdx
@@ -0,0 +1,83 @@
+---
+id: span-op-changes
+title: "Span operations were consolidated"
+phase: verify
+category: behavior
+severity: behavior-change
+frameworks: all
+features: [tracing]
+platformCategory: all
+order: 70
+---
+
+Span ops were aligned to a smaller, framework-neutral set. The detail that used to live in the op is preserved in attributes such as `code.function.name`, `sentry.origin`, `db.system.name`, `db.operation.name`, and `faas.trigger`.
+
+**Backend HTTP, handlers, middleware, and routers**
+
+| Area | Before | After |
+| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
+| Request handlers (Express, Koa, Connect, Fastify, Elysia, NestJS) | `request_handler.`, `handler.nestjs` | `handler` |
+| Hono `app.request()` in-process dispatch | `hono.request` | `http.server` |
+| Web-server middleware | `middleware.express`, `middleware.koa`, `middleware.hono`, `middleware.elysia`, `middleware.nestjs`, `middleware.nuxt`, `middleware.nitro`, `middleware.tanstackstart`, `hook.fastify`, `http.server.middleware` (Next.js) | `middleware` |
+| Backend router layers | `router.express`, `router.koa`, `router.hapi` | `router` |
+| Hapi server extensions | `server.ext.hapi` | `middleware` |
+| NestJS setup and lifecycle handlers | `app_creation.nestjs`, `request_context.nestjs`, `event.nestjs` | `function` |
+
+**Framework functions**
+
+| Area | Before | After |
+| --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
+| Loaders, actions, and server functions (Next.js, Remix, React Router, SvelteKit, SolidStart, TanStack Start) | `function.nextjs`, `function.sveltekit.load`, `function.react_router.loader`, `function.remix.document_request`, `loader.remix`, `action.remix`, `function.server_action`, `function.tanstackstart` | `function` |
+
+**Frontend and UI**
+
+| Area | Before | After |
+| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- |
+| Frontend routing | `ui.angular.routing`, `ui.sveltekit.routing`, `ui.ember.transition` | `router` |
+| React, Vue, and Svelte component lifecycles | `ui.react.mount`/`render`/`update`, `ui.svelte.init`/`update`, Vue `render`/`update`/`mount`/`create`/`activate`/`unmount`/`destroy` | `ui.mount`, `ui.render`, `ui.update`, `ui.unmount` |
+| Angular tracing decorators | `ui.angular.init`, `ui.angular.` | `ui.mount`, `function` |
+| Ember route hooks, runloop, and components | `ui.ember.route.`, `ui.ember.runloop.`, `ui.ember.component.render`/`definition`/`init` | `function`, `ui.task`, `ui.render`/`function`/`ui.mount` |
+| Browser paint entries | `paint` | `browser.paint` |
+
+**Databases, cache, and messaging**
+
+| Area | Before | After |
+| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
+| Redis commands and connect | `db.redis`, `db.redis.connect` | `db.query`, `db` |
+| Nuxt and Nitro storage (unstorage) | `cache.has_item`, `cache.get_item`, `cache.get_items`, `cache.get_keys`, `cache.set_item`, `cache.set_items`, `cache.remove_item`, `cache.clear` | `cache.get`, `cache.put`, `cache.remove` |
+| Kafka, AMQP, and OTel-inferred messaging | `message`, `message.produce`, `message.consume` | `queue.publish`, `queue.receive`, `queue.process` |
+
+**RPC and Gen AI**
+
+| Area | Before | After |
+| ---------------------------------------------------------- | ------------------------------------------- | ---------------------------------------- |
+| tRPC | `rpc.server` | `rpc` |
+| GCP gRPC calls | `grpc.` | `grpc` |
+| AWS Bedrock inference | `rpc` | `gen_ai.chat`, `gen_ai.generate_content` |
+| Gen AI fallbacks and model metadata (Vercel AI, LangGraph) | `gen_ai.unknown`, `ai.run`, `gen_ai.models` | `function` |
+
+**FaaS, serverless, and HTTP clients**
+
+| Area | Before | After |
+| ---------------------------------------------- | --------------------------------------------------------------------- | ------------------------------------------ |
+| AWS Lambda functions | `function.aws.lambda` | `function.aws` |
+| GCP functions | `function.gcp.http`, `function.gcp.event`, `function.gcp.cloud_event` | `function.gcp` |
+| Firebase functions | `http.request` | `function.gcp` |
+| Cloudflare cron, email, and workflow steps | `faas.cron`, `faas.email`, `function.step.do` | `function` |
+| OTel-inferred FaaS spans (from `faas.trigger`) | arbitrary trigger strings used verbatim | `http.server`, `queue.process`, `function` |
+| GCP HTTP client | `http.client.` | `http.client` |
+| Prefetch HTTP requests | `http.client.prefetch`, `http.server.prefetch` | `http.client`, `http.server` |
+
+**Casing normalized to snake_case**
+
+| Before | After |
+| ------------------------------- | ---------------------------------- |
+| `ui.long-task` | `ui.long_task` |
+| `ui.long-animation-frame` | `ui.long_animation_frame` |
+| `browser.unloadEvent` | `browser.unload_event` |
+| `browser.domContentLoadedEvent` | `browser.dom_content_loaded_event` |
+| `browser.loadEvent` | `browser.load_event` |
+| `browser.TLS/SSL` | `browser.tls_ssl` |
+| `browser.DNS` | `browser.dns` |
+
+Update anything that filters, groups, or alerts on span ops: dashboards, dynamic sampling rules, `ignoreSpans`, and `beforeSendSpan`. TypeScript does not catch these.
diff --git a/includes/migration/javascript-v11/span-streaming-default.mdx b/includes/migration/javascript-v11/span-streaming-default.mdx
new file mode 100644
index 0000000000000..a591f4705f9dd
--- /dev/null
+++ b/includes/migration/javascript-v11/span-streaming-default.mdx
@@ -0,0 +1,19 @@
+---
+id: span-streaming-default
+title: "Span streaming is now the default"
+phase: code-changes
+category: behavior
+severity: behavior-change
+frameworks: all
+features: [tracing]
+platformCategory: all
+order: 40
+---
+
+Spans are sent in small batches as they finish, instead of being buffered until the root span completes. The 1000-span-per-transaction limit is gone.
+
+No transaction events are produced any more, so read these if you use them:
+
+- [`beforeSendSpan` receives the streamed span format](#before-send-span-format)
+- [`beforeSendTransaction` no longer runs](#before-send-transaction-removed)
+- [`ignoreTransactions` is replaced by `ignoreSpans`](#ignore-transactions-removed)
diff --git a/includes/migration/javascript-v11/span-streaming-opt-out.mdx b/includes/migration/javascript-v11/span-streaming-opt-out.mdx
new file mode 100644
index 0000000000000..da4e8a8f88604
--- /dev/null
+++ b/includes/migration/javascript-v11/span-streaming-opt-out.mdx
@@ -0,0 +1,27 @@
+---
+id: span-streaming-opt-out
+title: "Opting out of span streaming"
+phase: code-changes
+category: behavior
+severity: informational
+frameworks: all
+features: [tracing]
+platformCategory: all
+order: 80
+---
+
+`traceLifecycle: 'static'` keeps the previous transaction-based model if you cannot migrate `beforeSendSpan`, `beforeSendTransaction`, or `ignoreTransactions` yet.
+
+```js
+Sentry.init({
+ traceLifecycle: 'static',
+
+ // `beforeSendSpan` MUST be wrapped with Sentry.withStaticSpan:
+ beforeSendSpan: Sentry.withStaticSpan(span => {
+ span.description = scrub(span.description);
+ return span;
+ }),
+});
+```
+
+In Node.js, Bun, Vercel Edge, and Cloudflare you can set `SENTRY_TRACE_LIFECYCLE=static` instead. The static lifecycle is planned for removal in a future major version.
diff --git a/includes/migration/javascript-v11/span-to-json-format.mdx b/includes/migration/javascript-v11/span-to-json-format.mdx
new file mode 100644
index 0000000000000..5a5d585761aef
--- /dev/null
+++ b/includes/migration/javascript-v11/span-to-json-format.mdx
@@ -0,0 +1,25 @@
+---
+id: span-to-json-format
+title: "`spanToJSON` returns the streamed span format"
+phase: code-changes
+category: behavior
+severity: action-required
+frameworks: all
+features: [tracing]
+platformCategory: all
+order: 75
+---
+
+`Sentry.spanToJSON` returns a `StreamedSpanJSON` object instead of `SpanJSON`, the same shape [`beforeSendSpan` receives](#before-send-span-format). `spanToStreamedSpanJSON` was removed.
+
+Replace calls to the removed helper:
+
+```js
+// Before
+const spanJson = Sentry.spanToStreamedSpanJSON(span);
+
+// After
+const spanJson = Sentry.spanToJSON(span);
+```
+
+If you [opt out of span streaming](#span-streaming-opt-out), use `spanToStaticSpanJSON` to get the old `SpanJSON` shape.
diff --git a/includes/migration/javascript-v11/sveltekit-source-maps-options.mdx b/includes/migration/javascript-v11/sveltekit-source-maps-options.mdx
new file mode 100644
index 0000000000000..36194c5300ffa
--- /dev/null
+++ b/includes/migration/javascript-v11/sveltekit-source-maps-options.mdx
@@ -0,0 +1,33 @@
+---
+id: sveltekit-source-maps-options
+title: "`sourceMapsUploadOptions` was removed from `sentrySvelteKit()`"
+phase: code-changes
+category: removed-api
+severity: action-required
+frameworks: [sveltekit]
+features: []
+platformCategory: all
+order: 370
+---
+
+The deprecated `sourceMapsUploadOptions` option was removed from `sentrySvelteKit()`. `url` was renamed to `sentryUrl`.
+
+Move its fields to the root of the `sentrySvelteKit()` options:
+
+```ts
+// vite.config.ts
+export default defineConfig({
+ plugins: [
+ sentrySvelteKit({
+ org: 'my-org',
+ project: 'my-project',
+ authToken: process.env.SENTRY_AUTH_TOKEN,
+ sentryUrl: 'https://my-sentry.example.com',
+ sourcemaps: {
+ assets: ['./build/**/*'],
+ },
+ }),
+ sveltekit(),
+ ],
+});
+```
diff --git a/includes/migration/javascript-v11/sveltekit-vite-subpath.mdx b/includes/migration/javascript-v11/sveltekit-vite-subpath.mdx
new file mode 100644
index 0000000000000..9a6b1563ba62f
--- /dev/null
+++ b/includes/migration/javascript-v11/sveltekit-vite-subpath.mdx
@@ -0,0 +1,23 @@
+---
+id: sveltekit-vite-subpath
+title: "`sentrySvelteKit` moved to the `@sentry/sveltekit/vite` subpath"
+phase: packages
+category: removed-api
+severity: action-required
+frameworks: [sveltekit]
+features: []
+platformCategory: all
+order: 70
+---
+
+The `sentrySvelteKit` Vite plugin is no longer re-exported from the main `@sentry/sveltekit` entry point.
+
+Import it from `@sentry/sveltekit/vite` in your `vite.config.ts`:
+
+```ts
+// Before
+import { sentrySvelteKit } from '@sentry/sveltekit';
+
+// After
+import { sentrySvelteKit } from '@sentry/sveltekit/vite';
+```
diff --git a/includes/migration/javascript-v11/tanstackstart-removed.mdx b/includes/migration/javascript-v11/tanstackstart-removed.mdx
new file mode 100644
index 0000000000000..93e00e5e5252d
--- /dev/null
+++ b/includes/migration/javascript-v11/tanstackstart-removed.mdx
@@ -0,0 +1,15 @@
+---
+id: tanstackstart-removed
+title: "`@sentry/tanstackstart` was removed"
+phase: packages
+category: package
+severity: action-required
+frameworks: [tanstackstart-react]
+features: []
+platformCategory: all
+order: 30
+---
+
+The `@sentry/tanstackstart` package was removed.
+
+Use `@sentry/tanstackstart-react` instead.
diff --git a/includes/migration/javascript-v11/third-party-error-filter-internal-frames.mdx b/includes/migration/javascript-v11/third-party-error-filter-internal-frames.mdx
new file mode 100644
index 0000000000000..72b0993350ce7
--- /dev/null
+++ b/includes/migration/javascript-v11/third-party-error-filter-internal-frames.mdx
@@ -0,0 +1,15 @@
+---
+id: third-party-error-filter-internal-frames
+title: "`thirdPartyErrorFilterIntegration` ignores Sentry frames by default"
+phase: verify
+category: behavior
+severity: behavior-change
+frameworks: all
+features: []
+platformCategory: browser
+order: 130
+---
+
+`ignoreSentryInternalFrames` is the default behavior of `thirdPartyErrorFilterIntegration` now, so errors are no longer attributed to third-party code because of a Sentry frame in the stack.
+
+If you filter on third-party errors, expect fewer of them after upgrading, and check any alerts built on that.
diff --git a/includes/migration/javascript-v11/trace-propagation-targets-case.mdx b/includes/migration/javascript-v11/trace-propagation-targets-case.mdx
new file mode 100644
index 0000000000000..0f194d00a91aa
--- /dev/null
+++ b/includes/migration/javascript-v11/trace-propagation-targets-case.mdx
@@ -0,0 +1,15 @@
+---
+id: trace-propagation-targets-case
+title: "`tracePropagationTargets` matching is now case-insensitive"
+phase: verify
+category: behavior
+severity: behavior-change
+frameworks: all
+features: [tracing]
+platformCategory: all
+order: 50
+---
+
+String and regular-expression matching for `tracePropagationTargets` is now case-insensitive.
+
+Nothing to do, unless you relied on case sensitivity to exclude a target. It will now match and receive trace headers.
diff --git a/includes/migration/javascript-v11/track-fetch-stream-performance.mdx b/includes/migration/javascript-v11/track-fetch-stream-performance.mdx
new file mode 100644
index 0000000000000..191643fb609c1
--- /dev/null
+++ b/includes/migration/javascript-v11/track-fetch-stream-performance.mdx
@@ -0,0 +1,27 @@
+---
+id: track-fetch-stream-performance
+title: "`trackFetchStreamPerformance` moved to its own integration"
+phase: code-changes
+category: removed-api
+severity: action-required
+frameworks: all
+features: [tracing]
+platformCategory: browser
+order: 230
+---
+
+The deprecated `trackFetchStreamPerformance` option of `browserTracingIntegration` was removed.
+
+Add `fetchStreamPerformanceIntegration()` instead:
+
+```js
+// Before
+Sentry.init({
+ integrations: [Sentry.browserTracingIntegration({ trackFetchStreamPerformance: true })],
+});
+
+// After
+Sentry.init({
+ integrations: [Sentry.browserTracingIntegration(), Sentry.fetchStreamPerformanceIntegration()],
+});
+```
diff --git a/includes/migration/javascript-v11/type-changes.mdx b/includes/migration/javascript-v11/type-changes.mdx
new file mode 100644
index 0000000000000..e0529982aa6c4
--- /dev/null
+++ b/includes/migration/javascript-v11/type-changes.mdx
@@ -0,0 +1,21 @@
+---
+id: type-changes
+title: "Type changes"
+phase: cleanup
+category: type
+severity: informational
+frameworks: all
+features: []
+platformCategory: all
+order: 10
+---
+
+Several public types changed:
+
+- `StackFrame`, `SamplingContext`, `SentryError`, and `User` use `unknown` instead of `any`.
+- Attribute typing and serialization were unified.
+- `attributes` on `ScopeData` is required.
+- `endTimestamp` was removed from `SentrySpanArguments`.
+- `BrowserOptions` supports the `TransportOptions` generic.
+
+Narrow types where you relied on `any`, add `attributes: {}` where you construct `ScopeData` manually, and replace `endTimestamp` with `span.end(timestamp)`.
diff --git a/includes/migration/javascript-v11/typescript-version.mdx b/includes/migration/javascript-v11/typescript-version.mdx
new file mode 100644
index 0000000000000..afade7cc45d84
--- /dev/null
+++ b/includes/migration/javascript-v11/typescript-version.mdx
@@ -0,0 +1,15 @@
+---
+id: typescript-version
+title: "TypeScript 5.0.4 is the new minimum"
+phase: prerequisites
+category: version-support
+severity: action-required
+frameworks: all
+features: []
+platformCategory: all
+order: 40
+---
+
+The minimum supported TypeScript version is now 5.0.4, and the SDK no longer emits down-leveled types.
+
+Upgrade TypeScript to 5.0.4 or higher.
diff --git a/includes/migration/javascript-v11/unstable-bundler-plugin-options.mdx b/includes/migration/javascript-v11/unstable-bundler-plugin-options.mdx
new file mode 100644
index 0000000000000..be5e49ab0eff4
--- /dev/null
+++ b/includes/migration/javascript-v11/unstable-bundler-plugin-options.mdx
@@ -0,0 +1,36 @@
+---
+id: unstable-bundler-plugin-options
+title: "The `unstable_sentry*PluginOptions` escape hatch was removed"
+phase: code-changes
+category: removed-api
+severity: action-required
+frameworks: [astro, nextjs, nuxt, react-router, solidstart, sveltekit]
+features: []
+platformCategory: all
+order: 350
+---
+
+The `unstable_sentry*PluginOptions` escape hatch was removed from every SDK:
+
+| SDK | Removed option |
+| ---------------------- | ----------------------------------------------------------------------------------- |
+| `@sentry/astro` | `unstable_sentryVitePluginOptions` (top-level and inside `sourceMapsUploadOptions`) |
+| `@sentry/nextjs` | `unstable_sentryWebpackPluginOptions` (top-level and inside `webpack`) |
+| `@sentry/nuxt` | `unstable_sentryBundlerPluginOptions` |
+| `@sentry/react-router` | `unstable_sentryVitePluginOptions` |
+| `@sentry/solidstart` | `unstable_sentryVitePluginOptions` |
+| `@sentry/sveltekit` | `unstable_sentryVitePluginOptions` |
+
+Set the option directly on the Sentry build options:
+
+```js
+// Before
+unstable_sentryWebpackPluginOptions: {
+ applicationKey: 'my-app',
+},
+
+// After
+applicationKey: 'my-app',
+```
+
+`moduleMetadata` and `sourcemaps.resolveSourceMap` are first-class build options now. `release.uploadLegacySourcemaps`, `_experiments`, and the whole-plugin `disable` flag have no equivalent; use `sourcemaps.disable` instead. Passing a removed option logs a build-time warning.
diff --git a/includes/migration/javascript-v11/vercel-ai-v3-dropped.mdx b/includes/migration/javascript-v11/vercel-ai-v3-dropped.mdx
new file mode 100644
index 0000000000000..d84d2dd3f6fa5
--- /dev/null
+++ b/includes/migration/javascript-v11/vercel-ai-v3-dropped.mdx
@@ -0,0 +1,15 @@
+---
+id: vercel-ai-v3-dropped
+title: "Vercel AI SDK 3 support was dropped"
+phase: prerequisites
+category: version-support
+severity: action-required
+frameworks: all
+features: [ai-agents]
+platformCategory: server
+order: 130
+---
+
+The Vercel AI integration covers `ai` 4, 5, and 6 through the orchestrion transform, and `ai` 7 through its native `ai:telemetry` channel. Version 3 is no longer instrumented.
+
+Upgrade `ai` to 4 or higher, or expect no AI spans.
diff --git a/includes/migration/javascript-v11/with-streamed-span-noop.mdx b/includes/migration/javascript-v11/with-streamed-span-noop.mdx
new file mode 100644
index 0000000000000..c90c336a50ed1
--- /dev/null
+++ b/includes/migration/javascript-v11/with-streamed-span-noop.mdx
@@ -0,0 +1,23 @@
+---
+id: with-streamed-span-noop
+title: "`withStreamedSpan()` is now a no-op"
+phase: cleanup
+category: removed-api
+severity: informational
+frameworks: all
+features: [tracing]
+platformCategory: all
+order: 30
+---
+
+`Sentry.withStreamedSpan()` is a no-op and deprecated. It will be removed in v12.
+
+Remove the wrapper:
+
+```js
+// Before
+beforeSendSpan: Sentry.withStreamedSpan(span => span);
+
+// After
+beforeSendSpan: span => span;
+```
diff --git a/package.json b/package.json
index 67cf61010e40c..40e4ac98fbc09 100644
--- a/package.json
+++ b/package.json
@@ -35,6 +35,7 @@
"lint:fix": "pnpm run lint:prettier:fix && pnpm run lint:eslint:fix",
"test": "vitest",
"test:ci": "vitest run",
+ "migration-items": "node scripts/validate-migration-items.mjs",
"enforce-redirects": "node ./scripts/no-vercel-json-redirects.mjs"
},
"dependencies": {
diff --git a/scripts/validate-migration-items.mjs b/scripts/validate-migration-items.mjs
new file mode 100644
index 0000000000000..c7e6b7104630f
--- /dev/null
+++ b/scripts/validate-migration-items.mjs
@@ -0,0 +1,189 @@
+/**
+ * Validates the v11 migration item collection in `includes/migration/javascript-v11/`.
+ *
+ * Each item is a standalone MDX file with tagged frontmatter, rendered by the
+ * interactive migration guide at
+ * `docs/platforms/javascript/common/migration/v10-to-v11.mdx`. Because the page
+ * filters, counts and serializes items based on that frontmatter, a mistyped
+ * facet silently drops an item from a user's guide rather than failing loudly.
+ * This script is the guard against that.
+ *
+ * `loadItems` is called from `migrationGuide.spec.ts`, so `pnpm test` fails on
+ * invalid frontmatter. Run `pnpm migration-items` for the breakdown by phase,
+ * severity and category as well.
+ */
+import {readdirSync, readFileSync} from 'fs';
+import path from 'path';
+
+import matter from 'gray-matter';
+
+const ITEMS_DIR = 'includes/migration/javascript-v11';
+const GUIDES_DIR = 'docs/platforms/javascript/guides';
+
+/** The change itself, stated once. Anything longer is an explanation. */
+const MAX_FIRST_PARAGRAPH = 400;
+
+export const PHASES = ['prerequisites', 'packages', 'code-changes', 'verify', 'cleanup'];
+export const CATEGORIES = [
+ 'version-support',
+ 'behavior',
+ 'removed-api',
+ 'package',
+ 'rename',
+ 'type',
+];
+export const SEVERITIES = ['action-required', 'behavior-change', 'informational'];
+export const FEATURES = [
+ 'tracing',
+ 'profiling',
+ 'logs',
+ 'metrics',
+ 'ai-agents',
+ 'custom-otel',
+];
+export const PLATFORM_CATEGORIES = ['browser', 'server', 'serverless', 'all'];
+
+/** Reads and validates every item. Returns `{items, errors}`. */
+export function loadItems(root = process.cwd()) {
+ const dir = path.join(root, ITEMS_DIR);
+ const guides = new Set(readdirSync(path.join(root, GUIDES_DIR)));
+ const files = readdirSync(dir).filter(f => f.endsWith('.mdx'));
+
+ const errors = [];
+ const items = [];
+ const ids = new Set();
+ const orders = new Map();
+
+ for (const file of files) {
+ const {data, content} = matter(readFileSync(path.join(dir, file), 'utf8'));
+ const fail = message => errors.push(`${file}: ${message}`);
+
+ const expectedId = file.replace(/\.mdx$/, '');
+ if (data.id !== expectedId) {
+ fail(`id "${data.id}" does not match filename`);
+ }
+ if (ids.has(data.id)) {
+ fail(`duplicate id "${data.id}"`);
+ }
+ ids.add(data.id);
+
+ if (!data.title) {
+ fail('missing title');
+ }
+ if (!PHASES.includes(data.phase)) {
+ fail(`unknown phase "${data.phase}"`);
+ }
+ if (!CATEGORIES.includes(data.category)) {
+ fail(`unknown category "${data.category}"`);
+ }
+ if (!SEVERITIES.includes(data.severity)) {
+ fail(`unknown severity "${data.severity}"`);
+ }
+ if (!PLATFORM_CATEGORIES.includes(data.platformCategory)) {
+ fail(`unknown platformCategory "${data.platformCategory}"`);
+ }
+ if (typeof data.order !== 'number') {
+ fail('missing or non-numeric order');
+ }
+
+ if (data.frameworks !== 'all') {
+ if (!Array.isArray(data.frameworks)) {
+ fail('frameworks must be "all" or an array of guide slugs');
+ } else {
+ data.frameworks
+ .filter(f => !guides.has(f))
+ .forEach(f => fail(`unknown framework "${f}"`));
+ }
+ }
+ (data.features ?? [])
+ .filter(f => !FEATURES.includes(f))
+ .forEach(f => fail(`unknown feature "${f}"`));
+
+ // Items are meant to be scannable: what changed, then what to do about it.
+ // These checks keep the collection from drifting back into prose.
+ const body = content.trim();
+ if (!body) {
+ fail('empty body');
+ }
+ for (const heading of ['**What changed**', '**What you need to do**', '**Why**']) {
+ if (body.includes(heading)) {
+ fail(
+ `remove the "${heading.replaceAll('*', '')}" heading, lead with the change instead`
+ );
+ }
+ }
+ if (/[\u2014\u2013]/.test(body)) {
+ fail('uses an em or en dash, rewrite the sentence');
+ }
+ // Items are re-ordered by severity and hidden by the reader's filters, so
+ // "the next three items" can point at nothing. Link the item by anchor.
+ for (const positional of [
+ /\b(next|previous|following|preceding)\s+(\w+\s+)?items?\b/i,
+ /\bitems?\s+(that\s+)?(follows?|precedes?)\b/i,
+ /\bitems?\s+(above|below)\b/i,
+ ]) {
+ if (positional.test(body)) {
+ fail('refers to an item by position, link to its anchor instead');
+ }
+ }
+ const firstParagraph = body.split('\n\n')[0];
+ if (firstParagraph.length > MAX_FIRST_PARAGRAPH) {
+ fail(
+ `opening paragraph is ${firstParagraph.length} characters, keep it under ${MAX_FIRST_PARAGRAPH}`
+ );
+ }
+
+ const orderKey = `${data.phase}/${data.order}`;
+ if (orders.has(orderKey)) {
+ fail(`duplicate order ${orderKey}, also used by ${orders.get(orderKey)}`);
+ }
+ orders.set(orderKey, file);
+
+ items.push({file, ...data});
+ }
+
+ return {items, errors};
+}
+
+function main() {
+ const {items, errors} = loadItems();
+ const count = (list, predicate) => list.filter(predicate).length;
+ const pad = (value, width) => String(value).padEnd(width);
+
+ console.log(`${items.length} migration items\n`);
+
+ console.log('BY PHASE');
+ PHASES.forEach(p => console.log(` ${pad(p, 16)}${count(items, i => i.phase === p)}`));
+
+ console.log('\nBY SEVERITY');
+ SEVERITIES.forEach(s =>
+ console.log(` ${pad(s, 16)}${count(items, i => i.severity === s)}`)
+ );
+
+ console.log('\nBY CATEGORY');
+ CATEGORIES.forEach(c =>
+ console.log(` ${pad(c, 16)}${count(items, i => i.category === c)}`)
+ );
+
+ const unusedFeatures = FEATURES.filter(
+ f => !count(items, i => (i.features ?? []).includes(f))
+ );
+ if (unusedFeatures.length) {
+ // A facet no item carries renders a checkbox that filters nothing, which
+ // reads as broken to anyone who ticks it.
+ console.log('\nWARNING: facets with no items (remove them from the filter panel):');
+ unusedFeatures.forEach(f => console.log(` ${f}`));
+ }
+
+ if (errors.length) {
+ console.log(`\n${errors.length} error(s):`);
+ errors.forEach(e => console.log(` ${e}`));
+ process.exit(1);
+ }
+
+ console.log('\nAll items valid.');
+}
+
+if (import.meta.url === `file://${process.argv[1]}`) {
+ main();
+}
diff --git a/src/components/migrationGuide/client.tsx b/src/components/migrationGuide/client.tsx
new file mode 100644
index 0000000000000..667df590b75f7
--- /dev/null
+++ b/src/components/migrationGuide/client.tsx
@@ -0,0 +1,748 @@
+'use client';
+
+import {QuestionMarkCircledIcon} from '@radix-ui/react-icons';
+import * as Tooltip from '@radix-ui/react-tooltip';
+import {Button, Checkbox, Theme} from '@radix-ui/themes';
+import * as Sentry from '@sentry/nextjs';
+import classNames from 'classnames';
+import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
+import {usePlausibleEvent} from 'sentry-docs/hooks/usePlausibleEvent';
+import {DocMetrics} from 'sentry-docs/metrics';
+
+import {
+ isItemVisible,
+ migrationGuideHref,
+ migrationGuideLabel,
+ MigrationItem,
+ PHASES,
+ SEVERITIES,
+} from './constants';
+import {detect, Detection} from './detect';
+import styles from './styles.module.scss';
+
+const FILTER_PARAM = 'using';
+
+/** The facets offered on this page, which vary by guide. */
+export type Facet = {description: string; id: string; label: string};
+
+/**
+ * Error monitoring has no checkbox to tick: every setup has it, and the changes
+ * that come with it apply to everyone. It is rendered as a locked option rather
+ * than left out, so the panel reads as the full set of what the SDK does, the
+ * same way the onboarding options do.
+ */
+const ERRORS_OPTION = {
+ label: 'Errors',
+ description: 'Error monitoring is part of every setup, so those changes always apply.',
+};
+
+/** An item body, rendered on the server and matched to its item by id. */
+export type ItemBody = {body: React.ReactNode; id: string};
+
+type Props = {
+ /**
+ * Rendered item bodies, keyed by item id rather than by position: matching
+ * them to `items` by array index would misplace every body after the first
+ * divergence instead of failing.
+ */
+ bodies: ItemBody[];
+ /**
+ * Facets carried by at least one item on this page. A browser-only guide has
+ * no OpenTelemetry or AI items, so those checkboxes are simply absent there.
+ */
+ facets: readonly Facet[];
+ /** Guide slug, used to scope checklist storage and label the agent prompt. */
+ framework: string;
+ /** Human-readable platform or guide name, for the headline. */
+ frameworkLabel: string;
+ items: MigrationItem[];
+ /** Item count before framework filtering, used for the headline. */
+ totalItems: number;
+};
+
+export function MigrationGuideClient({
+ items,
+ bodies: renderedBodies,
+ facets,
+ framework,
+ frameworkLabel,
+ totalItems,
+}: Props) {
+ const allFacets = useMemo(() => facets.map(f => f.id), [facets]);
+ const bodies = useMemo(
+ () => new Map(renderedBodies.map(({id, body}) => [id, body])),
+ [renderedBodies]
+ );
+
+ // Everything is selected until the reader narrows it, so the page reads as a
+ // complete guide before anyone touches a checkbox — and still does with
+ // JavaScript disabled, since the server renders every item.
+ const [selected, setSelected] = useState>(() => new Set(allFacets));
+ const [urlRead, setUrlRead] = useState(false);
+
+ // Filter state is mirrored into the URL so a narrowed guide can be shared.
+ //
+ // It is read and written directly rather than through `useSearchParams`,
+ // because that hook client-renders the tree up to the nearest Suspense
+ // boundary, and this docs route is `force-static`. The guide is the page
+ // body, so it would drop out of the prerendered HTML: `.md` exports are
+ // generated from that HTML, so every item would disappear from `.md`,
+ // along with what crawlers and readers without JavaScript get. Note this
+ // only shows up in a production build; in development the hook does not
+ // suspend and everything looks fine.
+ //
+ // `history.replaceState` is what Next.js documents for updating the query
+ // string without a navigation.
+ useEffect(() => {
+ const raw = new URLSearchParams(window.location.search).get(FILTER_PARAM);
+ if (raw !== null) {
+ // Ignore facets from a shared URL that this guide does not offer.
+ setSelected(new Set(raw.split(',').filter(f => allFacets.includes(f))));
+ }
+ setUrlRead(true);
+ }, [allFacets]);
+
+ useEffect(() => {
+ if (!urlRead) {
+ return;
+ }
+ const params = new URLSearchParams(window.location.search);
+ if (selected.size === allFacets.length) {
+ params.delete(FILTER_PARAM);
+ } else {
+ params.set(FILTER_PARAM, [...selected].join(','));
+ }
+ const query = params.toString();
+ // The hash is carried over: items render as `#` anchors, and
+ // rewriting the URL without it would break a shared deep link on the first
+ // filter change — or on hydration, before the reader touches anything.
+ window.history.replaceState(
+ null,
+ '',
+ `${window.location.pathname}${query ? `?${query}` : ''}${window.location.hash}`
+ );
+ }, [selected, urlRead, allFacets]);
+
+ const toggleFacet = useCallback((id: string) => {
+ // Functional update, so two toggles in the same tick cannot clobber each
+ // other.
+ setSelected(prev => toggled(prev, id));
+ }, []);
+
+ // Checklist progress is scoped per framework: someone migrating a monorepo
+ // has separate checklists for their Next.js app and their Node service.
+ const storageKey = `sentry-v11-migration:${framework}`;
+ const [checked, setChecked] = useState>(new Set());
+ const [hydrated, setHydrated] = useState(false);
+
+ useEffect(() => {
+ try {
+ const stored = window.localStorage.getItem(storageKey);
+ setChecked(new Set(stored ? (JSON.parse(stored) as string[]) : []));
+ } catch {
+ // Private mode, disabled storage — the guide still works, it just forgets.
+ }
+ setHydrated(true);
+ }, [storageKey]);
+
+ // Persisted from an effect rather than from inside the state updater, which
+ // has to stay pure: React invokes updaters twice in development, and an
+ // updater that writes to storage would write twice per click.
+ useEffect(() => {
+ if (!hydrated) {
+ // Never write before the stored value has been read, or the first render
+ // would overwrite the reader's saved progress with an empty set.
+ return;
+ }
+ try {
+ window.localStorage.setItem(storageKey, JSON.stringify([...checked]));
+ } catch {
+ // See above.
+ }
+ }, [checked, hydrated, storageKey]);
+
+ const toggleChecked = useCallback((id: string) => {
+ setChecked(prev => toggled(prev, id));
+ }, []);
+
+ const [showHidden, setShowHidden] = useState(false);
+
+ const visible = useMemo(
+ () => items.filter(item => isItemVisible(item, selected)),
+ [items, selected]
+ );
+ const visibleIds = useMemo(() => new Set(visible.map(i => i.id)), [visible]);
+ const hiddenCount = items.length - visible.length;
+
+ const actionRequired = visible.filter(i => i.severity === 'action-required');
+ // Every item is checkable: the action-required ones as work, the rest as an
+ // acknowledgement that you read them and decided they need nothing. So
+ // progress counts all of them, not just the work.
+ const doneCount = visible.filter(i => checked.has(i.id)).length;
+ const progress = visible.length ? Math.round((doneCount / visible.length) * 100) : 100;
+
+ const shown = showHidden ? items : visible;
+
+ return (
+
+ );
+}
+
+/**
+ * One filter checkbox, built from the same Radix pair as the onboarding option
+ * buttons so the two pickers read as one control. Without `onToggle` the option
+ * is locked on, for things every setup has.
+ */
+function FacetOption({
+ label,
+ description,
+ checked,
+ onToggle,
+}: {
+ description: string;
+ label: string;
+ checked?: boolean;
+ onToggle?: () => void;
+}) {
+ const locked = !onToggle;
+
+ return (
+
+ );
+}
+
+const EMPTY_DETECTION: Detection = {
+ facets: new Set(),
+ signals: [],
+ framework: undefined,
+ gaps: {missingInit: true, missingManifest: true},
+};
+
+/**
+ * Reads the reader's setup off a pasted `package.json` or `Sentry.init()` block
+ * and sets the filters from it, because a hand-ticked selection is only as
+ * accurate as their memory of their own config.
+ *
+ * Everything below is optional: the checkboxes above work untouched.
+ */
+function PasteDetect({
+ onDetect,
+ framework,
+ frameworkLabel,
+ facets,
+}: {
+ facets: readonly Facet[];
+ framework: string;
+ frameworkLabel: string;
+ onDetect: (next: Set) => void;
+}) {
+ const [open, setOpen] = useState(false);
+ const [text, setText] = useState('');
+ const [detection, setDetection] = useState(EMPTY_DETECTION);
+ // The last selection this component applied. A detected result narrows the
+ // filters, which means overwriting what the reader ticked — fair on a fresh
+ // paste, but not on every keystroke afterwards: re-applying an unchanged
+ // result would silently undo a correction they made by hand.
+ const lastApplied = useRef(undefined);
+
+ const handleChange = useCallback(
+ (value: string) => {
+ setText(value);
+
+ // Guard against someone pasting a lockfile: the rules are cheap, but
+ // there is no reason to scan megabytes of it.
+ const result = detect(value.slice(0, 200_000));
+
+ // A pasted config can mention things this guide has no items for — a
+ // React app's dependencies may include an AI SDK used only on its server.
+ // Keep the detection honest by intersecting it with what this page can
+ // actually show.
+ const applicable = new Set(
+ [...result.facets].filter(facet => facets.some(f => f.id === facet))
+ );
+ setDetection({
+ ...result,
+ facets: applicable,
+ signals: result.signals.filter(s => applicable.has(s.facet)),
+ });
+
+ if (applicable.size === 0) {
+ // Nothing to apply, and an emptied box should not count as "already
+ // applied" — re-pasting the same config has to work.
+ lastApplied.current = undefined;
+ return;
+ }
+ const key = [...applicable].sort().join(',');
+ if (key !== lastApplied.current) {
+ lastApplied.current = key;
+ onDetect(new Set(applicable));
+ }
+ },
+ [facets, onDetect]
+ );
+
+ const {facets: detectedFacets, signals, gaps, framework: detectedFramework} = detection;
+ // Only a framework this reader is *not* reading about is worth mentioning.
+ // One package serves several guides — `@sentry/node` backs Express, Fastify,
+ // Koa and more — so being outside that set is the test, not an exact match.
+ const wrongGuide =
+ detectedFramework && !detectedFramework.guides.includes(framework)
+ ? detectedFramework
+ : undefined;
+
+ return (
+
+
+
+ {failed
+ ? 'Could not copy. Your browser blocked clipboard access, so select the steps below and copy them instead.'
+ : 'Paste into Claude Code, Cursor or any coding agent with access to your repo.'}
+
+
+ );
+}
+
+function Item({
+ item,
+ body,
+ checked,
+ onToggle,
+ dimmed,
+}: {
+ body: React.ReactNode;
+ checked: boolean;
+ dimmed: boolean;
+ item: MigrationItem;
+ onToggle: () => void;
+}) {
+ // An item that needs no code change still needs reading, so the label says
+ // what ticking it means rather than claiming work that was never there.
+ const label =
+ item.severity === 'action-required'
+ ? `Mark "${item.title}" as done`
+ : `Acknowledge "${item.title}"`;
+
+ return (
+
+
+
+
+
{item.title}
+ {SEVERITIES[item.severity].label}
+ {dimmed && not selected}
+
+
+
{body}
+
+ );
+}
+
+/**
+ * Builds the copy-for-agent payload: the reader's filtered guide, plus enough
+ * instruction that an agent applies it rather than summarizing it.
+ */
+export function buildAgentPrompt(
+ items: MigrationItem[],
+ framework: string,
+ selected: Set,
+ facets: readonly Facet[]
+): string {
+ const selectedLabels = [...selected].map(id => facetLabel(facets, id));
+
+ const lines = [
+ '# Upgrade the Sentry JavaScript SDK from v10 to v11',
+ '',
+ 'Apply the following migration steps to this repository.',
+ '',
+ 'Rules:',
+ '- Apply every step marked "Action required". Steps marked "Behavior change" or',
+ ' "FYI" usually need no code edit. Read them, and only act if they affect this repo.',
+ '- Run the project type-check and test suite after each step that changes code.',
+ '- Do not invent APIs. If a step is ambiguous for this codebase, stop and ask.',
+ '- Some steps affect dashboards or alerts in Sentry rather than code. Call those out',
+ ' in your summary instead of trying to change them.',
+ '',
+ `Setup: ${framework}${selectedLabels.length ? `, using ${selectedLabels.join(', ')}` : ''}.`,
+ `${items.length} steps apply, ${items.filter(i => i.severity === 'action-required').length} of which require action.`,
+ '',
+ '---',
+ '',
+ ];
+
+ for (const phase of PHASES) {
+ const phaseItems = items.filter(item => item.phase === phase.id);
+ if (phaseItems.length === 0) {
+ continue;
+ }
+ lines.push(`## ${phase.title}`, '');
+ for (const item of phaseItems) {
+ lines.push(
+ `### ${item.title}`,
+ '',
+ `_${SEVERITIES[item.severity].label}_`,
+ '',
+ item.markdown.trim(),
+ ''
+ );
+ }
+ }
+
+ return lines.join('\n');
+}
+
+/** Human-readable name for a facet, falling back to its id. */
+function facetLabel(facets: readonly Facet[], id: string): string {
+ return facets.find(f => f.id === id)?.label ?? id;
+}
diff --git a/src/components/migrationGuide/constants.ts b/src/components/migrationGuide/constants.ts
new file mode 100644
index 0000000000000..904c50d7e2d84
--- /dev/null
+++ b/src/components/migrationGuide/constants.ts
@@ -0,0 +1,171 @@
+/**
+ * Shared vocabulary for the interactive v10-to-v11 migration guide.
+ *
+ * These values mirror the frontmatter facets on the item files in
+ * `includes/migration/javascript-v11/`, which are validated by
+ * `scripts/validate-migration-items.mjs`. The two vocabularies are asserted
+ * equal in `migrationGuide.spec.ts`, because a facet the validator accepts but
+ * the filter panel never offers hides every item tagged with it.
+ */
+
+export const PHASES = [
+ {
+ id: 'prerequisites',
+ title: 'Check prerequisites',
+ description: 'Runtime, language and framework versions you need before upgrading.',
+ },
+ {
+ id: 'packages',
+ title: 'Update packages',
+ description: 'Installs, removals and moved entry points.',
+ },
+ {
+ id: 'code-changes',
+ title: 'Make required code changes',
+ description: 'Options, APIs and config that changed and need your attention.',
+ },
+ {
+ id: 'verify',
+ title: 'Verify behavior changes',
+ description: 'Things that changed underneath you. Check dashboards and alerts.',
+ },
+ {
+ id: 'cleanup',
+ title: 'Optional cleanup',
+ description: 'Deprecations and type tightening you can address at your own pace.',
+ },
+] as const;
+
+export type PhaseId = (typeof PHASES)[number]['id'];
+
+export const SEVERITIES = {
+ 'action-required': {label: 'Action required'},
+ 'behavior-change': {label: 'Behavior change'},
+ informational: {label: 'FYI'},
+} as const;
+
+export type Severity = keyof typeof SEVERITIES;
+
+/**
+ * The one dimension the reader controls. Framework comes from the URL, and
+ * everything else is either universal or better inferred than asked about.
+ *
+ * An earlier draft also had a "Setup" group (bundler plugin, CDN bundle, Sentry
+ * CLI, Lambda layer, `@sentry/node-core`). Measured against the item
+ * collection, each of those gated exactly one item on any given guide, because
+ * the `frameworks` tag already did that work — so they added five checkboxes
+ * for no filtering. `custom-otel` was the exception and is the only survivor:
+ * it gates four substantial action-required items and is orthogonal to
+ * framework in a way a URL cannot express.
+ */
+export const FEATURES = [
+ {
+ id: 'tracing',
+ label: 'Tracing',
+ description: 'Spans, sampling, and trace propagation.',
+ },
+ {
+ id: 'profiling',
+ label: 'Profiling',
+ description: 'Trace-based or continuous profiling, in the browser or on the server.',
+ },
+ {id: 'logs', label: 'Logs', description: 'Logs sent through the SDK.'},
+ {id: 'metrics', label: 'Metrics', description: 'Counters, gauges, and distributions.'},
+ {
+ id: 'ai-agents',
+ label: 'AI Agents',
+ description: 'Instrumentation for AI SDKs, model calls, and agents.',
+ },
+ {
+ id: 'custom-otel',
+ label: 'Your own OpenTelemetry',
+ description: 'You run an OpenTelemetry setup of your own alongside Sentry.',
+ },
+] as const;
+
+export type FacetId = (typeof FEATURES)[number]['id'];
+
+/** One breaking change, as rendered by the guide. */
+export interface MigrationItem {
+ category: string;
+ features: string[];
+ id: string;
+ /** Raw MDX body, used to build the copy-for-agent output. */
+ markdown: string;
+ /** Curated position within a phase, from the item frontmatter. */
+ order: number;
+ phase: PhaseId;
+ severity: Severity;
+ title: string;
+}
+
+/**
+ * Reading order within a phase: the curated `order` from the item frontmatter,
+ * and nothing else.
+ *
+ * Sorting by severity instead was tried and reverted. Three of the five phases
+ * carry a single severity, so it only reordered two of them, and there it broke
+ * deliberate sequences: an item that introduces a change (span streaming,
+ * OpenTelemetry ownership) is a behavior change, so it sorted below the
+ * action-required items that follow from it. Ordering is a content decision,
+ * so it stays in the content.
+ */
+export function compareItems(a: MigrationItem, b: MigrationItem): number {
+ return a.order - b.order;
+}
+
+/**
+ * Whether an item survives the reader's facet selection.
+ *
+ * An untagged item is universal and always applies. A tagged item applies when
+ * the reader selected at least one of its tags — items are tagged with what
+ * they are *about*, so any overlap makes it relevant.
+ *
+ * Framework and platform-category filtering happens server-side, because those
+ * come from the URL rather than from a checkbox.
+ */
+export function itemMatchesFacets(item: MigrationItem, selected: Set): boolean {
+ if (item.features.length === 0) {
+ return true;
+ }
+ return item.features.some(tag => selected.has(tag));
+}
+
+/**
+ * Items that must never be hidden by a facet selection.
+ *
+ * A guide that silently swallows a breaking change because someone left a box
+ * unchecked is worse than one that shows too much, so universal
+ * action-required items always render.
+ */
+export function isAlwaysVisible(item: MigrationItem): boolean {
+ return item.severity === 'action-required' && item.features.length === 0;
+}
+
+/**
+ * Whether the guide renders an item, given what the reader selected.
+ *
+ * This is the whole visibility rule, and the only thing the guide calls — the
+ * two predicates above are its parts. Composing them here means the invariant
+ * "a universal action-required item is always rendered" is testable and cannot
+ * be lost by a later change to {@link itemMatchesFacets} alone.
+ */
+export function isItemVisible(item: MigrationItem, selected: Set): boolean {
+ return isAlwaysVisible(item) || itemMatchesFacets(item, selected);
+}
+
+/** The v11 migration page for a guide slug, or for the platform itself. */
+export function migrationGuideHref(slug: string): string {
+ return slug === 'javascript'
+ ? '/platforms/javascript/migration/v10-to-v11/'
+ : `/platforms/javascript/guides/${slug}/migration/v10-to-v11/`;
+}
+
+/**
+ * Reading name for a guide slug. Slugs match their URL and read well enough for
+ * guides, but the platform page has no guide slug to show — "the javascript
+ * guide" names nothing the reader can go to.
+ */
+export function migrationGuideLabel(slug: string): string {
+ return slug === 'javascript' ? 'Browser JavaScript' : slug;
+}
diff --git a/src/components/migrationGuide/detect.ts b/src/components/migrationGuide/detect.ts
new file mode 100644
index 0000000000000..52629276fa78b
--- /dev/null
+++ b/src/components/migrationGuide/detect.ts
@@ -0,0 +1,355 @@
+/**
+ * Infers which migration facets apply from a pasted `package.json` and/or
+ * `Sentry.init()` block.
+ *
+ * Ticking boxes by hand is only as accurate as the reader's memory of their own
+ * config, and being wrong means a breaking change silently disappears from
+ * their guide. Reading it off their actual setup is more reliable, so this is
+ * the preferred way to narrow the guide — but the checkboxes work on their own,
+ * and a detected result is always overridable by hand.
+ *
+ * Detection deliberately errs toward false positives: showing an extra section
+ * costs a reader some scrolling, whereas a missed one costs them a broken
+ * upgrade.
+ */
+
+export type DetectionSignal = {
+ /** The literal that matched, shown back to the reader so the result is checkable. */
+ evidence: string;
+ facet: string;
+};
+
+/** The guide a pasted dependency list belongs to. */
+export type FrameworkMatch = {
+ /**
+ * Every guide this package serves. `@sentry/node` backs the Express, Fastify,
+ * Koa, Hapi, Connect and Firebase guides as well as the Node one, so a match
+ * narrows the reader down to a set of pages, not to a single one.
+ */
+ guides: readonly string[];
+ /** The package that matched, quoted back to the reader. */
+ pkg: string;
+ /** The guide to link to when the reader is on none of `guides`. */
+ primary: string;
+};
+
+export type Detection = {
+ facets: Set;
+ /** Undefined when no Sentry SDK package is present. */
+ framework: FrameworkMatch | undefined;
+ gaps: DetectionGaps;
+ signals: DetectionSignal[];
+};
+
+export type DetectionGaps = {
+ missingInit: boolean;
+ missingManifest: boolean;
+};
+
+type Rule = {
+ facet: string;
+ /**
+ * Each pattern must carry a capture group or match a literal we can quote
+ * back as evidence.
+ */
+ patterns: RegExp[];
+};
+
+const RULES: Rule[] = [
+ {
+ facet: 'tracing',
+ patterns: [
+ /\btracesSampleRate\b/,
+ /\btracesSampler\b/,
+ /\btracePropagationTargets\b/,
+ /\bbrowserTracingIntegration\b/,
+ /\btraceLifecycle\b/,
+ /\bbeforeSendTransaction\b/,
+ /\bignoreTransactions\b/,
+ /\bbeforeSendSpan\b/,
+ /\bstartSpan\b/,
+ ],
+ },
+ {
+ facet: 'profiling',
+ patterns: [
+ /@sentry\/profiling-node/,
+ /\bprofilesSampleRate\b/,
+ /\bprofilesSampler\b/,
+ /\bprofileSessionSampleRate\b/,
+ /\bprofileLifecycle\b/,
+ /\bnodeProfilingIntegration\b/,
+ /\bbrowserProfilingIntegration\b/,
+ ],
+ },
+ {
+ facet: 'logs',
+ patterns: [
+ /\benableLogs\b/,
+ /Sentry\.logger\b/,
+ /\bconsoleLoggingIntegration\b/,
+ /@sentry\/pino-transport/,
+ ],
+ },
+ {
+ facet: 'metrics',
+ patterns: [/Sentry\.metrics\b/, /\benableMetrics\b/, /\bbeforeSendMetric\b/],
+ },
+ {
+ facet: 'ai-agents',
+ patterns: [
+ /\bvercelAIIntegration\b/,
+ /\bopenAIIntegration\b/,
+ /\banthropicAIIntegration\b/,
+ /\bgoogleGenAIIntegration\b/,
+ /\blangChainIntegration\b/,
+ /\blangGraphIntegration\b/,
+ /\binstrument(OpenAi|AnthropicAi|GoogleGenAI|WorkersAi)Client\b/,
+ /\binstrument(LangGraph|StateGraph)\b/,
+ /"(openai|@anthropic-ai\/sdk|@google\/genai|langchain|@langchain\/langgraph|ai)"\s*:/,
+ ],
+ },
+ {
+ facet: 'custom-otel',
+ patterns: [
+ /"@opentelemetry\/[a-z-]+"\s*:/,
+ /\bskipOpenTelemetrySetup\b/,
+ /\benableOpenTelemetrySetup\b/,
+ /\bNodeTracerProvider\b/,
+ /\bSentrySpanProcessor\b/,
+ /\bSentryContextManager\b/,
+ /\bSentrySampler\b/,
+ /\botlpIntegration\b/,
+ /@sentry\/opentelemetry/,
+ /@sentry\/node-core\/light\/otlp/,
+ ],
+ },
+];
+
+/**
+ * Guide slugs we can recognize from a pasted dependency list, keyed by the
+ * package that identifies them.
+ *
+ * One package can serve several guides: `@sentry/node` is the SDK behind the
+ * Express, Fastify, Koa, Hapi, Connect and Firebase guides, so seeing it does
+ * not mean the reader belongs on the Node page. `primary` is only used as the
+ * link target when the reader is on none of the guides the package serves.
+ *
+ * Order matters: framework SDKs are checked before the generic `@sentry/react`,
+ * `@sentry/node` and `@sentry/browser`, which they depend on and which
+ * therefore appear in a lockfile-flattened dependency list too.
+ *
+ * `migrationGuide.spec.ts` asserts these guide lists against the `sdk:`
+ * frontmatter of every JavaScript guide, so a new guide on an existing SDK
+ * cannot silently fall out of the mapping.
+ */
+export const FRAMEWORK_PACKAGES: Array<{
+ guides: readonly string[];
+ pattern: RegExp;
+ pkg: string;
+}> = [
+ {pkg: '@sentry/nextjs', guides: ['nextjs'], pattern: /"@sentry\/nextjs"\s*:/},
+ {pkg: '@sentry/nuxt', guides: ['nuxt'], pattern: /"@sentry\/nuxt"\s*:/},
+ {pkg: '@sentry/sveltekit', guides: ['sveltekit'], pattern: /"@sentry\/sveltekit"\s*:/},
+ {pkg: '@sentry/svelte', guides: ['svelte'], pattern: /"@sentry\/svelte"\s*:/},
+ {
+ pkg: '@sentry/astro',
+ guides: ['astro', 'azure-functions'],
+ pattern: /"@sentry\/astro"\s*:/,
+ },
+ {pkg: '@sentry/remix', guides: ['remix'], pattern: /"@sentry\/remix"\s*:/},
+ {
+ pkg: '@sentry/react-router',
+ guides: ['react-router'],
+ pattern: /"@sentry\/react-router"\s*:/,
+ },
+ {
+ pkg: '@sentry/solidstart',
+ guides: ['solidstart'],
+ pattern: /"@sentry\/solidstart"\s*:/,
+ },
+ {pkg: '@sentry/solid', guides: ['solid'], pattern: /"@sentry\/solid"\s*:/},
+ {
+ pkg: '@sentry/tanstackstart-react',
+ guides: ['tanstackstart-react'],
+ pattern: /"@sentry\/tanstackstart-react"\s*:/,
+ },
+ {pkg: '@sentry/angular', guides: ['angular'], pattern: /"@sentry\/angular"\s*:/},
+ {pkg: '@sentry/vue', guides: ['vue'], pattern: /"@sentry\/vue"\s*:/},
+ {pkg: '@sentry/ember', guides: ['ember'], pattern: /"@sentry\/ember"\s*:/},
+ {pkg: '@sentry/gatsby', guides: ['gatsby'], pattern: /"@sentry\/gatsby"\s*:/},
+ {pkg: '@sentry/nestjs', guides: ['nestjs'], pattern: /"@sentry\/nestjs"\s*:/},
+ {pkg: '@sentry/nitro', guides: ['nitro'], pattern: /"@sentry\/nitro"\s*:/},
+ {pkg: '@sentry/elysia', guides: ['elysia'], pattern: /"@sentry\/elysia"\s*:/},
+ {pkg: '@sentry/hono', guides: ['hono'], pattern: /"@sentry\/hono"\s*:/},
+ {pkg: '@sentry/effect', guides: ['effect'], pattern: /"@sentry\/effect"\s*:/},
+ {
+ pkg: '@sentry/cloudflare',
+ guides: ['cloudflare'],
+ pattern: /"@sentry\/cloudflare"\s*:/,
+ },
+ {pkg: '@sentry/deno', guides: ['deno'], pattern: /"@sentry\/deno"\s*:/},
+ {pkg: '@sentry/bun', guides: ['bun'], pattern: /"@sentry\/bun"\s*:/},
+ {
+ pkg: '@sentry/aws-serverless',
+ guides: ['aws-lambda'],
+ pattern: /"@sentry\/aws-serverless"\s*:/,
+ },
+ {
+ pkg: '@sentry/google-cloud-serverless',
+ guides: ['gcp-functions'],
+ pattern: /"@sentry\/google-cloud-serverless"\s*:/,
+ },
+ {pkg: '@sentry/react', guides: ['react'], pattern: /"@sentry\/react"\s*:/},
+ {
+ pkg: '@sentry/node',
+ guides: ['node', 'connect', 'express', 'fastify', 'firebase', 'hapi', 'koa'],
+ pattern: /"@sentry\/node"\s*:/,
+ },
+ // The bare browser SDK belongs to the platform page rather than a guide, which
+ // `MigrationGuide` identifies as `javascript`.
+ {pkg: '@sentry/browser', guides: ['javascript'], pattern: /"@sentry\/browser"\s*:/},
+];
+
+/**
+ * Reads a pasted setup once and returns everything the UI needs from it.
+ *
+ * Comments are stripped before matching so that a commented-out option does not
+ * count as usage — pasted `Sentry.init` blocks routinely carry commented
+ * alternatives, and those describe a setup the reader does not have.
+ */
+export function detect(text: string): Detection {
+ const haystack = stripComments(text);
+ const {facets, signals} = matchFacets(haystack);
+
+ return {
+ facets,
+ signals,
+ framework: matchFramework(haystack),
+ gaps: findGaps(haystack),
+ };
+}
+
+/**
+ * Removes `//` and block comments without touching `//` that is part of the
+ * config itself.
+ *
+ * A regex cannot do this: `tracePropagationTargets: [/^\/\//]` and a
+ * protocol-relative `"//cdn.example.com"` both contain a literal `//` that must
+ * survive, and dropping the rest of that line silently loses every option after
+ * it — which deselects facets and hides real breaking changes. So this walks the
+ * text tracking string literals and escapes instead. Quotes are also closed at a
+ * line break, so an apostrophe in prose can at worst affect its own line.
+ */
+function stripComments(text: string): string {
+ let out = '';
+ let quote: string | undefined;
+
+ for (let index = 0; index < text.length; index++) {
+ const char = text[index];
+
+ if (quote) {
+ out += char;
+ if (char === '\\') {
+ // An escaped character cannot close the literal.
+ out += text[index + 1] ?? '';
+ index++;
+ } else if (char === quote || (char === '\n' && quote !== '`')) {
+ quote = undefined;
+ }
+ continue;
+ }
+
+ if (char === '"' || char === "'" || char === '`') {
+ quote = char;
+ out += char;
+ continue;
+ }
+
+ if (char === '\\') {
+ // Outside a string this is a regex-literal escape such as `\/`; keeping
+ // both characters stops the escaped slash from reading as a comment.
+ out += char + (text[index + 1] ?? '');
+ index++;
+ continue;
+ }
+
+ if (char === '/' && text[index + 1] === '/' && text[index - 1] !== ':') {
+ // Line comment. Leave the newline so line-anchored patterns still work.
+ const end = text.indexOf('\n', index);
+ if (end === -1) {
+ return `${out} `;
+ }
+ out += ' ';
+ index = end - 1;
+ continue;
+ }
+
+ if (char === '/' && text[index + 1] === '*') {
+ const end = text.indexOf('*/', index + 2);
+ out += ' ';
+ if (end === -1) {
+ return out;
+ }
+ index = end + 1;
+ continue;
+ }
+
+ out += char;
+ }
+
+ return out;
+}
+
+function matchFacets(haystack: string): {
+ facets: Set;
+ signals: DetectionSignal[];
+} {
+ const facets = new Set();
+ const signals: DetectionSignal[] = [];
+
+ for (const rule of RULES) {
+ for (const pattern of rule.patterns) {
+ const match = haystack.match(pattern);
+ if (match) {
+ facets.add(rule.facet);
+ // Patterns that match a JSON key capture the surrounding quotes and
+ // colon; trim them so the evidence reads as the bare identifier.
+ signals.push({
+ facet: rule.facet,
+ evidence: match[0].replace(/^["\s]+|["\s:]+$/g, ''),
+ });
+ break; // One piece of evidence per facet is enough to show the reader.
+ }
+ }
+ }
+
+ return {facets, signals};
+}
+
+/**
+ * What the pasted text cannot tell us, so the UI can ask for the missing half.
+ *
+ * Runtime options such as `tracesSampleRate` only appear in `Sentry.init()`,
+ * and installed packages only in a manifest. Pasting one without the other
+ * silently under-detects, which deselects facets and hides real breaking
+ * changes — the exact failure this feature exists to prevent. Naming the gap is
+ * cheaper than guessing at it.
+ *
+ * `Sentry.init` is matched loosely, because a named `init()` import or a
+ * framework wrapper is init too and telling those readers to paste what they
+ * just pasted is worse than staying quiet.
+ */
+function findGaps(haystack: string): DetectionGaps {
+ return {
+ missingInit: !/(\bSentry\.init\s*\(|\binit\s*\(\s*\{|\bdsn\s*:)/.test(haystack),
+ missingManifest: !/"(dependencies|devDependencies)"\s*:/.test(haystack),
+ };
+}
+
+function matchFramework(haystack: string): FrameworkMatch | undefined {
+ const entry = FRAMEWORK_PACKAGES.find(({pattern}) => pattern.test(haystack));
+ return entry
+ ? {pkg: entry.pkg, guides: entry.guides, primary: entry.guides[0]}
+ : undefined;
+}
diff --git a/src/components/migrationGuide/index.tsx b/src/components/migrationGuide/index.tsx
new file mode 100644
index 0000000000000..76cdd3e8cfe44
--- /dev/null
+++ b/src/components/migrationGuide/index.tsx
@@ -0,0 +1,130 @@
+import {readdirSync} from 'fs';
+import path from 'path';
+import {getCurrentPlatformOrGuide, nodeForPath} from 'sentry-docs/docTree';
+import {getMDXComponent} from 'sentry-docs/getMDXComponent';
+import {getFileBySlugWithCache} from 'sentry-docs/mdx';
+import {mdxComponents} from 'sentry-docs/mdxComponents';
+import {serverContext} from 'sentry-docs/serverContext';
+import {PlatformCategory} from 'sentry-docs/types';
+
+import {MigrationGuideClient} from './client';
+import {compareItems, FEATURES, MigrationItem, PhaseId} from './constants';
+
+const ITEMS_DIR = 'includes/migration/javascript-v11';
+
+// Built once rather than per item body: `mdxComponents()` assembles a ~70-entry
+// map, and this component renders 81 bodies on each of ~38 migration pages.
+const components = mdxComponents();
+
+// `getMDXComponent` compiles its source with `new Function`, so the same item
+// body is compiled once instead of once per page it appears on. Keyed by source,
+// so an edit in development produces a new entry rather than a stale component.
+const componentBySource = new Map>();
+
+function ItemBody({mdxSource}: {mdxSource: string}) {
+ let MDXLayout = componentBySource.get(mdxSource);
+ if (!MDXLayout) {
+ MDXLayout = getMDXComponent(mdxSource);
+ componentBySource.set(mdxSource, MDXLayout);
+ }
+ return ;
+}
+
+/**
+ * The interactive "Migrate from 10.x to 11.x" guide.
+ *
+ * Reads the item collection in `includes/migration/javascript-v11/`, drops everything that
+ * cannot apply to the guide currently being rendered, and hands the rest to a
+ * client shell that owns filtering and checklist state.
+ *
+ * The framework axis is the URL rather than a checkbox: this page lives in
+ * `common/` and is rendered into every JavaScript guide, so an item tagged for
+ * Next.js is simply absent from the SvelteKit page. Feature and package
+ * filtering is the reader's job and happens client-side.
+ */
+export async function MigrationGuide() {
+ const {rootNode, path: urlPath} = serverContext();
+ const platformOrGuide = getCurrentPlatformOrGuide(rootNode, urlPath);
+
+ // `name` is the on-disk guide directory (`nextjs`), which is what item
+ // frontmatter tags against. The bare JavaScript platform has no framework, so
+ // only universal items apply.
+ const framework = platformOrGuide?.type === 'guide' ? platformOrGuide.name : undefined;
+ const categories: PlatformCategory[] = platformOrGuide?.categories ?? [];
+
+ // `platformOrGuide.title` is the abbreviated form used by the platform picker
+ // (`platformTitle` in config.yml), which for this platform is just
+ // "JavaScript" — a meaningless scope in a JavaScript SDK migration guide,
+ // since all 81 changes are JavaScript changes. Read the platform's own
+ // frontmatter title instead, which is the real scope: "Browser JavaScript".
+ // Guides are unaffected; their title is already the specific one.
+ const scopeNode = platformOrGuide
+ ? nodeForPath(rootNode, platformOrGuide.url.split('/').filter(Boolean))
+ : undefined;
+ const scopeLabel =
+ scopeNode?.frontmatter.title ?? platformOrGuide?.title ?? 'your setup';
+
+ const slugs = readdirSync(path.join(process.cwd(), ITEMS_DIR))
+ .filter(file => file.endsWith('.mdx'))
+ .map(file => file.replace(/\.mdx$/, ''));
+
+ const docs = await Promise.all(
+ slugs.map(slug => getFileBySlugWithCache(`${ITEMS_DIR}/${slug}`))
+ );
+
+ const rendered = docs
+ .map(doc => {
+ const data = doc.frontMatter as Record;
+ return {
+ id: data.id as string,
+ title: data.title as string,
+ phase: data.phase as PhaseId,
+ category: data.category as string,
+ severity: data.severity,
+ frameworks: data.frameworks as string[] | 'all',
+ features: (data.features ?? []) as string[],
+ platformCategory: data.platformCategory as PlatformCategory | 'all',
+ order: data.order as number,
+ markdown: doc.matter?.content ?? '',
+ mdxSource: doc.mdxSource,
+ };
+ })
+ .filter(item => {
+ if (item.frameworks !== 'all' && !item.frameworks.includes(framework ?? '')) {
+ return false;
+ }
+ // A guide with no declared categories (or a platform-level page) should
+ // not lose content, so only filter when we actually know the categories.
+ if (item.platformCategory !== 'all' && categories.length > 0) {
+ return categories.includes(item.platformCategory);
+ }
+ return true;
+ })
+ .sort(compareItems);
+
+ const items: MigrationItem[] = rendered.map(
+ ({mdxSource: _mdxSource, frameworks: _frameworks, ...item}) => item
+ );
+
+ // Only offer facets that something on this page actually carries. The browser
+ // SDK has never used OpenTelemetry, and v11 drops AI integrations from it, so
+ // both of those checkboxes would sit on a React or Vue page filtering nothing
+ // — and a control that does nothing when ticked reads as broken.
+ const facets = FEATURES.filter(facet =>
+ items.some(item => item.features.includes(facet.id))
+ );
+
+ return (
+ ({
+ id: item.id,
+ body: ,
+ }))}
+ facets={facets}
+ framework={framework ?? 'javascript'}
+ frameworkLabel={scopeLabel}
+ totalItems={slugs.length}
+ />
+ );
+}
diff --git a/src/components/migrationGuide/migrationGuide.spec.ts b/src/components/migrationGuide/migrationGuide.spec.ts
new file mode 100644
index 0000000000000..1c4246a168cf2
--- /dev/null
+++ b/src/components/migrationGuide/migrationGuide.spec.ts
@@ -0,0 +1,384 @@
+import {readdirSync, readFileSync} from 'fs';
+import matter from 'gray-matter';
+import path from 'path';
+import {describe, expect, it} from 'vitest';
+
+// The validator is plain Node ESM, imported here so `pnpm test` runs it. Without
+// this, a mistyped facet in an item file fails nothing and the item silently
+// disappears from every reader's guide.
+import {
+ FEATURES as VALIDATOR_FEATURES,
+ loadItems,
+ PHASES as VALIDATOR_PHASES,
+ SEVERITIES as VALIDATOR_SEVERITIES,
+} from '../../../scripts/validate-migration-items.mjs';
+import {
+ compareItems,
+ FEATURES,
+ isAlwaysVisible,
+ isItemVisible,
+ itemMatchesFacets,
+ migrationGuideHref,
+ MigrationItem,
+ PHASES,
+ SEVERITIES,
+} from './constants';
+import {detect, FRAMEWORK_PACKAGES} from './detect';
+
+function item(overrides: Partial = {}): MigrationItem {
+ return {
+ id: 'test-item',
+ title: 'Test item',
+ phase: 'code-changes',
+ category: 'behavior',
+ severity: 'action-required',
+ features: [],
+ markdown: '',
+ order: 0,
+ ...overrides,
+ };
+}
+
+describe('itemMatchesFacets', () => {
+ it('always matches an untagged item, whatever the reader selected', () => {
+ expect(itemMatchesFacets(item(), new Set())).toBe(true);
+ expect(itemMatchesFacets(item(), new Set(['tracing']))).toBe(true);
+ });
+
+ it('matches when the reader selected any one of the item tags', () => {
+ const subject = item({features: ['tracing', 'metrics']});
+ expect(itemMatchesFacets(subject, new Set(['tracing']))).toBe(true);
+ expect(itemMatchesFacets(subject, new Set(['metrics']))).toBe(true);
+ });
+
+ it('does not match when the reader selected none of the item tags', () => {
+ const subject = item({features: ['profiling']});
+ expect(itemMatchesFacets(subject, new Set(['tracing', 'logs']))).toBe(false);
+ });
+});
+
+describe('isAlwaysVisible', () => {
+ it('protects universal action-required items from being filtered away', () => {
+ expect(isAlwaysVisible(item())).toBe(true);
+ });
+
+ it('does not protect items that are scoped to a feature', () => {
+ expect(isAlwaysVisible(item({features: ['tracing']}))).toBe(false);
+ expect(isAlwaysVisible(item({features: ['custom-otel']}))).toBe(false);
+ });
+
+ it('does not protect items that need no action', () => {
+ expect(isAlwaysVisible(item({severity: 'behavior-change'}))).toBe(false);
+ expect(isAlwaysVisible(item({severity: 'informational'}))).toBe(false);
+ });
+});
+
+describe('isItemVisible', () => {
+ // This is the invariant the guide rests on: whatever the reader unticks, a
+ // breaking change that applies to everybody stays on the page.
+ it('renders a universal action-required item even with nothing selected', () => {
+ expect(isItemVisible(item(), new Set())).toBe(true);
+ });
+
+ it('renders a universal informational item even with nothing selected', () => {
+ expect(isItemVisible(item({severity: 'informational'}), new Set())).toBe(true);
+ });
+
+ it('hides a feature-scoped item the reader did not select', () => {
+ expect(isItemVisible(item({features: ['profiling']}), new Set(['logs']))).toBe(false);
+ });
+
+ it('renders a feature-scoped item the reader did select', () => {
+ expect(isItemVisible(item({features: ['profiling']}), new Set(['profiling']))).toBe(
+ true
+ );
+ });
+});
+
+describe('compareItems', () => {
+ it('follows the curated order, regardless of severity', () => {
+ // Severity-based ordering was tried and reverted: it split items from the
+ // ones that introduce them. Ordering lives in the item frontmatter.
+ const sorted = [
+ item({id: 'third', severity: 'action-required', order: 30}),
+ item({id: 'first', severity: 'informational', order: 10}),
+ item({id: 'second', severity: 'behavior-change', order: 20}),
+ ].sort(compareItems);
+ expect(sorted.map(i => i.id)).toEqual(['first', 'second', 'third']);
+ });
+});
+
+describe('migrationGuideHref', () => {
+ it('points at the guide page for a guide slug', () => {
+ expect(migrationGuideHref('nextjs')).toBe(
+ '/platforms/javascript/guides/nextjs/migration/v10-to-v11/'
+ );
+ });
+
+ it('points at the platform page for browser JavaScript, which has no guide', () => {
+ expect(migrationGuideHref('javascript')).toBe(
+ '/platforms/javascript/migration/v10-to-v11/'
+ );
+ });
+});
+
+describe('detect: facets', () => {
+ it('reads features off a Sentry.init block', () => {
+ const {facets} = detect(`
+ Sentry.init({
+ dsn: process.env.SENTRY_DSN,
+ tracesSampleRate: 1.0,
+ profilesSampleRate: 1.0,
+ enableLogs: true,
+ });
+ `);
+ expect([...facets].sort()).toEqual(['logs', 'profiling', 'tracing']);
+ });
+
+ it('reads packages off a package.json dependency list', () => {
+ const {facets} = detect(`
+ {
+ "dependencies": {
+ "@sentry/nextjs": "^10.5.0",
+ "@sentry/profiling-node": "^10.5.0",
+ "openai": "^4.0.0"
+ }
+ }
+ `);
+ expect([...facets].sort()).toEqual(['ai-agents', 'profiling']);
+ });
+
+ it('detects a bring-your-own OpenTelemetry setup', () => {
+ const {facets} = detect(`
+ "@opentelemetry/sdk-trace-node": "^2.0.0",
+ Sentry.init({skipOpenTelemetrySetup: true});
+ `);
+ expect(facets.has('custom-otel')).toBe(true);
+ });
+
+ it('ignores options that are only present as comments', () => {
+ const {facets} = detect(`
+ Sentry.init({
+ dsn: '__DSN__',
+ // profilesSampleRate: 1.0,
+ /* enableLogs: true, */
+ tracesSampleRate: 1.0,
+ });
+ `);
+ expect(facets.has('tracing')).toBe(true);
+ expect(facets.has('profiling')).toBe(false);
+ expect(facets.has('logs')).toBe(false);
+ });
+
+ it('does not treat the // in a URL as the start of a comment', () => {
+ const {facets} = detect(`
+ {"repository": "https://github.com/example/app",
+ "dependencies": {"@sentry/profiling-node": "^10.5.0"}}
+ `);
+ expect(facets.has('profiling')).toBe(true);
+ });
+
+ it('keeps reading a line after a // inside a regex literal', () => {
+ // `/^\/\//` ends in two slashes that are not a comment. Treating them as one
+ // would drop `enableLogs` and hide every logs breaking change.
+ const {facets} = detect(
+ 'Sentry.init({tracePropagationTargets: [/^\\/\\//], enableLogs: true});'
+ );
+ expect(facets.has('logs')).toBe(true);
+ });
+
+ it('keeps reading a line after a protocol-relative URL string', () => {
+ const {facets} = detect(
+ 'Sentry.init({tunnel: "//sentry.example.com", enableMetrics: true});'
+ );
+ expect(facets.has('metrics')).toBe(true);
+ });
+
+ it('returns nothing for input with no Sentry setup in it', () => {
+ const {facets, signals} = detect('{"dependencies": {"lodash": "^4.0.0"}}');
+ expect(facets.size).toBe(0);
+ expect(signals).toEqual([]);
+ });
+
+ it('reports the literal it matched, so the reader can check the result', () => {
+ const {signals} = detect('Sentry.init({tracesSampleRate: 1.0})');
+ expect(signals).toEqual([{facet: 'tracing', evidence: 'tracesSampleRate'}]);
+ });
+
+ it('reports a JSON dependency key without its quotes and colon', () => {
+ const {signals} = detect('{"dependencies": {"openai": "^4.0.0"}}');
+ expect(signals).toEqual([{facet: 'ai-agents', evidence: 'openai'}]);
+ });
+
+ it('does not tag a bring-your-own-OpenTelemetry item as tracing', () => {
+ // Setup 3 in the migration guide turns Sentry tracing off and lets
+ // OpenTelemetry own spans, so these two facets are independent.
+ const {facets} = detect('Sentry.init({integrations: [Sentry.otlpIntegration()]});');
+ expect(facets.has('custom-otel')).toBe(true);
+ expect(facets.has('tracing')).toBe(false);
+ });
+});
+
+describe('detect: gaps', () => {
+ it('flags a dependency list with no init block, where features hide', () => {
+ expect(detect('{"dependencies": {"@sentry/nextjs": "^10.5.0"}}').gaps).toEqual({
+ missingInit: true,
+ missingManifest: false,
+ });
+ });
+
+ it('flags an init block with no dependency list, where packages hide', () => {
+ expect(detect('Sentry.init({tracesSampleRate: 1.0});').gaps).toEqual({
+ missingInit: false,
+ missingManifest: true,
+ });
+ });
+
+ it('flags nothing when both halves are present', () => {
+ expect(
+ detect(`
+ {"dependencies": {"@sentry/nextjs": "^10.5.0"}}
+ Sentry.init({tracesSampleRate: 1.0});
+ `).gaps
+ ).toEqual({missingInit: false, missingManifest: false});
+ });
+
+ it('does not count a commented-out init call as present', () => {
+ expect(detect('// Sentry.init({});').gaps.missingInit).toBe(true);
+ });
+
+ it('counts a named init import as an init block', () => {
+ // Telling someone to paste the init block they just pasted reads as broken.
+ expect(
+ detect('import {init} from "@sentry/react"; init({tracesSampleRate: 1});').gaps
+ .missingInit
+ ).toBe(false);
+ });
+
+ it('counts a framework wrapper config as an init block', () => {
+ expect(
+ detect('sentry({dsn: "https://examplePublicKey@o0.ingest.sentry.io/0"})').gaps
+ .missingInit
+ ).toBe(false);
+ });
+});
+
+describe('detect: framework', () => {
+ it('identifies the framework SDK from a dependency list', () => {
+ expect(detect('"@sentry/sveltekit": "^10.0.0"').framework?.primary).toBe('sveltekit');
+ expect(detect('"@sentry/cloudflare": "^10.0.0"').framework?.primary).toBe(
+ 'cloudflare'
+ );
+ });
+
+ it('prefers the framework SDK over the generic SDKs it depends on', () => {
+ // A flattened dependency list contains both; `@sentry/nextjs` is the answer.
+ expect(
+ detect(`
+ "@sentry/nextjs": "^10.5.0",
+ "@sentry/react": "^10.5.0",
+ "@sentry/node": "^10.5.0"
+ `).framework?.primary
+ ).toBe('nextjs');
+ });
+
+ it('returns undefined when no Sentry SDK is present', () => {
+ expect(detect('{"dependencies": {"react": "^19.0.0"}}').framework).toBeUndefined();
+ });
+
+ it('treats @sentry/node as belonging to every guide it backs', () => {
+ // An Express reader on the Express guide must not be told to leave it.
+ const {framework: match} = detect('"@sentry/node": "^10.5.0"');
+ expect(match?.guides).toContain('express');
+ expect(match?.guides).toContain('fastify');
+ expect(match?.guides).toContain('koa');
+ expect(match?.primary).toBe('node');
+ });
+
+ it('maps the bare browser SDK to the platform page', () => {
+ expect(detect('"@sentry/browser": "^10.5.0"').framework?.primary).toBe('javascript');
+ });
+});
+
+describe('facet vocabulary', () => {
+ it('has no duplicate ids', () => {
+ const ids = FEATURES.map(f => f.id);
+ expect(new Set(ids).size).toBe(ids.length);
+ });
+
+ it('describes every facet, since each one renders a tooltip', () => {
+ expect(FEATURES.every(f => f.description.length > 0)).toBe(true);
+ });
+
+ it('gives every facet a label, since ids surface in the agent prompt', () => {
+ expect(
+ FEATURES.every(f => f.label.length > 0 && String(f.label) !== String(f.id))
+ ).toBe(true);
+ });
+
+ // The filter panel and the frontmatter validator carry the vocabulary
+ // separately. If they drift, the validator accepts a facet that gets no
+ // checkbox, and every item tagged with it is hidden from every reader.
+ it('matches the vocabulary the item validator enforces', () => {
+ expect(FEATURES.map(f => f.id)).toEqual(VALIDATOR_FEATURES);
+ expect(PHASES.map(p => p.id)).toEqual(VALIDATOR_PHASES);
+ expect(Object.keys(SEVERITIES)).toEqual(VALIDATOR_SEVERITIES);
+ });
+});
+
+describe('migration item collection', () => {
+ it('has valid frontmatter on every item', () => {
+ const {items, errors} = loadItems();
+ expect(errors).toEqual([]);
+ expect(items.length).toBeGreaterThan(0);
+ });
+
+ it('has an item for every facet the filter panel offers', () => {
+ // A checkbox that filters nothing reads as broken to anyone who ticks it.
+ const {items} = loadItems() as {items: Array<{features?: string[]}>};
+ const tagged = new Set(items.flatMap(i => i.features ?? []));
+ expect(FEATURES.map(f => f.id).filter(id => !tagged.has(id))).toEqual([]);
+ });
+});
+
+describe('framework package mapping', () => {
+ // `detectFramework` tells readers they are on the wrong page. Guides sharing
+ // an SDK (`@sentry/node` backs seven of them) must all be listed, or a reader
+ // on the right page is sent away from it.
+ const guidesDir = path.join(process.cwd(), 'docs/platforms/javascript/guides');
+
+ const guidesBySdk = new Map();
+ for (const guide of readdirSync(guidesDir)) {
+ let source: string;
+ try {
+ source = readFileSync(path.join(guidesDir, guide, 'index.mdx'), 'utf8');
+ } catch {
+ continue; // Not a guide directory with a landing page.
+ }
+ const sdk = matter(source).data.sdk;
+ if (typeof sdk === 'string') {
+ guidesBySdk.set(sdk, [...(guidesBySdk.get(sdk) ?? []), guide]);
+ }
+ }
+
+ it.each(FRAMEWORK_PACKAGES.map(entry => [entry.pkg, entry] as const))(
+ 'lists every guide built on %s',
+ (pkg, entry) => {
+ const expected = guidesBySdk.get(
+ `sentry.javascript.${pkg.replace('@sentry/', '')}`
+ );
+ if (!expected) {
+ // `@sentry/browser` is the platform itself rather than a guide.
+ expect(entry.guides).toEqual(['javascript']);
+ return;
+ }
+ expect([...entry.guides].sort()).toEqual([...expected].sort());
+ }
+ );
+
+ it('names a primary guide that is one of the guides it lists', () => {
+ for (const {guides} of FRAMEWORK_PACKAGES) {
+ expect(guides.length).toBeGreaterThan(0);
+ }
+ });
+});
diff --git a/src/components/migrationGuide/styles.module.scss b/src/components/migrationGuide/styles.module.scss
new file mode 100644
index 0000000000000..ad206aa963de9
--- /dev/null
+++ b/src/components/migrationGuide/styles.module.scss
@@ -0,0 +1,425 @@
+// Colors use the Radix scale variables the rest of the docs rely on, so light
+// and dark mode both work without a parallel set of overrides.
+
+.guide {
+ margin: 1.5rem 0;
+}
+
+/* Filter panel */
+
+.filterPanel {
+ border: 1px solid var(--gray-5);
+ border-radius: 0.5rem;
+ background: var(--gray-2);
+ padding: 1rem 1.25rem 1.25rem;
+ margin-bottom: 1.5rem;
+}
+
+.filterHeader {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: 1rem;
+ flex-wrap: wrap;
+}
+
+.filterTitle {
+ margin: 0;
+ font-size: 1rem;
+ font-weight: 600;
+ color: var(--gray-12);
+}
+
+.filterActions {
+ display: flex;
+ gap: 0.75rem;
+
+ button {
+ background: none;
+ border: none;
+ padding: 0;
+ font-size: 0.8125rem;
+ color: var(--accent-11);
+ cursor: pointer;
+ text-decoration: underline;
+
+ &:hover {
+ color: var(--accent-12);
+ }
+ }
+}
+
+.filterOptions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ margin-top: 0.75rem;
+}
+
+.filterOption {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ cursor: pointer;
+}
+
+.filterHelp {
+ display: inline-flex;
+ align-items: center;
+ cursor: help;
+ outline: none;
+ color: var(--gray-10);
+}
+
+// Matches the onboarding option tooltips, which are portalled to the body and
+// so need their own colors rather than inheriting the panel's.
+.tooltipContent {
+ border-radius: 4px;
+ padding: 8px 12px;
+ max-width: 250px;
+ font-size: 12px;
+ line-height: 1.3;
+ text-align: center;
+ user-select: none;
+ pointer-events: none;
+ color: var(--gray-11);
+ background-color: white;
+ box-shadow: var(--shadow-6);
+ z-index: 9999;
+}
+
+.tooltipArrow {
+ fill: white;
+}
+
+:global(.dark) {
+ .tooltipContent {
+ color: var(--foreground);
+ background-color: var(--gray-4);
+ }
+
+ .tooltipArrow {
+ fill: var(--gray-4);
+ }
+}
+
+/* Paste-to-detect */
+
+.paste {
+ margin-top: 1rem;
+ border-top: 1px solid var(--gray-4);
+ padding-top: 0.75rem;
+}
+
+.pasteToggle {
+ background: none;
+ border: none;
+ padding: 0;
+ font-size: 0.8125rem;
+ color: var(--accent-11);
+ cursor: pointer;
+ text-align: left;
+
+ &:hover {
+ color: var(--accent-12);
+ }
+}
+
+.pasteBody {
+ margin-top: 0.625rem;
+}
+
+.pasteInput {
+ width: 100%;
+ border: 1px solid var(--gray-6);
+ border-radius: 0.375rem;
+ background: var(--gray-1);
+ color: var(--gray-12);
+ padding: 0.5rem 0.625rem;
+ font-family: var(--font-family-code, monospace);
+ font-size: 0.8125rem;
+ line-height: 1.5;
+ resize: vertical;
+
+ &:focus {
+ outline: 2px solid var(--accent-8);
+ outline-offset: -1px;
+ }
+}
+
+.pasteResult {
+ margin: 0.5rem 0 0;
+ font-size: 0.8125rem;
+ color: var(--gray-11);
+
+ strong {
+ color: var(--gray-12);
+ font-weight: 600;
+ }
+}
+
+.pasteEvidence {
+ font-family: var(--font-family-code, monospace);
+ font-size: 0.9em;
+ color: var(--gray-10);
+}
+
+.pasteWarning {
+ margin: 0.5rem 0 0;
+ font-size: 0.8125rem;
+ color: var(--gray-12);
+ border-left: 3px solid var(--accent-9);
+ padding-left: 0.625rem;
+}
+
+/* Summary + progress */
+
+.summary {
+ margin-bottom: 1rem;
+}
+
+.summaryHeadline {
+ margin: 0;
+ font-size: 1.0625rem;
+ color: var(--gray-12);
+
+ strong {
+ font-weight: 700;
+ }
+}
+
+.progress {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ margin-top: 0.625rem;
+}
+
+.progressTrack {
+ flex: 1;
+ height: 0.5rem;
+ border-radius: 999px;
+ background: var(--gray-4);
+ overflow: hidden;
+ max-width: 22rem;
+}
+
+.progressBar {
+ height: 100%;
+ border-radius: 999px;
+ background: var(--accent-9);
+ transition: width 200ms ease;
+}
+
+.progressLabel {
+ font-size: 0.8125rem;
+ color: var(--gray-11);
+ white-space: nowrap;
+}
+
+/* Toolbar */
+
+.toolbar {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ flex-wrap: wrap;
+ margin-bottom: 1.25rem;
+}
+
+.copyButton {
+ border: 1px solid var(--gray-6);
+ border-radius: 0.375rem;
+ background: var(--gray-1);
+ color: var(--gray-12);
+ font-size: 0.8125rem;
+ font-weight: 500;
+ padding: 0.375rem 0.75rem;
+ cursor: pointer;
+
+ &:hover {
+ background: var(--gray-3);
+ border-color: var(--gray-7);
+ }
+}
+
+.toolbarHint {
+ font-size: 0.8125rem;
+ color: var(--gray-11);
+}
+
+/* Hidden-items bar */
+
+.hiddenBar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ width: 100%;
+ border: 1px dashed var(--gray-7);
+ border-radius: 0.375rem;
+ background: none;
+ color: var(--gray-11);
+ font-size: 0.8125rem;
+ padding: 0.5rem 0.75rem;
+ margin-bottom: 1.5rem;
+ cursor: pointer;
+ text-align: left;
+
+ &:hover {
+ border-color: var(--gray-8);
+ color: var(--gray-12);
+ }
+}
+
+.hiddenBarAction {
+ color: var(--accent-11);
+ text-decoration: underline;
+ white-space: nowrap;
+}
+
+/* Phases */
+
+.phase {
+ margin-bottom: 2.5rem;
+}
+
+.phaseHeader {
+ display: flex;
+ align-items: baseline;
+ gap: 0.75rem;
+ border-bottom: 1px solid var(--gray-5);
+ padding-bottom: 0.375rem;
+}
+
+.phaseTitle {
+ margin: 0;
+ font-size: 1.25rem;
+ font-weight: 600;
+ color: var(--gray-12);
+}
+
+.phaseCount {
+ font-size: 0.8125rem;
+ color: var(--gray-11);
+ white-space: nowrap;
+}
+
+.phaseDescription {
+ margin: 0.375rem 0 1rem;
+ font-size: 0.875rem;
+ color: var(--gray-11);
+}
+
+/* Items */
+
+.item {
+ border-left: 3px solid var(--gray-6);
+ padding: 0.25rem 0 0.25rem 1rem;
+ margin-bottom: 1.75rem;
+}
+
+.severity-action-required {
+ border-left-color: var(--accent-9);
+}
+
+.severity-behavior-change {
+ border-left-color: var(--gray-7);
+}
+
+.severity-informational {
+ border-left-color: var(--gray-5);
+}
+
+.itemChecked {
+ opacity: 0.55;
+}
+
+.itemDimmed {
+ opacity: 0.45;
+}
+
+.itemHeader {
+ display: flex;
+ align-items: flex-start;
+ gap: 0.625rem;
+}
+
+.itemCheckbox {
+ display: flex;
+ align-items: center;
+ height: 1.75rem;
+
+ input {
+ cursor: pointer;
+ accent-color: var(--accent-9);
+ width: 1rem;
+ height: 1rem;
+ }
+}
+
+.itemHeading {
+ display: flex;
+ align-items: baseline;
+ gap: 0.5rem;
+ flex-wrap: wrap;
+ min-width: 0;
+}
+
+.itemTitle {
+ margin: 0;
+ font-size: 1.0625rem;
+ font-weight: 600;
+ color: var(--gray-12);
+
+ code {
+ font-size: 0.9375em;
+ }
+}
+
+.badge,
+.badgeMuted {
+ font-size: 0.6875rem;
+ font-weight: 500;
+ text-transform: uppercase;
+ letter-spacing: 0.03em;
+ border-radius: 999px;
+ padding: 0.0625rem 0.5rem;
+ white-space: nowrap;
+}
+
+.badge {
+ background: var(--gray-3);
+ color: var(--gray-11);
+}
+
+.severity-action-required .badge {
+ background: var(--accent-3);
+ color: var(--accent-11);
+}
+
+.badgeMuted {
+ background: none;
+ border: 1px solid var(--gray-6);
+ color: var(--gray-10);
+}
+
+.itemBody {
+ padding-left: 1.625rem;
+
+ // A paragraph that is only bold text is a group label inside a long item,
+ // such as the sections in the span attribute and span op tables. Set those
+ // apart from the inline bold in the surrounding prose.
+ > p > strong:only-child {
+ display: inline-block;
+ margin-top: 0.5rem;
+ font-size: 0.8125rem;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ color: var(--gray-11);
+ }
+
+ table {
+ font-size: 0.875rem;
+ }
+}
diff --git a/src/mdxComponents.ts b/src/mdxComponents.ts
index 20f3337d4ba25..e9ee1800117b2 100644
--- a/src/mdxComponents.ts
+++ b/src/mdxComponents.ts
@@ -29,6 +29,7 @@ import {LambdaLayerDetail} from './components/lambdaLayerDetail';
import {LinkCard, LinkCardGrid} from './components/linkCard';
import {LinkWithPlatformIcon} from './components/linkWithPlatformIcon';
import {LLMSteering} from './components/llmSteering';
+import {MigrationGuide} from './components/migrationGuide';
import {
OnboardingOption,
OnboardingOptionButtons,
@@ -108,6 +109,7 @@ export function mdxComponents(
Link: SmartLink,
LinkWithPlatformIcon,
LLMSteering,
+ MigrationGuide,
OrgAuthTokenNote,
PageGrid,
ParamTable,
diff --git a/src/metrics.ts b/src/metrics.ts
index cd77fc9a9bcd9..ba6c251f404f2 100644
--- a/src/metrics.ts
+++ b/src/metrics.ts
@@ -166,13 +166,13 @@ export const DocMetrics = {
* @param pathname - Page where the prompt was copied
* @param skill - Skill name if present (e.g., "sentry-nextjs-sdk")
* @param success - Whether the clipboard copy succeeded
- * @param source - Where the copy was triggered from ('callout' for the full banner, 'inline_link' for platform list buttons)
+ * @param source - Where the copy was triggered from ('callout' for the full banner, 'inline_link' for platform list buttons, 'migration_guide' for the interactive migration checklist)
*/
copyAIPrompt: (
pathname: string,
skill: string | undefined,
success: boolean,
- source: 'callout' | 'inline_link' | 'homepage_card' = 'callout'
+ source: 'callout' | 'inline_link' | 'homepage_card' | 'migration_guide' = 'callout'
) => {
Sentry.metrics.count('docs.copy_ai_prompt', 1, {
attributes: {