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-orchestrion-webpack/src/app.mjs b/dev-packages/e2e-tests/test-applications/node-esbuild/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-esbuild/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-esbuild/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-esbuild/src/entry.mjs diff --git a/dev-packages/e2e-tests/test-applications/node-orchestrion-webpack/assert.mjs b/dev-packages/e2e-tests/test-applications/node-orchestrion-webpack/assert.mjs deleted file mode 100644 index e4178c60573c..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-orchestrion-webpack/assert.mjs +++ /dev/null @@ -1,41 +0,0 @@ -/** - * 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. - * - * @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)); - -// `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'; - -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 app = bundleText('entry'); - -check(app.includes(MARKER), 'orchestrion is bundled by default when Sentry.init() runs'); - -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-orchestrion-webpack/build.mjs b/dev-packages/e2e-tests/test-applications/node-orchestrion-webpack/build.mjs deleted file mode 100644 index 08be7f25a103..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-orchestrion-webpack/build.mjs +++ /dev/null @@ -1,43 +0,0 @@ -// 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. -import { dirname, join } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import webpack from 'webpack'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); - -function build(name) { - return new Promise((resolve, reject) => { - webpack( - { - entry: join(__dirname, 'src', `${name}.mjs`), - mode: 'production', - target: 'node', - experiments: { topLevelAwait: true, outputModule: true }, - output: { - path: join(__dirname, 'dist', name), - filename: 'main.mjs', - module: true, - 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 }, - }, - (err, stats) => { - if (err) return reject(err); - if (stats.hasErrors()) { - return reject(new Error(`webpack build of ${name} failed:\n${stats.toString({ errors: true })}`)); - } - // eslint-disable-next-line no-console - console.log(`built ${name} (webpack ${webpack.version})`); - resolve(); - }, - ); - }); -} - -await build('entry'); 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..9a1a7b0e8f08 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-vite/build.mjs @@ -0,0 +1,46 @@ +// 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, + // 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}`)] }, + }, + 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'); diff --git a/dev-packages/e2e-tests/test-applications/node-webpack/assert.mjs b/dev-packages/e2e-tests/test-applications/node-webpack/assert.mjs new file mode 100644 index 000000000000..0dcaf54128ed --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-webpack/assert.mjs @@ -0,0 +1,48 @@ +/** + * 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 + */ +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), + 'sentryWebpackPlugin 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-webpack/build.mjs b/dev-packages/e2e-tests/test-applications/node-webpack/build.mjs new file mode 100644 index 000000000000..0584a631b9ad --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-webpack/build.mjs @@ -0,0 +1,60 @@ +// 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, plugins) { + return new Promise((resolve, reject) => { + webpack( + { + entry: join(__dirname, 'src', 'entry.mjs'), + mode: 'production', + target: 'node', + experiments: { topLevelAwait: true, outputModule: true }, + output: { + path: join(__dirname, 'dist', name), + filename: 'main.mjs', + module: true, + library: { type: 'module' }, + chunkFormat: 'module', + }, + // 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); + if (stats.hasErrors()) { + return reject(new Error(`webpack build of ${name} failed:\n${stats.toString({ errors: true })}`)); + } + // eslint-disable-next-line no-console + console.log(`built ${name} (webpack ${webpack.version})`); + resolve(); + }, + ); + }); +} + +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 }, + }), + ], +); 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 68% 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 index 69dd20caf346..c9c80129f8a4 100644 --- a/dev-packages/e2e-tests/test-applications/node-orchestrion-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" diff --git a/dev-packages/e2e-tests/test-applications/node-webpack/src/app.mjs b/dev-packages/e2e-tests/test-applications/node-webpack/src/app.mjs new file mode 100644 index 000000000000..e66db6685328 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-webpack/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-webpack/src/entry.mjs b/dev-packages/e2e-tests/test-applications/node-webpack/src/entry.mjs new file mode 100644 index 000000000000..5c03b545d672 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-webpack/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/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 }, + }); + }); +});