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

No one assigned

    Projects

    Status
    Waiting for: Product Owner

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions