Skip to content

fix(server-utils): Keep orchestrion instrumentation working in bundled servers - #23706

Closed
logaretm wants to merge 14 commits into
fix/orchestrion-detect-bundlingfrom
awad/orchestrion-bundling-survives
Closed

fix(server-utils): Keep orchestrion instrumentation working in bundled servers#23706
logaretm wants to merge 14 commits into
fix/orchestrion-detect-bundlingfrom
awad/orchestrion-bundling-survives

Conversation

@logaretm

@logaretm logaretm commented Aug 27, 2026

Copy link
Copy Markdown
Member

Makes orchestrion's runtime channel injection survive a downstream bundler inlining @sentry/server-utils, so a bundled Node server records auto-instrumented spans instead of silently recording none. Stacks on #23675, whose probe and warning stay on as the fallback.

Three things had to change. meriyah and astring are reached through require(), which resolves to their CJS builds, and @rollup/plugin-commonjs splits those into an empty object plus a bare side-effect import that tree-shakers delete; both also publish ESM builds, so resolving them without the require condition avoids the shape entirely and drops a duplicate meriyah copy the build was shipping twice. source-map has no ESM build, so it is pinned with a sideEffects allowlist, which has to land in the generated build/esm/package.json rather than the package's own to have any effect at all.

Separately, webpack-bundled apps installed no hook even with the transformer intact, because node:module exports a function and webpack's synthetic namespace for it carries only default.

Verified against the repro from #23664 on esbuild, and the node-orchestrion-webpack e2e now runs its bundle and asserts spans arrive instead of grepping for a string. On Node 24.13+ a bundle works with no @sentry/* on disk; below that #23675's warning still applies.

Refs #23664

mydea and others added 13 commits August 27, 2026 13:11
`@sentry/node`'s `init()` installs a runtime module-transform hook from
`@sentry/server-utils/orchestrion/register`, which drives a vendored code
transformer (meriyah/astring/source-map) and is designed to run from
`node_modules`. If a downstream bundler inlines and tree-shakes
`@sentry/server-utils`, that transformer is stripped to empty objects, so at
runtime `parse`/`generate` are `undefined` and the first module the hook tries
to transform throws `TypeError: parse is not a function` — deep in the loader,
once per module, and only when `debug: true` (otherwise it fails silently).

Detect this once, up front: run a throwaway in-memory transform over a synthetic
snippet before installing any hook. A healthy build returns normally; a
tree-shaken one throws a `TypeError`. On detection, emit a single, always-on,
actionable warning (via `consoleSandbox`, deduped on a global marker) and skip
installing hooks that can't work, instead of letting the cryptic per-module
error surface. The existing registration `catch` is likewise upgraded to an
always-on warning.

All of this lives inside `registerDiagnosticsChannelInjection`, so it tree-shakes
away with the whole block when `bundleSizeOptimizations.excludeChannelInjection`
sets `__SENTRY_CHANNEL_INJECTION__` to `false`.

Ref #23664

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n plugin

The runtime hook (reached via `@sentry/node`) must stay external so it resolves
from `node_modules`; bundling it strips the transformer and breaks the
`Module.register` self-reference. `@sentry/node` is a different package from the
`@sentry/server-utils` barrel the plugin force-bundles (`ssr.noExternal`), so the
vite plugin now also adds `@sentry/node` to `ssr.external`. Explicit `ssr.external`
entries win over `noExternal`, so this holds even against a preset that sets
`ssr.noExternal: true` — verified with a real vite SSR build.

This covers the vite-based frameworks (SvelteKit, Astro, React Router, TanStack);
the nitro/rollup frameworks (Nuxt, SolidStart) rely on the runtime warning above,
with a nitro-level externalization guard as a follow-up.

Ref #23664

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a "Bundling your server" note to the Node README (and a Nuxt troubleshoot
note) explaining that the runtime instrumentation hook must stay external, and
pointing to the build-time bundler-plugin instrumentation as the alternative.

Ref #23664

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Forcing `@sentry/node` into `ssr.external` broke Cloudflare/worker builds: the
shared vite orchestrion plugin also runs under `@cloudflare/vite-plugin` (and
frameworks deploying to workerd), where `@sentry/node` is unused and setting
`resolve.external` on a worker environment is rejected outright — and the worker
environment is even named `ssr`, so there's no reliable node-vs-worker
discriminator in the `config()` hook. Vite already externalizes `@sentry/node`
for node SSR by default anyway, and the runtime probe in `orchestrion/register`
covers the cases where it does get bundled, so drop the forced externalization.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… a bundled hook

If `@sentry/server-utils` was bundled AND the build-time bundler plugin ran (a
defined `__SENTRY_ORCHESTRION__.bundler` Set), instrumentation is already injected
at build time and the runtime hook is redundant — a supported setup. In that case
downgrade the "bundled" message to a debug log instead of an always-on warning.
The always-on warning now fires only when nothing instrumented the app (bundled
and no build-time plugin). Also corrects the Node README: bundling doesn't
disable auto-instrumentation when the build-time plugin is used.

Ref #23664

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…builds

The code transformer reaches both through `require()`, so node-resolve picked
their CJS builds and `@rollup/plugin-commonjs` emitted each as an empty
`_virtual/<dep>.js` proxy populated from a separate module through a bare
side-effect import. Downstream bundlers delete that import, leaving `parse` and
`generate` undefined at runtime.

Re-resolving the specifier without the `require` condition picks each package's
ESM build instead, which the transformer then binds by value. It also drops the
duplicate meriyah copy the build was shipping, ~324 kB per format.
…n away

source-map 0.6.1 ships no ESM build, so it keeps the empty-proxy shape that a
downstream bundler strips. A `sideEffects` allowlist pins it, but the value that
actually governs the ESM build is the generated `build/esm/package.json`, which
is nearer to those files than the package's own. Copying the root list verbatim
would leave every glob pointing at a path that cannot match, so re-anchor the
entries that belong to the output directory and drop the rest.
webpack compiles a `node:module` external to `createRequire(...)('node:module')`
and wraps the result in a synthetic namespace. Because `node:module` exports a
function, that wrapper carries only `default`, so `registerHooks` and `register`
both read as undefined and a webpack-bundled SDK gave up with "no available Node
API" before installing any hook.

Node's own ESM namespace exposes the same object under `default`, and the CJS
build has no `default` at all, so preferring it covers every shape.
…undle

Re-bundles the built `orchestrion/register` entry the way a downstream bundler
does, honouring `sideEffects`, then runs the result in a child process and
asserts it installs hooks rather than reporting the transformer unavailable.
The app only grepped for a config string, which survives tree-shaking even when
the code transformer has been stripped to an empty object. It now keeps graphql
external, runs the bundle, and asserts channel-based graphql spans arrive.
Keeping the transformer alive costs ~53 kB gzip on every bundled Node entry, so
the three affected size limits go up. The bundling notes lose the "keep it
external" instruction, which no longer applies, and keep only the two cases that
still need a choice.
@github-actions

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 28.57 kB added added
@sentry/browser - with treeshaking flags 26.92 kB added added
@sentry/browser - with treeshaking flags tracing without tracing 26.82 kB added added
@sentry/browser (incl. Tracing) 48.63 kB added added
@sentry/browser (incl. Tracing + Span Streaming) 48.65 kB added added
@sentry/browser (incl. Tracing, Profiling) 51.56 kB added added
@sentry/browser (incl. Tracing, Replay) 88.11 kB added added
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 77.51 kB added added
@sentry/browser (incl. Tracing, Replay with Canvas) 92.82 kB added added
@sentry/browser (incl. Tracing, Replay, Feedback) 105.51 kB added added
@sentry/browser (incl. Feedback) 45.79 kB added added
@sentry/browser (incl. sendFeedback) 33.35 kB added added
@sentry/browser (incl. FeedbackAsync) 38.46 kB added added
@sentry/browser (incl. Metrics) 29.51 kB added added
@sentry/browser (incl. Logs) 29.8 kB added added
@sentry/browser (incl. Metrics & Logs) 30.43 kB added added
@sentry/react 30.31 kB added added
@sentry/react (incl. Tracing) 50.84 kB added added
@sentry/vue 35.69 kB added added
@sentry/vue (incl. Tracing) 50.88 kB added added
@sentry/svelte 28.59 kB added added
CDN Bundle 30.36 kB added added
CDN Bundle (incl. Tracing) 49.12 kB added added
CDN Bundle (incl. Logs, Metrics) 32.56 kB added added
CDN Bundle (incl. Tracing, Logs, Metrics) 51.01 kB added added
CDN Bundle (incl. Replay, Logs, Metrics) 73 kB added added
CDN Bundle (incl. Tracing, Replay) 86.62 kB added added
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 88.52 kB added added
CDN Bundle (incl. Tracing, Replay, Feedback) 92.4 kB added added
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 94.33 kB added added
CDN Bundle - uncompressed 89.97 kB added added
CDN Bundle (incl. Tracing) - uncompressed 146.82 kB added added
CDN Bundle (incl. Logs, Metrics) - uncompressed 96.26 kB added added
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 152.51 kB added added
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 225.42 kB added added
CDN Bundle (incl. Tracing, Replay) - uncompressed 266.32 kB added added
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 272 kB added added
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 280.01 kB added added
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 285.68 kB added added
@sentry/nextjs (client) 53.41 kB added added
@sentry/sveltekit (client) 49.08 kB added added
@sentry/core/server 65.3 kB added added
@sentry/core/browser 52.37 kB added added
@sentry/node 176.2 kB added added
@sentry/node/import (ESM hook with diagnostics-channel injection) 81.83 kB added added
@sentry/node - without tracing 141.39 kB added added
@sentry/node - without channel injection 102.04 kB added added
@sentry/aws-serverless 149.57 kB added added
@sentry/cloudflare (withSentry) - minified 199.57 kB added added
@sentry/cloudflare (withSentry) 495.71 kB added added

The legacy registration path resolves against a path baked in at build time, so
it also fails when a webpack bundle runs on a machine other than the one it was
built on, not only when `node_modules` is absent.
@mydea
mydea force-pushed the fix/orchestrion-detect-bundling branch from 1bb710e to da0de0d Compare August 28, 2026 09:26
@logaretm logaretm closed this Aug 28, 2026
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.

2 participants