From 17976865aabd01ab2902e093262951e19902c0e7 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 27 Aug 2026 13:11:44 +0200 Subject: [PATCH 01/15] feat(server-utils): Warn when the orchestrion runtime hook was bundled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `@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) --- packages/core/src/utils/worldwide.ts | 8 ++ .../src/orchestrion/runtime/register.ts | 81 +++++++++++++++++-- .../test/orchestrion/register.test.ts | 65 +++++++++++++++ 3 files changed, 147 insertions(+), 7 deletions(-) create mode 100644 packages/server-utils/test/orchestrion/register.test.ts diff --git a/packages/core/src/utils/worldwide.ts b/packages/core/src/utils/worldwide.ts index 78f31d194911..ca88dd225db6 100644 --- a/packages/core/src/utils/worldwide.ts +++ b/packages/core/src/utils/worldwide.ts @@ -77,6 +77,14 @@ export type InternalGlobal = { * `init()` and instantiates them. */ integrations?: Map Integration>; + /** + * Set once `registerDiagnosticsChannelInjection()` has run but could not + * install the runtime module hooks — most commonly because + * `@sentry/server-utils` was bundled into the app (which strips its vendored + * code transformer) or the Node runtime lacks the required module-hook API. + * Dedupes the one-time warning and short-circuits repeat calls. + */ + runtimeUnavailable?: boolean; }; } & Carrier; diff --git a/packages/server-utils/src/orchestrion/runtime/register.ts b/packages/server-utils/src/orchestrion/runtime/register.ts index 10865145b605..f42a4dfb1891 100644 --- a/packages/server-utils/src/orchestrion/runtime/register.ts +++ b/packages/server-utils/src/orchestrion/runtime/register.ts @@ -1,6 +1,7 @@ -import { debug, getClient, GLOBAL_OBJ, parseSemver } from '@sentry/core'; +import { consoleSandbox, debug, getClient, GLOBAL_OBJ, parseSemver } from '@sentry/core'; import * as Module from 'node:module'; import { pathToFileURL } from 'node:url'; +import { create } from '@apm-js-collab/code-transformer'; import { SENTRY_INSTRUMENTATIONS } from '../config'; import type { register } from 'node:module'; import ModulePatch from '@apm-js-collab/tracing-hooks'; @@ -12,6 +13,9 @@ type NodeModule = { register?: typeof register; }; +// Surfaced in the always-on warnings below so users can find the fix. +const BUNDLING_DOCS_URL = 'https://docs.sentry.io/platforms/javascript/guides/node/troubleshooting/'; + /** `Module.registerHooks` only became stable in Node 24.13 / 25.1. */ function hasStableSyncModuleHooks(isDeno: boolean): boolean { // The minimum supported Deno (2.8.3) always has stable sync module hooks. @@ -23,6 +27,52 @@ function hasStableSyncModuleHooks(isDeno: boolean): boolean { return major > 25 || (major === 25 && minor >= 1) || (major === 24 && minor >= 13); } +/** + * Detect whether the vendored code-transformer chain (meriyah/astring/source-map, bundled into this + * package) survived downstream bundling. + * + * This package ships the transformer inline and is meant to run from `node_modules` (external). When + * an app bundler instead inlines `@sentry/server-utils` and tree-shakes it, those vendored deps are + * stripped to empty objects, so `parse`/`generate` become `undefined` and the FIRST module the hook + * tries to transform throws `TypeError: parse is not a function` — deep in the loader, once per + * module, only visible with `debug: true`. Running one throwaway in-memory transform up front turns + * that into a single, actionable, always-on warning (see `warnRuntimeUnavailable`). A healthy build + * returns normally; a tree-shaken one throws a `TypeError`. + */ +function isTransformerTreeShaken(): boolean { + try { + create( + [ + { + channelName: 'probe', + module: { name: '@sentry/orchestrion-probe', versionRange: '*', filePath: 'probe.js' }, + functionQuery: { className: 'C', methodName: 'm', kind: 'Async' }, + }, + ], + 'node:diagnostics_channel', + ) + .getTransformer('@sentry/orchestrion-probe', '0.0.0', 'probe.js') + ?.transform('class C { async m(x) { return x; } }', 'esm'); + return false; + } catch (error) { + // Tree-shaken: `parse`/`generate`/`create` are `undefined` → TypeError. A healthy build either + // succeeds or throws a domain `Error` (e.g. "Failed to find injection points"), never a TypeError. + return error instanceof TypeError; + } +} + +/** + * Emit a single, always-on warning that runtime channel injection is disabled, with the actionable + * fix. Unlike `debug.warn` (gated behind `debug: true`), this reaches every user — otherwise the + * SDK silently records no channel-based spans. Deduped via a global marker so repeat calls (e.g. + * `init()` plus `--import`) warn at most once. + */ +function warnRuntimeUnavailable(message: string): void { + consoleSandbox(() => { + GLOBAL_OBJ.console?.warn(`[Sentry] ${message} See ${BUNDLING_DOCS_URL}`); + }); +} + /** * Synchronously register the diagnostics-channel injection module hooks. * @@ -36,7 +86,23 @@ function hasStableSyncModuleHooks(isDeno: boolean): boolean { * the channel-based integrations subscribe to. */ export function registerDiagnosticsChannelInjection(): void { - if (GLOBAL_OBJ?.__SENTRY_ORCHESTRION__?.runtime) { + const marker = (GLOBAL_OBJ.__SENTRY_ORCHESTRION__ ??= {}); + + // Already hooked, or we already ran and found runtime injection unavailable (and warned once). + if (marker.runtime || marker.runtimeUnavailable) { + return; + } + + // A downstream bundler that inlined + tree-shook this package strips the vendored transformer, so + // every runtime transform would throw a cryptic `TypeError` deep in the loader. Detect that once, + // warn actionably, and don't install hooks that can't work. + if (isTransformerTreeShaken()) { + marker.runtimeUnavailable = true; + warnRuntimeUnavailable( + '`@sentry/server-utils` was bundled into your application, so diagnostics-channel ' + + 'auto-instrumentation is disabled. Keep `@sentry/server-utils` external in your server bundle, ' + + 'or use the Sentry bundler plugin for build-time instrumentation.', + ); return; } @@ -102,17 +168,18 @@ export function registerDiagnosticsChannelInjection(): void { new ModulePatch({ instrumentations: SENTRY_INSTRUMENTATIONS }).patch(); debug.log('Registered diagnostics-channel injection via Module.register()'); } else { + marker.runtimeUnavailable = true; debug.warn('No available Node API to register diagnostics-channel injection hooks; skipping.'); return; } } catch (error) { - debug.warn( - 'Failed to register diagnostics-channel injection hooks; channel-based integrations will not record spans.', - error, + marker.runtimeUnavailable = true; + warnRuntimeUnavailable( + 'Failed to register diagnostics-channel injection hooks, so channel-based integrations will not record spans.', ); + debug.warn('Diagnostics-channel injection registration error:', error); return; } - GLOBAL_OBJ.__SENTRY_ORCHESTRION__ = GLOBAL_OBJ.__SENTRY_ORCHESTRION__ || {}; - GLOBAL_OBJ.__SENTRY_ORCHESTRION__.runtime = GLOBAL_OBJ.__SENTRY_ORCHESTRION__.runtime || []; + marker.runtime = marker.runtime || []; } diff --git a/packages/server-utils/test/orchestrion/register.test.ts b/packages/server-utils/test/orchestrion/register.test.ts new file mode 100644 index 000000000000..ec2d82fcafcd --- /dev/null +++ b/packages/server-utils/test/orchestrion/register.test.ts @@ -0,0 +1,65 @@ +import type * as SentryCore from '@sentry/core'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +// Simulate the vendored code-transformer chain. A tree-shaken build (this package bundled into an +// app and stripped) throws a `TypeError` from `create(...).getTransformer(...).transform(...)`; a +// healthy build does not. See `isTransformerTreeShaken` in `runtime/register.ts`. +const createMock = vi.fn(); +vi.mock('@apm-js-collab/code-transformer', () => ({ + create: (...args: unknown[]) => createMock(...args), +})); + +// Neutralise `consoleSandbox` (it swaps in the pristine console during its callback, which would +// bypass a spy) so we can assert the always-on warning directly. +vi.mock('@sentry/core', async importOriginal => { + const actual = await importOriginal(); + return { ...actual, consoleSandbox: (cb: () => unknown) => cb() }; +}); + +import { GLOBAL_OBJ } from '@sentry/core'; +import { registerDiagnosticsChannelInjection } from '../../src/orchestrion/runtime/register'; + +describe('registerDiagnosticsChannelInjection - bundled/tree-shaken detection', () => { + let warnSpy: ReturnType; + + beforeEach(() => { + delete GLOBAL_OBJ.__SENTRY_ORCHESTRION__; + createMock.mockReset(); + warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + }); + + afterEach(() => { + delete GLOBAL_OBJ.__SENTRY_ORCHESTRION__; + warnSpy.mockRestore(); + }); + + it('warns once and disables runtime injection when the transformer was tree-shaken', () => { + // A tree-shaken chain: `parse`/`generate` are `undefined`, so a transform throws a TypeError. + createMock.mockImplementation(() => { + throw new TypeError('parse is not a function'); + }); + + registerDiagnosticsChannelInjection(); + + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('was bundled into your application')); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('docs.sentry.io')); + // Marked unavailable, and NOT marked as runtime-hooked (hooks were never installed). + expect(GLOBAL_OBJ.__SENTRY_ORCHESTRION__?.runtimeUnavailable).toBe(true); + expect(GLOBAL_OBJ.__SENTRY_ORCHESTRION__?.runtime).toBeUndefined(); + }); + + it('does not warn again on subsequent calls (deduped)', () => { + createMock.mockImplementation(() => { + throw new TypeError('parse is not a function'); + }); + + registerDiagnosticsChannelInjection(); + registerDiagnosticsChannelInjection(); + registerDiagnosticsChannelInjection(); + + expect(warnSpy).toHaveBeenCalledTimes(1); + // The probe runs only on the first call; the marker short-circuits the rest. + expect(createMock).toHaveBeenCalledTimes(1); + }); +}); From 77b537594c809c780042bf031ec5c70e6522a9b9 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 27 Aug 2026 13:11:44 +0200 Subject: [PATCH 02/15] feat(server-utils): Keep @sentry/node external in the vite orchestrion plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../src/orchestrion/bundler/vite.ts | 20 +++++++++++++++++-- .../test/orchestrion/bundler.test.ts | 11 ++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/packages/server-utils/src/orchestrion/bundler/vite.ts b/packages/server-utils/src/orchestrion/bundler/vite.ts index 5160bf70ea55..36569e51f366 100644 --- a/packages/server-utils/src/orchestrion/bundler/vite.ts +++ b/packages/server-utils/src/orchestrion/bundler/vite.ts @@ -83,7 +83,7 @@ export function sentryOrchestrionPlugin(options: PluginOptions = {}): Plugin { // calls never land in a browser (`client`) bundle (where they'd throw `X is not a function`). return environment.config.consumer === 'server'; }, - config(): { ssr: { noExternal: string[] } } { + config(): { ssr: { noExternal: string[]; external: string[] } } { // Force-bundle every instrumented package so the code transform actually // sees its source. Vite externalizes dependencies in SSR builds by // default, leaving them as bare `require()`/`import` calls resolved from @@ -99,8 +99,24 @@ export function sentryOrchestrionPlugin(options: PluginOptions = {}): Plugin { // ESM entry — a link-time crash at server startup. Bundling sidesteps // external ESM/CJS interop on both Vite majors, and the ESM barrel // tree-shakes to just the helper and the factories actually referenced. + // + // Conversely, `@sentry/node` must stay EXTERNAL. Its `init()` installs the + // runtime diagnostics-channel hook via `@sentry/server-utils/orchestrion/ + // register`, which loads the vendored code transformer and, on older Node, + // `Module.register`s a hook module by a self-referential specifier that + // only resolves from the package's real `node_modules` location. Bundling + // `@sentry/node` therefore strips the transformer (tree-shaking) AND breaks + // that self-reference. It's a different package from the `@sentry/server- + // utils` barrel above, so listing it here is not a package-granularity + // conflict; explicit `ssr.external` entries also win over `noExternal`, so + // this holds even against a preset that would otherwise inline it. A + // matching runtime warning in `orchestrion/register` covers bundlers this + // plugin can't reach. return { - ssr: { noExternal: [...instrumentedModuleNames(options.instrumentations), '@sentry/server-utils'] }, + ssr: { + noExternal: [...instrumentedModuleNames(options.instrumentations), '@sentry/server-utils'], + external: ['@sentry/node'], + }, }; }, configResolved(config: ResolvedConfig): void { diff --git a/packages/server-utils/test/orchestrion/bundler.test.ts b/packages/server-utils/test/orchestrion/bundler.test.ts index 1c1cd1c32977..c92021cb7ad9 100644 --- a/packages/server-utils/test/orchestrion/bundler.test.ts +++ b/packages/server-utils/test/orchestrion/bundler.test.ts @@ -198,6 +198,17 @@ describe('sentryOrchestrionPlugin (vite)', () => { expect(config.ssr.noExternal).toContain('mysql'); }); + it('keeps @sentry/node external so the runtime diagnostics-channel hook is never bundled', () => { + const plugin = vitePlugin(); + const config = (plugin.config as () => { ssr: { external: string[] } })(); + + // Bundling @sentry/node would strip the vendored transformer and break the + // `Module.register` self-reference in `orchestrion/register`. Explicit + // `ssr.external` entries win over `noExternal`, so this holds even when a + // preset sets `ssr.noExternal: true`. + expect(config.ssr.external).toContain('@sentry/node'); + }); + it('gates the transform on the ssr flag (Vite 5 ignores applyToEnvironment)', () => { const plugin = vitePlugin(); const transform = plugin.transform as ( From 34084c72b2051b2464b55cabb2f961220c50693d Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 27 Aug 2026 13:11:44 +0200 Subject: [PATCH 03/15] docs(node): Document keeping @sentry/server-utils external when bundling 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) --- packages/node/README.md | 14 ++++++++++++++ packages/nuxt/README.md | 5 +++++ 2 files changed, 19 insertions(+) diff --git a/packages/node/README.md b/packages/node/README.md index 6471538fb4f0..51158d866a77 100644 --- a/packages/node/README.md +++ b/packages/node/README.md @@ -72,6 +72,20 @@ If it is not possible for you to pass the `--import` flag to the Node.js binary, NODE_OPTIONS="--import ./instrument.mjs" npm run start ``` +### Bundling your server + +`@sentry/node` installs its automatic (diagnostics-channel) instrumentation through a runtime module +hook that ships in `@sentry/server-utils` and is designed to run from `node_modules`. If you bundle +your server (esbuild, webpack, rollup, or a framework that bundles the server output), keep +`@sentry/server-utils` **external** — do not inline it into the bundle. Bundling it strips its +internal code transformer, which silently disables auto-instrumentation (`@sentry/node` will warn at +startup when it detects this). + +Most setups don't bundle the SDK. If you do, either mark `@sentry/server-utils` as external in your +bundler config, or use the build-time instrumentation from the Sentry bundler plugins instead +(`@sentry/node/esbuild`, `@sentry/node/webpack`, `@sentry/node/vite`, `@sentry/node/rollup`), which +inject the instrumentation into your bundled dependencies at build time. + ## Links - [Official SDK Docs](https://docs.sentry.io/quickstart/) diff --git a/packages/nuxt/README.md b/packages/nuxt/README.md index b7978c288ffd..13fe27528588 100644 --- a/packages/nuxt/README.md +++ b/packages/nuxt/README.md @@ -28,4 +28,9 @@ functionality related to Nuxt. ## Troubleshoot +If your server-side auto-instrumentation stops recording spans after bundling (e.g. certain Nitro +presets), make sure `@sentry/server-utils` is kept **external** in the Nitro/server build rather than +inlined — its runtime module hook must resolve from `node_modules`. `@sentry/node` logs a warning at +startup when it detects it was bundled. + If you encounter any issues with error tracking or integrations, refer to the official [Sentry Nuxt SDK documentation](https://docs.sentry.io/platforms/javascript/guides/nuxt/). If the documentation does not provide the necessary information, consider opening an issue on GitHub. From 14c8a999739c97b73252dc334538b0f4253271d2 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 27 Aug 2026 13:27:20 +0200 Subject: [PATCH 04/15] fix(server-utils): Don't force @sentry/node external in the vite plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../src/orchestrion/bundler/vite.ts | 26 +++++++------------ .../test/orchestrion/bundler.test.ts | 11 -------- 2 files changed, 9 insertions(+), 28 deletions(-) diff --git a/packages/server-utils/src/orchestrion/bundler/vite.ts b/packages/server-utils/src/orchestrion/bundler/vite.ts index 36569e51f366..e9ff403c00b1 100644 --- a/packages/server-utils/src/orchestrion/bundler/vite.ts +++ b/packages/server-utils/src/orchestrion/bundler/vite.ts @@ -83,7 +83,7 @@ export function sentryOrchestrionPlugin(options: PluginOptions = {}): Plugin { // calls never land in a browser (`client`) bundle (where they'd throw `X is not a function`). return environment.config.consumer === 'server'; }, - config(): { ssr: { noExternal: string[]; external: string[] } } { + config(): { ssr: { noExternal: string[] } } { // Force-bundle every instrumented package so the code transform actually // sees its source. Vite externalizes dependencies in SSR builds by // default, leaving them as bare `require()`/`import` calls resolved from @@ -100,23 +100,15 @@ export function sentryOrchestrionPlugin(options: PluginOptions = {}): Plugin { // external ESM/CJS interop on both Vite majors, and the ESM barrel // tree-shakes to just the helper and the factories actually referenced. // - // Conversely, `@sentry/node` must stay EXTERNAL. Its `init()` installs the - // runtime diagnostics-channel hook via `@sentry/server-utils/orchestrion/ - // register`, which loads the vendored code transformer and, on older Node, - // `Module.register`s a hook module by a self-referential specifier that - // only resolves from the package's real `node_modules` location. Bundling - // `@sentry/node` therefore strips the transformer (tree-shaking) AND breaks - // that self-reference. It's a different package from the `@sentry/server- - // utils` barrel above, so listing it here is not a package-granularity - // conflict; explicit `ssr.external` entries also win over `noExternal`, so - // this holds even against a preset that would otherwise inline it. A - // matching runtime warning in `orchestrion/register` covers bundlers this - // plugin can't reach. + // Note: we deliberately do NOT force `@sentry/node` into `ssr.external` + // here. Vite already externalizes it for node SSR by default (so the + // runtime hook resolves from `node_modules`), and this same plugin also + // runs in worker builds (`@sentry/cloudflare`, frameworks on + // `@cloudflare/vite-plugin`) where `@sentry/node` is unused and setting + // `resolve.external` is rejected outright. The runtime probe in + // `orchestrion/register` covers the cases where it does get bundled. return { - ssr: { - noExternal: [...instrumentedModuleNames(options.instrumentations), '@sentry/server-utils'], - external: ['@sentry/node'], - }, + ssr: { noExternal: [...instrumentedModuleNames(options.instrumentations), '@sentry/server-utils'] }, }; }, configResolved(config: ResolvedConfig): void { diff --git a/packages/server-utils/test/orchestrion/bundler.test.ts b/packages/server-utils/test/orchestrion/bundler.test.ts index c92021cb7ad9..1c1cd1c32977 100644 --- a/packages/server-utils/test/orchestrion/bundler.test.ts +++ b/packages/server-utils/test/orchestrion/bundler.test.ts @@ -198,17 +198,6 @@ describe('sentryOrchestrionPlugin (vite)', () => { expect(config.ssr.noExternal).toContain('mysql'); }); - it('keeps @sentry/node external so the runtime diagnostics-channel hook is never bundled', () => { - const plugin = vitePlugin(); - const config = (plugin.config as () => { ssr: { external: string[] } })(); - - // Bundling @sentry/node would strip the vendored transformer and break the - // `Module.register` self-reference in `orchestrion/register`. Explicit - // `ssr.external` entries win over `noExternal`, so this holds even when a - // preset sets `ssr.noExternal: true`. - expect(config.ssr.external).toContain('@sentry/node'); - }); - it('gates the transform on the ssr flag (Vite 5 ignores applyToEnvironment)', () => { const plugin = vitePlugin(); const transform = plugin.transform as ( From a3e6fe25c4507ce93ae216d755802ff70133cd7c Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 27 Aug 2026 13:27:20 +0200 Subject: [PATCH 05/15] feat(server-utils): Stay quiet when build-time instrumentation covers a bundled hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- packages/node/README.md | 25 +++++++++++-------- .../src/orchestrion/runtime/register.ts | 25 +++++++++++++------ .../test/orchestrion/register.test.ts | 15 +++++++++++ 3 files changed, 48 insertions(+), 17 deletions(-) diff --git a/packages/node/README.md b/packages/node/README.md index 51158d866a77..a2a32c69ab9f 100644 --- a/packages/node/README.md +++ b/packages/node/README.md @@ -75,16 +75,21 @@ NODE_OPTIONS="--import ./instrument.mjs" npm run start ### Bundling your server `@sentry/node` installs its automatic (diagnostics-channel) instrumentation through a runtime module -hook that ships in `@sentry/server-utils` and is designed to run from `node_modules`. If you bundle -your server (esbuild, webpack, rollup, or a framework that bundles the server output), keep -`@sentry/server-utils` **external** — do not inline it into the bundle. Bundling it strips its -internal code transformer, which silently disables auto-instrumentation (`@sentry/node` will warn at -startup when it detects this). - -Most setups don't bundle the SDK. If you do, either mark `@sentry/server-utils` as external in your -bundler config, or use the build-time instrumentation from the Sentry bundler plugins instead -(`@sentry/node/esbuild`, `@sentry/node/webpack`, `@sentry/node/vite`, `@sentry/node/rollup`), which -inject the instrumentation into your bundled dependencies at build time. +hook that ships in `@sentry/server-utils` and is designed to run from `node_modules`. There are two +supported ways to keep auto-instrumentation working when you bundle your server: + +1. **Keep `@sentry/server-utils` external** (do not inline it into the bundle) so the runtime hook + loads from `node_modules`. Most bundlers externalize `node_modules` for a Node target by default; + if yours inlines everything, mark `@sentry/server-utils` as external explicitly. +2. **Instrument at build time** with the Sentry bundler plugins (`@sentry/node/esbuild`, + `@sentry/node/webpack`, `@sentry/node/vite`, `@sentry/node/rollup`), which inject the + instrumentation into your bundled dependencies during the build. In this mode the runtime hook is + not needed. + +If you bundle `@sentry/server-utils` **and** don't use the build-time plugin, its internal code +transformer is stripped and runtime auto-instrumentation is disabled — `@sentry/node` warns at +startup when it detects this. (When the build-time plugin is used, there is no warning, since +instrumentation is already in place.) ## Links diff --git a/packages/server-utils/src/orchestrion/runtime/register.ts b/packages/server-utils/src/orchestrion/runtime/register.ts index f42a4dfb1891..e8b919ca67c3 100644 --- a/packages/server-utils/src/orchestrion/runtime/register.ts +++ b/packages/server-utils/src/orchestrion/runtime/register.ts @@ -94,15 +94,26 @@ export function registerDiagnosticsChannelInjection(): void { } // A downstream bundler that inlined + tree-shook this package strips the vendored transformer, so - // every runtime transform would throw a cryptic `TypeError` deep in the loader. Detect that once, - // warn actionably, and don't install hooks that can't work. + // every runtime transform would throw a cryptic `TypeError` deep in the loader. Detect that once + // and don't install hooks that can't work. if (isTransformerTreeShaken()) { marker.runtimeUnavailable = true; - warnRuntimeUnavailable( - '`@sentry/server-utils` was bundled into your application, so diagnostics-channel ' + - 'auto-instrumentation is disabled. Keep `@sentry/server-utils` external in your server bundle, ' + - 'or use the Sentry bundler plugin for build-time instrumentation.', - ); + // If the build-time bundler plugin ran (a defined `bundler` marker Set, set by its entry banner), + // 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) { + debug.log( + 'Runtime diagnostics-channel injection is disabled because `@sentry/server-utils` was bundled; ' + + 'build-time instrumentation is active, so this is expected.', + ); + } else { + warnRuntimeUnavailable( + '`@sentry/server-utils` was bundled into your application, so diagnostics-channel ' + + 'auto-instrumentation is disabled. Keep `@sentry/server-utils` external in your server bundle, ' + + 'or use the Sentry bundler plugin for build-time instrumentation.', + ); + } return; } diff --git a/packages/server-utils/test/orchestrion/register.test.ts b/packages/server-utils/test/orchestrion/register.test.ts index ec2d82fcafcd..b21e0227d9ae 100644 --- a/packages/server-utils/test/orchestrion/register.test.ts +++ b/packages/server-utils/test/orchestrion/register.test.ts @@ -49,6 +49,21 @@ describe('registerDiagnosticsChannelInjection - bundled/tree-shaken detection', expect(GLOBAL_OBJ.__SENTRY_ORCHESTRION__?.runtime).toBeUndefined(); }); + it('does not warn when build-time instrumentation is active (bundler marker present)', () => { + createMock.mockImplementation(() => { + throw new TypeError('parse is not a function'); + }); + // A defined `bundler` Set signals the build-time plugin ran, so the runtime hook is redundant. + GLOBAL_OBJ.__SENTRY_ORCHESTRION__ = { bundler: new Set() }; + + registerDiagnosticsChannelInjection(); + + // No user-facing warning — this is an expected, supported setup. + expect(warnSpy).not.toHaveBeenCalled(); + expect(GLOBAL_OBJ.__SENTRY_ORCHESTRION__?.runtimeUnavailable).toBe(true); + expect(GLOBAL_OBJ.__SENTRY_ORCHESTRION__?.runtime).toBeUndefined(); + }); + it('does not warn again on subsequent calls (deduped)', () => { createMock.mockImplementation(() => { throw new TypeError('parse is not a function'); From f94b60f1102648d5e5525cda2a062b75c96e2fd4 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 27 Aug 2026 13:45:29 +0200 Subject: [PATCH 06/15] better comment --- packages/server-utils/src/orchestrion/bundler/vite.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/server-utils/src/orchestrion/bundler/vite.ts b/packages/server-utils/src/orchestrion/bundler/vite.ts index e9ff403c00b1..5160bf70ea55 100644 --- a/packages/server-utils/src/orchestrion/bundler/vite.ts +++ b/packages/server-utils/src/orchestrion/bundler/vite.ts @@ -99,14 +99,6 @@ export function sentryOrchestrionPlugin(options: PluginOptions = {}): Plugin { // ESM entry — a link-time crash at server startup. Bundling sidesteps // external ESM/CJS interop on both Vite majors, and the ESM barrel // tree-shakes to just the helper and the factories actually referenced. - // - // Note: we deliberately do NOT force `@sentry/node` into `ssr.external` - // here. Vite already externalizes it for node SSR by default (so the - // runtime hook resolves from `node_modules`), and this same plugin also - // runs in worker builds (`@sentry/cloudflare`, frameworks on - // `@cloudflare/vite-plugin`) where `@sentry/node` is unused and setting - // `resolve.external` is rejected outright. The runtime probe in - // `orchestrion/register` covers the cases where it does get bundled. return { ssr: { noExternal: [...instrumentedModuleNames(options.instrumentations), '@sentry/server-utils'] }, }; From 4140c0ad773925d5d7d2d3f4495bad567d3b9c9c Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 27 Aug 2026 13:51:54 +0200 Subject: [PATCH 07/15] just use console --- packages/server-utils/src/orchestrion/runtime/register.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/server-utils/src/orchestrion/runtime/register.ts b/packages/server-utils/src/orchestrion/runtime/register.ts index e8b919ca67c3..086c6bd3662f 100644 --- a/packages/server-utils/src/orchestrion/runtime/register.ts +++ b/packages/server-utils/src/orchestrion/runtime/register.ts @@ -69,7 +69,8 @@ function isTransformerTreeShaken(): boolean { */ function warnRuntimeUnavailable(message: string): void { consoleSandbox(() => { - GLOBAL_OBJ.console?.warn(`[Sentry] ${message} See ${BUNDLING_DOCS_URL}`); + // oxlint-disable-next-line no-console + console.warn(`[Sentry] ${message} See ${BUNDLING_DOCS_URL}`); }); } From e1f15a7caca64f0404664a9cc4b793d56856a7db Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 27 Aug 2026 13:53:37 +0200 Subject: [PATCH 08/15] better comment --- packages/server-utils/src/orchestrion/runtime/register.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server-utils/src/orchestrion/runtime/register.ts b/packages/server-utils/src/orchestrion/runtime/register.ts index 086c6bd3662f..e486e4f49205 100644 --- a/packages/server-utils/src/orchestrion/runtime/register.ts +++ b/packages/server-utils/src/orchestrion/runtime/register.ts @@ -106,7 +106,7 @@ export function registerDiagnosticsChannelInjection(): void { if (marker.bundler instanceof Set) { debug.log( 'Runtime diagnostics-channel injection is disabled because `@sentry/server-utils` was bundled; ' + - 'build-time instrumentation is active, so this is expected.', + 'build-time instrumentation is active.', ); } else { warnRuntimeUnavailable( From da0de0d0e64352947b5f6382a9a5abda2844b790 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 28 Aug 2026 11:24:15 +0200 Subject: [PATCH 09/15] small fixes --- packages/server-utils/package.json | 1 + packages/server-utils/src/orchestrion/runtime/register.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/server-utils/package.json b/packages/server-utils/package.json index d95d222c5f3c..51360b990abf 100644 --- a/packages/server-utils/package.json +++ b/packages/server-utils/package.json @@ -99,6 +99,7 @@ "@sentry/core": "10.67.0" }, "devDependencies": { + "@apm-js-collab/code-transformer": "^0.18.1", "@apm-js-collab/code-transformer-bundler-plugins": "^0.7.4", "@apm-js-collab/tracing-hooks": "^0.13.0", "@types/node": "^18.19.1", diff --git a/packages/server-utils/src/orchestrion/runtime/register.ts b/packages/server-utils/src/orchestrion/runtime/register.ts index e486e4f49205..340bdaf066d4 100644 --- a/packages/server-utils/src/orchestrion/runtime/register.ts +++ b/packages/server-utils/src/orchestrion/runtime/register.ts @@ -64,8 +64,8 @@ function isTransformerTreeShaken(): boolean { /** * Emit a single, always-on warning that runtime channel injection is disabled, with the actionable * fix. Unlike `debug.warn` (gated behind `debug: true`), this reaches every user — otherwise the - * SDK silently records no channel-based spans. Deduped via a global marker so repeat calls (e.g. - * `init()` plus `--import`) warn at most once. + * SDK silently records no channel-based spans. Deduped via a a global marker (carrier.runtimeAvailable) + * so repeat calls (e.g. `init()` plus `--import`) warn at most once. */ function warnRuntimeUnavailable(message: string): void { consoleSandbox(() => { From 7bd9c469878d84848d58f7f5ad3e24b195c24388 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 28 Aug 2026 11:48:57 +0200 Subject: [PATCH 10/15] bump size limit --- .size-limit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.size-limit.js b/.size-limit.js index dfdbd49bfbe9..6ad5eb831484 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -406,7 +406,7 @@ module.exports = [ import: createImport('init'), ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: true, - limit: '123 KB', + limit: '127 KB', disablePlugins: ['@size-limit/esbuild'], }, { From 2d039c7b50ea8d6f1b39923cf4179293785a8025 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 28 Aug 2026 11:50:06 +0200 Subject: [PATCH 11/15] fix test --- .../test/orchestrion/moduleInjectedTransform.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/server-utils/test/orchestrion/moduleInjectedTransform.test.ts b/packages/server-utils/test/orchestrion/moduleInjectedTransform.test.ts index 581d9cf552fa..85ed0e6e11b6 100644 --- a/packages/server-utils/test/orchestrion/moduleInjectedTransform.test.ts +++ b/packages/server-utils/test/orchestrion/moduleInjectedTransform.test.ts @@ -3,6 +3,8 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import * as barrel from '../../src/index'; +import { SENTRY_INSTRUMENTATIONS } from '../../src/orchestrion/config'; import { CHANNEL_INTEGRATION_DEFINITIONS, subscriberExportForModule, @@ -28,17 +30,15 @@ describe('channel integration definitions', () => { expect(subscriberExportForModule('not-a-package')).toBeUndefined(); }); - it('references only real named exports of @sentry/server-utils', async () => { + it('references only real named exports of @sentry/server-utils', () => { // The injected snippet imports each factory from `@sentry/server-utils` // (the `DEFAULT_IMPORT_SPECIFIER`), so the export must exist on that entry. - const barrel = await import('../../src/index'); for (const { exportName } of CHANNEL_INTEGRATION_DEFINITIONS) { expect(typeof (barrel as Record)[exportName]).toBe('function'); } }); - it('covers every instrumented module that has a channel-subscriber integration', async () => { - const { SENTRY_INSTRUMENTATIONS } = await import('../../src/orchestrion/config'); + it('covers every instrumented module that has a channel-subscriber integration', () => { const configured = new Set(SENTRY_INSTRUMENTATIONS.map(c => c.module.name)); const defined = new Set(CHANNEL_INTEGRATION_DEFINITIONS.flatMap(d => d.modules as readonly string[])); From 39428c8b286e4d54410e91cb733b55b08c0aa401 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 27 Aug 2026 14:52:43 +0200 Subject: [PATCH 12/15] feat(server-runtime-injection): Split runtime orchestrion injection into 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) --- .craft.yml | 4 + .github/CODEOWNERS | 1 + .size-limit.js | 2 +- .../aws-serverless/src/stack.ts | 4 +- .../rollup-utils/code/importHookTemplate.js | 2 +- dev-packages/rollup-utils/npmHelpers.mjs | 8 +- package.json | 1 + packages/astro/package.json | 1 + packages/aws-serverless/package.json | 1 + packages/bun/src/plugin.ts | 4 +- packages/core/src/utils/worldwide.ts | 2 +- packages/deno/package.json | 1 + packages/deno/src/import.mjs | 2 +- packages/google-cloud-serverless/package.json | 1 + packages/nextjs/package.json | 1 + packages/nextjs/scripts/buildRollup.ts | 18 +-- .../src/config/diagnosticsChannelInjection.ts | 39 ++++-- .../diagnosticsChannelInjection.test.ts | 35 +++--- .../webpack/constructWebpackConfig.test.ts | 4 +- packages/node/README.md | 8 +- packages/node/package.json | 1 + packages/node/src/sdk/index.ts | 2 +- .../sdk/diagnosticsChannelInjection.test.ts | 2 +- packages/nuxt/README.md | 2 +- packages/server-runtime-injection/README.md | 22 ++++ .../server-runtime-injection/package.json | 94 ++++++++++++++ .../rollup.npm.config.mjs | 116 ++++++++++++++++++ .../src}/apm-js-collab-tracing-hooks.d.ts | 0 .../src}/hook.mjs | 2 +- .../src}/import-hook.mjs | 4 +- .../src}/register.ts | 25 ++-- .../test}/register.test.ts | 2 +- .../server-runtime-injection/tsconfig.json | 12 ++ .../tsconfig.test.json | 10 ++ .../tsconfig.types.json | 10 ++ .../server-runtime-injection/vite.config.ts | 9 ++ packages/server-utils/package.json | 24 ++-- packages/server-utils/rollup.npm.config.mjs | 46 ++----- .../src/orchestrion/config/index.ts | 5 - .../test/orchestrion/bundler.test.ts | 9 +- packages/server-utils/tsconfig.json | 9 +- packages/tanstackstart-react/package.json | 1 + 42 files changed, 402 insertions(+), 144 deletions(-) create mode 100644 packages/server-runtime-injection/README.md create mode 100644 packages/server-runtime-injection/package.json create mode 100644 packages/server-runtime-injection/rollup.npm.config.mjs rename packages/{server-utils/src/orchestrion/runtime => server-runtime-injection/src}/apm-js-collab-tracing-hooks.d.ts (100%) rename packages/{server-utils/src/orchestrion/runtime => server-runtime-injection/src}/hook.mjs (92%) rename packages/{server-utils/src/orchestrion/runtime => server-runtime-injection/src}/import-hook.mjs (88%) rename packages/{server-utils/src/orchestrion/runtime => server-runtime-injection/src}/register.ts (89%) rename packages/{server-utils/test/orchestrion => server-runtime-injection/test}/register.test.ts (97%) create mode 100644 packages/server-runtime-injection/tsconfig.json create mode 100644 packages/server-runtime-injection/tsconfig.test.json create mode 100644 packages/server-runtime-injection/tsconfig.types.json create mode 100644 packages/server-runtime-injection/vite.config.ts diff --git a/.craft.yml b/.craft.yml index 35e3968c7a8f..c5e537806bcb 100644 --- a/.craft.yml +++ b/.craft.yml @@ -12,6 +12,10 @@ targets: - name: npm id: '@sentry/server-utils' includeNames: /^sentry-server-utils-\d.*\.tgz$/ + # Depends on @sentry/server-utils (for the shared instrumentation config); publish after it. + - name: npm + id: '@sentry/server-runtime-injection' + includeNames: /^sentry-server-runtime-injection-\d.*\.tgz$/ ## 1.3 Browser Utils package - name: npm id: '@sentry/browser-utils' diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 19853fb40696..4eb40336293b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -10,6 +10,7 @@ # Node/server runtimes and related packages # TEMP: whole JS SDK team reviews orchestrion work; revert to team-javascript-sdks-server after /packages/node/ @getsentry/team-javascript-sdks +/packages/server-runtime-injection/ @getsentry/team-javascript-sdks /packages/server-utils/ @getsentry/team-javascript-sdks /packages/node-native/ @getsentry/team-javascript-sdks-server /packages/profiling-node/ @getsentry/team-javascript-sdks-server diff --git a/.size-limit.js b/.size-limit.js index 6ad5eb831484..635c316bea82 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -411,7 +411,7 @@ module.exports = [ }, { name: '@sentry/node/import (ESM hook with diagnostics-channel injection)', - path: ['packages/server-utils/build/esm/orchestrion/runtime/hook.js', 'packages/node/build/import-hook.mjs'], + path: ['packages/server-runtime-injection/build/esm/hook.js', 'packages/node/build/import-hook.mjs'], ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: true, limit: '91 KB', diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/stack.ts b/dev-packages/e2e-tests/test-applications/aws-serverless/src/stack.ts index 1d56430e0575..87b5d2b7f8c3 100644 --- a/dev-packages/e2e-tests/test-applications/aws-serverless/src/stack.ts +++ b/dev-packages/e2e-tests/test-applications/aws-serverless/src/stack.ts @@ -55,14 +55,14 @@ export class LocalLambdaStack extends Stack { const packageLockPath = path.join(lambdaPath, 'package-lock.json'); const nodeModulesPath = path.join(lambdaPath, 'node_modules'); - // `dir` is the package directory under `packages/`; `name` is the published - // npm name (most are `@sentry/`, but `server-utils` is `@sentry-internal`). + // `dir` is the package directory under `packages/`; `name` is the published npm name. const packagesToLink: Array<{ dir: string; name: string }> = [ { dir: 'aws-serverless', name: '@sentry/aws-serverless' }, { dir: 'node', name: '@sentry/node' }, { dir: 'core', name: '@sentry/core' }, { dir: 'opentelemetry', name: '@sentry/opentelemetry' }, { dir: 'server-utils', name: '@sentry/server-utils' }, + { dir: 'server-runtime-injection', name: '@sentry/server-runtime-injection' }, { dir: 'bundler-plugins', name: '@sentry/bundler-plugins' }, ]; const dependencies: Record = {}; diff --git a/dev-packages/rollup-utils/code/importHookTemplate.js b/dev-packages/rollup-utils/code/importHookTemplate.js index 590a81734c10..950f5d6f527e 100644 --- a/dev-packages/rollup-utils/code/importHookTemplate.js +++ b/dev-packages/rollup-utils/code/importHookTemplate.js @@ -1 +1 @@ -import '@sentry/server-utils/orchestrion/import-hook'; +import '@sentry/server-runtime-injection/import-hook'; diff --git a/dev-packages/rollup-utils/npmHelpers.mjs b/dev-packages/rollup-utils/npmHelpers.mjs index 064dd6f4e8e9..8668f960a1a3 100644 --- a/dev-packages/rollup-utils/npmHelpers.mjs +++ b/dev-packages/rollup-utils/npmHelpers.mjs @@ -192,9 +192,9 @@ export function makeNPMConfigVariants(baseConfig, options = {}) { /** * Emits the `@sentry//import` entry (`build/import-hook.mjs`) as part of the rollup build, * used as `node --import @sentry//import app.js`. The generated hook imports - * `@sentry/server-utils/orchestrion/import-hook`, which registers the orchestrion - * diagnostics-channel injection, so the consuming package must declare `@sentry/server-utils` as a - * dependency. + * `@sentry/server-runtime-injection/import-hook`, which registers the orchestrion + * diagnostics-channel injection, so the consuming package must declare + * `@sentry/server-runtime-injection` as a dependency. * * @param {string} outputFolder Build output folder. */ @@ -209,7 +209,7 @@ export function makeOrchestrionLoader(outputFolder) { ); } - const requiredDep = '@sentry/server-utils'; + const requiredDep = '@sentry/server-runtime-injection'; const foundRequiredDep = Object.keys(packageDotJSON.dependencies ?? {}).some(key => { return key === requiredDep; diff --git a/package.json b/package.json index 4c88d266077b..a25b4dd72738 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "packages/replay-internal", "packages/replay-canvas", "packages/replay-worker", + "packages/server-runtime-injection", "packages/server-utils", "packages/solid", "packages/solidstart", diff --git a/packages/astro/package.json b/packages/astro/package.json index e58785f37a14..f7c3f35da64b 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -55,6 +55,7 @@ "@sentry/core": "10.67.0", "@sentry/conventions": "^0.20.0", "@sentry/node": "10.67.0", + "@sentry/server-runtime-injection": "10.67.0", "@sentry/server-utils": "10.67.0", "@sentry/bundler-plugins": "10.67.0" }, diff --git a/packages/aws-serverless/package.json b/packages/aws-serverless/package.json index c4c0fe8711ab..c957bab5f64c 100644 --- a/packages/aws-serverless/package.json +++ b/packages/aws-serverless/package.json @@ -59,6 +59,7 @@ "@sentry/conventions": "^0.20.0", "@sentry/core": "10.67.0", "@sentry/node": "10.67.0", + "@sentry/server-runtime-injection": "10.67.0", "@sentry/server-utils": "10.67.0", "@types/aws-lambda": "^8.10.161" }, diff --git a/packages/bun/src/plugin.ts b/packages/bun/src/plugin.ts index 7c0e3abf3637..956ee611ac8b 100644 --- a/packages/bun/src/plugin.ts +++ b/packages/bun/src/plugin.ts @@ -38,9 +38,11 @@ type UnknownPlugin = any; // module system. import codeTransformer from '@apm-js-collab/code-transformer-bundler-plugins/bun'; import { - INSTRUMENTED_MODULE_NAMES, moduleInjectedTransforms, ORCHESTRION_BUNDLER_MARKER_BANNER, +} from '@sentry/server-utils/orchestrion/bundler-transforms'; +import { + INSTRUMENTED_MODULE_NAMES, SENTRY_INSTRUMENTATIONS, withoutInstrumentedExternals, } from '@sentry/server-utils/orchestrion/config'; diff --git a/packages/core/src/utils/worldwide.ts b/packages/core/src/utils/worldwide.ts index ca88dd225db6..e26e9db8933d 100644 --- a/packages/core/src/utils/worldwide.ts +++ b/packages/core/src/utils/worldwide.ts @@ -80,7 +80,7 @@ export type InternalGlobal = { /** * Set once `registerDiagnosticsChannelInjection()` has run but could not * install the runtime module hooks — most commonly because - * `@sentry/server-utils` was bundled into the app (which strips its vendored + * `@sentry/server-runtime-injection` was bundled into the app (which strips its vendored * code transformer) or the Node runtime lacks the required module-hook API. * Dedupes the one-time warning and short-circuits repeat calls. */ diff --git a/packages/deno/package.json b/packages/deno/package.json index f68c05a6dc28..25541f23eee6 100644 --- a/packages/deno/package.json +++ b/packages/deno/package.json @@ -30,6 +30,7 @@ "@opentelemetry/api": "^1.9.1", "@sentry/conventions": "^0.20.0", "@sentry/core": "10.67.0", + "@sentry/server-runtime-injection": "10.67.0", "@sentry/server-utils": "10.67.0" }, "scripts": { diff --git a/packages/deno/src/import.mjs b/packages/deno/src/import.mjs index dd501c169a8f..3db393b646c1 100644 --- a/packages/deno/src/import.mjs +++ b/packages/deno/src/import.mjs @@ -12,4 +12,4 @@ * * @module */ -import '@sentry/server-utils/orchestrion/import-hook'; +import '@sentry/server-runtime-injection/import-hook'; diff --git a/packages/google-cloud-serverless/package.json b/packages/google-cloud-serverless/package.json index ca9486f02f30..b36eb35e039d 100644 --- a/packages/google-cloud-serverless/package.json +++ b/packages/google-cloud-serverless/package.json @@ -39,6 +39,7 @@ "@sentry/conventions": "^0.20.0", "@sentry/core": "10.67.0", "@sentry/node": "10.67.0", + "@sentry/server-runtime-injection": "10.67.0", "@sentry/server-utils": "10.67.0" }, "devDependencies": { diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 3d1e1d11d42a..2bfbefc83692 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -81,6 +81,7 @@ "@sentry/node": "10.67.0", "@sentry/opentelemetry": "10.67.0", "@sentry/react": "10.67.0", + "@sentry/server-runtime-injection": "10.67.0", "@sentry/server-utils": "10.67.0", "@sentry/vercel-edge": "10.67.0", "rollup": "^4.60.3", diff --git a/packages/nextjs/scripts/buildRollup.ts b/packages/nextjs/scripts/buildRollup.ts index 69ba523ecb23..56f16f972549 100644 --- a/packages/nextjs/scripts/buildRollup.ts +++ b/packages/nextjs/scripts/buildRollup.ts @@ -24,28 +24,28 @@ fs.readdirSync(esmTemplateDir).forEach(templateFile => ); // Generate the orchestrion runtime forwarders (see `src/config/diagnosticsChannelInjection.ts`) -// from `@sentry/server-utils`' own exports map, so a new subpath there is forwarded automatically. -// Only `require`-able entries get one, since the emitted external is a `require()`. Written as -// plain CJS, not built by rollup: they are loaded by specifier, never bundled. -const SERVER_UTILS = '@sentry/server-utils'; +// from `@sentry/server-runtime-injection`' own exports map, so a new subpath there is forwarded +// automatically. Only `require`-able entries get one, since the emitted external is a `require()`. +// Written as plain CJS, not built by rollup: they are loaded by specifier, never bundled. +const RUNTIME_INJECTION = '@sentry/server-runtime-injection'; const orchestrionRuntimeBuildDir = 'build/orchestrion-runtime'; -const serverUtilsExports = ( - JSON.parse(fs.readFileSync(require.resolve(`${SERVER_UTILS}/package.json`), 'utf8')) as { +const runtimeInjectionExports = ( + JSON.parse(fs.readFileSync(require.resolve(`${RUNTIME_INJECTION}/package.json`), 'utf8')) as { exports: Record; } ).exports; -for (const [key, conditions] of Object.entries(serverUtilsExports)) { +for (const [key, conditions] of Object.entries(runtimeInjectionExports)) { if (key === './package.json' || typeof conditions === 'string' || !conditions.require) { continue; } - // '.' → 'index', './orchestrion/register' → 'orchestrion/register' + // '.' → 'index', './register' → 'register' const forwarderPath = path.join(orchestrionRuntimeBuildDir, `${key === '.' ? 'index' : key.slice(2)}.js`); fs.mkdirSync(path.dirname(forwarderPath), { recursive: true }); fs.writeFileSync( forwarderPath, - `// Generated by scripts/buildRollup.ts — do not edit.\nmodule.exports = require('${SERVER_UTILS}${key.slice(1)}');\n`, + `// Generated by scripts/buildRollup.ts — do not edit.\nmodule.exports = require('${RUNTIME_INJECTION}${key.slice(1)}');\n`, ); } diff --git a/packages/nextjs/src/config/diagnosticsChannelInjection.ts b/packages/nextjs/src/config/diagnosticsChannelInjection.ts index 3040fc6041dc..36939c446f2b 100644 --- a/packages/nextjs/src/config/diagnosticsChannelInjection.ts +++ b/packages/nextjs/src/config/diagnosticsChannelInjection.ts @@ -1,4 +1,4 @@ -import { resolveOrchestrionRuntimeRequest } from '@sentry/server-utils/orchestrion/webpack'; +import { createRequire } from 'node:module'; /** * Instrumented packages verified (via e2e) to bundle correctly, removed from Sentry's own @@ -9,17 +9,38 @@ import { resolveOrchestrionRuntimeRequest } from '@sentry/server-utils/orchestri export const BUNDLE_SAFE_INSTRUMENTED_PACKAGES = ['ioredis']; /** - * `@sentry/server-utils` (where `register.ts` and the bundled orchestrion runtime ship) must stay - * external: `register.ts` passes its own `__filename`/`import.meta.url` as the `parentURL` for - * `Module.register('@sentry/server-utils/orchestrion/hook.mjs', …)`, so that self-reference only + * `@sentry/server-runtime-injection` (where `register.ts` and the bundled orchestrion runtime ship) + * must stay external: `register.ts` passes its own `__filename`/`import.meta.url` as the `parentURL` + * for `Module.register('@sentry/server-runtime-injection/hook', …)`, so that self-reference only * resolves while the code still lives at its real `node_modules` location. Bundled into an app * server chunk instead, the specifier would have to resolve from the chunk's output location, * which fails under isolated installs (pnpm) where the package is a transitive dependency. * - * (The `@apm-js-collab/*` packages no longer appear here: they are bundled into - * `@sentry/server-utils`' build, so no import of them exists at runtime.) + * `@sentry/server-utils` (the barrel + bundler plugins) is NOT here — it is meant to be bundled; the + * build-time snippet's `@sentry/server-utils` import is handled separately by the code-transform. */ -export const ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES = ['@sentry/server-utils']; +export const ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES = ['@sentry/server-runtime-injection']; + +// `require` anchored at THIS package (`@sentry/nextjs`), which depends on +// `@sentry/server-runtime-injection` — so the resolvability check below works even under isolated +// installs (pnpm), where a resolver anchored at `@sentry/server-utils` could not see it. +let nextjsRequire: NodeJS.Require; +/*! rollup-include-cjs-only */ +nextjsRequire = createRequire(__filename); +/*! rollup-include-cjs-only-end */ +/*! rollup-include-esm-only */ +nextjsRequire = createRequire(import.meta.url); +/*! rollup-include-esm-only-end */ + +/** Whether `request` resolves as a `require`-able module (skips ESM-only subpaths like `/hook`). */ +function isRequireResolvable(request: string): boolean { + try { + nextjsRequire.resolve(request); + return true; + } catch { + return false; + } +} /** Remove the given packages from a `serverExternalPackages` list. */ export function filterInstrumentedExternals(externals: string[], packagesToBundle: string[]): string[] { @@ -28,7 +49,7 @@ export function filterInstrumentedExternals(externals: string[], packagesToBundl } /** - * Where the generated forwarders live — one CJS one-liner per `@sentry/server-utils` entrypoint + * Where the generated forwarders live — one CJS one-liner per `@sentry/server-runtime-injection` entrypoint * (see `scripts/buildRollup.ts`). Forwarding through `@sentry/nextjs`, always a direct dependency, * is what makes the emitted specifier both resolvable from `.next/server/**` and relocation-safe. */ @@ -67,7 +88,7 @@ export async function externalizeOrchestrionRuntimePackages({ } // Not `require`-able (ESM-only subpath, or a typo): webpack reports it better than we can. - if (!resolveOrchestrionRuntimeRequest(request)) { + if (!isRequireResolvable(request)) { return undefined; } diff --git a/packages/nextjs/test/config/diagnosticsChannelInjection.test.ts b/packages/nextjs/test/config/diagnosticsChannelInjection.test.ts index 4790ce65d540..effe24b8bef8 100644 --- a/packages/nextjs/test/config/diagnosticsChannelInjection.test.ts +++ b/packages/nextjs/test/config/diagnosticsChannelInjection.test.ts @@ -40,8 +40,8 @@ describe('getServerExternalPackagesPatch (build-time instrumentation)', () => { expect(externals).toContain('mysql'); expect(externals).toContain('pg'); expect(externals).toContain('pg-pool'); - // The orchestrion machinery must be external for the runtime hook to work. - expect(externals).toContain('@sentry/server-utils'); + // The runtime injection package must be external for the runtime hook to work. + expect(externals).toContain('@sentry/server-runtime-injection'); }); it('respects user-provided externals even for bundle-safe packages', () => { @@ -61,10 +61,8 @@ describe('getServerExternalPackagesPatch (build-time instrumentation)', () => { describe('externalizeOrchestrionRuntimePackages', () => { // An absolute path here breaks every deploy that relocates the output, so it has to stay bare. it.each([ - ['@sentry/server-utils', '@sentry/nextjs/orchestrion-runtime/index'], - ['@sentry/server-utils/orchestrion/config', '@sentry/nextjs/orchestrion-runtime/orchestrion/config'], - ['@sentry/server-utils/orchestrion/register', '@sentry/nextjs/orchestrion-runtime/orchestrion/register'], - ['@sentry/server-utils/orchestrion/webpack', '@sentry/nextjs/orchestrion-runtime/orchestrion/webpack'], + ['@sentry/server-runtime-injection', '@sentry/nextjs/orchestrion-runtime/index'], + ['@sentry/server-runtime-injection/register', '@sentry/nextjs/orchestrion-runtime/register'], ])('externalizes %s as the relocatable bare specifier %s', async (request, expected) => { const external = await externalizeOrchestrionRuntimePackages({ request }); @@ -72,19 +70,19 @@ describe('externalizeOrchestrionRuntimePackages', () => { expect(isAbsolute(expected)).toBe(false); }); - it('ignores the bundled @apm-js-collab packages — no import of them exists in the dist anymore', async () => { + it('does not externalize @sentry/server-utils — it is meant to be bundled', async () => { await expect( - externalizeOrchestrionRuntimePackages({ request: '@apm-js-collab/tracing-hooks' }), + externalizeOrchestrionRuntimePackages({ request: '@sentry/server-utils/orchestrion/config' }), ).resolves.toBeUndefined(); }); // A `commonjs` external could never load these, so webpack gets to report them instead. - it('ignores subpaths @sentry/server-utils does not expose to require()', async () => { + it('ignores subpaths @sentry/server-runtime-injection does not expose to require()', async () => { await expect( - externalizeOrchestrionRuntimePackages({ request: '@sentry/server-utils/orchestrion/hook' }), + externalizeOrchestrionRuntimePackages({ request: '@sentry/server-runtime-injection/hook' }), ).resolves.toBeUndefined(); await expect( - externalizeOrchestrionRuntimePackages({ request: '@sentry/server-utils/does-not-exist' }), + externalizeOrchestrionRuntimePackages({ request: '@sentry/server-runtime-injection/does-not-exist' }), ).resolves.toBeUndefined(); }); @@ -92,27 +90,28 @@ describe('externalizeOrchestrionRuntimePackages', () => { await expect(externalizeOrchestrionRuntimePackages({ request: 'some-other-package' })).resolves.toBeUndefined(); // Prefix matching must not leak beyond a package-name boundary. await expect( - externalizeOrchestrionRuntimePackages({ request: '@sentry/server-utils-extras' }), + externalizeOrchestrionRuntimePackages({ request: '@sentry/server-runtime-injection-extras' }), ).resolves.toBeUndefined(); await expect(externalizeOrchestrionRuntimePackages({})).resolves.toBeUndefined(); }); }); // Exercises the generated artifacts, so it needs the package built — as does this file's import of -// `@sentry/server-utils`. +// `@sentry/server-runtime-injection`. describe('orchestrion runtime forwarders (generated)', () => { const nodeRequire = createRequire(import.meta.url); const forwarderDir = fileURLToPath(new URL('../../build/orchestrion-runtime/', import.meta.url)); - /** Every `@sentry/server-utils` entrypoint that a `commonjs` external could load. */ + /** Every `@sentry/server-runtime-injection` entrypoint that a `commonjs` external could load. */ const requireableSubpaths = Object.entries( - (nodeRequire('@sentry/server-utils/package.json') as { exports: Record }).exports, + (nodeRequire('@sentry/server-runtime-injection/package.json') as { exports: Record }) + .exports, ) .filter(([key, conditions]) => key !== './package.json' && conditions.require) .map(([key]) => key); it.each(requireableSubpaths)('generates a forwarder for %s that re-exports it unchanged', subpath => { - const request = `@sentry/server-utils${subpath.slice(1)}`; + const request = `@sentry/server-runtime-injection${subpath.slice(1)}`; const forwarderFile = `${forwarderDir}${subpath === '.' ? 'index' : subpath.slice(2)}.js`; expect(existsSync(forwarderFile)).toBe(true); @@ -122,8 +121,8 @@ describe('orchestrion runtime forwarders (generated)', () => { // The emitted specifier must resolve the way it will from a chunk: through the package exports. it.each(requireableSubpaths)('exposes the forwarder for %s through the package exports', subpath => { const specifier = getOrchestrionForwarderSpecifier( - `@sentry/server-utils${subpath.slice(1)}`, - '@sentry/server-utils', + `@sentry/server-runtime-injection${subpath.slice(1)}`, + '@sentry/server-runtime-injection', ); expect(nodeRequire.resolve(specifier)).toBe(`${forwarderDir}${subpath === '.' ? 'index' : subpath.slice(2)}.js`); diff --git a/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts b/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts index 96eb751bc697..13371b1ce170 100644 --- a/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts +++ b/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts @@ -759,8 +759,8 @@ describe('constructWebpackConfigFunction()', () => { const externals = finalWebpackConfig.externals as ((data: { request?: string }) => Promise)[]; expect(Array.isArray(externals)).toBe(true); - await expect(externals[0]({ request: '@sentry/server-utils/orchestrion/register' })).resolves.toBe( - 'commonjs @sentry/nextjs/orchestrion-runtime/orchestrion/register', + await expect(externals[0]({ request: '@sentry/server-runtime-injection/register' })).resolves.toBe( + 'commonjs @sentry/nextjs/orchestrion-runtime/register', ); await expect(externals[0]({ request: 'some-other-package' })).resolves.toBeUndefined(); }); diff --git a/packages/node/README.md b/packages/node/README.md index a2a32c69ab9f..28fe7f0bd68f 100644 --- a/packages/node/README.md +++ b/packages/node/README.md @@ -75,18 +75,18 @@ NODE_OPTIONS="--import ./instrument.mjs" npm run start ### Bundling your server `@sentry/node` installs its automatic (diagnostics-channel) instrumentation through a runtime module -hook that ships in `@sentry/server-utils` and is designed to run from `node_modules`. There are two +hook that ships in `@sentry/server-runtime-injection` and is designed to run from `node_modules`. There are two supported ways to keep auto-instrumentation working when you bundle your server: -1. **Keep `@sentry/server-utils` external** (do not inline it into the bundle) so the runtime hook +1. **Keep `@sentry/server-runtime-injection` external** (do not inline it into the bundle) so the runtime hook loads from `node_modules`. Most bundlers externalize `node_modules` for a Node target by default; - if yours inlines everything, mark `@sentry/server-utils` as external explicitly. + if yours inlines everything, mark `@sentry/server-runtime-injection` as external explicitly. 2. **Instrument at build time** with the Sentry bundler plugins (`@sentry/node/esbuild`, `@sentry/node/webpack`, `@sentry/node/vite`, `@sentry/node/rollup`), which inject the instrumentation into your bundled dependencies during the build. In this mode the runtime hook is not needed. -If you bundle `@sentry/server-utils` **and** don't use the build-time plugin, its internal code +If you bundle `@sentry/server-runtime-injection` **and** don't use the build-time plugin, its internal code transformer is stripped and runtime auto-instrumentation is disabled — `@sentry/node` warns at startup when it detects this. (When the build-time plugin is used, there is no warning, since instrumentation is already in place.) diff --git a/packages/node/package.json b/packages/node/package.json index c3045e02e893..5970c580d110 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -81,6 +81,7 @@ "@sentry/conventions": "^0.20.0", "@sentry/core": "10.67.0", "@sentry/opentelemetry": "10.67.0", + "@sentry/server-runtime-injection": "10.67.0", "@sentry/server-utils": "10.67.0", "@sentry/bundler-plugins": "10.67.0" }, diff --git a/packages/node/src/sdk/index.ts b/packages/node/src/sdk/index.ts index 623a1ec3805c..d98b800c8abb 100644 --- a/packages/node/src/sdk/index.ts +++ b/packages/node/src/sdk/index.ts @@ -17,7 +17,7 @@ import { } from '@sentry/core'; import { isMainThread, parentPort } from 'node:worker_threads'; import { detectOrchestrionSetup, getErrorIntegrations, getTracingIntegrations } from '@sentry/server-utils'; -import { registerDiagnosticsChannelInjection } from '@sentry/server-utils/orchestrion/register'; +import { registerDiagnosticsChannelInjection } from '@sentry/server-runtime-injection/register'; import { DEBUG_BUILD } from '../debug-build'; import { childProcessIntegration } from '../integrations/childProcess'; import { consoleIntegration } from '../integrations/console'; diff --git a/packages/node/test/sdk/diagnosticsChannelInjection.test.ts b/packages/node/test/sdk/diagnosticsChannelInjection.test.ts index 782b7a1c093d..fab55e968706 100644 --- a/packages/node/test/sdk/diagnosticsChannelInjection.test.ts +++ b/packages/node/test/sdk/diagnosticsChannelInjection.test.ts @@ -6,7 +6,7 @@ const { registerDiagnosticsChannelInjection, detectOrchestrionSetup } = vi.hoist detectOrchestrionSetup: vi.fn(), })); -vi.mock('@sentry/server-utils/orchestrion/register', () => ({ +vi.mock('@sentry/server-runtime-injection/register', () => ({ registerDiagnosticsChannelInjection, })); vi.mock('@sentry/server-utils', async importOriginal => { diff --git a/packages/nuxt/README.md b/packages/nuxt/README.md index 13fe27528588..695c6972400b 100644 --- a/packages/nuxt/README.md +++ b/packages/nuxt/README.md @@ -29,7 +29,7 @@ functionality related to Nuxt. ## Troubleshoot If your server-side auto-instrumentation stops recording spans after bundling (e.g. certain Nitro -presets), make sure `@sentry/server-utils` is kept **external** in the Nitro/server build rather than +presets), make sure `@sentry/server-runtime-injection` is kept **external** in the Nitro/server build rather than inlined — its runtime module hook must resolve from `node_modules`. `@sentry/node` logs a warning at startup when it detects it was bundled. diff --git a/packages/server-runtime-injection/README.md b/packages/server-runtime-injection/README.md new file mode 100644 index 000000000000..e7c09f0935f0 --- /dev/null +++ b/packages/server-runtime-injection/README.md @@ -0,0 +1,22 @@ +

+ + Sentry + +

+ +# Sentry Server Runtime Injection + +[![npm version](https://img.shields.io/npm/v/@sentry/server-runtime-injection.svg)](https://www.npmjs.com/package/@sentry/server-runtime-injection) + +This is an internal package for the Sentry JavaScript SDKs. It is not part of the public API contract +and may change at any time. + +It contains the **runtime** diagnostics-channel injection used by the server SDKs — the module hooks +that transform instrumented dependencies as they load at runtime (`register`, `hook`, `import-hook`), +together with the vendored code transformer they rely on. + +> **Important:** this package must be kept **external** (not bundled) when bundling a server. Its +> runtime hook loads a transformer that self-references its own on-disk `node_modules` location; +> bundling it strips the transformer and breaks that self-reference. When you bundle your server, +> either keep `@sentry/server-runtime-injection` external, or rely on the build-time instrumentation +> from the Sentry bundler plugins instead. diff --git a/packages/server-runtime-injection/package.json b/packages/server-runtime-injection/package.json new file mode 100644 index 000000000000..90cd06ef804c --- /dev/null +++ b/packages/server-runtime-injection/package.json @@ -0,0 +1,94 @@ +{ + "name": "@sentry/server-runtime-injection", + "version": "10.67.0", + "description": "Runtime diagnostics-channel injection hooks for the Sentry JavaScript SDKs. Must be kept external (not bundled) when bundling a server.", + "repository": "git://github.com/getsentry/sentry-javascript.git", + "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/server-runtime-injection", + "author": "Sentry", + "license": "MIT", + "engines": { + "node": ">=20.19.0 <22.0.0 || >=22.12.0 <23.0.0 || >=23.2.0" + }, + "files": [ + "/build" + ], + "main": "build/cjs/register.js", + "module": "build/esm/register.js", + "types": "build/types/register.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "types": "./build/types/register.d.ts", + "import": "./build/esm/register.js", + "require": "./build/cjs/register.js" + }, + "./register": { + "types": "./build/types/register.d.ts", + "import": "./build/esm/register.js", + "require": "./build/cjs/register.js" + }, + "./hook": { + "import": "./build/esm/hook.js" + }, + "./import-hook": { + "import": "./build/import-hook.mjs" + } + }, + "typesVersions": { + "*": { + "register": [ + "build/types/register.d.ts" + ] + } + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@sentry/core": "10.67.0", + "@sentry/server-utils": "10.67.0" + }, + "devDependencies": { + "@apm-js-collab/code-transformer": "^0.18.1", + "@apm-js-collab/tracing-hooks": "^0.13.0", + "@types/node": "^18.19.1", + "meriyah": "^6.1.4" + }, + "scripts": { + "build": "run-p build:transpile build:types", + "build:dev": "yarn build", + "build:transpile": "rollup -c rollup.npm.config.mjs", + "build:types": "tsc -p tsconfig.types.json", + "build:watch": "run-p build:transpile:watch", + "build:dev:watch": "run-p build:transpile:watch", + "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", + "build:tarball": "npm pack", + "clean": "rimraf build coverage sentry-server-runtime-injection-*.tgz", + "lint:fix": "oxlint . --fix --type-aware", + "lint": "oxlint . --type-aware", + "lint:types": "oxlint src --type-aware --type-check", + "lint:es-compatibility": "es-check es2020 ./build/cjs/*.js && es-check es2020 ./build/esm/*.js --module", + "test:unit": "vitest run", + "test": "vitest run", + "test:watch": "vitest --watch", + "yalc:publish": "yalc publish --push --sig" + }, + "volta": { + "extends": "../../package.json" + }, + "sideEffects": false, + "nx": { + "targets": { + "build:transpile": { + "outputs": [ + "{projectRoot}/build/esm", + "{projectRoot}/build/cjs", + "{projectRoot}/build/npm/esm", + "{projectRoot}/build/npm/cjs", + "{projectRoot}/build/orchestrion", + "{projectRoot}/build/import-hook.mjs" + ] + } + } + } +} diff --git a/packages/server-runtime-injection/rollup.npm.config.mjs b/packages/server-runtime-injection/rollup.npm.config.mjs new file mode 100644 index 000000000000..8a121a88834f --- /dev/null +++ b/packages/server-runtime-injection/rollup.npm.config.mjs @@ -0,0 +1,116 @@ +import { builtinModules } from 'node:module'; +import commonjs from '@rollup/plugin-commonjs'; +import license from 'rollup-plugin-license'; +import { defineConfig } from 'rollup'; +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; + +// The orchestrion runtime dependency chain (`@apm-js-collab/tracing-hooks` → +// `@apm-js-collab/code-transformer` → meriyah/esquery/astring/…) is bundled into this package's +// build instead of installed as runtime dependencies. Everything in the chain is plain JS, and +// bundling removes two whole classes of downstream breakage: +// +// 1. `require(esm)`: the chain's only sync entry (`hook-sync.mjs`) is ESM-only, so an installed +// dependency forces our CJS build through Node's `require(esm)` bridge — unavailable on the AWS +// Lambda runtime (`--no-experimental-require-module`) and broken on `Module.register()` loader +// threads on Node 22.15–24.12 (`The resolveSync() method is not implemented`). Compiled into our +// own dual build, the CJS variant is genuine CJS. +// 2. Tracer/runtime exports-map mismatches: meriyah 6.1's `module-sync`-first exports map is +// resolved differently by build-time tracers (`@vercel/nft`, nf3, Nitro externals) than by the +// runtime CJS loader, producing pruned server bundles that crash with `MODULE_NOT_FOUND` +// (https://github.com/vercel/nft/issues/603, https://github.com/nitrojs/nitro/issues/4456). +// Bundled, there is no runtime package resolution left to get wrong. +// +// `@sentry/*` deps (including `@sentry/server-utils`, from which `register.ts` imports +// `SENTRY_INSTRUMENTATIONS`) stay external — the base config keeps them out of the bundle, so they +// resolve from `node_modules` at runtime. +// +// `requireReturnsDefault: 'auto'`: node-resolve prefers a dependency's ESM build even for CJS +// `require()`s inside the vendored graph. Default-export-only ESM (e.g. esquery) must then resolve +// to the default itself, not a `{ default }` namespace — CJS callers use it as +// `require('esquery').parse(...)`. +// +// `strictRequires: false`: the default `'auto'` wraps conditionally-required modules (e.g. +// `debug`'s browser/node split) in lazy initializers exported as `__require` — an export name that +// downstream re-bundlers mishandle (Turbopack renames it, producing `.require is not a function` +// crashes in Next.js on Cloudflare). Hoisting is safe here: the vendored graph is closed (nothing +// optional/missing) and has no require cycles that depend on lazy evaluation. +const commonJSOptions = { transformMixedEsModules: true, requireReturnsDefault: 'auto', strictRequires: false }; +const commonJSPlugin = commonjs(commonJSOptions); + +// Always vendor `debug`'s Node build. Its default entry picks browser vs node at require time, +// which drags the browser build into this server-only bundle — and, hoisted by +// `strictRequires: false`, the browser build's storage detection probes `localStorage` at import +// time, which on Node >= 26 emits an ExperimentalWarning that pollutes stderr and console +// breadcrumbs in every user app. `order: 'pre'` because the base config's node-resolve plugin +// sorts ahead of package-specific plugins and would otherwise resolve `debug` first. +const debugNodeAlias = { + name: 'debug-node-alias', + resolveId: { + order: 'pre', + handler(source, importer) { + return source === 'debug' ? this.resolve('debug/src/node.js', importer, { skipSelf: true }) : null; + }, + }, +}; + +// Bundling files from the repo-root `node_modules` moves rollup's common source ancestor up to the +// repo root, so `preserveModules` names our own files `packages/server-runtime-injection/src/...` — +// strip that prefix to keep the `build/cjs/register.js` layout the `exports` map points at. And npm +// never packs `node_modules` directories, so the vendored dependencies must not be emitted under +// that name. +const sanitizedFileNames = info => + `${info.name.replace(/^packages\/server-runtime-injection\/src\//, '').replace(/node_modules/g, 'vendored')}.js`; + +// The vendored dependencies (see above) are third-party code redistributed inside this package's +// published `build/`, so their licenses require us to carry each one's copyright/permission notice +// (and, for Apache-2.0 deps like `@apm-js-collab/*`, the upstream NOTICE). Rollup strips per-file +// banners, so instead we aggregate them into a single `build/THIRD-PARTY-LICENSES.txt`. +const thirdPartyLicensePlugin = license({ + thirdParty: { + includePrivate: false, + output: { + file: 'build/THIRD-PARTY-LICENSES.txt', + }, + }, +}); + +const orchestrionRuntimeHooks = [ + // The side-effecting `--import` entry SDKs reference via a `--import` flag. We pass it through + // rollup only to copy it to `build/import-hook.mjs` at the path the package.json `exports` map + // expects; `external: /.*/` keeps every import (`@sentry/server-runtime-injection/register`) a + // runtime resolution against the installed package. + defineConfig({ + input: 'src/import-hook.mjs', + external: /.*/, + output: { format: 'esm', file: 'build/import-hook.mjs' }, + }), +]; + +export default [ + ...orchestrionRuntimeHooks, + ...makeNPMConfigVariants( + makeBaseNPMConfig({ + // `register.ts` backs `./register` (the Node SDK `require`s it synchronously from + // `Sentry.init()`); `hook.mjs` backs `./hook` (the async `Module.register()` target, loaded on + // Node's ESM loader thread, which cannot resolve bare specifiers into the vendored chunks — but + // relative imports work, so it shares the ESM build's vendored chunks). `./hook` only maps its + // `import` condition, so the `build/cjs` copy is unused. + entrypoints: ['src/register.ts', 'src/hook.mjs'], + packageSpecificConfig: { + plugins: [debugNodeAlias, commonJSPlugin, thirdPartyLicensePlugin], + output: { + exports: 'named', + preserveModules: true, + entryFileNames: sanitizedFileNames, + // The commonjs-converted vendored dependencies import Node builtins as default imports + // (`require('path')` → default import of `path`), and builtins have no `.default` in CJS — + // so builtins need `'default'` interop (the module itself is the default export). + interop: id => (id && (id.startsWith('node:') || builtinModules.includes(id)) ? 'default' : 'esModule'), + // The vendored dependencies import builtins unprefixed (`import … from 'tty'`), which Deno + // rejects and vite-node (Node 26) misresolves as a relative path. Emit them `node:`-prefixed. + paths: Object.fromEntries(builtinModules.map(m => [m, `node:${m}`])), + }, + }, + }), + ), +]; diff --git a/packages/server-utils/src/orchestrion/runtime/apm-js-collab-tracing-hooks.d.ts b/packages/server-runtime-injection/src/apm-js-collab-tracing-hooks.d.ts similarity index 100% rename from packages/server-utils/src/orchestrion/runtime/apm-js-collab-tracing-hooks.d.ts rename to packages/server-runtime-injection/src/apm-js-collab-tracing-hooks.d.ts diff --git a/packages/server-utils/src/orchestrion/runtime/hook.mjs b/packages/server-runtime-injection/src/hook.mjs similarity index 92% rename from packages/server-utils/src/orchestrion/runtime/hook.mjs rename to packages/server-runtime-injection/src/hook.mjs index 93460849cade..e0f187804615 100644 --- a/packages/server-utils/src/orchestrion/runtime/hook.mjs +++ b/packages/server-runtime-injection/src/hook.mjs @@ -6,5 +6,5 @@ // on-disk ES module graph — the loader thread cannot resolve bare specifiers into the dependency // graph this package bundles away, but it can follow relative imports. This shim is therefore an // entrypoint of the regular ESM build (sharing the vendored dependency chunks) and exposed via the -// `@sentry/server-utils/orchestrion/hook` subpath. +// `@sentry/server-runtime-injection/hook` subpath. export * from '@apm-js-collab/tracing-hooks/hook.mjs'; diff --git a/packages/server-utils/src/orchestrion/runtime/import-hook.mjs b/packages/server-runtime-injection/src/import-hook.mjs similarity index 88% rename from packages/server-utils/src/orchestrion/runtime/import-hook.mjs rename to packages/server-runtime-injection/src/import-hook.mjs index a99882e686e3..99f52d5a075d 100644 --- a/packages/server-utils/src/orchestrion/runtime/import-hook.mjs +++ b/packages/server-runtime-injection/src/import-hook.mjs @@ -8,11 +8,11 @@ // and the `init()` path can never drift apart. This file is just the // side-effecting wrapper that invokes it. // -// This file is shipped as-is to `build/orchestrion/import-hook.mjs`. Keep it a +// This file is shipped as-is to `build/import-hook.mjs`. Keep it a // single self-contained `.mjs` file with no relative-path imports — `--import` // resolves it (and the bare specifier below) via Node's module resolution // against the installed package. -import { registerDiagnosticsChannelInjection } from '@sentry/server-utils/orchestrion/register'; +import { registerDiagnosticsChannelInjection } from '@sentry/server-runtime-injection/register'; registerDiagnosticsChannelInjection(); diff --git a/packages/server-utils/src/orchestrion/runtime/register.ts b/packages/server-runtime-injection/src/register.ts similarity index 89% rename from packages/server-utils/src/orchestrion/runtime/register.ts rename to packages/server-runtime-injection/src/register.ts index 340bdaf066d4..4a5bd458502a 100644 --- a/packages/server-utils/src/orchestrion/runtime/register.ts +++ b/packages/server-runtime-injection/src/register.ts @@ -2,7 +2,7 @@ import { consoleSandbox, debug, getClient, GLOBAL_OBJ, parseSemver } from '@sent import * as Module from 'node:module'; import { pathToFileURL } from 'node:url'; import { create } from '@apm-js-collab/code-transformer'; -import { SENTRY_INSTRUMENTATIONS } from '../config'; +import { SENTRY_INSTRUMENTATIONS } from '@sentry/server-utils/orchestrion/config'; import type { register } from 'node:module'; import ModulePatch from '@apm-js-collab/tracing-hooks'; import { initialize, load, resolve, createDiagnosticsPort } from '@apm-js-collab/tracing-hooks/hook-sync.mjs'; @@ -32,8 +32,8 @@ function hasStableSyncModuleHooks(isDeno: boolean): boolean { * package) survived downstream bundling. * * This package ships the transformer inline and is meant to run from `node_modules` (external). When - * an app bundler instead inlines `@sentry/server-utils` and tree-shakes it, those vendored deps are - * stripped to empty objects, so `parse`/`generate` become `undefined` and the FIRST module the hook + * an app bundler instead inlines `@sentry/server-runtime-injection` and tree-shakes it, those vendored + * deps are stripped to empty objects, so `parse`/`generate` become `undefined` and the FIRST module the hook * tries to transform throws `TypeError: parse is not a function` — deep in the loader, once per * module, only visible with `debug: true`. Running one throwaway in-memory transform up front turns * that into a single, actionable, always-on warning (see `warnRuntimeUnavailable`). A healthy build @@ -105,14 +105,14 @@ export function registerDiagnosticsChannelInjection(): void { // surface an always-on, actionable warning. if (marker.bundler instanceof Set) { debug.log( - 'Runtime diagnostics-channel injection is disabled because `@sentry/server-utils` was bundled; ' + - 'build-time instrumentation is active.', + 'Runtime diagnostics-channel injection is disabled because `@sentry/server-runtime-injection` was ' + + 'bundled; build-time instrumentation is active.', ); } else { warnRuntimeUnavailable( - '`@sentry/server-utils` was bundled into your application, so diagnostics-channel ' + - 'auto-instrumentation is disabled. Keep `@sentry/server-utils` external in your server bundle, ' + - 'or use the Sentry bundler plugin for build-time instrumentation.', + '`@sentry/server-runtime-injection` was bundled into your application, so diagnostics-channel ' + + 'auto-instrumentation is disabled. Keep `@sentry/server-runtime-injection` external in your server ' + + 'bundle, or use the Sentry bundler plugin for build-time instrumentation.', ); } return; @@ -163,10 +163,11 @@ export function registerDiagnosticsChannelInjection(): void { parentURL = import.meta.url; /*! rollup-include-esm-only-end */ - // Our own bundled copy of the tracing-hooks async hooks (see - // `src/orchestrion/runtime/hook.mjs`) — the dependency itself is bundled into this package's - // build and no longer resolvable as a bare specifier at runtime. - mod.register('@sentry/server-utils/orchestrion/hook', { + // Our own bundled copy of the tracing-hooks async hooks (see `src/hook.mjs`) — the dependency + // itself is bundled into this package's build and no longer resolvable as a bare specifier at + // runtime. This self-referential specifier only resolves while this package lives at its real + // `node_modules` location, which is why it must stay external (never bundled into an app). + mod.register('@sentry/server-runtime-injection/hook', { parentURL, data: { instrumentations: SENTRY_INSTRUMENTATIONS, diagnosticsPort }, transferList: [diagnosticsPort], diff --git a/packages/server-utils/test/orchestrion/register.test.ts b/packages/server-runtime-injection/test/register.test.ts similarity index 97% rename from packages/server-utils/test/orchestrion/register.test.ts rename to packages/server-runtime-injection/test/register.test.ts index b21e0227d9ae..af34afab7712 100644 --- a/packages/server-utils/test/orchestrion/register.test.ts +++ b/packages/server-runtime-injection/test/register.test.ts @@ -17,7 +17,7 @@ vi.mock('@sentry/core', async importOriginal => { }); import { GLOBAL_OBJ } from '@sentry/core'; -import { registerDiagnosticsChannelInjection } from '../../src/orchestrion/runtime/register'; +import { registerDiagnosticsChannelInjection } from '../src/register'; describe('registerDiagnosticsChannelInjection - bundled/tree-shaken detection', () => { let warnSpy: ReturnType; diff --git a/packages/server-runtime-injection/tsconfig.json b/packages/server-runtime-injection/tsconfig.json new file mode 100644 index 000000000000..6af250d0358c --- /dev/null +++ b/packages/server-runtime-injection/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + + "include": ["src/**/*"], + + "compilerOptions": {}, + // `hook.mjs` and `import-hook.mjs` are hand-written `.mjs` files that self-reference this package's + // own subpaths. If tsc picks them up it follows those subpath exports back to emitted `.d.ts` + // files and collides with what it wants to emit. Excluding them keeps tsc focused on the `.ts` + // sources — rollup copies the `.mjs` files through to `build/` unchanged. + "exclude": ["src/**/*.mjs", "src/**/*.cjs"] +} diff --git a/packages/server-runtime-injection/tsconfig.test.json b/packages/server-runtime-injection/tsconfig.test.json new file mode 100644 index 000000000000..851f2a16b733 --- /dev/null +++ b/packages/server-runtime-injection/tsconfig.test.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + + "include": ["test/**/*", "vite.config.ts"], + + "compilerOptions": { + // should include all types from `./tsconfig.json` plus types for all test frameworks used + "types": ["node", "vitest"] + } +} diff --git a/packages/server-runtime-injection/tsconfig.types.json b/packages/server-runtime-injection/tsconfig.types.json new file mode 100644 index 000000000000..ab12a03a64f2 --- /dev/null +++ b/packages/server-runtime-injection/tsconfig.types.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "build/types", + "rootDir": "src" + } +} diff --git a/packages/server-runtime-injection/vite.config.ts b/packages/server-runtime-injection/vite.config.ts new file mode 100644 index 000000000000..841ff483d7c4 --- /dev/null +++ b/packages/server-runtime-injection/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; +import baseConfig from '../../vite/vite.config'; + +export default defineConfig({ + ...baseConfig, + test: { + ...baseConfig.test, + }, +}); diff --git a/packages/server-utils/package.json b/packages/server-utils/package.json index 51360b990abf..20ac27e66f33 100644 --- a/packages/server-utils/package.json +++ b/packages/server-utils/package.json @@ -32,11 +32,6 @@ "import": "./build/esm/orchestrion/config/index.js", "require": "./build/cjs/orchestrion/config/index.js" }, - "./orchestrion/register": { - "types": "./build/types/orchestrion/runtime/register.d.ts", - "import": "./build/esm/orchestrion/runtime/register.js", - "require": "./build/cjs/orchestrion/runtime/register.js" - }, "./orchestrion/vite": { "types": "./build/types/orchestrion/bundler/vite.d.ts", "import": "./build/esm/orchestrion/bundler/vite.js", @@ -61,11 +56,10 @@ "import": "./build/esm/orchestrion/bundler/esbuild.js", "require": "./build/cjs/orchestrion/bundler/esbuild.js" }, - "./orchestrion/import-hook": { - "import": "./build/orchestrion/import-hook.mjs" - }, - "./orchestrion/hook": { - "import": "./build/esm/orchestrion/runtime/hook.js" + "./orchestrion/bundler-transforms": { + "types": "./build/types/orchestrion/bundler/moduleInjectedTransform.d.ts", + "import": "./build/esm/orchestrion/bundler/moduleInjectedTransform.js", + "require": "./build/cjs/orchestrion/bundler/moduleInjectedTransform.js" } }, "typesVersions": { @@ -73,9 +67,6 @@ "orchestrion/config": [ "build/types/orchestrion/config/index.d.ts" ], - "orchestrion/register": [ - "build/types/orchestrion/runtime/register.d.ts" - ], "orchestrion/vite": [ "build/types/orchestrion/bundler/vite.d.ts" ], @@ -87,6 +78,9 @@ ], "orchestrion/esbuild": [ "build/types/orchestrion/bundler/esbuild.d.ts" + ], + "orchestrion/bundler-transforms": [ + "build/types/orchestrion/bundler/moduleInjectedTransform.d.ts" ] } }, @@ -101,7 +95,6 @@ "devDependencies": { "@apm-js-collab/code-transformer": "^0.18.1", "@apm-js-collab/code-transformer-bundler-plugins": "^0.7.4", - "@apm-js-collab/tracing-hooks": "^0.13.0", "@types/node": "^18.19.1", "meriyah": "^6.1.4", "vite": "^6.4.3" @@ -136,8 +129,7 @@ "{projectRoot}/build/esm", "{projectRoot}/build/cjs", "{projectRoot}/build/npm/esm", - "{projectRoot}/build/npm/cjs", - "{projectRoot}/build/orchestrion" + "{projectRoot}/build/npm/cjs" ] } } diff --git a/packages/server-utils/rollup.npm.config.mjs b/packages/server-utils/rollup.npm.config.mjs index 6b7c30b5a292..1a64c3879e17 100644 --- a/packages/server-utils/rollup.npm.config.mjs +++ b/packages/server-utils/rollup.npm.config.mjs @@ -1,20 +1,15 @@ import { builtinModules } from 'node:module'; import commonjs from '@rollup/plugin-commonjs'; import license from 'rollup-plugin-license'; -import { defineConfig } from 'rollup'; import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; -// The orchestrion runtime dependency chain (`@apm-js-collab/tracing-hooks` → -// `@apm-js-collab/code-transformer` → meriyah/esquery/astring/…) is bundled into this package's -// build instead of installed as runtime dependencies. Everything in the chain is plain JS, and -// bundling removes two whole classes of downstream breakage: +// The orchestrion build-time bundler-plugin chain (`@apm-js-collab/code-transformer-bundler-plugins` +// → `@apm-js-collab/code-transformer` → meriyah/esquery/astring/…) is bundled into this package's +// build instead of installed as runtime dependencies. (The runtime injection chain lives in +// `@sentry/server-runtime-injection`.) Everything here is plain JS, and bundling removes a class of +// downstream breakage: // -// 1. `require(esm)`: the chain's only sync entry (`hook-sync.mjs`) is ESM-only, so an installed -// dependency forces our CJS build through Node's `require(esm)` bridge — unavailable on the AWS -// Lambda runtime (`--no-experimental-require-module`) and broken on `Module.register()` loader -// threads on Node 22.15–24.12 (`The resolveSync() method is not implemented`). Compiled into our -// own dual build, the CJS variant is genuine CJS. -// 2. Tracer/runtime exports-map mismatches: meriyah 6.1's `module-sync`-first exports map is +// Tracer/runtime exports-map mismatches: meriyah 6.1's `module-sync`-first exports map is // resolved differently by build-time tracers (`@vercel/nft`, nf3, Nitro externals) than by the // runtime CJS loader, producing pruned server bundles that crash with `MODULE_NOT_FOUND` // (https://github.com/vercel/nft/issues/603, https://github.com/nitrojs/nitro/issues/4456). @@ -79,21 +74,7 @@ const thirdPartyLicensePlugin = license({ }, }); -const orchestrionRuntimeHooks = [ - // EXPERIMENTAL — orchestrion.js runtime hook. A hand-written `.mjs` shim that SDKs reference via - // a `--import .../orchestrion/import-hook` flag. We pass it through rollup only to copy it into - // `build/orchestrion/` at the path the package.json `exports` map expects; `external: /.*/` keeps - // every import (e.g. `@sentry/server-utils/orchestrion/config`) as a runtime resolution against - // the installed package. - defineConfig({ - input: 'src/orchestrion/runtime/import-hook.mjs', - external: /.*/, - output: { format: 'esm', file: 'build/orchestrion/import-hook.mjs' }, - }), -]; - export default [ - ...orchestrionRuntimeHooks, ...makeNPMConfigVariants( makeBaseNPMConfig({ // `src/orchestrion/config/index.ts` and the `src/orchestrion/bundler/*.ts` @@ -101,21 +82,14 @@ export default [ // `.../orchestrion/vite`, etc.) — none are reachable from `src/index.ts`, so // we list them as separate entrypoints to guarantee they end up in build/esm // and build/cjs. + // + // The runtime diagnostics-channel injection (`register`/`hook`/`import-hook` + the vendored + // transformer chain) lives in `@sentry/server-runtime-injection` — it must stay external when + // apps bundle, so it is a separate package rather than a subpath here. entrypoints: [ 'src/index.ts', 'src/index.no-diagnostic-channels.ts', 'src/orchestrion/config/index.ts', - // `src/orchestrion/runtime/register.ts` backs the `./orchestrion/register` - // subpath export; the Node SDK `require`s it synchronously from - // `Sentry.init()` to install the channel-injection hooks. - 'src/orchestrion/runtime/register.ts', - // The async module hooks passed to `Module.register()`. They load on Node's ESM loader - // thread, which cannot resolve bare specifiers into our bundled dependency graph — but - // relative imports of on-disk files work, and `build/esm` is a `"type": "module"` scope, so - // this entrypoint shares the vendored chunks with the rest of the build. The `./orchestrion/ - // hook` export only maps its `import` condition (nothing ever `require()`s it), so the copy - // in `build/cjs` is unused. - 'src/orchestrion/runtime/hook.mjs', 'src/orchestrion/bundler/vite.ts', 'src/orchestrion/bundler/rollup.ts', 'src/orchestrion/bundler/webpack.ts', diff --git a/packages/server-utils/src/orchestrion/config/index.ts b/packages/server-utils/src/orchestrion/config/index.ts index d105b967a72a..c2e8415448e8 100644 --- a/packages/server-utils/src/orchestrion/config/index.ts +++ b/packages/server-utils/src/orchestrion/config/index.ts @@ -34,11 +34,6 @@ import { vercelAiConfig } from './vercel-ai'; // Kept sorted alphabetically by module so concurrent additions insert at different // points rather than all appending to the end (fewer merge conflicts). -// Re-exported here for bundler integrations that compose the upstream -// code-transformer plugin themselves instead of using one of our wrappers -// (`@sentry/bun`'s plugin uses the upstream `/bun` entry directly). -export { moduleInjectedTransforms, ORCHESTRION_BUNDLER_MARKER_BANNER } from '../bundler/moduleInjectedTransform'; - /** * The orchestrion code-transform configs. Every instrumentable library is here * so the transform is all-or-nothing: whenever orchestrion is enabled, all of diff --git a/packages/server-utils/test/orchestrion/bundler.test.ts b/packages/server-utils/test/orchestrion/bundler.test.ts index 1c1cd1c32977..86ccfb1a42c9 100644 --- a/packages/server-utils/test/orchestrion/bundler.test.ts +++ b/packages/server-utils/test/orchestrion/bundler.test.ts @@ -285,13 +285,8 @@ describe('buildTimeInstrumentation: false', () => { describe('resolveOrchestrionRuntimeRequest', () => { it.each([ // Self-references — resolve through this package's own exports map to the CJS build. - '@sentry/server-utils/orchestrion/register', '@sentry/server-utils/orchestrion/config', - // Dependencies of this package, including subpaths only reachable from its location. - '@apm-js-collab/tracing-hooks', - '@apm-js-collab/tracing-hooks/hook.mjs', - '@apm-js-collab/tracing-hooks/hook-sync.mjs', - '@apm-js-collab/tracing-hooks/lib/diagnostics.js', + // Dependencies of this package, resolvable only from its location. '@apm-js-collab/code-transformer', ])('resolves %s to an existing absolute path', request => { const resolved = resolveOrchestrionRuntimeRequest(request); @@ -302,7 +297,7 @@ describe('resolveOrchestrionRuntimeRequest', () => { }); it('resolves self-references with require conditions, so the paths are loadable via require()', () => { - expect(resolveOrchestrionRuntimeRequest('@sentry/server-utils/orchestrion/register')).toMatch(/[/\\]cjs[/\\]/); + expect(resolveOrchestrionRuntimeRequest('@sentry/server-utils/orchestrion/config')).toMatch(/[/\\]cjs[/\\]/); }); it('returns undefined for unresolvable requests', () => { diff --git a/packages/server-utils/tsconfig.json b/packages/server-utils/tsconfig.json index 72d1dd3759a1..b0eb9ecb6476 100644 --- a/packages/server-utils/tsconfig.json +++ b/packages/server-utils/tsconfig.json @@ -3,12 +3,5 @@ "include": ["src/**/*"], - "compilerOptions": {}, - // The orchestrion runtime hook is a hand-written `.mjs` file that self-references - // `@sentry/server-utils/orchestrion/config`. If tsc picks it up, it - // follows that subpath export back to `build/types/orchestrion/config/index.d.ts`, - // treats the .d.ts as an input, and then collides with the .d.ts it wants to - // emit from `src/orchestrion/config/index.ts`. Excluding it keeps tsc focused on the - // .ts sources — rollup copies the file through to `build/orchestrion/` unchanged. - "exclude": ["src/orchestrion/runtime/**/*.mjs", "src/orchestrion/runtime/**/*.cjs"] + "compilerOptions": {} } diff --git a/packages/tanstackstart-react/package.json b/packages/tanstackstart-react/package.json index 56840786c1bb..14ac56214323 100644 --- a/packages/tanstackstart-react/package.json +++ b/packages/tanstackstart-react/package.json @@ -57,6 +57,7 @@ "@sentry/core": "10.67.0", "@sentry/node": "10.67.0", "@sentry/react": "10.67.0", + "@sentry/server-runtime-injection": "10.67.0", "@sentry/server-utils": "10.67.0", "@sentry/bundler-plugins": "10.67.0" }, From 8f2f48b6ad845f82997378f63f8fba59d409e169 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 27 Aug 2026 15:28:49 +0200 Subject: [PATCH 13/15] refs and fixes --- packages/bun/src/plugin.ts | 123 +----------------- packages/server-utils/package.json | 12 +- packages/server-utils/rollup.npm.config.mjs | 5 +- .../src/orchestrion/bundler/bun.ts | 88 +++++++++++++ 4 files changed, 97 insertions(+), 131 deletions(-) create mode 100644 packages/server-utils/src/orchestrion/bundler/bun.ts diff --git a/packages/bun/src/plugin.ts b/packages/bun/src/plugin.ts index 956ee611ac8b..83c42f3126cd 100644 --- a/packages/bun/src/plugin.ts +++ b/packages/bun/src/plugin.ts @@ -11,127 +11,8 @@ * }); * ``` * - * This is BUILD-ONLY. Runtime instrumentation (`bun run`) is intentionally not - * offered: a module returned by a runtime `onLoad` plugin in Bun loses its - * CommonJS named exports. - * - * When https://github.com/oven-sh/bun/pull/31770 lands, we can revisit. - * - * Until then, Bun apps must bundle to get build-time instrumentation. In dev - * (ie, `bun run`) there is simply no instrumentation, which is clearer than - * partial/inconsistent coverage. - * - * Shipped as both ESM and CJS (via the `@sentry/bun/plugin` subpath) so a user's - * `bun build` script can be authored in either module system. It's a plain - * library import here (not a `--import`/`--preload` hook), so CJS is fine; Bun - * resolves the underlying ESM-only transformer in either module system. + * This is BUILD-ONLY. Runtime instrumentation (`bun run`) is currently not supported. * * @module */ - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type UnknownPlugin = any; - -// `@apm-js-collab/code-transformer-bundler-plugins/bun` is published ESM-only -// (no `require` arm, unlike its `/vite` entry). The ESM build imports it; the -// CJS build requires it. Bun resolves correctly for ESM modules in either -// module system. -import codeTransformer from '@apm-js-collab/code-transformer-bundler-plugins/bun'; -import { - moduleInjectedTransforms, - ORCHESTRION_BUNDLER_MARKER_BANNER, -} from '@sentry/server-utils/orchestrion/bundler-transforms'; -import { - INSTRUMENTED_MODULE_NAMES, - SENTRY_INSTRUMENTATIONS, - withoutInstrumentedExternals, -} from '@sentry/server-utils/orchestrion/config'; - -// Minimal shape of Bun's `PluginBuilder` that we touch. Typed locally instead -// of depending on `bun-types`, which would pull Bun's globals. -interface BunPluginBuilder { - config?: { banner?: string; external?: string[]; packages?: 'bundle' | 'external' }; -} - -/** - * Returns the Sentry code-transform plugin for Bun's bundler, configured - * with the central `SENTRY_INSTRUMENTATIONS`. The plugin injects - * `diagnostics_channel.tracingChannel` calls into the instrumented libraries as - * `bun build` bundles them — plus, via the module-injected transform, the - * snippet that records each module on `globalThis.__SENTRY_ORCHESTRION__` when - * it is evaluated — and injects the marker banner so `bundler` is set (to an - * empty `Set`) from boot, which is what gates the SDK's channel-integration - * setup at `init()`. - * - * Pass the result to `Bun.build({ plugins: [...] })`. - * - * @example - * ```ts - * import { sentryBunPlugin } from '@sentry/bun/plugin'; - * await Bun.build({ entrypoints: ['./app.ts'], plugins: [sentryBunPlugin()] }); - * ``` - */ -export function sentryBunPlugin(): UnknownPlugin { - // Typed upstream as an esbuild `Plugin`, but Bun passes its own - // `PluginBuilder` (which has the `onLoad` the transform uses) to `setup`. - // Cast to the Bun-compatible shape so we can forward Bun's builder to its - // `setup`. - const transformer = codeTransformer({ - instrumentations: SENTRY_INSTRUMENTATIONS, - customTransforms: moduleInjectedTransforms(), - }) as unknown as { - setup: (build: BunPluginBuilder) => void; - }; - - return { - name: 'sentry-orchestrion', - setup(build: BunPluginBuilder): void { - // Inject the marker banner via Bun's native `banner` config (unlike the - // upstream `injectDiagnostics` path, it needs no `outdir`). `config` is - // the `Bun.build` config and is present when this plugin is passed to - // `Bun.build({ plugins: [...] })`. - if (build.config) { - const existing = build.config.banner ?? ''; - build.config.banner = existing - ? `${existing}\n${ORCHESTRION_BUNDLER_MARKER_BANNER}` - : ORCHESTRION_BUNDLER_MARKER_BANNER; - - // Force-bundle every instrumented package. An externalized dependency - // is resolved from `node_modules` at runtime and never passes throug - // the transform's `onLoad`, so its diagnostics_channel calls would - // be silently never injected. Bun has no runtime fallback here, so - // bundling is the only injection path. - build.config.external = withoutInstrumentedExternals(build.config.external); - - // A blanket externalization strategy like `packages: 'external'` or - // `'*'` in `external` externalizes instrumented packages too, and - // `withoutInstrumentedExternals` only strips exact names/subpaths (not - // these), so those packages ship un-transformed with no runtime - // fallback. Forcing them back in via `onResolve` is not an option: Bun - // ignores `{ external: false }` against a blanket strategy, and - // returning a resolved `path` corrupts the package's ESM/CJS interop. - // So warn instead. This runs in the user's build script, where the - // Sentry debug logger isn't enabled, and `console` is the thing to use. - const blanketExternal = - build.config.packages === 'external' - ? "packages: 'external'" - : build.config.external?.includes('*') - ? "'*' in external" - : undefined; - if (blanketExternal) { - // eslint-disable-next-line no-console - console.warn( - `[Sentry] This Bun build externalizes all dependencies (${blanketExternal}), so Sentry ` + - 'cannot instrument bundled libraries. Instrumentation will be missing for any of ' + - `these packages your app uses: ${INSTRUMENTED_MODULE_NAMES.join(', ')}. To instrument them, ` + - 'externalize only the specific packages you need external instead of all of them.', - ); - } - } - - // Delegate to the upstream code-transformer, which registers the `onLoad` - // hook that does the actual channel injection. - transformer.setup(build); - }, - }; -} +export { sentryOrchestrionPlugin as sentryBunPlugin } from '@sentry/server-utils/orchestrion/bun'; diff --git a/packages/server-utils/package.json b/packages/server-utils/package.json index 20ac27e66f33..83f0137541e6 100644 --- a/packages/server-utils/package.json +++ b/packages/server-utils/package.json @@ -56,10 +56,10 @@ "import": "./build/esm/orchestrion/bundler/esbuild.js", "require": "./build/cjs/orchestrion/bundler/esbuild.js" }, - "./orchestrion/bundler-transforms": { - "types": "./build/types/orchestrion/bundler/moduleInjectedTransform.d.ts", - "import": "./build/esm/orchestrion/bundler/moduleInjectedTransform.js", - "require": "./build/cjs/orchestrion/bundler/moduleInjectedTransform.js" + "./orchestrion/bun": { + "types": "./build/types/orchestrion/bundler/bun.d.ts", + "import": "./build/esm/orchestrion/bundler/bun.js", + "require": "./build/cjs/orchestrion/bundler/bun.js" } }, "typesVersions": { @@ -79,8 +79,8 @@ "orchestrion/esbuild": [ "build/types/orchestrion/bundler/esbuild.d.ts" ], - "orchestrion/bundler-transforms": [ - "build/types/orchestrion/bundler/moduleInjectedTransform.d.ts" + "orchestrion/bun": [ + "build/types/orchestrion/bundler/bun.d.ts" ] } }, diff --git a/packages/server-utils/rollup.npm.config.mjs b/packages/server-utils/rollup.npm.config.mjs index 1a64c3879e17..675a81424abe 100644 --- a/packages/server-utils/rollup.npm.config.mjs +++ b/packages/server-utils/rollup.npm.config.mjs @@ -82,10 +82,6 @@ export default [ // `.../orchestrion/vite`, etc.) — none are reachable from `src/index.ts`, so // we list them as separate entrypoints to guarantee they end up in build/esm // and build/cjs. - // - // The runtime diagnostics-channel injection (`register`/`hook`/`import-hook` + the vendored - // transformer chain) lives in `@sentry/server-runtime-injection` — it must stay external when - // apps bundle, so it is a separate package rather than a subpath here. entrypoints: [ 'src/index.ts', 'src/index.no-diagnostic-channels.ts', @@ -95,6 +91,7 @@ export default [ 'src/orchestrion/bundler/webpack.ts', 'src/orchestrion/bundler/webpack-loader.ts', 'src/orchestrion/bundler/esbuild.ts', + 'src/orchestrion/bundler/bun.ts', ], packageSpecificConfig: { plugins: [debugNodeAlias, commonJSPlugin, thirdPartyLicensePlugin], diff --git a/packages/server-utils/src/orchestrion/bundler/bun.ts b/packages/server-utils/src/orchestrion/bundler/bun.ts new file mode 100644 index 000000000000..8fb84b4ff201 --- /dev/null +++ b/packages/server-utils/src/orchestrion/bundler/bun.ts @@ -0,0 +1,88 @@ +import codeTransformer from '@apm-js-collab/code-transformer-bundler-plugins/bun'; +import { INSTRUMENTED_MODULE_NAMES, SENTRY_INSTRUMENTATIONS, withoutInstrumentedExternals } from '../config'; +import { moduleInjectedTransforms, ORCHESTRION_BUNDLER_MARKER_BANNER } from './moduleInjectedTransform'; + +// oxlint-disable-next-line typescript/no-explicit-any +type UnknownPlugin = any; + +// Minimal shape of Bun's `PluginBuilder` that we touch. Typed locally instead +// of depending on `bun-types`, which would pull Bun's globals into this build. +interface BunPluginBuilder { + config?: { banner?: string; external?: string[]; packages?: 'bundle' | 'external' }; +} + +/** + * Sentry orchestrion code-transform plugin for Bun's bundler (`bun build`), exposed to users via the + * `@sentry/bun/plugin` subpath (which re-exports this as `sentryBunPlugin`). + * + * This is BUILD-ONLY. Runtime instrumentation (`bun run`) is intentionally not offered: a module + * returned by a runtime `onLoad` plugin in Bun loses its CommonJS named exports. When + * https://github.com/oven-sh/bun/pull/31770 lands, we can revisit. Until then, Bun apps must bundle + * to get build-time instrumentation; in dev (`bun run`) there is simply no instrumentation, which is + * clearer than partial/inconsistent coverage. + * + * The plugin injects `diagnostics_channel.tracingChannel` calls into the instrumented libraries as + * `bun build` bundles them — plus, via the module-injected transform, the snippet that records each + * module on `globalThis.__SENTRY_ORCHESTRION__` when it is evaluated — and injects the marker banner + * so `bundler` is set (to an empty `Set`) from boot, which gates the SDK's channel-integration setup + * at `init()`. + */ +export function sentryOrchestrionPlugin(): UnknownPlugin { + // Typed upstream as an esbuild `Plugin`, but Bun passes its own `PluginBuilder` (which has the + // `onLoad` the transform uses) to `setup`. Cast to the Bun-compatible shape so we can forward + // Bun's builder to its `setup`. + const transformer = codeTransformer({ + instrumentations: SENTRY_INSTRUMENTATIONS, + customTransforms: moduleInjectedTransforms(), + }) as unknown as { + setup: (build: BunPluginBuilder) => void; + }; + + return { + name: 'sentry-orchestrion', + setup(build: BunPluginBuilder): void { + // Inject the marker banner via Bun's native `banner` config (unlike the upstream + // `injectDiagnostics` path, it needs no `outdir`). `config` is the `Bun.build` config and is + // present when this plugin is passed to `Bun.build({ plugins: [...] })`. + if (build.config) { + const existing = build.config.banner ?? ''; + build.config.banner = existing + ? `${existing}\n${ORCHESTRION_BUNDLER_MARKER_BANNER}` + : ORCHESTRION_BUNDLER_MARKER_BANNER; + + // Force-bundle every instrumented package. An externalized dependency is resolved from + // `node_modules` at runtime and never passes through the transform's `onLoad`, so its + // diagnostics_channel calls would be silently never injected. Bun has no runtime fallback + // here, so bundling is the only injection path. + build.config.external = withoutInstrumentedExternals(build.config.external); + + // A blanket externalization strategy like `packages: 'external'` or `'*'` in `external` + // externalizes instrumented packages too, and `withoutInstrumentedExternals` only strips + // exact names/subpaths (not these), so those packages ship un-transformed with no runtime + // fallback. Forcing them back in via `onResolve` is not an option: Bun ignores + // `{ external: false }` against a blanket strategy, and returning a resolved `path` corrupts + // the package's ESM/CJS interop. So warn instead. This runs in the user's build script, + // where the Sentry debug logger isn't enabled, and `console` is the thing to use. + const blanketExternal = + build.config.packages === 'external' + ? "packages: 'external'" + : build.config.external?.includes('*') + ? "'*' in external" + : undefined; + if (blanketExternal) { + // oxlint-disable-next-line no-console + console.warn( + `[Sentry] This Bun build externalizes all dependencies (${blanketExternal}), so Sentry ` + + 'cannot instrument bundled libraries. Instrumentation will be missing for any of ' + + `these packages your app uses: ${INSTRUMENTED_MODULE_NAMES.join(', ')}. To instrument them, ` + + 'externalize only the specific packages you need external instead of all of them.', + ); + } + } + + // Delegate to the upstream code-transformer, which registers the `onLoad` hook that does the + // actual channel injection. + transformer.setup(build); + }, + }; +} From fe6c45be9958916e46db18cae7193967dc61c0b0 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 27 Aug 2026 15:35:22 +0200 Subject: [PATCH 14/15] fix remix --- packages/remix/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/remix/package.json b/packages/remix/package.json index 22dc3d368c58..ba51cb8e10fe 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -58,6 +58,7 @@ "@sentry/core": "10.67.0", "@sentry/node": "10.67.0", "@sentry/react": "10.67.0", + "@sentry/server-runtime-injection": "10.67.0", "@sentry/server-utils": "10.67.0", "yargs": "^17.6.0" }, From 2a2a5e3756a6f4d814b3c74450abb680dc06b9ba Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 27 Aug 2026 15:42:42 +0200 Subject: [PATCH 15/15] fix(bun): Drop the now-unused @apm-js-collab/code-transformer-bundler-plugins dependency `@sentry/bun/plugin` now only re-exports from `@sentry/server-utils/orchestrion/bun` (which vendors the transformer), so the direct import is gone. Remove the leftover runtime dependency so installs don't pull an unused package. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/bun/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/bun/package.json b/packages/bun/package.json index 49cf849095b0..e8cc53f11073 100644 --- a/packages/bun/package.json +++ b/packages/bun/package.json @@ -42,7 +42,6 @@ "access": "public" }, "dependencies": { - "@apm-js-collab/code-transformer-bundler-plugins": "^0.7.4", "@sentry/core": "10.67.0", "@sentry/conventions": "^0.20.0", "@sentry/node": "10.67.0",