Repository navigation
@sentry/nextjs: "node" export condition is a bare CJS path with no import/require split, so ESM consumers get a partial namespace #22791
Description
Package: @sentry/[email protected]
Node: v22.18.0
Summary
exports["."]["node"] is a bare string pointing at the CJS build. Every other platform condition in the same map splits import/require; node does not. Because node is active for any Node process and is declared before import, any ESM consumer under a plain-Node loader resolves build/cjs/index.server.js rather than build/esm/index.server.js — and cjs-module-lexer cannot see the bindings that arrive via export * from @sentry/core / @sentry/node, so the resulting namespace is partial.
The manifest
Read directly from the installed package:
"exports": {
".": {
"types": "./build/types/index.types.d.ts",
"edge": { "import": "./build/esm/edge/index.js", "require": "./build/cjs/edge/index.js" },
"edge-light": { "import": "./build/esm/edge/index.js", "require": "./build/cjs/edge/index.js" },
"worker": { "import": "./build/esm/edge/index.js", "require": "./build/cjs/edge/index.js" },
"workerd": { "import": "./build/esm/edge/index.js", "require": "./build/cjs/edge/index.js" },
"browser": { "import": "./build/esm/index.client.js", "require": "./build/cjs/index.client.js" },
"node": "./build/cjs/index.server.js", // <-- bare string, CJS only
"import": "./build/esm/index.server.js" // <-- unreachable for Node consumers
}
}browser and all four edge/worker conditions split import/require. node is the only platform condition that doesn't — which makes the "import" entry below it effectively dead code for Node, since node always matches first.
Reproduction
From any project with the package installed — no other setup:
$ node --input-type=module -e "console.log(import.meta.resolve('@sentry/nextjs'))"
file:///.../@sentry/nextjs/build/cjs/index.server.js # CJS, not ESM
$ node --input-type=module -e "import { addBreadcrumb } from '@sentry/nextjs'"
SyntaxError: Named export 'addBreadcrumb' not found. The requested module
'@sentry/nextjs' is a CommonJS module, which may not support all
module.exports as named exports.
$ node --input-type=module -e "import * as S from '@sentry/nextjs'; console.log(typeof S.init, typeof S.captureException, typeof S.isEnabled)"
function undefined undefinedImpact
Named imports fail loudly at link time, which is the safe case.
Namespace imports are the dangerous one. They link cleanly and the missing bindings are undefined, so the failure surfaces as TypeError: X is not a function at call time, arbitrarily far from the import. The detected/undetected split follows nothing a consumer can predict:
| present | undefined |
|---|---|
init, startSpan, captureRequestError, withSentryConfig |
captureException, captureMessage, setTag, setContext, addBreadcrumb, isEnabled, isInitialized, consoleLoggingIntegration |
captureException is a particularly unhappy landing spot, since it is usually called from inside a catch block — so the resulting TypeError is frequently swallowed by the very error handler that invoked it, and the error is silently never reported.
This does not affect apps built by Next itself (webpack/turbopack prefer the import condition), nor Vite-based loaders. It affects plain-Node ESM tooling: scripts, CLIs, health-check probes and test harnesses that import the SDK outside the bundler.
A default import (import S from '@sentry/nextjs') recovers the full server-build surface, because it reads the runtime module.exports object rather than the lexer's static guess. That is a viable workaround, but consumers have to know to apply it, and it is easily "tidied" back into a namespace import by a later reader.
Suggested fix
Give node the same import/require split its sibling conditions already have:
"node": {
"import": "./build/esm/index.server.js",
"require": "./build/cjs/index.server.js"
}Backwards-compatible for CJS consumers (require still resolves the CJS build), and it makes the existing "import" entry reachable for ESM ones.
Possibly related
#20038 — workerd/worker conditions resolving to @sentry/node, breaking Cloudflare Workers. Same class (an export condition resolving to the wrong build), different condition.
If this is intentional
If node deliberately pins CJS — for instance because OpenTelemetry instrumentation patching relies on require semantics — that would be very useful to have stated, since it makes the default-import workaround permanent rather than transitional. Given browser does split, the asymmetry currently reads as an oversight rather than a decision.
Metadata
Metadata
Assignees
Projects
- StatusShow more project fieldsNo status
Activity
linear-code commentedon Jul 28, 2026
JS-3215
s1gr1d commentedon Jul 29, 2026
Hello and thanks for writing in!
I just want to point out this part, because it's important:
Right now, we cannot safely split this in the
package.jsonbecause the code is not ESM-safe (it's using__dirname).We could however make sure that the full namespace is exported (it would still be CJS, but ESM-compatible).
ref(nextjs)!: Move `withSentryConfig` to `@sentry/nextjs/config`
ref(nextjs)!: Move `withSentryConfig` to `@sentry/nextjs/config`
ref(nextjs)!: Move `withSentryConfig` to `@sentry/nextjs/config`
ref(nextjs)!: Move `withSentryConfig` to `@sentry/nextjs/config` (#23628