Skip to content

@sentry/server-utils ships the orchestrion bundler plugins (and a full JS parser) as production dependencies #23611

Description

@julianmesa-gitkraken

Is there an existing issue for this?

How do you use Sentry?

Sentry Saas (sentry.io)

Which SDK are you using?

@sentry/electron (which depends on @sentry/node)

SDK Version

@sentry/electron 7.16.0, @sentry/node 10.67.0, @sentry/server-utils 10.67.0 (also verified against 10.71.0, see below)

Framework Version

Electron 43.3.0, Node 24.15.0, packaged with electron-builder

Link to Sentry event

N/A — this is a packaging/bundle-size issue, not a runtime error.

Reproduction Example/SDK Setup

Any app that depends on @sentry/node (directly or via @sentry/electron) and is packaged by a tool that walks the production dependencies tree rather than the call graph — electron-builder, npm install --omit=dev in a Docker image, a serverless bundle, etc.

import * as Sentry from '@sentry/electron/main';
Sentry.init({ dsn: '...' });

Steps to Reproduce

  1. Depend on @sentry/node (or @sentry/electron, or anything else that pulls in @sentry/server-utils).
  2. Install production dependencies only, or package the app with electron-builder.
  3. Inspect the resulting node_modules tree.

Expected Result

The production dependency tree contains runtime code only. Build-time bundler plugins and the JavaScript parser / AST toolchain they need are not part of it.

Actual Result

@sentry/server-utils declares its bundler plugins as production dependencies:

// @sentry/server-utils@10.67.0
"dependencies": {
  "@apm-js-collab/code-transformer-bundler-plugins": "^0.7.1",
  "@apm-js-collab/tracing-hooks": "^0.13.0",
  "@sentry/conventions": "^0.16.0",
  "@sentry/core": "10.67.0"
},
"devDependencies": { "@types/node": "...", "vite": "..." }

There is no peerDependenciesMeta.optional and no optionalDependencies, so every consumer installs the full transitive closure of those two packages:

@apm-js-collab/code-transformer-bundler-plugins  ->  @apm-js-collab/code-transformer, es-module-lexer,
                                                     magic-string, module-details-from-path
@apm-js-collab/code-transformer                  ->  meriyah, esquery, astring, estraverse,
                                                     source-map, semifies, @types/estree

That is a complete JavaScript parser (meriyah), an AST query engine (esquery), a code generator (astring) and a source-map toolchain, shipped to production consumers. Installed sizes in our tree:

package installed
meriyah 1472 KB
esquery 1104 KB
source-map 824 KB
@apm-js-collab/code-transformer-bundler-plugins 668 KB
magic-string 476 KB
astring 292 KB
es-module-lexer 192 KB
@jridgewell/sourcemap-codec 164 KB
@apm-js-collab/code-transformer 104 KB
estraverse 52 KB
@apm-js-collab/tracing-hooks 48 KB
semifies 24 KB

These are build-time consumers only. Every reference to the parser comes from the bundler plugin entrypoints:

  • @apm-js-collab/code-transformer/lib/{transformer,transforms}.js requires meriyah, esquery, astring, source-map
  • build/cjs/orchestrion/bundler/{vite,rollup,webpack,esbuild}.js are the only files in @sentry/server-utils that require @apm-js-collab/code-transformer-bundler-plugins
  • build/cjs/index.js (the main entry) never reaches any of them

We consume @sentry/electron/main, @sentry/electron/renderer and @sentry/node, and never import an orchestrion/* subpath or call experimentalUseDiagnosticsChannelInjection. Confirmed empirically: patching Module._resolveFilename to make all 11 packages unresolvable, then loading and initializing the SDK (Sentry.init + captureException) in both plain Node and a real Electron main process, produces zero requests for any of them. Removing them from a packaged Electron app entirely (so they are physically absent from disk) and then requiring @sentry/node, @sentry/server-utils and @sentry/node-core from inside that tree also works: init and capture succeed.

In our case this is 2.26 MiB of a packaged Electron app.asar (measured: 48,171,148 → 45,802,904 bytes) that no code path can reach.

Why consumers cannot cleanly fix this themselves

Excluding the packages at packaging time works today but is fragile, because @sentry/node's main entry eagerly loads the door into this subsystem:

// @sentry/node/build/cjs/index.js
require('./sdk/experimentalUseDiagnosticsChannelInjection.js');

// that module, at top level:
const orchestrion = require('@sentry/server-utils/orchestrion');
const register    = require('@sentry/server-utils/orchestrion/register');

The parser stays unreachable only because orchestrion/runtime/register.js resolves tracing-hooks lazily, inside registerDiagnosticsChannelInjection(), which runs only when a consumer opts in. So a consumer-side exclusion is one dynamic require away from becoming a production startup crash — and one that no typecheck, test suite or successful build would catch, since it only manifests in the packaged artifact.

That risk is not hypothetical for a subsystem still marked experimental: 10.71.0 added meriyah as a direct dependency of @sentry/server-utils for a new orchestrion/bundler/subscribeInjection.js. Still unreachable from the main entry (I checked), but it shows the tree moves between patch releases, so consumers would have to re-verify reachability on every bump.

Suggested fix

Move @apm-js-collab/code-transformer-bundler-plugins (and meriyah in 10.71.0+) out of dependencies — either to devDependencies if the bundler entrypoints are expected to be used from a dev-time context that already has them, or to peerDependenciesMeta.optional so consumers who do use a bundler plugin install it explicitly while runtime-only consumers pay nothing.

@apm-js-collab/tracing-hooks is the more interesting one: it is genuinely runtime, but only for consumers who enable the diagnostics-channel injection, and it drags in the same parser through @apm-js-collab/code-transformer. Making it optional too (the register.js code path already resolves it lazily and could fail gracefully) would let runtime-only consumers avoid the whole AST toolchain.

Related

#22794 looks like the same root cause with a worse symptom: there a build-time bundler plugin is reachable from a runtime entry, and its inlined WASM makes WebAssembly.compile() run at module evaluation, crashing on every Cloudflare Workers cold start. Our report is the benign variant of the same shape — not reachable, just shipped. Fixing the dependency classification would address both classes.

Metadata

Metadata

Assignees

Projects

Status
No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions