diff --git a/packages/node/package.json b/packages/node/package.json index 4f3f1b142b9c..2306e7982448 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -52,12 +52,48 @@ "require": { "default": "./build/cjs/preload.js" } + }, + "./bundler-plugins/vite": { + "import": { + "types": "./build/types/bundler-plugins/vite.d.ts", + "default": "./build/esm/bundler-plugins/vite.js" + } + }, + "./bundler-plugins/rollup": { + "import": { + "types": "./build/types/bundler-plugins/rollup.d.ts", + "default": "./build/esm/bundler-plugins/rollup.js" + } + }, + "./bundler-plugins/webpack": { + "import": { + "types": "./build/types/bundler-plugins/webpack.d.ts", + "default": "./build/esm/bundler-plugins/webpack.js" + } + }, + "./bundler-plugins/esbuild": { + "import": { + "types": "./build/types/bundler-plugins/esbuild.d.ts", + "default": "./build/esm/bundler-plugins/esbuild.js" + } } }, "typesVersions": { "<5.0": { "build/types/index.d.ts": [ "build/types-ts3.8/index.d.ts" + ], + "bundler-plugins/vite": [ + "build/types-ts3.8/bundler-plugins/vite.d.ts" + ], + "bundler-plugins/rollup": [ + "build/types-ts3.8/bundler-plugins/rollup.d.ts" + ], + "bundler-plugins/webpack": [ + "build/types-ts3.8/bundler-plugins/webpack.d.ts" + ], + "bundler-plugins/esbuild": [ + "build/types-ts3.8/bundler-plugins/esbuild.d.ts" ] } }, @@ -68,6 +104,7 @@ "@opentelemetry/api": "^1.9.1", "@opentelemetry/instrumentation": "^0.220.0", "@opentelemetry/sdk-trace-base": "^2.9.0", + "@sentry/bundler-plugins": "10.64.0", "@sentry/conventions": "^0.15.1", "@sentry/core": "10.65.0", "@sentry/node-core": "10.65.0", diff --git a/packages/node/rollup.npm.config.mjs b/packages/node/rollup.npm.config.mjs index 3f6d1b28bf93..c402e72e7c4e 100644 --- a/packages/node/rollup.npm.config.mjs +++ b/packages/node/rollup.npm.config.mjs @@ -9,7 +9,18 @@ export default [ ...makeOtelLoaders('./build', 'otel', { injectDiagnosticsChannel: true }), ...makeNPMConfigVariants( makeBaseNPMConfig({ - entrypoints: ['src/index.ts', 'src/init.ts', 'src/preload.ts'], + entrypoints: [ + 'src/index.ts', + 'src/init.ts', + 'src/preload.ts', + // Combined Sentry bundler plugins + orchestrion code transform, exposed + // ESM-only via the `@sentry/node/bundler-plugins/*` subpath exports + // (they import the ESM-only `@sentry/server-utils/orchestrion/*` plugins). + 'src/bundler-plugins/vite.ts', + 'src/bundler-plugins/rollup.ts', + 'src/bundler-plugins/webpack.ts', + 'src/bundler-plugins/esbuild.ts', + ], packageSpecificConfig: { external: [/^@sentry\/opentelemetry/], output: { diff --git a/packages/node/src/bundler-plugins/esbuild.ts b/packages/node/src/bundler-plugins/esbuild.ts new file mode 100644 index 000000000000..bade47b5c188 --- /dev/null +++ b/packages/node/src/bundler-plugins/esbuild.ts @@ -0,0 +1,43 @@ +import { sentryEsbuildPlugin as sentryEsbuildBundlerPlugin } from '@sentry/bundler-plugins/esbuild'; +import type { SentryEsbuildPluginOptions as SentryEsbuildPluginOptionsBase } from '@sentry/bundler-plugins/esbuild'; +import { sentryOrchestrionPlugin } from '@sentry/server-utils/orchestrion/esbuild'; + +export type SentryEsbuildPluginOptions = SentryEsbuildPluginOptionsBase & { + /** + * @ignore This is for internal use only when this plugin is consumed by a framework SDK + */ + instrumentations?: NonNullable[0]>['instrumentations']; +}; + +type EsbuildPlugin = ReturnType; + +/** + * esbuild plugin that bundles the Sentry esbuild bundler plugin (source maps, + * release injection, …) together with the orchestrion code transform + * (build-time `diagnostics_channel` instrumentation for Node libraries). + * + * It is a drop-in replacement for `@sentry/bundler-plugins/esbuild` and accepts + * the same options. + * + * esbuild does not flatten nested `plugins` arrays, so this returns a single + * plugin that runs both plugins' `setup` against the same build. + * + * @example + * ```ts + * // build.mjs + * import { sentryEsbuildPlugin } from '@sentry/node/bundler-plugins/esbuild'; + * await esbuild.build({ plugins: [sentryEsbuildPlugin({ org: '…', project: '…' })] }); + * ``` + */ +export function sentryEsbuildPlugin(options?: SentryEsbuildPluginOptions): EsbuildPlugin { + const bundlerPlugin = sentryEsbuildBundlerPlugin(options); + const orchestrionPlugin = sentryOrchestrionPlugin(options); + + return { + name: 'sentry-node-esbuild', + setup(build): void { + bundlerPlugin.setup(build); + orchestrionPlugin.setup(build); + }, + }; +} diff --git a/packages/node/src/bundler-plugins/rollup.ts b/packages/node/src/bundler-plugins/rollup.ts new file mode 100644 index 000000000000..945c83cda869 --- /dev/null +++ b/packages/node/src/bundler-plugins/rollup.ts @@ -0,0 +1,35 @@ +import { sentryRollupPlugin as sentryRollupBundlerPlugin } from '@sentry/bundler-plugins/rollup'; +import type { SentryRollupPluginOptions as SentryRollupPluginOptionsBase } from '@sentry/bundler-plugins/rollup'; +import { sentryOrchestrionPlugin } from '@sentry/server-utils/orchestrion/rollup'; + +export type SentryRollupPluginOptions = SentryRollupPluginOptionsBase & { + /** + * @ignore This is for internal use only when this plugin is consumed by a framework SDK + */ + instrumentations?: NonNullable[0]>['instrumentations']; +}; + +type RollupPlugin = ReturnType; + +/** + * Rollup plugin that bundles the Sentry Rollup bundler plugin (source maps, + * release injection, bundle size optimizations, …) together with the + * orchestrion code transform (build-time `diagnostics_channel` instrumentation + * for Node libraries). + * + * It is a drop-in replacement for `@sentry/bundler-plugins/rollup` and accepts + * the same options. + * + * Rollup flattens nested `plugins` arrays, so `plugins: [sentryRollupPlugin(opts)]` + * works directly. + * + * @example + * ```ts + * // rollup.config.js + * import { sentryRollupPlugin } from '@sentry/node/bundler-plugins/rollup'; + * export default { plugins: [sentryRollupPlugin({ org: '…', project: '…' })] }; + * ``` + */ +export function sentryRollupPlugin(options?: SentryRollupPluginOptions): RollupPlugin[] { + return [sentryRollupBundlerPlugin(options), sentryOrchestrionPlugin(options)]; +} diff --git a/packages/node/src/bundler-plugins/vite.ts b/packages/node/src/bundler-plugins/vite.ts new file mode 100644 index 000000000000..5d953b7dd42d --- /dev/null +++ b/packages/node/src/bundler-plugins/vite.ts @@ -0,0 +1,35 @@ +import { sentryVitePlugin as sentryViteBundlerPlugin } from '@sentry/bundler-plugins/vite'; +import type { SentryVitePluginOptions as SentryVitePluginOptionsBase } from '@sentry/bundler-plugins/vite'; +import { sentryOrchestrionPlugin } from '@sentry/server-utils/orchestrion/vite'; + +export type SentryVitePluginOptions = SentryVitePluginOptionsBase & { + /** + * @ignore This is for internal use only when this plugin is consumed by a framework SDK + */ + instrumentations?: NonNullable[0]>['instrumentations']; +}; + +type VitePlugin = ReturnType; + +/** + * Vite plugin that bundles the Sentry Vite bundler plugin (source maps, release + * injection, bundle size optimizations, …) together with the orchestrion code + * transform (build-time `diagnostics_channel` instrumentation for Node + * libraries). + * + * It is a drop-in replacement for `@sentry/bundler-plugins/vite` and accepts the + * same options. + * + * Vite flattens nested `plugins` arrays, so `plugins: [sentryVitePlugin(opts)]` + * works directly. + * + * @example + * ```ts + * // vite.config.ts + * import { sentryVitePlugin } from '@sentry/node/bundler-plugins/vite'; + * export default { plugins: [sentryVitePlugin({ org: '…', project: '…' })] }; + * ``` + */ +export function sentryVitePlugin(options?: SentryVitePluginOptions): VitePlugin[] { + return [...sentryViteBundlerPlugin(options), sentryOrchestrionPlugin(options)]; +} diff --git a/packages/node/src/bundler-plugins/webpack.ts b/packages/node/src/bundler-plugins/webpack.ts new file mode 100644 index 000000000000..3ea8b6571c9d --- /dev/null +++ b/packages/node/src/bundler-plugins/webpack.ts @@ -0,0 +1,44 @@ +import { sentryWebpackPlugin as sentryWebpackBundlerPlugin } from '@sentry/bundler-plugins/webpack'; +import type { SentryWebpackPluginOptions as SentryWebpackPluginOptionsBase } from '@sentry/bundler-plugins/webpack'; +import { sentryOrchestrionPlugin } from '@sentry/server-utils/orchestrion/webpack'; + +export type SentryWebpackPluginOptions = SentryWebpackPluginOptionsBase & { + /** + * @ignore This is for internal use only when this plugin is consumed by a framework SDK + */ + instrumentations?: NonNullable[0]>['instrumentations']; +}; + +type WebpackCompiler = Parameters['apply']>[0]; + +/** + * webpack plugin that bundles the Sentry webpack bundler plugin (source maps, + * release injection, …) together with the orchestrion code transform + * (build-time `diagnostics_channel` instrumentation for Node libraries). + * + * It is a drop-in replacement for `@sentry/bundler-plugins/webpack` and accepts + * the same options. + * + * webpack does not flatten nested `plugins` arrays, so this returns a single + * plugin that runs both plugins' `apply` against the same compiler. + * + * @example + * ```ts + * // webpack.config.mjs + * import { sentryWebpackPlugin } from '@sentry/node/bundler-plugins/webpack'; + * export default { plugins: [sentryWebpackPlugin({ org: '…', project: '…' })] }; + * ``` + */ +export function sentryWebpackPlugin(options?: SentryWebpackPluginOptions): { + apply: (compiler: WebpackCompiler) => void; +} { + const bundlerPlugin = sentryWebpackBundlerPlugin(options); + const orchestrionPlugin = sentryOrchestrionPlugin(options); + + return { + apply(compiler: WebpackCompiler): void { + bundlerPlugin.apply(compiler); + orchestrionPlugin.apply(compiler); + }, + }; +} diff --git a/packages/server-utils/package.json b/packages/server-utils/package.json index 749dd600cb80..91664028fd25 100644 --- a/packages/server-utils/package.json +++ b/packages/server-utils/package.json @@ -41,11 +41,19 @@ "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", "require": "./build/cjs/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" } @@ -67,8 +75,14 @@ "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" ] }, "*": { @@ -84,8 +98,14 @@ "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" ] } }, @@ -94,13 +114,13 @@ }, "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.65.0", "magic-string": "~0.30.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 5f02614f716d..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,7 +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..391cdd0229dc --- /dev/null +++ b/packages/server-utils/src/orchestrion/bundler/esbuild.ts @@ -0,0 +1,42 @@ +import codeTransformer from '@apm-js-collab/code-transformer-bundler-plugins/esbuild'; +import { instrumentedModuleNames, withoutInstrumentedExternals } from '../config'; +import type { PluginOptions } from './options'; +import { orchestrionTransformOptions } from './options'; + +/** + * 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(options: PluginOptions = {}): ReturnType { + const transformer = codeTransformer(orchestrionTransformOptions(options)); + + return { + name: 'sentry-orchestrion', + 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 + // 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, + instrumentedModuleNames(options.instrumentations), + ); + await 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..b285b3074236 --- /dev/null +++ b/packages/server-utils/src/orchestrion/bundler/options.ts @@ -0,0 +1,28 @@ +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 + * 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(options: PluginOptions): Parameters[0] { + return { + 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 new file mode 100644 index 000000000000..3f2f482bc913 --- /dev/null +++ b/packages/server-utils/src/orchestrion/bundler/rollup.ts @@ -0,0 +1,36 @@ +import codeTransformer from '@apm-js-collab/code-transformer-bundler-plugins/rollup'; +import { instrumentedModuleNames, withoutInstrumentedExternals } from '../config'; +import type { PluginOptions } from './options'; +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(options: PluginOptions = {}): ReturnType { + return { + ...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` + // 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')) { + 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 4cadc7b40925..75ad1ab49855 100644 --- a/packages/server-utils/src/orchestrion/bundler/vite.ts +++ b/packages/server-utils/src/orchestrion/bundler/vite.ts @@ -1,26 +1,7 @@ -// 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 { instrumentedModuleNames } from '../config'; +import type { PluginOptions } from './options'; +import { orchestrionTransformOptions } from './options'; /** * Vite plugin that runs the orchestrion code transform on the bundled output. @@ -29,44 +10,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(options: PluginOptions = {}): ReturnType { return { - name: 'sentry-orchestrion-marker', - enforce: 'pre' as const, + ...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 @@ -75,16 +28,7 @@ function bundlerMarkerPlugin(): UnknownPlugin { // 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 } }; - }, - 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 }) }; + 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 b0e4d3e8b6d1..bb686f2ecb17 100644 --- a/packages/server-utils/src/orchestrion/config/index.ts +++ b/packages/server-utils/src/orchestrion/config/index.ts @@ -36,14 +36,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[] = uniq(SENTRY_INSTRUMENTATIONS.map(i => i.module.name)); +export function instrumentedModuleNames(instrumentations: InstrumentationConfig[] = []): string[] { + return uniq([...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 @@ -52,14 +59,17 @@ export const INSTRUMENTED_MODULE_NAMES: string[] = uniq(SENTRY_INSTRUMENTATIONS. * (`'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']); + }); +});