Skip to content

fix(node-core): Read __SENTRY_SERVER_MODULES__ lazily so Turbopack injection is honored#21339

Open
sergical wants to merge 3 commits into
developfrom
fix/node-core-turbopack-server-modules
Open

fix(node-core): Read __SENTRY_SERVER_MODULES__ lazily so Turbopack injection is honored#21339
sergical wants to merge 3 commits into
developfrom
fix/node-core-turbopack-server-modules

Conversation

@sergical
Copy link
Copy Markdown
Member

@sergical sergical commented Jun 4, 2026

Summary

Fixes the root cause behind #19147. On Next.js 16 / Turbopack production builds (e.g. Vercel), modulesIntegration returns no injected modules, which silently disables every module-detection-based auto integration — vercelAIIntegration, openAIIntegration, anthropicAIIntegration, googleGenAIIntegration, langChainIntegration, langGraphIntegration — and leaves event.modules missing server dependencies. The result users see: raw ai.* spans (op: default) instead of gen_ai.*.

Root cause

packages/node-core/src/integrations/modules.ts captured the injected value into a module-level const at evaluation time:

const SERVER_MODULES = typeof __SENTRY_SERVER_MODULES__ === 'undefined' ? {} : __SENTRY_SERVER_MODULES__;

The two bundlers inject __SENTRY_SERVER_MODULES__ differently:

The catch: the instrumentation.* file's ESM imports are hoisted above the injected assignment. Verified in a real Turbopack build (.next/server/chunks/[root-of-the-server]__*.js):

769449, e=>{ "use strict";
  var r = e.i(298962);                                  // import @sentry/* — evaluates modules.ts (SERVER_MODULES captured = {})
  async function s(){ await e.A(145684) }               // register()
  globalThis.__SENTRY_SERVER_MODULES__ = {/* …deps… */} // injection runs AFTER the import
}

So @sentry/node-core/modules evaluates before the global is assigned, and the const is frozen as {}. The other two sources in collectModules() also come up empty on a bundled server (no full-dependency package.json at process.cwd(); ai is bundled so it's not in require.cache, and the server is ESM not CJS). Net: getModules().ai is undefinedshouldForceIntegration returns falseaddVercelAiProcessors never attaches.

Why #19231 didn't catch it

#19231 was unit-tested at the config-generation layer (asserting the value-injection rule is emitted). The nextjs-16 AI E2E that asserts gen_ai.* spans passes for the wrong reason — it runs next start locally, where getModulesFromPackageJson() reads process.cwd()/package.json (present, lists ai) and masks the broken SERVER_MODULES path. On Vercel that fallback is empty, so detection fails.

Fix

Read the value lazily (per call) instead of capturing it at module-eval time, and support both injection styles:

function getServerModules(): Record<string, string> {
  if (typeof __SENTRY_SERVER_MODULES__ !== 'undefined') return __SENTRY_SERVER_MODULES__; // webpack
  return (GLOBAL_OBJ as ...).__SENTRY_SERVER_MODULES__ ?? {};                              // turbopack
}

By the time getModules() is first called (during integration afterAllSetup, i.e. after register()Sentry.init()), the instrumentation module body has fully executed and the global is set. webpack is unaffected (token still replaced).

Regression test

packages/node-core/test/integrations/modules.test.ts re-imports the module with no global set (mirroring Turbopack), then assigns globalThis.__SENTRY_SERVER_MODULES__ after import and asserts getModules() reflects it. This fails on the previous code and passes with the fix.

Blast radius

Low. webpack path unchanged; Turbopack now honored; event.modules restored on Turbopack. Re-enables all module-detection-based auto integrations on Next.js 16 without requiring vercelAIIntegration({ force: true }).

Follow-up (separate)

The existing nextjs-16 AI E2E should be hardened so it can't pass via the process.cwd() package.json fallback — e.g. a --turbopack build variant run from a working directory whose package.json does not list the AI SDK, asserting gen_ai.* spans still appear. Happy to do this in a follow-up.

🤖 Generated with Claude Code

…jection is honored

`modulesIntegration` captured `__SENTRY_SERVER_MODULES__` into a module-level
`const` at evaluation time. That works for webpack (DefinePlugin replaces the
token with a literal at build time) but not for Turbopack: the value-injection
loader assigns `globalThis.__SENTRY_SERVER_MODULES__` at runtime, and the
instrumentation file's ESM imports are hoisted above that assignment — so this
module evaluates before the global is set and the capture is always empty.

As a result, on Next.js 16 / Turbopack production builds (e.g. Vercel) every
module-detection-based auto integration silently never activates
(vercelAI, openAI, anthropic, googleGenAI, langChain, langGraph), and
`event.modules` is missing server dependencies.

Read the value lazily instead: prefer the build-time-replaced token (webpack),
then fall back to `GLOBAL_OBJ.__SENTRY_SERVER_MODULES__` (Turbopack). Adds a
regression test that fails when the value is captured at module-eval time.

Ref: #19147

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sergical sergical requested a review from a team as a code owner June 4, 2026 19:18
@sergical sergical requested review from JPeer264 and andreiborza and removed request for a team June 4, 2026 19:18
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant