From 0f4146729a817acd898f68c58c7c9a466e38df68 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 18 Feb 2026 15:27:22 +0100 Subject: [PATCH] Add reproduction for sentry-javascript#19367 Reproduces: Next.js 16 + Turbopack duplicates @opentelemetry/api across server-side chunks, causing infinite .with() recursion and a fatal RangeError: Maximum call stack size exceeded. The check-otel-dedup script confirms 7 duplicate OTel chunks in the build output, matching the root cause described in the issue. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- sentry-javascript/19367/README.md | 97 +++++++++++++++++++ sentry-javascript/19367/app/api/test/route.ts | 18 ++++ sentry-javascript/19367/app/layout.tsx | 11 +++ sentry-javascript/19367/app/page.tsx | 20 ++++ sentry-javascript/19367/instrumentation.ts | 9 ++ sentry-javascript/19367/next.config.js | 17 ++++ sentry-javascript/19367/package.json | 24 +++++ .../19367/scripts/check-otel-dedup.js | 62 ++++++++++++ .../19367/sentry.client.config.ts | 6 ++ sentry-javascript/19367/sentry.edge.config.ts | 6 ++ .../19367/sentry.server.config.ts | 12 +++ sentry-javascript/19367/tsconfig.json | 41 ++++++++ 12 files changed, 323 insertions(+) create mode 100644 sentry-javascript/19367/README.md create mode 100644 sentry-javascript/19367/app/api/test/route.ts create mode 100644 sentry-javascript/19367/app/layout.tsx create mode 100644 sentry-javascript/19367/app/page.tsx create mode 100644 sentry-javascript/19367/instrumentation.ts create mode 100644 sentry-javascript/19367/next.config.js create mode 100644 sentry-javascript/19367/package.json create mode 100644 sentry-javascript/19367/scripts/check-otel-dedup.js create mode 100644 sentry-javascript/19367/sentry.client.config.ts create mode 100644 sentry-javascript/19367/sentry.edge.config.ts create mode 100644 sentry-javascript/19367/sentry.server.config.ts create mode 100644 sentry-javascript/19367/tsconfig.json diff --git a/sentry-javascript/19367/README.md b/sentry-javascript/19367/README.md new file mode 100644 index 0000000..d114d14 --- /dev/null +++ b/sentry-javascript/19367/README.md @@ -0,0 +1,97 @@ +# Reproduction for sentry-javascript#19367 + +**Issue:** https://github.com/getsentry/sentry-javascript/issues/19367 + +## Description + +Next.js 16 with Turbopack (the default bundler) splits `@opentelemetry/api` across +multiple server-side chunks instead of deduplicating it into a single module instance. +When two chunks each contain their own copy of the OTel `ContextAPI`, the `.with()` +method of each copy delegates to the _other_ copy's `.with()`, creating infinite mutual +recursion that fatally crashes the Node.js process with: + +``` +RangeError: Maximum call stack size exceeded +``` + +This reproduces with `@sentry/nextjs` 10.38.0 + Next.js 16.1.6 Turbopack and does **not** +reproduce on `@sentry/nextjs` 10.8.0. + +## Steps to Reproduce + +1. Install dependencies: + ```bash + npm install + ``` + +2. (Optional) Export your Sentry DSN – the app works without one, but events won't be sent: + ```bash + export SENTRY_DSN=https://your-key@oXXXXXX.ingest.sentry.io/XXXXXX + ``` + +3. Build with Turbopack (the default for Next.js 16): + ```bash + npm run build + ``` + +4. **Detect the duplicate OTel chunks immediately after the build:** + ```bash + npm run check-otel-dedup + ``` + Expected output shows `@opentelemetry/api` duplicated across 7 server-side chunks. + +5. Start the production server: + ```bash + npm start + ``` + +6. Send requests to trigger OTel context propagation: + ```bash + # Single request + curl http://localhost:3000/api/test + + # Load test – the crash is intermittent; sustained traffic triggers it + for i in $(seq 1 500); do curl -s http://localhost:3000/api/test > /dev/null; done + ``` + +The server may crash with `RangeError: Maximum call stack size exceeded` during or after +the load test. The crash is non-deterministic – it can happen within minutes or after +several hours of traffic (matching the original report). + +## Expected Behavior + +`@opentelemetry/api` is loaded as a single module instance. The `.with()` context method +works without recursion and the server remains stable. + +## Actual Behavior + +`npm run check-otel-dedup` reports: + +``` +✗ BUG DETECTED: @opentelemetry/api module definition found in 7 chunks: + - [root-of-the-server]__14b38a08._.js + - [root-of-the-server]__1a01c8dc._.js + - [root-of-the-server]__6126aa9f._.js + - [root-of-the-server]__ab5f2c12._.js + - [root-of-the-server]__da904e4a._.js + - [root-of-the-server]__f934a92d._.js + - node_modules_@opentelemetry_a01cbabd._.js +``` + +Under sustained traffic the server crashes: + +``` +RangeError: Maximum call stack size exceeded + at ContextAPI.with (.next/server/chunks/[root-of-the-server]__14b38a08._.js:...) + at ContextAPI.with (.next/server/chunks/node_modules_@opentelemetry_a01cbabd._.js:...) + at ContextAPI.with (.next/server/chunks/[root-of-the-server]__14b38a08._.js:...) + ... +``` + +## Environment + +- Node.js: v24.12.0 (also reproduces on v22) +- `@sentry/nextjs`: 10.38.0 +- `next`: 16.1.6 (Turbopack) +- `@prisma/instrumentation`: ^7.4.0 +- OS: Linux (Debian 12) / macOS (development) diff --git a/sentry-javascript/19367/app/api/test/route.ts b/sentry-javascript/19367/app/api/test/route.ts new file mode 100644 index 0000000..e761b18 --- /dev/null +++ b/sentry-javascript/19367/app/api/test/route.ts @@ -0,0 +1,18 @@ +import { NextResponse } from "next/server"; + +// This route triggers OTel context propagation on every request. +// With @sentry/nextjs 10.38.0 + Next.js 16 Turbopack, @opentelemetry/api ends up +// bundled in two separate chunks. Each chunk's ContextAPI.with() delegates to the +// other copy's with(), creating infinite mutual recursion → +// RangeError: Maximum call stack size exceeded +export async function GET() { + // Simulate a minimal workload so Sentry/OTel creates spans + const start = Date.now(); + await new Promise((resolve) => setTimeout(resolve, 1)); + + return NextResponse.json({ + status: "ok", + timestamp: Date.now(), + duration: Date.now() - start, + }); +} diff --git a/sentry-javascript/19367/app/layout.tsx b/sentry-javascript/19367/app/layout.tsx new file mode 100644 index 0000000..225b603 --- /dev/null +++ b/sentry-javascript/19367/app/layout.tsx @@ -0,0 +1,11 @@ +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/sentry-javascript/19367/app/page.tsx b/sentry-javascript/19367/app/page.tsx new file mode 100644 index 0000000..39018c4 --- /dev/null +++ b/sentry-javascript/19367/app/page.tsx @@ -0,0 +1,20 @@ +export default function Home() { + return ( +
+

Repro: sentry-javascript#19367

+

+ Next.js 16 + Turbopack duplicates @opentelemetry/api across + chunks, causing infinite .with() recursion. +

+

+ Hit the /api/test endpoint repeatedly (or under + load) to trigger OTel context propagation. The server may crash with{" "} + RangeError: Maximum call stack size exceeded. +

+

+ Run npm run check-otel-dedup after building to detect + duplicate @opentelemetry/api chunks in the output. +

+
+ ); +} diff --git a/sentry-javascript/19367/instrumentation.ts b/sentry-javascript/19367/instrumentation.ts new file mode 100644 index 0000000..f8a929b --- /dev/null +++ b/sentry-javascript/19367/instrumentation.ts @@ -0,0 +1,9 @@ +export async function register() { + if (process.env.NEXT_RUNTIME === "nodejs") { + await import("./sentry.server.config"); + } + + if (process.env.NEXT_RUNTIME === "edge") { + await import("./sentry.edge.config"); + } +} diff --git a/sentry-javascript/19367/next.config.js b/sentry-javascript/19367/next.config.js new file mode 100644 index 0000000..cd850cd --- /dev/null +++ b/sentry-javascript/19367/next.config.js @@ -0,0 +1,17 @@ +const { withSentryConfig } = require("@sentry/nextjs"); + +/** @type {import('next').NextConfig} */ +const nextConfig = { + // Turbopack is the default in Next.js 16, no extra config needed +}; + +module.exports = withSentryConfig(nextConfig, { + org: "your-org", + project: "your-project", + // Suppress build output noise for the repro + silent: true, + // Disable source map upload since we have no real DSN + sourcemaps: { + disable: true, + }, +}); diff --git a/sentry-javascript/19367/package.json b/sentry-javascript/19367/package.json new file mode 100644 index 0000000..cd1cbaf --- /dev/null +++ b/sentry-javascript/19367/package.json @@ -0,0 +1,24 @@ +{ + "name": "repro-sentry-javascript-19367", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "start": "next start", + "check-otel-dedup": "node scripts/check-otel-dedup.js" + }, + "dependencies": { + "@prisma/instrumentation": "^7.4.0", + "@sentry/nextjs": "10.38.0", + "next": "16.1.6", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "typescript": "^5" + } +} diff --git a/sentry-javascript/19367/scripts/check-otel-dedup.js b/sentry-javascript/19367/scripts/check-otel-dedup.js new file mode 100644 index 0000000..963d4c5 --- /dev/null +++ b/sentry-javascript/19367/scripts/check-otel-dedup.js @@ -0,0 +1,62 @@ +#!/usr/bin/env node +/** + * Checks the Next.js build output (.next/server/chunks/) for duplicate + * @opentelemetry/api module definitions, which is the root cause of the + * infinite .with() recursion described in sentry-javascript#19367. + * + * Run after `npm run build`: + * node scripts/check-otel-dedup.js + */ + +const fs = require("fs"); +const path = require("path"); + +const chunksDir = path.join(__dirname, "../.next/server/chunks"); + +if (!fs.existsSync(chunksDir)) { + console.error( + "ERROR: .next/server/chunks not found. Run `npm run build` first." + ); + process.exit(1); +} + +const files = fs.readdirSync(chunksDir).filter((f) => f.endsWith(".js")); + +const otelChunks = []; + +for (const file of files) { + const content = fs.readFileSync(path.join(chunksDir, file), "utf8"); + // @opentelemetry/api registers itself via a global symbol; look for the module definition + if ( + content.includes("@opentelemetry/api") && + (content.includes("ContextAPI") || + content.includes("context._currentContext") || + content.includes("Symbol.for(\"opentelemetry.js.api")) + ) { + otelChunks.push(file); + } +} + +console.log(`\nScanned ${files.length} server chunks in ${chunksDir}\n`); + +if (otelChunks.length === 0) { + console.log("✓ No @opentelemetry/api module definitions found (may be externalized)."); +} else if (otelChunks.length === 1) { + console.log( + `✓ @opentelemetry/api appears in exactly 1 chunk: ${otelChunks[0]}` + ); + console.log(" This is the expected (non-duplicated) state."); +} else { + console.error( + `✗ BUG DETECTED: @opentelemetry/api module definition found in ${otelChunks.length} chunks:` + ); + for (const f of otelChunks) { + console.error(` - ${f}`); + } + console.error( + "\n Two copies of @opentelemetry/api means their .with() methods will\n" + + " delegate to each other infinitely → RangeError: Maximum call stack\n" + + " size exceeded (sentry-javascript#19367)." + ); + process.exit(1); +} diff --git a/sentry-javascript/19367/sentry.client.config.ts b/sentry-javascript/19367/sentry.client.config.ts new file mode 100644 index 0000000..3e36b9b --- /dev/null +++ b/sentry-javascript/19367/sentry.client.config.ts @@ -0,0 +1,6 @@ +import * as Sentry from "@sentry/nextjs"; + +Sentry.init({ + dsn: process.env.SENTRY_DSN || "", + tracesSampleRate: 1, +}); diff --git a/sentry-javascript/19367/sentry.edge.config.ts b/sentry-javascript/19367/sentry.edge.config.ts new file mode 100644 index 0000000..3e36b9b --- /dev/null +++ b/sentry-javascript/19367/sentry.edge.config.ts @@ -0,0 +1,6 @@ +import * as Sentry from "@sentry/nextjs"; + +Sentry.init({ + dsn: process.env.SENTRY_DSN || "", + tracesSampleRate: 1, +}); diff --git a/sentry-javascript/19367/sentry.server.config.ts b/sentry-javascript/19367/sentry.server.config.ts new file mode 100644 index 0000000..f71354b --- /dev/null +++ b/sentry-javascript/19367/sentry.server.config.ts @@ -0,0 +1,12 @@ +import * as Sentry from "@sentry/nextjs"; + +Sentry.init({ + dsn: process.env.SENTRY_DSN || "", + tracesSampleRate: 1, + integrations: [ + // prismaIntegration triggers @prisma/instrumentation which registers OTel instrumentations. + // Combined with Turbopack's chunk splitting, this leads to two copies of @opentelemetry/api + // whose .with() methods recursively call each other → RangeError: Maximum call stack size exceeded. + Sentry.prismaIntegration(), + ], +}); diff --git a/sentry-javascript/19367/tsconfig.json b/sentry-javascript/19367/tsconfig.json new file mode 100644 index 0000000..e7ff3a2 --- /dev/null +++ b/sentry-javascript/19367/tsconfig.json @@ -0,0 +1,41 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +}