Skip to content

fix(node): Detect + warn when the orchestrion runtime hook is bundled - #23675

Open
mydea wants to merge 11 commits into
developfrom
fix/orchestrion-detect-bundling
Open

fix(node): Detect + warn when the orchestrion runtime hook is bundled#23675
mydea wants to merge 11 commits into
developfrom
fix/orchestrion-detect-bundling

Conversation

@mydea

@mydea mydea commented Aug 27, 2026

Copy link
Copy Markdown
Member

@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 (external). Issue #23664 reported that when a downstream bundler inlines and tree-shakes @sentry/server-utils, that transformer is stripped and instrumentation breaks.

Root cause & why this approach

The runtime hook is broken two ways when bundled: (1) tree-shaking empties meriyah/astring/source-map, and (2) Module.register('@sentry/server-utils/orchestrion/hook', { parentURL: <own __filename> }) is a self-reference that only resolves from the real on-disk path. So bundling the hook is fundamentally unsupported — the fix is to keep it external (or instrument at build time), not to make the bundled copy work. The default node --import setup and well-configured frameworks (Next.js already guards this) are unaffected. Today it also fails silently (debug.warn is gated behind debug: true).

An earlier attempt (draft PR #23668) instead co-located the vendored chunk so the bundled transformer survived tree-shaking — but that added ~61 kB to every bundle that inlines @sentry/server-utils, only partly fixed the problem, and optimised a discouraged config. That PR is closed in favour of this one.

What this does

  • Runtime detection (register.ts): a cheap in-memory probe runs the transformer over a synthetic snippet before installing any hook. When it detects the transformer was tree-shaken:
    • if the build-time bundler plugin ran (a defined __SENTRY_ORCHESTRION__.bundler marker) → instrumentation is already injected at build time, so the runtime hook is redundant and we stay quiet (debug log only) — a supported setup;
    • otherwise → one actionable, deduped, always-on warning (with a docs link) and an early return, instead of the cryptic per-module TypeError.
      Verified end-to-end against tree-shaken esbuild bundles (warns without build-time, silent with it).
  • Tree-shakes with the opt-out: the whole probe lives inside registerDiagnosticsChannelInjection, so bundleSizeOptimizations.excludeChannelInjection (__SENTRY_CHANNEL_INJECTION__ = false) strips it and the transformer entirely — verified.
  • Docs: a "Bundling your server" note in the Node README (keep it external or use the build-time bundler plugins) and a Nuxt troubleshoot note.

Not included: forcing @sentry/node into vite ssr.external was explored but dropped — the shared vite plugin also runs under @cloudflare/vite-plugin, which forbids resolve.external in worker environments (where @sentry/node is unused), and there's no reliable node-vs-worker discriminator in the config() hook. Vite already externalizes @sentry/node for node SSR by default, and the runtime detection covers the rest.

No size-limit impact: @sentry/node stays at 119.76 kB (limit 123 kB); no +61 kB.

Follow-ups (not in this PR)

  • A new e2e that bundles a Node server and asserts channel telemetry actually flows (the existing node-orchestrion-webpack app only greps a config string that survives tree-shaking).
  • A nitro-level externalization guard for Nuxt/SolidStart (rollup-via-nitro), covered by the runtime warning here in the meantime.

Fixes #23664

🤖 Generated with Claude Code

Comment thread packages/server-utils/src/orchestrion/bundler/vite.ts Outdated
Comment thread packages/server-utils/src/orchestrion/bundler/vite.ts Outdated
@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

⚠️ Warning: Base artifact is not the latest one, because the latest workflow run is not done yet. This may lead to incorrect results. Try to re-run all tests to get up to date results.

Path Size % Change Change
@sentry/browser 28.56 kB - -
@sentry/browser - with treeshaking flags 26.92 kB - -
@sentry/browser - with treeshaking flags tracing without tracing 26.82 kB - -
@sentry/browser (incl. Tracing) 48.63 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 48.64 kB - -
@sentry/browser (incl. Tracing, Profiling) 51.55 kB - -
@sentry/browser (incl. Tracing, Replay) 88.09 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 77.52 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 92.79 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 105.74 kB - -
@sentry/browser (incl. Feedback) 46.05 kB - -
@sentry/browser (incl. sendFeedback) 33.63 kB - -
@sentry/browser (incl. FeedbackAsync) 38.73 kB - -
@sentry/browser (incl. Metrics) 29.51 kB - -
@sentry/browser (incl. Logs) 29.8 kB - -
@sentry/browser (incl. Metrics & Logs) 30.43 kB - -
@sentry/react 30.3 kB - -
@sentry/react (incl. Tracing) 50.83 kB - -
@sentry/vue 35.69 kB - -
@sentry/vue (incl. Tracing) 50.86 kB - -
@sentry/svelte 28.59 kB - -
CDN Bundle 30.35 kB - -
CDN Bundle (incl. Tracing) 49.24 kB - -
CDN Bundle (incl. Logs, Metrics) 32.57 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 51.12 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 73.17 kB - -
CDN Bundle (incl. Tracing, Replay) 86.72 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 88.61 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 92.62 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 94.51 kB - -
CDN Bundle - uncompressed 89.95 kB - -
CDN Bundle (incl. Tracing) - uncompressed 146.82 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 96.24 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 152.51 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 225.41 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 266.32 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 272 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 280.02 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 285.69 kB - -
@sentry/nextjs (client) 53.41 kB - -
@sentry/sveltekit (client) 49.07 kB - -
@sentry/core/server 65.15 kB - -
@sentry/core/browser 51.88 kB - -
@sentry/node 123.08 kB +0.29% +355 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 85.64 kB +0.48% +406 B 🔺
@sentry/node - without tracing 88.15 kB +0.5% +431 B 🔺
@sentry/node - without channel injection 102.4 kB +0.02% +16 B 🔺
@sentry/aws-serverless 96.34 kB +0.49% +467 B 🔺
@sentry/cloudflare (withSentry) - minified 199.69 kB - -
@sentry/cloudflare (withSentry) 496.19 kB - -

View base workflow run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 1540609. Configure here.

Comment thread packages/server-utils/src/orchestrion/runtime/register.ts
@mydea mydea changed the title fix(node): Detect + warn when the orchestrion runtime hook is bundled, keep it external in vite fix(node): Detect + warn when the orchestrion runtime hook is bundled Aug 27, 2026
@linear-code

linear-code Bot commented Aug 27, 2026

Copy link
Copy Markdown

JS-3451

@mydea
mydea marked this pull request as ready for review August 27, 2026 13:30
@mydea
mydea requested review from a team as code owners August 27, 2026 13:30
@mydea
mydea requested review from chargome and s1gr1d and removed request for a team August 27, 2026 13:30
Comment thread packages/server-utils/src/orchestrion/runtime/register.ts
Comment thread packages/nuxt/README.md

## Troubleshoot

If your server-side auto-instrumentation stops recording spans after bundling (e.g. certain Nitro

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this also be added to the Nitro SDK readme?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'd look into a follow up here overall to try to fix this in nitro, if possible!

Comment thread packages/server-utils/src/orchestrion/runtime/register.ts Outdated
Comment thread packages/server-utils/src/orchestrion/runtime/register.ts
// instrumentation was already injected at build time and the runtime hook is redundant — this is
// an expected, supported setup, so stay quiet (debug-only). Otherwise nothing is instrumented, so
// surface an always-on, actionable warning.
if (marker.bundler instanceof Set) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have some bundler instanceof Set guards in some files. Should we create a helper named bundlerPluginRun() or something? Then it's also more understandable what it does.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm I think most other places I've seen (or found at least) are more type-guarding this which seems a bit different of a use case then this?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👀
hm, yes then a method actually does not really make sense. disregard this :D

Comment on lines -109 to -111
debug.warn(
'Failed to register diagnostics-channel injection hooks; channel-based integrations will not record spans.',
error,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a debug.warn before (gated with the debug flag). Is this on purpose, that this should now always be printed?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be a proper console warn, this means nothing will work really so users should know 😅

mydea and others added 9 commits August 28, 2026 11:26
`@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>
@mydea
mydea force-pushed the fix/orchestrion-detect-bundling branch from 1bb710e to da0de0d Compare August 28, 2026 09:26
mydea added a commit that referenced this pull request Aug 28, 2026
…nto a dedicated package

The runtime diagnostics-channel injection (`register`/`hook`/`import-hook` + the
vendored transformer chain meriyah/esquery/astring/source-map/tracing-hooks) must
stay EXTERNAL when apps bundle their server, while the rest of `@sentry/server-utils`
(barrel + config + bundler plugins) is meant to be bundled. Sharing one package made
externalization fiddly. This moves the runtime injection into a new package,
`@sentry/server-runtime-injection`, so "keep this external" is a clean package boundary.

- New package holds `register`/`hook`/`import-hook` (subpaths `/register`, `/hook`,
  `/import-hook`) and vendors the transformer chain; it depends on `@sentry/server-utils`
  and imports `SENTRY_INSTRUMENTATIONS` from `./orchestrion/config` (config stays put).
- `@sentry/server-utils` drops the runtime dir/exports and the runtime-only vendored deps;
  the lone `config/index.ts` bundler re-export moves to a new `./orchestrion/bundler-transforms`
  subpath so importing config stays transformer-free (bun updated).
- References updated: node SDK + test mock, deno import, the shared `--import` template +
  `makeOrchestrionLoader` guard, Next.js externalization (`ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES`,
  nextjs-anchored require-ability check, forwarder generation) + tests, `.size-limit.js`.
- Dep added to runtime-injection consumers (node, nextjs, deno, aws/gcp-serverless, astro,
  tanstackstart-react); package registered in workspaces, `.craft.yml`, CODEOWNERS, aws e2e.

Stacked on the detect/warn PR (#23675).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment on lines +54 to +56
.getTransformer('@sentry/orchestrion-probe', '0.0.0', 'probe.js')
?.transform('class C { async m(x) { return x; } }', 'esm');
return false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The tree-shaking detection can fail silently. If getTransformer() returns undefined, the optional chaining on .transform() will cause the check to pass incorrectly, leading to broken hooks being installed.
Severity: HIGH

Suggested Fix

Remove the optional chaining from the .transform() call. Instead, explicitly check if the value returned by getTransformer() is undefined. If it is, treat it as a failure case by throwing an error. This ensures that a non-matching probe configuration correctly signals a problem with the transformer.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: packages/server-utils/src/orchestrion/runtime/register.ts#L54-L56

Potential issue: The tree-shaking detection mechanism uses optional chaining on a
`.transform()` call. The preceding function, `getTransformer()`, can return `undefined`
if the synthetic probe configuration does not match. In this scenario, the optional
chaining (`?.`) will cause the `.transform()` call to be skipped silently, without
throwing an error. The function will then incorrectly determine that the transformer is
healthy and proceed to install broken runtime hooks. This defeats the purpose of the
check and will lead to the original `TypeError: parse is not a function` error during
module loading.

mydea added a commit that referenced this pull request Aug 28, 2026
…nto a dedicated package

The runtime diagnostics-channel injection (`register`/`hook`/`import-hook` + the
vendored transformer chain meriyah/esquery/astring/source-map/tracing-hooks) must
stay EXTERNAL when apps bundle their server, while the rest of `@sentry/server-utils`
(barrel + config + bundler plugins) is meant to be bundled. Sharing one package made
externalization fiddly. This moves the runtime injection into a new package,
`@sentry/server-runtime-injection`, so "keep this external" is a clean package boundary.

- New package holds `register`/`hook`/`import-hook` (subpaths `/register`, `/hook`,
  `/import-hook`) and vendors the transformer chain; it depends on `@sentry/server-utils`
  and imports `SENTRY_INSTRUMENTATIONS` from `./orchestrion/config` (config stays put).
- `@sentry/server-utils` drops the runtime dir/exports and the runtime-only vendored deps;
  the lone `config/index.ts` bundler re-export moves to a new `./orchestrion/bundler-transforms`
  subpath so importing config stays transformer-free (bun updated).
- References updated: node SDK + test mock, deno import, the shared `--import` template +
  `makeOrchestrionLoader` guard, Next.js externalization (`ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES`,
  nextjs-anchored require-ability check, forwarder generation) + tests, `.size-limit.js`.
- Dep added to runtime-injection consumers (node, nextjs, deno, aws/gcp-serverless, astro,
  tanstackstart-react); package registered in workspaces, `.craft.yml`, CODEOWNERS, aws e2e.

Stacked on the detect/warn PR (#23675).

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

@isaacs isaacs left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Raised what I think is a pretty significant false-alarm issue, but the intent is sound and the scoping decision, to keep the hook external rather than make a bundled copy work, definitely matches the constraints. The core detection works: I reproduced it end-to-end against an esbuild bundle and it fires correctly.

// instrumentation was already injected at build time and the runtime hook is redundant — this is
// an expected, supported setup, so stay quiet (debug-only). Otherwise nothing is instrumented, so
// surface an always-on, actionable warning.
if (marker.bundler instanceof Set) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's an interesting issue here that'll lead to false alarms, because the esbuild can fail to add the banner, and then print a warning telling the user to add the bundler plugin they're already using.

The root cause is that the upstream entryOutputPaths/configuredEntryPoints in node_modules/@apm-js-collab/code-transformer-bundler-plugins/dist/esm/esbuild.mjs match metafile.outputs[].entryPoint against the configured entry point paths. @sentry/bundler-plugins/esbuild debug-id injection rewrites the entry, so eg a.mjs becomes a.mjs?sentryDebugIdProxy=true, which never matches, and the banner is skipped.

This is a pre-existing gap, but as of this PR, we're depending on it for the user-visible alarm, so it becomes a tangible problem that we should fix, imo. It wasn't a problem before, because nothing read the banner, we always did stuff like (g.bundler ??= new Set()).add(module), so the point where it was created didn't ever matter before.

We have some tests in node-integration-tests that could've caught this, but didn't check for this situation specifically. Applying this patch adds a test that reproduces the issue: https://gist.github.com/isaacs/0f0633e3f8af0b078a973448abcf0955

Also, ESM import hoisting will effectively put the banner after the call to Sentry.init() in a code-split build, so we'll get chunks like:

;(function(){…g.bundler=g.bundler||new Set();})();import "./chunk-cafebad0.js";

We can address both of these, though, by removing the probe, and then moving the check out of this function and down into the setDiagnosticsHook error callback function on line 128, which also reduces out a layer.

That callback fires when a module actually fails to transform, by which point every banner has run, so the ordering problem disappears. It also drops the init cost, and can name the failing module. The one behavior change is that there's no warning if the transformer is stripped but no instrumented module ever loads. But I think that's the correct outcome, since nothing was lost in that case.

These two patches would implement the test and suggested fix: https://gist.github.com/isaacs/8f58c5e1f9bac3479be2e9f2c6ee578d or pull the top two commits from isaacs/review-23675

Comment on lines 142 to 146
// runs both at `--import` time and (synchronously) inside `Sentry.init()`,
// so an unguarded throw would either abort startup or make `init()` throw.
// On any failure (e.g. dep resolution, `require(esm)` / Node-compat
// incompatibility) we warn (DEBUG only) and continue without channel
// injection

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low/question: I think this comment might be stale now?

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.

orchestrion deps are partially tree-shaken

3 participants