From f1196716c180296c2b1a6c1e38e5a76391daeb64 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 21 Aug 2026 12:09:04 +0200 Subject: [PATCH 1/7] feat(node): Default excludeChannelInjection when build-time instrumentation is enabled The `@sentry/node` bundler plugins inject the orchestrion diagnostics channels at build time. When that is on (the default), the SDK's runtime channel injection is redundant, so default `bundleSizeOptimizations.excludeChannelInjection` to `true` to tree-shake the runtime hooks out. Users can still override it explicitly, and the default is skipped when `buildTimeInstrumentation: false`. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/node/src/bundler-plugin/common.ts | 32 ++++++++++++++++++ packages/node/src/bundler-plugin/esbuild.ts | 3 +- packages/node/src/bundler-plugin/rollup.ts | 3 +- packages/node/src/bundler-plugin/vite.ts | 3 +- packages/node/src/bundler-plugin/webpack.ts | 5 ++- .../node/test/bundler-plugin/common.test.ts | 33 +++++++++++++++++++ 6 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 packages/node/src/bundler-plugin/common.ts create mode 100644 packages/node/test/bundler-plugin/common.test.ts diff --git a/packages/node/src/bundler-plugin/common.ts b/packages/node/src/bundler-plugin/common.ts new file mode 100644 index 000000000000..4989adb84613 --- /dev/null +++ b/packages/node/src/bundler-plugin/common.ts @@ -0,0 +1,32 @@ +interface WithBuildTimeInstrumentation { + buildTimeInstrumentation?: boolean; + bundleSizeOptimizations?: { excludeChannelInjection?: boolean } & Record; +} + +/** + * When build-time instrumentation (orchestrion diagnostics-channel injection) is enabled — which is + * the default — the SDK's *runtime* channel injection is redundant. So we default + * `bundleSizeOptimizations.excludeChannelInjection` to `true`, letting the bundler plugin tree-shake + * the runtime hooks out of the build. + * + * The user can still opt back in by explicitly setting `excludeChannelInjection` (e.g. `false`), and + * the default is not applied when build-time instrumentation is turned off + * (`buildTimeInstrumentation: false`), since the runtime injection is then the only thing wiring up + * the channels. + */ +export function withChannelInjectionExclusionDefault( + options?: T, +): T | undefined { + if (options?.buildTimeInstrumentation === false) { + return options; + } + + return { + ...(options as T), + bundleSizeOptimizations: { + excludeChannelInjection: true, + // A user-provided value takes precedence over the default above. + ...options?.bundleSizeOptimizations, + }, + }; +} diff --git a/packages/node/src/bundler-plugin/esbuild.ts b/packages/node/src/bundler-plugin/esbuild.ts index c58515fa6a20..852449e0c013 100644 --- a/packages/node/src/bundler-plugin/esbuild.ts +++ b/packages/node/src/bundler-plugin/esbuild.ts @@ -1,6 +1,7 @@ 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'; +import { withChannelInjectionExclusionDefault } from './common'; export type SentryEsbuildPluginOptions = SentryEsbuildPluginOptionsBase & { /** @@ -35,7 +36,7 @@ type EsbuildPlugin = ReturnType; * ``` */ export function sentryEsbuildPlugin(options?: SentryEsbuildPluginOptions): EsbuildPlugin { - const bundlerPlugin = sentryEsbuildBundlerPlugin(options) as EsbuildPlugin; + const bundlerPlugin = sentryEsbuildBundlerPlugin(withChannelInjectionExclusionDefault(options)) as EsbuildPlugin; const orchestrionPlugin = sentryOrchestrionPlugin(options); return { diff --git a/packages/node/src/bundler-plugin/rollup.ts b/packages/node/src/bundler-plugin/rollup.ts index 4258252e473c..867caafd8cc6 100644 --- a/packages/node/src/bundler-plugin/rollup.ts +++ b/packages/node/src/bundler-plugin/rollup.ts @@ -1,6 +1,7 @@ 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'; +import { withChannelInjectionExclusionDefault } from './common'; export type SentryRollupPluginOptions = SentryRollupPluginOptionsBase & { /** @@ -36,6 +37,6 @@ type RollupPlugin = ReturnType; * ``` */ export function sentryRollupPlugin(options?: SentryRollupPluginOptions): RollupPlugin[] { - const bundlerPlugins = sentryRollupBundlerPlugin(options); + const bundlerPlugins = sentryRollupBundlerPlugin(withChannelInjectionExclusionDefault(options)); return [...bundlerPlugins, sentryOrchestrionPlugin(options)]; } diff --git a/packages/node/src/bundler-plugin/vite.ts b/packages/node/src/bundler-plugin/vite.ts index 81c5ce5f2aa9..581749fd33d0 100644 --- a/packages/node/src/bundler-plugin/vite.ts +++ b/packages/node/src/bundler-plugin/vite.ts @@ -1,6 +1,7 @@ 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'; +import { withChannelInjectionExclusionDefault } from './common'; export type SentryVitePluginOptions = SentryVitePluginOptionsBase & { /** @@ -37,6 +38,6 @@ type VitePlugin = ReturnType; * ``` */ export function sentryVitePlugin(options?: SentryVitePluginOptions): VitePlugin[] { - const bundlerPlugins = sentryViteBundlerPlugin(options); + const bundlerPlugins = sentryViteBundlerPlugin(withChannelInjectionExclusionDefault(options)); return [...bundlerPlugins, sentryOrchestrionPlugin(options)]; } diff --git a/packages/node/src/bundler-plugin/webpack.ts b/packages/node/src/bundler-plugin/webpack.ts index fec2b4d1b254..6391e5e9ae67 100644 --- a/packages/node/src/bundler-plugin/webpack.ts +++ b/packages/node/src/bundler-plugin/webpack.ts @@ -1,6 +1,7 @@ import { sentryWebpackPlugin as sentryWebpackBundlerPlugin } from '@sentry/bundler-plugins/webpack'; import type { SentryWebpackPluginOptions as SentryWebpackPluginOptionsBase } from '@sentry/bundler-plugins/webpack'; import { sentryOrchestrionWebpackPlugin } from '@sentry/server-utils/orchestrion/webpack'; +import { withChannelInjectionExclusionDefault } from './common'; export type SentryWebpackPluginOptions = SentryWebpackPluginOptionsBase & { /** @@ -37,7 +38,9 @@ type WebpackCompiler = Parameters[ export function sentryWebpackPlugin(options?: SentryWebpackPluginOptions): { apply: (compiler: WebpackCompiler) => void; } { - const bundlerPlugin = sentryWebpackBundlerPlugin(options) as { apply: (compiler: WebpackCompiler) => void }; + const bundlerPlugin = sentryWebpackBundlerPlugin(withChannelInjectionExclusionDefault(options)) as { + apply: (compiler: WebpackCompiler) => void; + }; const orchestrionPlugin = sentryOrchestrionWebpackPlugin(options) as { apply: (compiler: WebpackCompiler) => void }; return { diff --git a/packages/node/test/bundler-plugin/common.test.ts b/packages/node/test/bundler-plugin/common.test.ts new file mode 100644 index 000000000000..80582370194b --- /dev/null +++ b/packages/node/test/bundler-plugin/common.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest'; +import { withChannelInjectionExclusionDefault } from '../../src/bundler-plugin/common'; + +describe('withChannelInjectionExclusionDefault', () => { + it('defaults excludeChannelInjection to true when build-time instrumentation is enabled (the default)', () => { + expect(withChannelInjectionExclusionDefault(undefined)).toEqual({ + bundleSizeOptimizations: { excludeChannelInjection: true }, + }); + + expect(withChannelInjectionExclusionDefault({ org: 'my-org' })).toEqual({ + org: 'my-org', + bundleSizeOptimizations: { excludeChannelInjection: true }, + }); + }); + + it('does not set the default when build-time instrumentation is disabled', () => { + expect(withChannelInjectionExclusionDefault({ buildTimeInstrumentation: false })).toEqual({ + buildTimeInstrumentation: false, + }); + }); + + it('lets the user override excludeChannelInjection', () => { + expect( + withChannelInjectionExclusionDefault({ bundleSizeOptimizations: { excludeChannelInjection: false } }), + ).toEqual({ bundleSizeOptimizations: { excludeChannelInjection: false } }); + }); + + it('preserves other bundleSizeOptimizations while adding the default', () => { + expect(withChannelInjectionExclusionDefault({ bundleSizeOptimizations: { excludeTracing: true } })).toEqual({ + bundleSizeOptimizations: { excludeChannelInjection: true, excludeTracing: true }, + }); + }); +}); From fc8f4f65996a6de4541eb9403b541da62b873e13 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 21 Aug 2026 13:10:09 +0200 Subject: [PATCH 2/7] test(e2e): Rename node-orchestrion-webpack to node-webpack and assert plugin excludes runtime injection Build the entry twice: plain webpack (runtime channel injection bundled by default) and with `sentryWebpackPlugin` (build-time instrumentation), which defaults `excludeChannelInjection` to true and tree-shakes the runtime injection out. assert.mjs verifies the marker is present in the plain build and absent in the plugin build. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../{node-orchestrion-webpack => node-webpack}/assert.mjs | 0 .../{node-orchestrion-webpack => node-webpack}/build.mjs | 0 .../{node-orchestrion-webpack => node-webpack}/package.json | 0 .../{node-orchestrion-webpack => node-webpack}/src/app.mjs | 0 .../{node-orchestrion-webpack => node-webpack}/src/entry.mjs | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename dev-packages/e2e-tests/test-applications/{node-orchestrion-webpack => node-webpack}/assert.mjs (100%) rename dev-packages/e2e-tests/test-applications/{node-orchestrion-webpack => node-webpack}/build.mjs (100%) rename dev-packages/e2e-tests/test-applications/{node-orchestrion-webpack => node-webpack}/package.json (100%) rename dev-packages/e2e-tests/test-applications/{node-orchestrion-webpack => node-webpack}/src/app.mjs (100%) rename dev-packages/e2e-tests/test-applications/{node-orchestrion-webpack => node-webpack}/src/entry.mjs (100%) diff --git a/dev-packages/e2e-tests/test-applications/node-orchestrion-webpack/assert.mjs b/dev-packages/e2e-tests/test-applications/node-webpack/assert.mjs similarity index 100% rename from dev-packages/e2e-tests/test-applications/node-orchestrion-webpack/assert.mjs rename to dev-packages/e2e-tests/test-applications/node-webpack/assert.mjs diff --git a/dev-packages/e2e-tests/test-applications/node-orchestrion-webpack/build.mjs b/dev-packages/e2e-tests/test-applications/node-webpack/build.mjs similarity index 100% rename from dev-packages/e2e-tests/test-applications/node-orchestrion-webpack/build.mjs rename to dev-packages/e2e-tests/test-applications/node-webpack/build.mjs diff --git a/dev-packages/e2e-tests/test-applications/node-orchestrion-webpack/package.json b/dev-packages/e2e-tests/test-applications/node-webpack/package.json similarity index 100% rename from dev-packages/e2e-tests/test-applications/node-orchestrion-webpack/package.json rename to dev-packages/e2e-tests/test-applications/node-webpack/package.json diff --git a/dev-packages/e2e-tests/test-applications/node-orchestrion-webpack/src/app.mjs b/dev-packages/e2e-tests/test-applications/node-webpack/src/app.mjs similarity index 100% rename from dev-packages/e2e-tests/test-applications/node-orchestrion-webpack/src/app.mjs rename to dev-packages/e2e-tests/test-applications/node-webpack/src/app.mjs diff --git a/dev-packages/e2e-tests/test-applications/node-orchestrion-webpack/src/entry.mjs b/dev-packages/e2e-tests/test-applications/node-webpack/src/entry.mjs similarity index 100% rename from dev-packages/e2e-tests/test-applications/node-orchestrion-webpack/src/entry.mjs rename to dev-packages/e2e-tests/test-applications/node-webpack/src/entry.mjs From 1527e5b097a61f8fbd8fe54aad06fbaf0ee06377 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 21 Aug 2026 13:16:35 +0200 Subject: [PATCH 3/7] test(e2e): Minify node-webpack builds so runtime-injection tree-shaking runs The dead `if (useChannelInjection)` branch is only pruned by the minifier, so `minimize: false` left the runtime injection in the bundle. Verified: with `minimize: true`, the real `sentryWebpackPlugin` excludes the runtime injection while a plain build keeps it. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test-applications/node-webpack/build.mjs | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/node-webpack/build.mjs b/dev-packages/e2e-tests/test-applications/node-webpack/build.mjs index 08be7f25a103..0584a631b9ad 100644 --- a/dev-packages/e2e-tests/test-applications/node-webpack/build.mjs +++ b/dev-packages/e2e-tests/test-applications/node-webpack/build.mjs @@ -1,17 +1,22 @@ -// Bundles the entrypoint with webpack (the pinned version in package.json -// kept current, since webpack's `createRequire` following has changed across -// releases). Output goes to ./dist/app/ for assert.mjs to inspect. +// Bundles the entrypoint with webpack twice: +// - `plain`: no Sentry plugin — the runtime diagnostics-channel injection is bundled (v11 default). +// - `plugin`: with `sentryWebpackPlugin` (build-time instrumentation) — which defaults +// `bundleSizeOptimizations.excludeChannelInjection` to `true`, tree-shaking the runtime +// injection out of the bundle. +// assert.mjs inspects both outputs. Kept unminified so tree-shaking (module elimination via +// `sideEffects: false`) is easy to debug. import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import webpack from 'webpack'; +import { sentryWebpackPlugin } from '@sentry/node/webpack'; const __dirname = dirname(fileURLToPath(import.meta.url)); -function build(name) { +function build(name, plugins) { return new Promise((resolve, reject) => { webpack( { - entry: join(__dirname, 'src', `${name}.mjs`), + entry: join(__dirname, 'src', 'entry.mjs'), mode: 'production', target: 'node', experiments: { topLevelAwait: true, outputModule: true }, @@ -22,10 +27,11 @@ function build(name) { library: { type: 'module' }, chunkFormat: 'module', }, - // Keep output readable; tree-shaking (module elimination via - // `sideEffects: false`) happens regardless of minification, and - // it's important to be able to debug when it messes up. - optimization: { minimize: false }, + // Minify so terser's dead-code elimination runs — the runtime injection is removed by the + // `if (useChannelInjection)` branch going dead once `__SENTRY_CHANNEL_INJECTION__` is `false`, + // which webpack only prunes via the minifier (not plain module tree-shaking). + optimization: { minimize: true }, + plugins, }, (err, stats) => { if (err) return reject(err); @@ -40,4 +46,15 @@ function build(name) { }); } -await build('entry'); +await build('plain', []); +await build( + 'plugin', + // No auth/release/telemetry — we only care about the build-time transforms and defines. + [ + sentryWebpackPlugin({ + telemetry: false, + sourcemaps: { disable: true }, + release: { create: false, finalize: false, inject: false }, + }), + ], +); From c00ea78366bd31fa18add49bbc5be15289495699 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 21 Aug 2026 13:27:15 +0200 Subject: [PATCH 4/7] test(e2e): Add node-vite and node-rollup runtime-injection-exclusion tests Mirror node-webpack for Vite and Rollup: build the entry plain and with the respective Sentry plugin, and assert the runtime channel-injection marker is present in the plain build but tree-shaken out with the plugin (build-time instrumentation defaults `excludeChannelInjection` to true). Verified locally that both bundlers tree-shake the runtime injection. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test-applications/node-rollup/assert.mjs | 48 +++++++++++++++++++ .../test-applications/node-rollup/build.mjs | 42 ++++++++++++++++ .../node-rollup/package.json | 25 ++++++++++ .../test-applications/node-rollup/src/app.mjs | 2 + .../node-rollup/src/entry.mjs | 9 ++++ .../test-applications/node-vite/assert.mjs | 48 +++++++++++++++++++ .../test-applications/node-vite/build.mjs | 43 +++++++++++++++++ .../test-applications/node-vite/package.json | 23 +++++++++ .../test-applications/node-vite/src/app.mjs | 2 + .../test-applications/node-vite/src/entry.mjs | 9 ++++ 10 files changed, 251 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/node-rollup/assert.mjs create mode 100644 dev-packages/e2e-tests/test-applications/node-rollup/build.mjs create mode 100644 dev-packages/e2e-tests/test-applications/node-rollup/package.json create mode 100644 dev-packages/e2e-tests/test-applications/node-rollup/src/app.mjs create mode 100644 dev-packages/e2e-tests/test-applications/node-rollup/src/entry.mjs create mode 100644 dev-packages/e2e-tests/test-applications/node-vite/assert.mjs create mode 100644 dev-packages/e2e-tests/test-applications/node-vite/build.mjs create mode 100644 dev-packages/e2e-tests/test-applications/node-vite/package.json create mode 100644 dev-packages/e2e-tests/test-applications/node-vite/src/app.mjs create mode 100644 dev-packages/e2e-tests/test-applications/node-vite/src/entry.mjs diff --git a/dev-packages/e2e-tests/test-applications/node-rollup/assert.mjs b/dev-packages/e2e-tests/test-applications/node-rollup/assert.mjs new file mode 100644 index 000000000000..e57550713301 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-rollup/assert.mjs @@ -0,0 +1,48 @@ +/** + * Asserts that the Sentry rollup plugin excludes the *runtime* diagnostics-channel injection by + * default (because it instruments at build time instead), while a plain build keeps it. + * + * @module + */ +import { readdirSync, readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +// This string literal lives only in `@sentry/server-utils`' runtime injection module +// (`orchestrion/runtime/register.ts`) — the code `registerDiagnosticsChannelInjection()` pulls in. +// Its presence means the runtime injection was bundled; its absence means it was tree-shaken. +const RUNTIME_INJECTION_MARKER = 'Registered diagnostics-channel injection'; + +function bundleText(name) { + const dir = join(__dirname, 'dist', name); + return readdirSync(dir) + .map(f => readFileSync(join(dir, f), 'utf8')) + .join('\n'); +} + +let failed = false; +function check(condition, message) { + // eslint-disable-next-line no-console + console.log(`${condition ? 'ok ' : 'FAIL'} - ${message}`); + if (!condition) failed = true; +} + +const plain = bundleText('plain'); +const plugin = bundleText('plugin'); + +check( + plain.includes(RUNTIME_INJECTION_MARKER), + 'plain build (no plugin) bundles the runtime channel injection by default', +); +check( + !plugin.includes(RUNTIME_INJECTION_MARKER), + 'sentryRollupPlugin excludes the runtime channel injection by default (build-time instrumentation)', +); + +if (failed) { + process.exit(1); +} +// eslint-disable-next-line no-console +console.log('All bundle assertions passed.'); diff --git a/dev-packages/e2e-tests/test-applications/node-rollup/build.mjs b/dev-packages/e2e-tests/test-applications/node-rollup/build.mjs new file mode 100644 index 000000000000..4c43bed35bba --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-rollup/build.mjs @@ -0,0 +1,42 @@ +// Bundles the entrypoint with Rollup twice: +// - `plain`: no Sentry plugin — the runtime diagnostics-channel injection is bundled (v11 default). +// - `plugin`: with `sentryRollupPlugin` (build-time instrumentation), which defaults +// `bundleSizeOptimizations.excludeChannelInjection` to `true`, so Rollup tree-shakes +// the runtime injection out. +// assert.mjs inspects both outputs. +import { builtinModules } from 'node:module'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import commonjs from '@rollup/plugin-commonjs'; +import { nodeResolve } from '@rollup/plugin-node-resolve'; +import { rollup } from 'rollup'; +import { sentryRollupPlugin } from '@sentry/node/rollup'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const external = [...builtinModules, ...builtinModules.map(m => `node:${m}`)]; + +async function run(name, extra) { + const bundle = await rollup({ + input: join(__dirname, 'src', 'entry.mjs'), + external, + plugins: [nodeResolve({ exportConditions: ['node', 'import', 'default'] }), commonjs(), ...extra], + onwarn: () => {}, + }); + await bundle.write({ dir: join(__dirname, 'dist', name), format: 'es', entryFileNames: 'main.mjs' }); + await bundle.close(); +} + +await run('plain', []); +await run( + 'plugin', + // `sentryRollupPlugin` returns an array of Rollup plugins. No auth/release/telemetry — we only care + // about the build-time transforms and defines. + sentryRollupPlugin({ + telemetry: false, + sourcemaps: { disable: true }, + release: { create: false, finalize: false, inject: false }, + }), +); + +// eslint-disable-next-line no-console +console.log('built plain + plugin with rollup'); diff --git a/dev-packages/e2e-tests/test-applications/node-rollup/package.json b/dev-packages/e2e-tests/test-applications/node-rollup/package.json new file mode 100644 index 000000000000..7b575d8b4755 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-rollup/package.json @@ -0,0 +1,25 @@ +{ + "name": "node-rollup", + "description": "ensure the Sentry rollup plugin excludes runtime channel injection by default", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "clean": "npx rimraf node_modules dist pnpm-lock.yaml", + "test:build": "pnpm install && node ./build.mjs", + "test:assert": "node ./assert.mjs" + }, + "dependencies": { + "@sentry/node": "file:../../packed/sentry-node-packed.tgz", + "@sentry/server-utils": "file:../../packed/sentry-server-utils-packed.tgz", + "@sentry/bundler-plugins": "file:../../packed/sentry-bundler-plugins-packed.tgz" + }, + "devDependencies": { + "rollup": "4.62.3", + "@rollup/plugin-node-resolve": "^16.0.0", + "@rollup/plugin-commonjs": "^28.0.0" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/node-rollup/src/app.mjs b/dev-packages/e2e-tests/test-applications/node-rollup/src/app.mjs new file mode 100644 index 000000000000..e66db6685328 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-rollup/src/app.mjs @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-console +console.log('this is the application'); diff --git a/dev-packages/e2e-tests/test-applications/node-rollup/src/entry.mjs b/dev-packages/e2e-tests/test-applications/node-rollup/src/entry.mjs new file mode 100644 index 000000000000..5c03b545d672 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-rollup/src/entry.mjs @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/node'; + +Sentry.init({ + traceLifecycle: 'static', + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1, +}); + +await import('./app.mjs'); diff --git a/dev-packages/e2e-tests/test-applications/node-vite/assert.mjs b/dev-packages/e2e-tests/test-applications/node-vite/assert.mjs new file mode 100644 index 000000000000..4abe35d98c53 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-vite/assert.mjs @@ -0,0 +1,48 @@ +/** + * Asserts that the Sentry vite plugin excludes the *runtime* diagnostics-channel injection by + * default (because it instruments at build time instead), while a plain build keeps it. + * + * @module + */ +import { readdirSync, readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +// This string literal lives only in `@sentry/server-utils`' runtime injection module +// (`orchestrion/runtime/register.ts`) — the code `registerDiagnosticsChannelInjection()` pulls in. +// Its presence means the runtime injection was bundled; its absence means it was tree-shaken. +const RUNTIME_INJECTION_MARKER = 'Registered diagnostics-channel injection'; + +function bundleText(name) { + const dir = join(__dirname, 'dist', name); + return readdirSync(dir) + .map(f => readFileSync(join(dir, f), 'utf8')) + .join('\n'); +} + +let failed = false; +function check(condition, message) { + // eslint-disable-next-line no-console + console.log(`${condition ? 'ok ' : 'FAIL'} - ${message}`); + if (!condition) failed = true; +} + +const plain = bundleText('plain'); +const plugin = bundleText('plugin'); + +check( + plain.includes(RUNTIME_INJECTION_MARKER), + 'plain build (no plugin) bundles the runtime channel injection by default', +); +check( + !plugin.includes(RUNTIME_INJECTION_MARKER), + 'sentryVitePlugin excludes the runtime channel injection by default (build-time instrumentation)', +); + +if (failed) { + process.exit(1); +} +// eslint-disable-next-line no-console +console.log('All bundle assertions passed.'); diff --git a/dev-packages/e2e-tests/test-applications/node-vite/build.mjs b/dev-packages/e2e-tests/test-applications/node-vite/build.mjs new file mode 100644 index 000000000000..454f9c9fb8b5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-vite/build.mjs @@ -0,0 +1,43 @@ +// Bundles the entrypoint with Vite twice: +// - `plain`: no Sentry plugin — the runtime diagnostics-channel injection is bundled (v11 default). +// - `plugin`: with `sentryVitePlugin` (build-time instrumentation), which defaults +// `bundleSizeOptimizations.excludeChannelInjection` to `true`, so Vite/Rollup +// tree-shakes the runtime injection out. +// assert.mjs inspects both outputs. +import { builtinModules } from 'node:module'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { build } from 'vite'; +import { sentryVitePlugin } from '@sentry/node/vite'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +function run(name, plugins) { + return build({ + logLevel: 'silent', + build: { + outDir: join(__dirname, 'dist', name), + emptyOutDir: true, + minify: true, + lib: { entry: join(__dirname, 'src', 'entry.mjs'), formats: ['es'], fileName: 'main' }, + rollupOptions: { external: [...builtinModules, ...builtinModules.map(m => `node:${m}`)] }, + }, + plugins, + }); +} + +await run('plain', []); +await run( + 'plugin', + // No auth/release/telemetry — we only care about the build-time transforms and defines. + [ + sentryVitePlugin({ + telemetry: false, + sourcemaps: { disable: true }, + release: { create: false, finalize: false, inject: false }, + }), + ], +); + +// eslint-disable-next-line no-console +console.log('built plain + plugin with vite'); diff --git a/dev-packages/e2e-tests/test-applications/node-vite/package.json b/dev-packages/e2e-tests/test-applications/node-vite/package.json new file mode 100644 index 000000000000..c2bad813a9cf --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-vite/package.json @@ -0,0 +1,23 @@ +{ + "name": "node-vite", + "description": "ensure the Sentry vite plugin excludes runtime channel injection by default", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "clean": "npx rimraf node_modules dist pnpm-lock.yaml", + "test:build": "pnpm install && node ./build.mjs", + "test:assert": "node ./assert.mjs" + }, + "dependencies": { + "@sentry/node": "file:../../packed/sentry-node-packed.tgz", + "@sentry/server-utils": "file:../../packed/sentry-server-utils-packed.tgz", + "@sentry/bundler-plugins": "file:../../packed/sentry-bundler-plugins-packed.tgz" + }, + "devDependencies": { + "vite": "6.4.3" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/node-vite/src/app.mjs b/dev-packages/e2e-tests/test-applications/node-vite/src/app.mjs new file mode 100644 index 000000000000..e66db6685328 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-vite/src/app.mjs @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-console +console.log('this is the application'); diff --git a/dev-packages/e2e-tests/test-applications/node-vite/src/entry.mjs b/dev-packages/e2e-tests/test-applications/node-vite/src/entry.mjs new file mode 100644 index 000000000000..5c03b545d672 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-vite/src/entry.mjs @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/node'; + +Sentry.init({ + traceLifecycle: 'static', + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1, +}); + +await import('./app.mjs'); From 7d99a066a3b1335fcb1611d934c82f4535ccc0a8 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 21 Aug 2026 13:30:15 +0200 Subject: [PATCH 5/7] test(e2e): Add node-esbuild build-time instrumentation test Build the entry plain and with `sentryEsbuildPlugin`. esbuild's single-pass tree-shaking keeps the (now dead) runtime injection in the bundle unlike webpack/vite/rollup, so this app only asserts the plain build bundles the runtime injection and the plugin build succeeds; the runtime-behavior side is covered separately. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test-applications/node-esbuild/assert.mjs | 45 +++++++++++++++++++ .../test-applications/node-esbuild/build.mjs | 41 +++++++++++++++++ .../node-esbuild/package.json | 23 ++++++++++ .../node-esbuild/src/app.mjs | 2 + .../node-esbuild/src/entry.mjs | 9 ++++ 5 files changed, 120 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/node-esbuild/assert.mjs create mode 100644 dev-packages/e2e-tests/test-applications/node-esbuild/build.mjs create mode 100644 dev-packages/e2e-tests/test-applications/node-esbuild/package.json create mode 100644 dev-packages/e2e-tests/test-applications/node-esbuild/src/app.mjs create mode 100644 dev-packages/e2e-tests/test-applications/node-esbuild/src/entry.mjs diff --git a/dev-packages/e2e-tests/test-applications/node-esbuild/assert.mjs b/dev-packages/e2e-tests/test-applications/node-esbuild/assert.mjs new file mode 100644 index 000000000000..1ea9829c1a2d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-esbuild/assert.mjs @@ -0,0 +1,45 @@ +/** + * Asserts that a plain esbuild build bundles the runtime diagnostics-channel injection by default, + * and that the Sentry esbuild plugin build (build-time instrumentation) succeeds. + * + * NOTE: unlike webpack/vite/rollup, esbuild's single-pass tree-shaking does NOT remove the runtime + * injection when `bundleSizeOptimizations.excludeChannelInjection` is defaulted on by the plugin: the + * `if (useChannelInjection)` branch goes dead (so it never runs at runtime), but esbuild keeps the + * module in the bundle. We therefore don't assert its absence here — we only assert the plugin build + * succeeds. The runtime-behavior side of this is covered by the node-vite-runtime-injection app. + * + * @module + */ +import { readdirSync, readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +const RUNTIME_INJECTION_MARKER = 'Registered diagnostics-channel injection'; + +function bundleText(name) { + const dir = join(__dirname, 'dist', name); + return readdirSync(dir) + .map(f => readFileSync(join(dir, f), 'utf8')) + .join('\n'); +} + +let failed = false; +function check(condition, message) { + // eslint-disable-next-line no-console + console.log(`${condition ? 'ok ' : 'FAIL'} - ${message}`); + if (!condition) failed = true; +} + +const plain = bundleText('plain'); +const plugin = bundleText('plugin'); + +check(plain.includes(RUNTIME_INJECTION_MARKER), 'plain build bundles the runtime channel injection by default'); +check(plugin.length > 0, 'sentryEsbuildPlugin build (build-time instrumentation) succeeds'); + +if (failed) { + process.exit(1); +} +// eslint-disable-next-line no-console +console.log('All bundle assertions passed.'); diff --git a/dev-packages/e2e-tests/test-applications/node-esbuild/build.mjs b/dev-packages/e2e-tests/test-applications/node-esbuild/build.mjs new file mode 100644 index 000000000000..5853ee3b2fb5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-esbuild/build.mjs @@ -0,0 +1,41 @@ +// Bundles the entrypoint with esbuild twice: +// - `plain`: no Sentry plugin — the runtime diagnostics-channel injection is bundled (v11 default). +// - `plugin`: with `sentryEsbuildPlugin` (build-time instrumentation). +// assert.mjs inspects both outputs. Note: unlike webpack/vite/rollup, esbuild's single-pass +// tree-shaking does not drop the (now dead) runtime injection code, so this app only asserts the +// plugin build succeeds — see assert.mjs. +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { build } from 'esbuild'; +import { sentryEsbuildPlugin } from '@sentry/node/esbuild'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +function run(name, plugins) { + return build({ + entryPoints: [join(__dirname, 'src', 'entry.mjs')], + outfile: join(__dirname, 'dist', name, 'main.mjs'), + bundle: true, + platform: 'node', + format: 'esm', + minify: true, + logLevel: 'silent', + plugins, + }); +} + +await run('plain', []); +await run( + 'plugin', + // No auth/release/telemetry — we only care about the build-time transforms and defines. + [ + sentryEsbuildPlugin({ + telemetry: false, + sourcemaps: { disable: true }, + release: { create: false, finalize: false, inject: false }, + }), + ], +); + +// eslint-disable-next-line no-console +console.log('built plain + plugin with esbuild'); diff --git a/dev-packages/e2e-tests/test-applications/node-esbuild/package.json b/dev-packages/e2e-tests/test-applications/node-esbuild/package.json new file mode 100644 index 000000000000..751b8cd3b670 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-esbuild/package.json @@ -0,0 +1,23 @@ +{ + "name": "node-esbuild", + "description": "ensure the Sentry esbuild plugin builds with build-time instrumentation", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "clean": "npx rimraf node_modules dist pnpm-lock.yaml", + "test:build": "pnpm install && node ./build.mjs", + "test:assert": "node ./assert.mjs" + }, + "dependencies": { + "@sentry/node": "file:../../packed/sentry-node-packed.tgz", + "@sentry/server-utils": "file:../../packed/sentry-server-utils-packed.tgz", + "@sentry/bundler-plugins": "file:../../packed/sentry-bundler-plugins-packed.tgz" + }, + "devDependencies": { + "esbuild": "0.28.2" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/node-esbuild/src/app.mjs b/dev-packages/e2e-tests/test-applications/node-esbuild/src/app.mjs new file mode 100644 index 000000000000..e66db6685328 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-esbuild/src/app.mjs @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-console +console.log('this is the application'); diff --git a/dev-packages/e2e-tests/test-applications/node-esbuild/src/entry.mjs b/dev-packages/e2e-tests/test-applications/node-esbuild/src/entry.mjs new file mode 100644 index 000000000000..5c03b545d672 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-esbuild/src/entry.mjs @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/node'; + +Sentry.init({ + traceLifecycle: 'static', + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1, +}); + +await import('./app.mjs'); From fbe02120c242f8496ed94767ab20c91b8e9be631 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 21 Aug 2026 13:32:31 +0200 Subject: [PATCH 6/7] test(e2e): Use esnext target for node-vite so top-level await builds Vite defaults to a browser target that rejects the entry's top-level await; set a node target. Co-Authored-By: Claude Opus 4.8 (1M context) --- dev-packages/e2e-tests/test-applications/node-vite/build.mjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dev-packages/e2e-tests/test-applications/node-vite/build.mjs b/dev-packages/e2e-tests/test-applications/node-vite/build.mjs index 454f9c9fb8b5..9a1a7b0e8f08 100644 --- a/dev-packages/e2e-tests/test-applications/node-vite/build.mjs +++ b/dev-packages/e2e-tests/test-applications/node-vite/build.mjs @@ -19,6 +19,9 @@ function run(name, plugins) { outDir: join(__dirname, 'dist', name), emptyOutDir: true, minify: true, + // Node target so top-level await (used in the entry) is allowed; Vite otherwise defaults to a + // browser target that rejects it. + target: 'esnext', lib: { entry: join(__dirname, 'src', 'entry.mjs'), formats: ['es'], fileName: 'main' }, rollupOptions: { external: [...builtinModules, ...builtinModules.map(m => `node:${m}`)] }, }, From 8a9b5ff4ee478a743f54943571ecaf79fe28b07a Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 21 Aug 2026 13:32:51 +0200 Subject: [PATCH 7/7] webpack test --- .../test-applications/node-webpack/assert.mjs | 25 ++++++++++++------- .../node-webpack/package.json | 7 +++--- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/node-webpack/assert.mjs b/dev-packages/e2e-tests/test-applications/node-webpack/assert.mjs index e4178c60573c..0dcaf54128ed 100644 --- a/dev-packages/e2e-tests/test-applications/node-webpack/assert.mjs +++ b/dev-packages/e2e-tests/test-applications/node-webpack/assert.mjs @@ -1,7 +1,6 @@ /** - * Asserts the orchestrion subtree is bundled by default. Channel-based (orchestrion - * diagnostics-channel) instrumentation is the v11 default, so `Sentry.init()` pulls in the - * orchestrion code path unconditionally — there is no longer an opt-in to tree-shake it away. + * Asserts that the Sentry webpack plugin excludes the *runtime* diagnostics-channel injection by + * default (because it instruments at build time instead), while a plain build keeps it. * * @module */ @@ -11,10 +10,10 @@ import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); -// `orchestrion:mysql:query` lives only in @sentry/server-utils' orchestrion -// subtree (channels.ts), never in @sentry/node — so finding it in a bundle -// means the orchestrion code path was pulled in. -const MARKER = 'orchestrion:mysql:query'; +// This string literal lives only in `@sentry/server-utils`' runtime injection module +// (`orchestrion/runtime/register.ts`) — the code `registerDiagnosticsChannelInjection()` pulls in. +// Its presence means the runtime injection was bundled; its absence means it was tree-shaken. +const RUNTIME_INJECTION_MARKER = 'Registered diagnostics-channel injection'; function bundleText(name) { const dir = join(__dirname, 'dist', name); @@ -30,9 +29,17 @@ function check(condition, message) { if (!condition) failed = true; } -const app = bundleText('entry'); +const plain = bundleText('plain'); +const plugin = bundleText('plugin'); -check(app.includes(MARKER), 'orchestrion is bundled by default when Sentry.init() runs'); +check( + plain.includes(RUNTIME_INJECTION_MARKER), + 'plain build (no plugin) bundles the runtime channel injection by default', +); +check( + !plugin.includes(RUNTIME_INJECTION_MARKER), + 'sentryWebpackPlugin excludes the runtime channel injection by default (build-time instrumentation)', +); if (failed) { process.exit(1); diff --git a/dev-packages/e2e-tests/test-applications/node-webpack/package.json b/dev-packages/e2e-tests/test-applications/node-webpack/package.json index 69dd20caf346..c9c80129f8a4 100644 --- a/dev-packages/e2e-tests/test-applications/node-webpack/package.json +++ b/dev-packages/e2e-tests/test-applications/node-webpack/package.json @@ -1,6 +1,6 @@ { - "name": "node-orchestrion-webpack", - "description": "ensure that orchestrion is not bundled inappropriately", + "name": "node-webpack", + "description": "ensure the Sentry webpack plugin excludes runtime channel injection by default", "version": "1.0.0", "private": true, "type": "module", @@ -11,7 +11,8 @@ }, "dependencies": { "@sentry/node": "file:../../packed/sentry-node-packed.tgz", - "@sentry/server-utils": "file:../../packed/sentry-server-utils-packed.tgz" + "@sentry/server-utils": "file:../../packed/sentry-server-utils-packed.tgz", + "@sentry/bundler-plugins": "file:../../packed/sentry-bundler-plugins-packed.tgz" }, "devDependencies": { "webpack": "5.107.2"