getsentry / getsentry/sentry-javascript

@sentry/nextjs: "node" export condition is a bare CJS path with no import/require split, so ESM consumers get a partial namespace

Offen
#22,791 2 Kommentare 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen
Bug Next.js Node.js
Vorherrschende Sprache
TypeScript
Sterne
8.7k
Forks
1.8k
Ø Merge
1 T. 17 Std.
Gemergte PRs (30 T.)
515

Beschreibung

**Package:** `@sentry/nextjs@10.29.0`
**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:

```jsonc
"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:

```bash
$ 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 undefined
```

### Impact

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:

```jsonc
"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

getsentry/sentry-javascript#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.

Beitragsleitfaden

Beitragsleitfaden öffnen

Rechercherichtung

Start by locating the @sentry/nextjs package export manifest and inspect the node and import conditions shown in the issue. Run the provided node --input-type=module reproductions to confirm the current resolution and namespace behavior. Done means Node ESM resolves the ESM server build while CommonJS require continues resolving the CJS build, with coverage for both paths if the package has manifest tests.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Bewertung

Tech-Stack
nextjs, nodejs, typescript
Bereich
developer-experience, tooling
Issue-Typ
Bug
Schwierigkeit
3/5
Geschätzter Aufwand
1-2 Tage
Aktivitätsstatus
Ruhig
Klarheit
Klar beschrieben
Anfängerfreundlichkeit
72/100

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.