From 1c327f38af20e39e8261eb3ec6f58ebf43bbf6b0 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 9 Jul 2026 00:49:58 +0200 Subject: [PATCH 1/5] feat(bundlers): Add orchestrion bundler plugins --- packages/server-utils/package.json | 35 +++++++++- packages/server-utils/rollup.npm.config.mjs | 11 +-- .../src/orchestrion/bundler/esbuild.ts | 48 +++++++++++++ .../src/orchestrion/bundler/options.ts | 29 ++++++++ .../src/orchestrion/bundler/rollup.ts | 34 ++++++++++ .../src/orchestrion/bundler/vite.ts | 67 ++----------------- .../src/orchestrion/bundler/webpack.ts | 19 ++++++ 7 files changed, 174 insertions(+), 69 deletions(-) create mode 100644 packages/server-utils/src/orchestrion/bundler/esbuild.ts create mode 100644 packages/server-utils/src/orchestrion/bundler/options.ts create mode 100644 packages/server-utils/src/orchestrion/bundler/rollup.ts create mode 100644 packages/server-utils/src/orchestrion/bundler/webpack.ts diff --git a/packages/server-utils/package.json b/packages/server-utils/package.json index a09654179232..1ebced9ecb64 100644 --- a/packages/server-utils/package.json +++ b/packages/server-utils/package.json @@ -41,6 +41,18 @@ "types": "./build/types/orchestrion/bundler/vite.d.ts", "import": "./build/esm/orchestrion/bundler/vite.js" }, + "./orchestrion/rollup": { + "types": "./build/types/orchestrion/bundler/rollup.d.ts", + "import": "./build/esm/orchestrion/bundler/rollup.js" + }, + "./orchestrion/webpack": { + "types": "./build/types/orchestrion/bundler/webpack.d.ts", + "import": "./build/esm/orchestrion/bundler/webpack.js" + }, + "./orchestrion/esbuild": { + "types": "./build/types/orchestrion/bundler/esbuild.d.ts", + "import": "./build/esm/orchestrion/bundler/esbuild.js" + }, "./orchestrion/import-hook": { "import": "./build/orchestrion/import-hook.mjs" } @@ -61,6 +73,15 @@ ], "orchestrion/vite": [ "build/types-ts3.8/orchestrion/bundler/vite.d.ts" + ], + "orchestrion/rollup": [ + "build/types-ts3.8/orchestrion/bundler/rollup.d.ts" + ], + "orchestrion/webpack": [ + "build/types-ts3.8/orchestrion/bundler/webpack.d.ts" + ], + "orchestrion/esbuild": [ + "build/types-ts3.8/orchestrion/bundler/esbuild.d.ts" ] }, "*": { @@ -75,6 +96,15 @@ ], "orchestrion/vite": [ "build/types/orchestrion/bundler/vite.d.ts" + ], + "orchestrion/rollup": [ + "build/types/orchestrion/bundler/rollup.d.ts" + ], + "orchestrion/webpack": [ + "build/types/orchestrion/bundler/webpack.d.ts" + ], + "orchestrion/esbuild": [ + "build/types/orchestrion/bundler/esbuild.d.ts" ] } }, @@ -83,13 +113,12 @@ }, "dependencies": { "@apm-js-collab/code-transformer-bundler-plugins": "^0.5.0", - "@apm-js-collab/code-transformer": "^0.15.0", "@apm-js-collab/tracing-hooks": "^0.10.1", "@sentry/conventions": "^0.15.1", - "@sentry/core": "10.64.0", - "magic-string": "~0.30.0" + "@sentry/core": "10.64.0" }, "devDependencies": { + "@apm-js-collab/code-transformer": "^0.15.0", "@types/node": "^18.19.1", "vite": "^6.4.3" }, diff --git a/packages/server-utils/rollup.npm.config.mjs b/packages/server-utils/rollup.npm.config.mjs index ca2c6cf19486..0b6412cee679 100644 --- a/packages/server-utils/rollup.npm.config.mjs +++ b/packages/server-utils/rollup.npm.config.mjs @@ -19,10 +19,10 @@ export default [ ...orchestrionRuntimeHooks, ...makeNPMConfigVariants( makeBaseNPMConfig({ - // `src/orchestrion/config/index.ts` and `src/orchestrion/bundler/vite.ts` are - // loaded via dedicated subpath exports (`.../orchestrion/config`, - // `.../orchestrion/vite`) — neither is reachable from `src/index.ts`, so we - // list them as separate entrypoints to guarantee they end up in build/esm + // `src/orchestrion/config/index.ts` and the `src/orchestrion/bundler/*.ts` + // plugins are loaded via dedicated subpath exports (`.../orchestrion/config`, + // `.../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. `src/orchestrion/index.ts` backs the `./orchestrion` // subpath export. entrypoints: [ @@ -34,6 +34,9 @@ export default [ // `Sentry.init()` to install the channel-injection hooks. 'src/orchestrion/runtime/register.ts', 'src/orchestrion/bundler/vite.ts', + 'src/orchestrion/bundler/rollup.ts', + 'src/orchestrion/bundler/webpack.ts', + 'src/orchestrion/bundler/esbuild.ts', ], packageSpecificConfig: { output: { diff --git a/packages/server-utils/src/orchestrion/bundler/esbuild.ts b/packages/server-utils/src/orchestrion/bundler/esbuild.ts new file mode 100644 index 000000000000..a6406004099c --- /dev/null +++ b/packages/server-utils/src/orchestrion/bundler/esbuild.ts @@ -0,0 +1,48 @@ +import codeTransformer from '@apm-js-collab/code-transformer-bundler-plugins/esbuild'; +import { withoutInstrumentedExternals } from '../config'; +import { orchestrionTransformOptions } from './options'; + +// Minimal shape of esbuild's `PluginBuild` that we touch, so we don't depend on +// the `esbuild` types. The upstream transform reads more off `build` +// (`onStart`/`onLoad`/`onEnd`), but we only forward `build` through to it and +// mutate `initialOptions.external`. +interface EsbuildPluginBuild { + initialOptions: { external?: string[] }; +} + +/** + * esbuild plugin that runs the orchestrion code transform on the bundled output. + * + * Use when bundling a Node app with esbuild. For unbundled Node processes use the + * runtime hook instead (`node --import @sentry/node/orchestrion app.js`). + * + * esbuild does not flatten nested `plugins` arrays, so this returns a single + * plugin that strips instrumented packages from an `external` denylist before + * delegating to the upstream transform. + * + * @example + * ```ts + * // build.mjs + * import { sentryOrchestrionPlugin } from '@sentry/server-utils/orchestrion/esbuild'; + * await esbuild.build({ plugins: [sentryOrchestrionPlugin()] }); + * ``` + */ +export function sentryOrchestrionPlugin(): ReturnType { + const transformer = codeTransformer(orchestrionTransformOptions()) as unknown as { + setup: (build: EsbuildPluginBuild) => void; + }; + + return { + name: 'sentry-orchestrion', + setup(build: EsbuildPluginBuild): void { + // Strip instrumented packages from an `external` denylist so esbuild + // bundles them and the transform's `onLoad` actually sees their source; + // an externalized dependency is resolved from `node_modules` at runtime + // and never gets the diagnostics_channel calls injected. Mutating + // `initialOptions` inside `setup` is respected by esbuild — the upstream + // plugin sets `initialOptions.metafile` the same way. + build.initialOptions.external = withoutInstrumentedExternals(build.initialOptions.external); + transformer.setup(build); + }, + }; +} diff --git a/packages/server-utils/src/orchestrion/bundler/options.ts b/packages/server-utils/src/orchestrion/bundler/options.ts new file mode 100644 index 000000000000..6bcdec262dfb --- /dev/null +++ b/packages/server-utils/src/orchestrion/bundler/options.ts @@ -0,0 +1,29 @@ +// EXPERIMENTAL — shared config for the orchestrion bundler plugins. Every +// bundler-specific entry (`vite`, `rollup`, `webpack`, `esbuild`, `bun`) feeds +// this exact same config to its `@apm-js-collab/code-transformer-bundler-plugins` +// plugin, so the set of instrumented libraries and the injected boot banner stay +// identical across bundlers. + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type CodeTransformerOptions = any; + +import { SENTRY_INSTRUMENTATIONS } from '../config'; + +/** + * The `@apm-js-collab/code-transformer-bundler-plugins` options shared by every + * orchestrion bundler plugin. + * + * `injectDiagnostics` sets `globalThis.__SENTRY_ORCHESTRION__.bundler = true` at + * app boot so the `_experimentalSetupOrchestrion()` detector can confirm the + * bundler path ran (rather than relying on a build-time flag that wouldn't be + * visible to the runtime). + */ +export function orchestrionTransformOptions(): CodeTransformerOptions { + return { + instrumentations: SENTRY_INSTRUMENTATIONS, + injectDiagnostics: () => { + return `globalThis.__SENTRY_ORCHESTRION__ = (globalThis.__SENTRY_ORCHESTRION__ || {}); +globalThis.__SENTRY_ORCHESTRION__.bundler = true;`; + }, + }; +} diff --git a/packages/server-utils/src/orchestrion/bundler/rollup.ts b/packages/server-utils/src/orchestrion/bundler/rollup.ts new file mode 100644 index 000000000000..cfff730d13f5 --- /dev/null +++ b/packages/server-utils/src/orchestrion/bundler/rollup.ts @@ -0,0 +1,34 @@ +import codeTransformer from '@apm-js-collab/code-transformer-bundler-plugins/rollup'; +import { withoutInstrumentedExternals } from '../config'; +import { orchestrionTransformOptions } from './options'; + +/** + * Rollup plugin that runs the orchestrion code transform on the bundled output. + * + * Use when bundling a Node app with Rollup. For unbundled Node processes use the + * runtime hook instead (`node --import @sentry/node/orchestrion app.js`). + * + * @example + * ```ts + * // rollup.config.js + * import { sentryOrchestrionPlugin } from '@sentry/server-utils/orchestrion/rollup'; + * export default { plugins: [sentryOrchestrionPlugin()] }; + * ``` + */ +export function sentryOrchestrionPlugin(): ReturnType { + return { + ...codeTransformer(orchestrionTransformOptions()), + // The `options` hook runs before the build starts and can rewrite the + // resolved input options. Rollup's `external` may be a string, RegExp, + // function, or array of `string | RegExp`; `withoutInstrumentedExternals` + // only understands a string denylist, so we touch only that form and leave + // every other shape untouched. + options(inputOptions) { + const { external } = inputOptions; + if (Array.isArray(external) && external.every(entry => typeof entry === 'string')) { + return { ...inputOptions, external: withoutInstrumentedExternals(external) }; + } + return null; + }, + }; +} diff --git a/packages/server-utils/src/orchestrion/bundler/vite.ts b/packages/server-utils/src/orchestrion/bundler/vite.ts index 4cadc7b40925..86ffa84cab54 100644 --- a/packages/server-utils/src/orchestrion/bundler/vite.ts +++ b/packages/server-utils/src/orchestrion/bundler/vite.ts @@ -1,26 +1,6 @@ -// EXPERIMENTAL — Vite plugin that runs the orchestrion code transform at build -// time, injecting `diagnostics_channel.tracingChannel` calls into the libraries -// listed in `SENTRY_INSTRUMENTATIONS`. -// -// This file is published ESM-only via the `@sentry/server-utils/orchestrion/vite` -// subpath export. `@apm-js-collab/code-transformer-bundler-plugins` is -// `"type": "module"`, so consuming it from a CJS build is intentionally -// unsupported — vite.config.ts is almost always ESM in practice. The CJS -// rollup variant still emits this file, but `package.json` only exposes the -// ESM entry, so attempts to `require('@sentry/server-utils/orchestrion/vite')` will -// fail at resolution time rather than producing a half-broken plugin. - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type UnknownPlugin = any; - import codeTransformer from '@apm-js-collab/code-transformer-bundler-plugins/vite'; -import MagicString from 'magic-string'; -import { INSTRUMENTED_MODULE_NAMES, SENTRY_INSTRUMENTATIONS } from '../config'; - -// `vite` types live in the package's ESM-only subpath; under Node16 module -// resolution with TS treating @sentry/server-utils as CJS, importing them produces a -// false positive. We don't need the runtime value for typing — `UnknownPlugin` -// is sufficient — so we omit the import entirely. +import { INSTRUMENTED_MODULE_NAMES } from '../config'; +import { orchestrionTransformOptions } from './options'; /** * Vite plugin that runs the orchestrion code transform on the bundled output. @@ -29,44 +9,16 @@ import { INSTRUMENTED_MODULE_NAMES, SENTRY_INSTRUMENTATIONS } from '../config'; * pipeline, SvelteKit). For unbundled Node processes use the runtime hook * instead (`node --import @sentry/node/orchestrion app.js`). * - * Returns two plugins: - * 1. `sentry-orchestrion-marker` — a `renderChunk` hook that prepends a - * single-line banner to entry chunks. The banner sets - * `globalThis.__SENTRY_ORCHESTRION__.bundler = true` at app boot, so the - * `_experimentalSetupOrchestrion()` detector can confirm the bundler path - * ran (rather than relying on a build-time flag that wouldn't be visible - * to the runtime). - * Also injects every instrumented package name into `ssr.noExternal` via - * the `config` hook, since externalized deps are `require()`d at runtime - * from `node_modules` and never pass through the transform. - * 2. The upstream `@apm-js-collab/code-transformer-bundler-plugins/vite` - * plugin, fed our central `SENTRY_INSTRUMENTATIONS` config. - * * @example * ```ts * // vite.config.ts - * import { sentryOrchestrionPlugin } from '@sentry/node/orchestrion/vite'; + * import { sentryOrchestrionPlugin } from '@sentry/server-utils/orchestrion/vite'; * export default { plugins: [sentryOrchestrionPlugin()] }; * ``` */ -export function sentryOrchestrionPlugin(): UnknownPlugin[] { - const codeTransformerPlugins = codeTransformer({ instrumentations: SENTRY_INSTRUMENTATIONS }); - const codeTransformerArray: UnknownPlugin[] = Array.isArray(codeTransformerPlugins) - ? codeTransformerPlugins - : [codeTransformerPlugins]; - return [bundlerMarkerPlugin(), ...codeTransformerArray]; -} - -function bundlerMarkerPlugin(): UnknownPlugin { - const banner = [ - 'globalThis.__SENTRY_ORCHESTRION__ = (globalThis.__SENTRY_ORCHESTRION__ || {});', - 'globalThis.__SENTRY_ORCHESTRION__.bundler = true;', - '', - ].join('\n'); - +export function sentryOrchestrionPlugin(): ReturnType { return { - name: 'sentry-orchestrion-marker', - enforce: 'pre' as const, + ...codeTransformer(orchestrionTransformOptions()), config(): { ssr: { noExternal: string[] } } { // Force-bundle every instrumented package so the code transform actually // sees its source. Vite externalizes dependencies in SSR builds by @@ -77,14 +29,5 @@ function bundlerMarkerPlugin(): UnknownPlugin { // their additions. return { ssr: { noExternal: INSTRUMENTED_MODULE_NAMES } }; }, - renderChunk(code: string, chunk: { isEntry: boolean }): { code: string; map: unknown } | null { - if (!chunk.isEntry) return null; - // Prepend via magic-string so the entry chunk's sourcemap stays aligned — - // returning `map: null` here would shift every mapping by the banner's - // line count and misattribute server stack traces. - const ms = new MagicString(code); - ms.prepend(banner); - return { code: ms.toString(), map: ms.generateMap({ hires: true }) }; - }, }; } diff --git a/packages/server-utils/src/orchestrion/bundler/webpack.ts b/packages/server-utils/src/orchestrion/bundler/webpack.ts new file mode 100644 index 000000000000..16aa903428e9 --- /dev/null +++ b/packages/server-utils/src/orchestrion/bundler/webpack.ts @@ -0,0 +1,19 @@ +import codeTransformer from '@apm-js-collab/code-transformer-bundler-plugins/webpack'; +import { orchestrionTransformOptions } from './options'; + +/** + * webpack plugin that runs the orchestrion code transform on the bundled output. + * + * Use when bundling a Node app with webpack. For unbundled Node processes use the + * runtime hook instead (`node --import @sentry/node/orchestrion app.js`). + * + * @example + * ```ts + * // webpack.config.mjs + * import { sentryOrchestrionPlugin } from '@sentry/server-utils/orchestrion/webpack'; + * export default { plugins: [sentryOrchestrionPlugin()] }; + * ``` + */ +export function sentryOrchestrionPlugin(): ReturnType { + return codeTransformer(orchestrionTransformOptions()); +} From c440ac1babe2c07ddf277e544a31760466624744 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 9 Jul 2026 00:56:07 +0200 Subject: [PATCH 2/5] shrink injected code --- packages/server-utils/src/orchestrion/bundler/options.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/server-utils/src/orchestrion/bundler/options.ts b/packages/server-utils/src/orchestrion/bundler/options.ts index 6bcdec262dfb..3860ceed4ffe 100644 --- a/packages/server-utils/src/orchestrion/bundler/options.ts +++ b/packages/server-utils/src/orchestrion/bundler/options.ts @@ -22,8 +22,7 @@ export function orchestrionTransformOptions(): CodeTransformerOptions { return { instrumentations: SENTRY_INSTRUMENTATIONS, injectDiagnostics: () => { - return `globalThis.__SENTRY_ORCHESTRION__ = (globalThis.__SENTRY_ORCHESTRION__ || {}); -globalThis.__SENTRY_ORCHESTRION__.bundler = true;`; + return '(globalThis.__SENTRY_ORCHESTRION__=globalThis.__SENTRY_ORCHESTRION__||{}).bundler=true;'; }, }; } From 4675d522940de4bfe46ebfc1cb3952358121de18 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 9 Jul 2026 04:24:38 +0200 Subject: [PATCH 3/5] Allow passing additional instrumentations --- .../src/orchestrion/bundler/esbuild.ts | 5 +++-- .../src/orchestrion/bundler/options.ts | 22 +++++++++---------- .../src/orchestrion/bundler/rollup.ts | 5 +++-- .../src/orchestrion/bundler/vite.ts | 5 +++-- .../src/orchestrion/bundler/webpack.ts | 5 +++-- 5 files changed, 23 insertions(+), 19 deletions(-) diff --git a/packages/server-utils/src/orchestrion/bundler/esbuild.ts b/packages/server-utils/src/orchestrion/bundler/esbuild.ts index a6406004099c..8cffd35cc8b2 100644 --- a/packages/server-utils/src/orchestrion/bundler/esbuild.ts +++ b/packages/server-utils/src/orchestrion/bundler/esbuild.ts @@ -1,5 +1,6 @@ import codeTransformer from '@apm-js-collab/code-transformer-bundler-plugins/esbuild'; import { withoutInstrumentedExternals } from '../config'; +import type { PluginOptions } from './options'; import { orchestrionTransformOptions } from './options'; // Minimal shape of esbuild's `PluginBuild` that we touch, so we don't depend on @@ -27,8 +28,8 @@ interface EsbuildPluginBuild { * await esbuild.build({ plugins: [sentryOrchestrionPlugin()] }); * ``` */ -export function sentryOrchestrionPlugin(): ReturnType { - const transformer = codeTransformer(orchestrionTransformOptions()) as unknown as { +export function sentryOrchestrionPlugin(options: PluginOptions = {}): ReturnType { + const transformer = codeTransformer(orchestrionTransformOptions(options)) as unknown as { setup: (build: EsbuildPluginBuild) => void; }; diff --git a/packages/server-utils/src/orchestrion/bundler/options.ts b/packages/server-utils/src/orchestrion/bundler/options.ts index 3860ceed4ffe..4bc756dccb05 100644 --- a/packages/server-utils/src/orchestrion/bundler/options.ts +++ b/packages/server-utils/src/orchestrion/bundler/options.ts @@ -1,13 +1,13 @@ -// EXPERIMENTAL — shared config for the orchestrion bundler plugins. Every -// bundler-specific entry (`vite`, `rollup`, `webpack`, `esbuild`, `bun`) feeds -// this exact same config to its `@apm-js-collab/code-transformer-bundler-plugins` -// plugin, so the set of instrumented libraries and the injected boot banner stay -// identical across bundlers. - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type CodeTransformerOptions = any; - +import type { InstrumentationConfig } from '@apm-js-collab/code-transformer'; import { SENTRY_INSTRUMENTATIONS } from '../config'; +import type codeTransformer from '@apm-js-collab/code-transformer-bundler-plugins/rollup'; + +export type PluginOptions = { + /** + * Additional instrumentations to include with the default instrumentation. + */ + instrumentations?: InstrumentationConfig[]; +}; /** * The `@apm-js-collab/code-transformer-bundler-plugins` options shared by every @@ -18,9 +18,9 @@ import { SENTRY_INSTRUMENTATIONS } from '../config'; * bundler path ran (rather than relying on a build-time flag that wouldn't be * visible to the runtime). */ -export function orchestrionTransformOptions(): CodeTransformerOptions { +export function orchestrionTransformOptions(options: PluginOptions): Parameters[0] { return { - instrumentations: SENTRY_INSTRUMENTATIONS, + instrumentations: [SENTRY_INSTRUMENTATIONS, ...(options.instrumentations || [])], injectDiagnostics: () => { return '(globalThis.__SENTRY_ORCHESTRION__=globalThis.__SENTRY_ORCHESTRION__||{}).bundler=true;'; }, diff --git a/packages/server-utils/src/orchestrion/bundler/rollup.ts b/packages/server-utils/src/orchestrion/bundler/rollup.ts index cfff730d13f5..3db687756cc9 100644 --- a/packages/server-utils/src/orchestrion/bundler/rollup.ts +++ b/packages/server-utils/src/orchestrion/bundler/rollup.ts @@ -1,5 +1,6 @@ import codeTransformer from '@apm-js-collab/code-transformer-bundler-plugins/rollup'; import { withoutInstrumentedExternals } from '../config'; +import type { PluginOptions } from './options'; import { orchestrionTransformOptions } from './options'; /** @@ -15,9 +16,9 @@ import { orchestrionTransformOptions } from './options'; * export default { plugins: [sentryOrchestrionPlugin()] }; * ``` */ -export function sentryOrchestrionPlugin(): ReturnType { +export function sentryOrchestrionPlugin(options: PluginOptions = {}): ReturnType { return { - ...codeTransformer(orchestrionTransformOptions()), + ...codeTransformer(orchestrionTransformOptions(options)), // The `options` hook runs before the build starts and can rewrite the // resolved input options. Rollup's `external` may be a string, RegExp, // function, or array of `string | RegExp`; `withoutInstrumentedExternals` diff --git a/packages/server-utils/src/orchestrion/bundler/vite.ts b/packages/server-utils/src/orchestrion/bundler/vite.ts index 86ffa84cab54..db0ebcb7b5b1 100644 --- a/packages/server-utils/src/orchestrion/bundler/vite.ts +++ b/packages/server-utils/src/orchestrion/bundler/vite.ts @@ -1,5 +1,6 @@ import codeTransformer from '@apm-js-collab/code-transformer-bundler-plugins/vite'; import { INSTRUMENTED_MODULE_NAMES } from '../config'; +import type { PluginOptions } from './options'; import { orchestrionTransformOptions } from './options'; /** @@ -16,9 +17,9 @@ import { orchestrionTransformOptions } from './options'; * export default { plugins: [sentryOrchestrionPlugin()] }; * ``` */ -export function sentryOrchestrionPlugin(): ReturnType { +export function sentryOrchestrionPlugin(options: PluginOptions = {}): ReturnType { return { - ...codeTransformer(orchestrionTransformOptions()), + ...codeTransformer(orchestrionTransformOptions(options)), config(): { ssr: { noExternal: string[] } } { // Force-bundle every instrumented package so the code transform actually // sees its source. Vite externalizes dependencies in SSR builds by diff --git a/packages/server-utils/src/orchestrion/bundler/webpack.ts b/packages/server-utils/src/orchestrion/bundler/webpack.ts index 16aa903428e9..b7b7589071b1 100644 --- a/packages/server-utils/src/orchestrion/bundler/webpack.ts +++ b/packages/server-utils/src/orchestrion/bundler/webpack.ts @@ -1,4 +1,5 @@ import codeTransformer from '@apm-js-collab/code-transformer-bundler-plugins/webpack'; +import type { PluginOptions } from './options'; import { orchestrionTransformOptions } from './options'; /** @@ -14,6 +15,6 @@ import { orchestrionTransformOptions } from './options'; * export default { plugins: [sentryOrchestrionPlugin()] }; * ``` */ -export function sentryOrchestrionPlugin(): ReturnType { - return codeTransformer(orchestrionTransformOptions()); +export function sentryOrchestrionPlugin(options: PluginOptions = {}): ReturnType { + return codeTransformer(orchestrionTransformOptions(options)); } From b15cb007d43854259d9a5f4f1741e0b754f4783f Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 9 Jul 2026 11:32:48 +0100 Subject: [PATCH 4/5] Fix review issues --- .../src/orchestrion/bundler/esbuild.ts | 16 +++------------- .../src/orchestrion/bundler/options.ts | 2 +- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/packages/server-utils/src/orchestrion/bundler/esbuild.ts b/packages/server-utils/src/orchestrion/bundler/esbuild.ts index 8cffd35cc8b2..917e4af298be 100644 --- a/packages/server-utils/src/orchestrion/bundler/esbuild.ts +++ b/packages/server-utils/src/orchestrion/bundler/esbuild.ts @@ -3,14 +3,6 @@ import { withoutInstrumentedExternals } from '../config'; import type { PluginOptions } from './options'; import { orchestrionTransformOptions } from './options'; -// Minimal shape of esbuild's `PluginBuild` that we touch, so we don't depend on -// the `esbuild` types. The upstream transform reads more off `build` -// (`onStart`/`onLoad`/`onEnd`), but we only forward `build` through to it and -// mutate `initialOptions.external`. -interface EsbuildPluginBuild { - initialOptions: { external?: string[] }; -} - /** * esbuild plugin that runs the orchestrion code transform on the bundled output. * @@ -29,13 +21,11 @@ interface EsbuildPluginBuild { * ``` */ export function sentryOrchestrionPlugin(options: PluginOptions = {}): ReturnType { - const transformer = codeTransformer(orchestrionTransformOptions(options)) as unknown as { - setup: (build: EsbuildPluginBuild) => void; - }; + const transformer = codeTransformer(orchestrionTransformOptions(options)); return { name: 'sentry-orchestrion', - setup(build: EsbuildPluginBuild): void { + async setup(build): Promise { // Strip instrumented packages from an `external` denylist so esbuild // bundles them and the transform's `onLoad` actually sees their source; // an externalized dependency is resolved from `node_modules` at runtime @@ -43,7 +33,7 @@ export function sentryOrchestrionPlugin(options: PluginOptions = {}): ReturnType // `initialOptions` inside `setup` is respected by esbuild — the upstream // plugin sets `initialOptions.metafile` the same way. build.initialOptions.external = withoutInstrumentedExternals(build.initialOptions.external); - transformer.setup(build); + await transformer.setup(build); }, }; } diff --git a/packages/server-utils/src/orchestrion/bundler/options.ts b/packages/server-utils/src/orchestrion/bundler/options.ts index 4bc756dccb05..b285b3074236 100644 --- a/packages/server-utils/src/orchestrion/bundler/options.ts +++ b/packages/server-utils/src/orchestrion/bundler/options.ts @@ -20,7 +20,7 @@ export type PluginOptions = { */ export function orchestrionTransformOptions(options: PluginOptions): Parameters[0] { return { - instrumentations: [SENTRY_INSTRUMENTATIONS, ...(options.instrumentations || [])], + instrumentations: [...SENTRY_INSTRUMENTATIONS, ...(options.instrumentations || [])], injectDiagnostics: () => { return '(globalThis.__SENTRY_ORCHESTRION__=globalThis.__SENTRY_ORCHESTRION__||{}).bundler=true;'; }, From f6fde4068ceb31b427c8bb63513790668457ea10 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Fri, 10 Jul 2026 12:08:31 +0100 Subject: [PATCH 5/5] More improvements --- .../src/orchestrion/bundler/esbuild.ts | 7 +++-- .../src/orchestrion/bundler/rollup.ts | 5 ++-- .../src/orchestrion/bundler/vite.ts | 4 +-- .../src/orchestrion/config/index.ts | 28 +++++++++++++------ .../test/orchestrion/config.test.ts | 28 ++++++++++++++++++- 5 files changed, 56 insertions(+), 16 deletions(-) diff --git a/packages/server-utils/src/orchestrion/bundler/esbuild.ts b/packages/server-utils/src/orchestrion/bundler/esbuild.ts index 917e4af298be..391cdd0229dc 100644 --- a/packages/server-utils/src/orchestrion/bundler/esbuild.ts +++ b/packages/server-utils/src/orchestrion/bundler/esbuild.ts @@ -1,5 +1,5 @@ import codeTransformer from '@apm-js-collab/code-transformer-bundler-plugins/esbuild'; -import { withoutInstrumentedExternals } from '../config'; +import { instrumentedModuleNames, withoutInstrumentedExternals } from '../config'; import type { PluginOptions } from './options'; import { orchestrionTransformOptions } from './options'; @@ -32,7 +32,10 @@ export function sentryOrchestrionPlugin(options: PluginOptions = {}): ReturnType // and never gets the diagnostics_channel calls injected. Mutating // `initialOptions` inside `setup` is respected by esbuild — the upstream // plugin sets `initialOptions.metafile` the same way. - build.initialOptions.external = withoutInstrumentedExternals(build.initialOptions.external); + build.initialOptions.external = withoutInstrumentedExternals( + build.initialOptions.external, + instrumentedModuleNames(options.instrumentations), + ); await transformer.setup(build); }, }; diff --git a/packages/server-utils/src/orchestrion/bundler/rollup.ts b/packages/server-utils/src/orchestrion/bundler/rollup.ts index 3db687756cc9..3f2f482bc913 100644 --- a/packages/server-utils/src/orchestrion/bundler/rollup.ts +++ b/packages/server-utils/src/orchestrion/bundler/rollup.ts @@ -1,5 +1,5 @@ import codeTransformer from '@apm-js-collab/code-transformer-bundler-plugins/rollup'; -import { withoutInstrumentedExternals } from '../config'; +import { instrumentedModuleNames, withoutInstrumentedExternals } from '../config'; import type { PluginOptions } from './options'; import { orchestrionTransformOptions } from './options'; @@ -27,7 +27,8 @@ export function sentryOrchestrionPlugin(options: PluginOptions = {}): ReturnType options(inputOptions) { const { external } = inputOptions; if (Array.isArray(external) && external.every(entry => typeof entry === 'string')) { - return { ...inputOptions, external: withoutInstrumentedExternals(external) }; + const moduleNames = instrumentedModuleNames(options.instrumentations); + return { ...inputOptions, external: withoutInstrumentedExternals(external, moduleNames) }; } return null; }, diff --git a/packages/server-utils/src/orchestrion/bundler/vite.ts b/packages/server-utils/src/orchestrion/bundler/vite.ts index db0ebcb7b5b1..75ad1ab49855 100644 --- a/packages/server-utils/src/orchestrion/bundler/vite.ts +++ b/packages/server-utils/src/orchestrion/bundler/vite.ts @@ -1,5 +1,5 @@ import codeTransformer from '@apm-js-collab/code-transformer-bundler-plugins/vite'; -import { INSTRUMENTED_MODULE_NAMES } from '../config'; +import { instrumentedModuleNames } from '../config'; import type { PluginOptions } from './options'; import { orchestrionTransformOptions } from './options'; @@ -28,7 +28,7 @@ export function sentryOrchestrionPlugin(options: PluginOptions = {}): ReturnType // diagnostics_channel calls never get injected. Vite merges array // `noExternal` entries with the user's config, so we don't overwrite // their additions. - return { ssr: { noExternal: INSTRUMENTED_MODULE_NAMES } }; + return { ssr: { noExternal: instrumentedModuleNames(options.instrumentations) } }; }, }; } diff --git a/packages/server-utils/src/orchestrion/config/index.ts b/packages/server-utils/src/orchestrion/config/index.ts index bf8fb24b2f21..275061e4f6ef 100644 --- a/packages/server-utils/src/orchestrion/config/index.ts +++ b/packages/server-utils/src/orchestrion/config/index.ts @@ -31,14 +31,21 @@ export const SENTRY_INSTRUMENTATIONS: InstrumentationConfig[] = [ /** * The unique set of package names instrumented by `SENTRY_INSTRUMENTATIONS` - * (e.g. `['mysql']`). + * merged with any caller-provided `instrumentations` (e.g. `['mysql']`). * * Bundler plugins MUST ensure these are actually bundled rather than * externalized: an externalized dependency is resolved from `node_modules` at * runtime and never passes through the code transform's `onLoad`, so its - * diagnostics_channel calls are silently never injected. + * diagnostics_channel calls are silently never injected. This includes a + * plugin's custom `instrumentations`, otherwise those extra packages can stay + * externalized and their transform never runs. */ -export const INSTRUMENTED_MODULE_NAMES: string[] = Array.from(new Set(SENTRY_INSTRUMENTATIONS.map(i => i.module.name))); +export function instrumentedModuleNames(instrumentations: InstrumentationConfig[] = []): string[] { + return Array.from(new Set([...SENTRY_INSTRUMENTATIONS, ...instrumentations].map(i => i.module.name))); +} + +/** The instrumented module names from the default Sentry config, with no custom additions. */ +export const INSTRUMENTED_MODULE_NAMES: string[] = instrumentedModuleNames(); /** * Returns `external` with any instrumented packages removed, so a bundler that @@ -47,14 +54,17 @@ export const INSTRUMENTED_MODULE_NAMES: string[] = Array.from(new Set(SENTRY_INS * (`'mysql/lib/...'`); wildcard/other patterns are left untouched. `undefined` * is returned unchanged. * - * (Vite uses an `ssr.noExternal` allowlist instead, so it consumes - * `INSTRUMENTED_MODULE_NAMES` directly rather than this helper.) + * Pass `moduleNames` from `instrumentedModuleNames(options.instrumentations)` so + * custom instrumentations are stripped too; it defaults to the Sentry config + * list. (Vite uses an `ssr.noExternal` allowlist instead, so it consumes + * `instrumentedModuleNames` directly rather than this helper.) */ -export function withoutInstrumentedExternals(external: readonly string[] | undefined): string[] | undefined { +export function withoutInstrumentedExternals( + external: readonly string[] | undefined, + moduleNames: string[] = INSTRUMENTED_MODULE_NAMES, +): string[] | undefined { if (!external) { return undefined; } - return external.filter( - entry => !INSTRUMENTED_MODULE_NAMES.some(name => entry === name || entry.startsWith(`${name}/`)), - ); + return external.filter(entry => !moduleNames.some(name => entry === name || entry.startsWith(`${name}/`))); } diff --git a/packages/server-utils/test/orchestrion/config.test.ts b/packages/server-utils/test/orchestrion/config.test.ts index adf18e807cd4..86e56ddd77a7 100644 --- a/packages/server-utils/test/orchestrion/config.test.ts +++ b/packages/server-utils/test/orchestrion/config.test.ts @@ -1,5 +1,10 @@ +import type { InstrumentationConfig } from '@apm-js-collab/code-transformer'; import { describe, expect, it } from 'vitest'; -import { INSTRUMENTED_MODULE_NAMES, withoutInstrumentedExternals } from '../../src/orchestrion/config'; +import { + INSTRUMENTED_MODULE_NAMES, + instrumentedModuleNames, + withoutInstrumentedExternals, +} from '../../src/orchestrion/config'; describe('orchestrion config — scoped @hapi/hapi module', () => { it('includes the scoped @hapi/hapi name in INSTRUMENTED_MODULE_NAMES', () => { @@ -13,3 +18,24 @@ describe('orchestrion config — scoped @hapi/hapi module', () => { expect(withoutInstrumentedExternals(external)).toEqual(['react']); }); }); + +describe('orchestrion config — custom instrumentations', () => { + const customInstrumentation = { module: { name: 'my-lib' } } as InstrumentationConfig; + + it('includes custom instrumentation module names alongside the defaults', () => { + const names = instrumentedModuleNames([customInstrumentation]); + expect(names).toContain('my-lib'); + expect(names).toContain('@hapi/hapi'); + }); + + it('strips custom instrumentation packages from an externals list', () => { + const external = ['react', 'my-lib', 'my-lib/sub']; + const names = instrumentedModuleNames([customInstrumentation]); + expect(withoutInstrumentedExternals(external, names)).toEqual(['react']); + }); + + it('leaves custom instrumentation packages externalized when only the defaults are used', () => { + const external = ['react', 'my-lib']; + expect(withoutInstrumentedExternals(external)).toEqual(['react', 'my-lib']); + }); +});