diff --git a/packages/core/src/build-time-plugins/buildTimeOptionsBase.ts b/packages/core/src/build-time-plugins/buildTimeOptionsBase.ts index 91ef3eaced37..eccfc3344f61 100644 --- a/packages/core/src/build-time-plugins/buildTimeOptionsBase.ts +++ b/packages/core/src/build-time-plugins/buildTimeOptionsBase.ts @@ -126,6 +126,15 @@ export interface BuildTimeOptionsBase { */ bundleSizeOptimizations?: BundleSizeOptimizationsOptions; + /** + * Automatic instrumentation of server-side dependencies at build time. + * + * Set to `false` to turn it off. + * + * @default true + */ + buildTimeInstrumentation?: boolean; + /** * A key that is used to identify the application in the Sentry bundler plugins. * This key is used by the `thirdPartyErrorFilterIntegration` to filter out errors diff --git a/packages/node/src/bundler-plugin/esbuild.ts b/packages/node/src/bundler-plugin/esbuild.ts index b72ac9f0c03b..c58515fa6a20 100644 --- a/packages/node/src/bundler-plugin/esbuild.ts +++ b/packages/node/src/bundler-plugin/esbuild.ts @@ -7,6 +7,15 @@ export type SentryEsbuildPluginOptions = SentryEsbuildPluginOptionsBase & { * @ignore This is for internal use only when this plugin is consumed by a framework SDK */ instrumentations?: NonNullable[0]>['instrumentations']; + + /** + * Automatic instrumentation of server-side dependencies at build time. + * + * Set to `false` to turn it off. + * + * @default true + */ + buildTimeInstrumentation?: boolean; }; type EsbuildPlugin = ReturnType; diff --git a/packages/node/src/bundler-plugin/rollup.ts b/packages/node/src/bundler-plugin/rollup.ts index e75ba75fe3f8..4258252e473c 100644 --- a/packages/node/src/bundler-plugin/rollup.ts +++ b/packages/node/src/bundler-plugin/rollup.ts @@ -7,6 +7,15 @@ export type SentryRollupPluginOptions = SentryRollupPluginOptionsBase & { * @ignore This is for internal use only when this plugin is consumed by a framework SDK */ instrumentations?: NonNullable[0]>['instrumentations']; + + /** + * Automatic instrumentation of server-side dependencies at build time. + * + * Set to `false` to turn it off. + * + * @default true + */ + buildTimeInstrumentation?: boolean; }; type RollupPlugin = ReturnType; @@ -27,5 +36,6 @@ type RollupPlugin = ReturnType; * ``` */ export function sentryRollupPlugin(options?: SentryRollupPluginOptions): RollupPlugin[] { - return [...sentryRollupBundlerPlugin(options), sentryOrchestrionPlugin(options)]; + const bundlerPlugins = sentryRollupBundlerPlugin(options); + return [...bundlerPlugins, sentryOrchestrionPlugin(options)]; } diff --git a/packages/node/src/bundler-plugin/vite.ts b/packages/node/src/bundler-plugin/vite.ts index a632dfa02f01..81c5ce5f2aa9 100644 --- a/packages/node/src/bundler-plugin/vite.ts +++ b/packages/node/src/bundler-plugin/vite.ts @@ -7,6 +7,15 @@ export type SentryVitePluginOptions = SentryVitePluginOptionsBase & { * @ignore This is for internal use only when this plugin is consumed by a framework SDK */ instrumentations?: NonNullable[0]>['instrumentations']; + + /** + * Automatic instrumentation of server-side dependencies at build time. + * + * Set to `false` to turn it off. + * + * @default true + */ + buildTimeInstrumentation?: boolean; }; type VitePlugin = ReturnType; @@ -28,5 +37,6 @@ type VitePlugin = ReturnType; * ``` */ export function sentryVitePlugin(options?: SentryVitePluginOptions): VitePlugin[] { - return [...sentryViteBundlerPlugin(options), sentryOrchestrionPlugin(options)]; + const bundlerPlugins = sentryViteBundlerPlugin(options); + return [...bundlerPlugins, sentryOrchestrionPlugin(options)]; } diff --git a/packages/node/src/bundler-plugin/webpack.ts b/packages/node/src/bundler-plugin/webpack.ts index 06bfe6bbaede..fec2b4d1b254 100644 --- a/packages/node/src/bundler-plugin/webpack.ts +++ b/packages/node/src/bundler-plugin/webpack.ts @@ -7,6 +7,15 @@ export type SentryWebpackPluginOptions = SentryWebpackPluginOptionsBase & { * @ignore This is for internal use only when this plugin is consumed by a framework SDK */ instrumentations?: NonNullable[0]>['instrumentations']; + + /** + * Automatic instrumentation of server-side dependencies at build time. + * + * Set to `false` to turn it off. + * + * @default true + */ + buildTimeInstrumentation?: boolean; }; type WebpackCompiler = Parameters['apply']>[0]; diff --git a/packages/node/test/bundler-plugin/bundler-plugin.test.ts b/packages/node/test/bundler-plugin/bundler-plugin.test.ts new file mode 100644 index 000000000000..dd50cffca04e --- /dev/null +++ b/packages/node/test/bundler-plugin/bundler-plugin.test.ts @@ -0,0 +1,55 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { sentryEsbuildPlugin } from '../../src/bundler-plugin/esbuild'; +import { sentryRollupPlugin } from '../../src/bundler-plugin/rollup'; +import { sentryVitePlugin } from '../../src/bundler-plugin/vite'; +import { sentryWebpackPlugin } from '../../src/bundler-plugin/webpack'; + +const orchestrionVite = vi.fn(() => ({ name: 'sentry-orchestrion-vite' })); +const orchestrionRollup = vi.fn(() => ({ name: 'sentry-orchestrion-rollup' })); +const orchestrionEsbuildSetup = vi.fn(); +const orchestrionWebpackApply = vi.fn(); + +vi.mock('@sentry/bundler-plugins/vite', () => ({ sentryVitePlugin: () => [{ name: 'sentry-vite' }] })); +vi.mock('@sentry/bundler-plugins/rollup', () => ({ sentryRollupPlugin: () => [{ name: 'sentry-rollup' }] })); +vi.mock('@sentry/bundler-plugins/esbuild', () => ({ + sentryEsbuildPlugin: () => ({ name: 'sentry-esbuild', setup: vi.fn() }), +})); +vi.mock('@sentry/bundler-plugins/webpack', () => ({ sentryWebpackPlugin: () => ({ apply: vi.fn() }) })); + +vi.mock('@sentry/server-utils/orchestrion/vite', () => ({ sentryOrchestrionPlugin: () => orchestrionVite() })); +vi.mock('@sentry/server-utils/orchestrion/rollup', () => ({ sentryOrchestrionPlugin: () => orchestrionRollup() })); +vi.mock('@sentry/server-utils/orchestrion/esbuild', () => ({ + sentryOrchestrionPlugin: () => ({ name: 'sentry-orchestrion-esbuild', setup: orchestrionEsbuildSetup }), +})); +vi.mock('@sentry/server-utils/orchestrion/webpack', () => ({ + sentryOrchestrionWebpackPlugin: () => ({ apply: orchestrionWebpackApply }), +})); + +describe('@sentry/node bundler plugins', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + // The wrappers always wire in the orchestrion plugin. Opting out via + // `buildTimeInstrumentation: false` is handled inside the orchestrion plugin + // itself (covered in @sentry/server-utils), which returns an inert plugin. + it('vite includes the orchestrion plugin', () => { + const plugins = sentryVitePlugin(); + expect(plugins.map(p => p.name)).toContain('sentry-orchestrion-vite'); + }); + + it('rollup includes the orchestrion plugin', () => { + const plugins = sentryRollupPlugin(); + expect(plugins.map(p => p.name)).toContain('sentry-orchestrion-rollup'); + }); + + it('esbuild runs the orchestrion setup', async () => { + await sentryEsbuildPlugin().setup({} as never); + expect(orchestrionEsbuildSetup).toHaveBeenCalledTimes(1); + }); + + it('webpack applies the orchestrion plugin', () => { + sentryWebpackPlugin().apply({} as never); + expect(orchestrionWebpackApply).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/server-utils/src/orchestrion/bundler/esbuild.ts b/packages/server-utils/src/orchestrion/bundler/esbuild.ts index 7215c48c7f3c..a11f1154a111 100644 --- a/packages/server-utils/src/orchestrion/bundler/esbuild.ts +++ b/packages/server-utils/src/orchestrion/bundler/esbuild.ts @@ -30,6 +30,11 @@ function matchesEsbuildExternal(entry: string, moduleName: string): boolean { * ``` */ export function sentryOrchestrionPlugin(options: PluginOptions = {}): Plugin { + if (options.buildTimeInstrumentation === false) { + // Inert plugin — no code transform, so no instrumentation lands in the bundle. + return { name: 'sentry-orchestrion-disabled', setup: () => undefined }; + } + const plugin = codeTransformer(orchestrionTransformOptions(options)); const moduleNames = instrumentedModuleNames(options.instrumentations); const setup = plugin.setup; diff --git a/packages/server-utils/src/orchestrion/bundler/options.ts b/packages/server-utils/src/orchestrion/bundler/options.ts index 6dea85440068..5610b394279e 100644 --- a/packages/server-utils/src/orchestrion/bundler/options.ts +++ b/packages/server-utils/src/orchestrion/bundler/options.ts @@ -8,6 +8,14 @@ export type PluginOptions = { * Additional instrumentations to include with the default instrumentation. */ instrumentations?: InstrumentationConfig[]; + /** + * Automatic instrumentation of server-side dependencies at build time. + * + * Set to `false` to make the plugin inert, so no instrumentation code is injected. + * + * @default true + */ + buildTimeInstrumentation?: boolean; /** * Custom transforms that can be applied using the `transform` option in each `InstrumentationConfig`. */ diff --git a/packages/server-utils/src/orchestrion/bundler/rollup.ts b/packages/server-utils/src/orchestrion/bundler/rollup.ts index f9262d9bd46e..ffa96ce86085 100644 --- a/packages/server-utils/src/orchestrion/bundler/rollup.ts +++ b/packages/server-utils/src/orchestrion/bundler/rollup.ts @@ -18,6 +18,11 @@ import { externalizedModulesWarning, orchestrionTransformOptions } from './optio * ``` */ export function sentryOrchestrionPlugin(options: PluginOptions = {}): Plugin { + if (options.buildTimeInstrumentation === false) { + // Inert plugin — no code transform, so no instrumentation lands in the bundle. + return { name: 'sentry-orchestrion-disabled' }; + } + const moduleNames = instrumentedModuleNames(options.instrumentations); return { diff --git a/packages/server-utils/src/orchestrion/bundler/vite.ts b/packages/server-utils/src/orchestrion/bundler/vite.ts index dc3426e9ba02..8c4cca0c5216 100644 --- a/packages/server-utils/src/orchestrion/bundler/vite.ts +++ b/packages/server-utils/src/orchestrion/bundler/vite.ts @@ -19,6 +19,13 @@ import { externalEntryMatchesModule, externalizedModulesWarning, orchestrionTran * ``` */ export function sentryOrchestrionPlugin(options: PluginOptions = {}): Plugin { + if (options.buildTimeInstrumentation === false) { + // Return an inert plugin so SDKs that unconditionally push it into their + // plugin array can still opt out without any code transform, `noExternal` + // force-bundling, or injected diagnostics landing in the build. + return { name: 'sentry-orchestrion-disabled' }; + } + return { ...codeTransformer(orchestrionTransformOptions(options)), applyToEnvironment(environment) { diff --git a/packages/server-utils/src/orchestrion/bundler/webpack.ts b/packages/server-utils/src/orchestrion/bundler/webpack.ts index 01c97e26ffa6..a44f2ffa5e34 100644 --- a/packages/server-utils/src/orchestrion/bundler/webpack.ts +++ b/packages/server-utils/src/orchestrion/bundler/webpack.ts @@ -112,6 +112,11 @@ function fixupLoaderPath(compiler: Compiler): void { * transform, so a compilation warning is emitted for them. */ export function sentryOrchestrionWebpackPlugin(options: PluginOptions = {}): { apply(compiler: Compiler): void } { + if (options.buildTimeInstrumentation === false) { + // Inert plugin — no code transform, so no instrumentation lands in the bundle. + return { apply: () => undefined }; + } + const plugin = codeTransformerWebpack(orchestrionTransformOptions(options)); const moduleNames = instrumentedModuleNames(options.instrumentations); // The upstream plugin is a class instance, so `apply` is overridden in place diff --git a/packages/server-utils/test/orchestrion/bundler.test.ts b/packages/server-utils/test/orchestrion/bundler.test.ts index ee1c746fc6de..6fae3f2e92a3 100644 --- a/packages/server-utils/test/orchestrion/bundler.test.ts +++ b/packages/server-utils/test/orchestrion/bundler.test.ts @@ -139,6 +139,47 @@ describe('sentryOrchestrionPlugin (vite)', () => { }); }); +describe('buildTimeInstrumentation: false', () => { + const disabled = { buildTimeInstrumentation: false }; + + it('returns an inert vite plugin without the transform hooks', () => { + const plugin = vitePlugin(disabled); + + expect(plugin.name).toBe('sentry-orchestrion-disabled'); + expect(plugin.config).toBeUndefined(); + expect(plugin.configResolved).toBeUndefined(); + expect(plugin.applyToEnvironment).toBeUndefined(); + }); + + it('returns an inert rollup plugin without the transform hooks', () => { + const plugin = rollupPlugin(disabled); + + expect(plugin.name).toBe('sentry-orchestrion-disabled'); + expect(plugin.buildStart).toBeUndefined(); + expect((plugin as { transform?: unknown }).transform).toBeUndefined(); + }); + + it('returns an inert esbuild plugin whose setup is a no-op', () => { + const build = { initialOptions: {}, onStart: vi.fn() } as unknown as PluginBuild; + const plugin = esbuildPlugin(disabled); + + expect(plugin.name).toBe('sentry-orchestrion-disabled'); + expect(plugin.setup(build)).toBeUndefined(); + expect(build.onStart).not.toHaveBeenCalled(); + }); + + it('returns an inert webpack plugin whose apply is a no-op', () => { + const tap = vi.fn(); + const compiler = { + options: { externals: ['mysql'] }, + hooks: { thisCompilation: { tap } }, + } as unknown as Compiler; + + expect(() => sentryOrchestrionWebpackPlugin(disabled).apply(compiler)).not.toThrow(); + expect(tap).not.toHaveBeenCalled(); + }); +}); + describe('resolveOrchestrionRuntimeRequest', () => { it.each([ // Self-references — resolve through this package's own exports map to the CJS build.