From 7bd0b6279a08605737b8a4f3cb19ac0cd57f64ea Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 21 Aug 2026 13:10:09 +0200 Subject: [PATCH 1/4] 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) 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(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(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) 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) fixes... --- .../test-applications/node-esbuild/assert.mjs | 49 ++++++++++++++++++ .../test-applications/node-esbuild/build.mjs | 40 +++++++++++++++ .../node-esbuild/package.json | 23 +++++++++ .../src/app.mjs | 0 .../src/entry.mjs | 0 .../node-orchestrion-webpack/assert.mjs | 41 --------------- .../node-rolldown/assert.mjs | 50 ++++++++++++++++++ .../test-applications/node-rolldown/build.mjs | 41 +++++++++++++++ .../node-rolldown/package.json | 26 ++++++++++ .../node-rolldown/src/app.mjs | 2 + .../node-rolldown/src/entry.mjs | 9 ++++ .../test-applications/node-rollup/assert.mjs | 49 ++++++++++++++++++ .../test-applications/node-rollup/build.mjs | 41 +++++++++++++++ .../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 | 49 ++++++++++++++++++ .../test-applications/node-vite/build.mjs | 51 +++++++++++++++++++ .../test-applications/node-vite/package.json | 23 +++++++++ .../test-applications/node-vite/src/app.mjs | 2 + .../test-applications/node-vite/src/entry.mjs | 9 ++++ .../test-applications/node-webpack/assert.mjs | 49 ++++++++++++++++++ .../build.mjs | 30 +++++++---- .../package.json | 7 +-- .../node-webpack/src/app.mjs | 2 + .../node-webpack/src/entry.mjs | 9 ++++ 26 files changed, 585 insertions(+), 53 deletions(-) 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 rename dev-packages/e2e-tests/test-applications/{node-orchestrion-webpack => node-esbuild}/src/app.mjs (100%) rename dev-packages/e2e-tests/test-applications/{node-orchestrion-webpack => node-esbuild}/src/entry.mjs (100%) delete mode 100644 dev-packages/e2e-tests/test-applications/node-orchestrion-webpack/assert.mjs create mode 100644 dev-packages/e2e-tests/test-applications/node-rolldown/assert.mjs create mode 100644 dev-packages/e2e-tests/test-applications/node-rolldown/build.mjs create mode 100644 dev-packages/e2e-tests/test-applications/node-rolldown/package.json create mode 100644 dev-packages/e2e-tests/test-applications/node-rolldown/src/app.mjs create mode 100644 dev-packages/e2e-tests/test-applications/node-rolldown/src/entry.mjs 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 create mode 100644 dev-packages/e2e-tests/test-applications/node-webpack/assert.mjs rename dev-packages/e2e-tests/test-applications/{node-orchestrion-webpack => node-webpack}/build.mjs (54%) rename dev-packages/e2e-tests/test-applications/{node-orchestrion-webpack => node-webpack}/package.json (69%) create mode 100644 dev-packages/e2e-tests/test-applications/node-webpack/src/app.mjs create mode 100644 dev-packages/e2e-tests/test-applications/node-webpack/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..63fa6e478d87 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-esbuild/assert.mjs @@ -0,0 +1,49 @@ +/** + * Asserts that `sentryEsbuildPlugin` performs build-time instrumentation: its code transform injects + * the orchestrion "bundler ran" banner into the entry chunk. A plain build (no plugin) does not. + * + * @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)); + +// A distinctive slice of the orchestrion banner that the bundler plugin's build-time code transform +// prepends to the entry chunk (see `ORCHESTRION_BUNDLER_MARKER_BANNER` in `@sentry/server-utils`). +// It is emitted only when the plugin's build-time instrumentation runs, so it tells a `plugin` build +// apart from a `plain` one. Builds are kept unminified so it survives verbatim. +const BUILD_TIME_TRANSFORM_MARKER = 'g.bundler=g.bundler||[]'; + +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(BUILD_TIME_TRANSFORM_MARKER), + 'plain build (no plugin) does not run build-time instrumentation', +); +check( + plugin.includes(BUILD_TIME_TRANSFORM_MARKER), + 'sentryEsbuildPlugin runs build-time instrumentation (injects the orchestrion banner)', +); + +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..67a1c575f2d8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-esbuild/build.mjs @@ -0,0 +1,40 @@ +// Bundles the entrypoint with esbuild twice: +// - `plain`: no Sentry plugin. +// - `plugin`: with `sentryEsbuildPlugin` (build-time instrumentation). +// Only the `plugin` build runs the orchestrion code transform, which prepends the "bundler ran" +// banner to the entry chunk. Kept unminified so assert.mjs can match the banner verbatim. +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: false, + 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..e9e33f245570 --- /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 performs 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-rolldown/assert.mjs b/dev-packages/e2e-tests/test-applications/node-rolldown/assert.mjs new file mode 100644 index 000000000000..3f38dde90028 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-rolldown/assert.mjs @@ -0,0 +1,50 @@ +/** + * Asserts that `sentryRollupPlugin` performs build-time instrumentation when bundling with Rolldown: + * its code transform injects the orchestrion "bundler ran" banner into the entry chunk. A plain build + * (no plugin) does not. + * + * @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)); + +// A distinctive slice of the orchestrion banner that the bundler plugin's build-time code transform +// prepends to the entry chunk (see `ORCHESTRION_BUNDLER_MARKER_BANNER` in `@sentry/server-utils`). +// It is emitted only when the plugin's build-time instrumentation runs, so it tells a `plugin` build +// apart from a `plain` one. Builds are kept unminified so it survives verbatim. +const BUILD_TIME_TRANSFORM_MARKER = 'g.bundler=g.bundler||[]'; + +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(BUILD_TIME_TRANSFORM_MARKER), + 'plain build (no plugin) does not run build-time instrumentation', +); +check( + plugin.includes(BUILD_TIME_TRANSFORM_MARKER), + 'sentryRollupPlugin runs build-time instrumentation (injects the orchestrion banner)', +); + +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-rolldown/build.mjs b/dev-packages/e2e-tests/test-applications/node-rolldown/build.mjs new file mode 100644 index 000000000000..cfb1f2e3a53e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-rolldown/build.mjs @@ -0,0 +1,41 @@ +// Bundles the entrypoint with Rolldown twice: +// - `plain`: no Sentry plugin. +// - `plugin`: with `sentryRollupPlugin` (build-time instrumentation). +// Only the `plugin` build runs the orchestrion code transform, which prepends the "bundler ran" +// banner to the entry chunk. Kept unminified so assert.mjs can match the banner verbatim. +// Rolldown is Rollup API-compatible, so it consumes the same `@sentry/node/rollup` plugin; it also +// resolves node modules and CommonJS natively, so no extra resolve/commonjs plugins are needed. +import { builtinModules } from 'node:module'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { rolldown } from 'rolldown'; +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 rolldown({ + input: join(__dirname, 'src', 'entry.mjs'), + external, + plugins: [...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 rolldown'); diff --git a/dev-packages/e2e-tests/test-applications/node-rolldown/package.json b/dev-packages/e2e-tests/test-applications/node-rolldown/package.json new file mode 100644 index 000000000000..ad28e67b784b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-rolldown/package.json @@ -0,0 +1,26 @@ +{ + "name": "node-rolldown", + "description": "ensure the Sentry rollup plugin performs build-time instrumentation when bundling with rolldown", + "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": { + "rolldown": "1.2.5" + }, + "volta": { + "extends": "../../package.json" + }, + "sentryTest": { + "skip": true + } +} diff --git a/dev-packages/e2e-tests/test-applications/node-rolldown/src/app.mjs b/dev-packages/e2e-tests/test-applications/node-rolldown/src/app.mjs new file mode 100644 index 000000000000..e66db6685328 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-rolldown/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-rolldown/src/entry.mjs b/dev-packages/e2e-tests/test-applications/node-rolldown/src/entry.mjs new file mode 100644 index 000000000000..5c03b545d672 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-rolldown/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-rollup/assert.mjs b/dev-packages/e2e-tests/test-applications/node-rollup/assert.mjs new file mode 100644 index 000000000000..cd300d921598 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-rollup/assert.mjs @@ -0,0 +1,49 @@ +/** + * Asserts that `sentryRollupPlugin` performs build-time instrumentation: its code transform injects + * the orchestrion "bundler ran" banner into the entry chunk. A plain build (no plugin) does not. + * + * @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)); + +// A distinctive slice of the orchestrion banner that the bundler plugin's build-time code transform +// prepends to the entry chunk (see `ORCHESTRION_BUNDLER_MARKER_BANNER` in `@sentry/server-utils`). +// It is emitted only when the plugin's build-time instrumentation runs, so it tells a `plugin` build +// apart from a `plain` one. Builds are kept unminified so it survives verbatim. +const BUILD_TIME_TRANSFORM_MARKER = 'g.bundler=g.bundler||[]'; + +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(BUILD_TIME_TRANSFORM_MARKER), + 'plain build (no plugin) does not run build-time instrumentation', +); +check( + plugin.includes(BUILD_TIME_TRANSFORM_MARKER), + 'sentryRollupPlugin runs build-time instrumentation (injects the orchestrion banner)', +); + +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..9f29551aad7d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-rollup/build.mjs @@ -0,0 +1,41 @@ +// Bundles the entrypoint with Rollup twice: +// - `plain`: no Sentry plugin. +// - `plugin`: with `sentryRollupPlugin` (build-time instrumentation). +// Only the `plugin` build runs the orchestrion code transform, which prepends the "bundler ran" +// banner to the entry chunk. Kept unminified so assert.mjs can match the banner verbatim. +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..787d4d9af92e --- /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 performs 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": { + "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..f528bb7de92f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-vite/assert.mjs @@ -0,0 +1,49 @@ +/** + * Asserts that `sentryVitePlugin` performs build-time instrumentation: its code transform injects the + * orchestrion "bundler ran" banner into the entry chunk. A plain build (no plugin) does not. + * + * @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)); + +// A distinctive slice of the orchestrion banner that the bundler plugin's build-time code transform +// prepends to the entry chunk (see `ORCHESTRION_BUNDLER_MARKER_BANNER` in `@sentry/server-utils`). +// It is emitted only when the plugin's build-time instrumentation runs, so it tells a `plugin` build +// apart from a `plain` one. Builds are kept unminified so it survives verbatim. +const BUILD_TIME_TRANSFORM_MARKER = 'g.bundler=g.bundler||[]'; + +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(BUILD_TIME_TRANSFORM_MARKER), + 'plain build (no plugin) does not run build-time instrumentation', +); +check( + plugin.includes(BUILD_TIME_TRANSFORM_MARKER), + 'sentryVitePlugin runs build-time instrumentation (injects the orchestrion banner)', +); + +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..840e9663257d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-vite/build.mjs @@ -0,0 +1,51 @@ +// Bundles the entrypoint with Vite (SSR) twice: +// - `plain`: no Sentry plugin. +// - `plugin`: with `sentryVitePlugin` (build-time instrumentation). +// The Sentry vite plugin's build-time code transform only applies to server builds (it gates itself +// on `consumer === 'server'`), so this uses an SSR build rather than a client `lib` build. Only the +// `plugin` build then injects the orchestrion "bundler ran" banner into the entry chunk. Kept +// unminified so assert.mjs can match the banner verbatim. +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: false, + // Node target so top-level await (used in the entry) is allowed; Vite otherwise defaults to a + // browser target that rejects it. + target: 'esnext', + // SSR build so the plugin's build-time transform applies (it only runs for server builds). + ssr: join(__dirname, 'src', 'entry.mjs'), + rollupOptions: { + external: [...builtinModules, ...builtinModules.map(m => `node:${m}`)], + output: { entryFileNames: 'main.mjs', format: 'es' }, + }, + }, + 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..d6d10a6c260c --- /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 performs 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": { + "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..7db3de3733cb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-webpack/assert.mjs @@ -0,0 +1,49 @@ +/** + * Asserts that `sentryWebpackPlugin` performs build-time instrumentation: its code transform injects + * the orchestrion "bundler ran" banner into the entry chunk. A plain build (no plugin) does not. + * + * @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)); + +// A distinctive slice of the orchestrion banner that the bundler plugin's build-time code transform +// prepends to the entry chunk (see `ORCHESTRION_BUNDLER_MARKER_BANNER` in `@sentry/server-utils`). +// It is emitted only when the plugin's build-time instrumentation runs, so it tells a `plugin` build +// apart from a `plain` one. Builds are kept unminified so it survives verbatim. +const BUILD_TIME_TRANSFORM_MARKER = 'g.bundler=g.bundler||[]'; + +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(BUILD_TIME_TRANSFORM_MARKER), + 'plain build (no plugin) does not run build-time instrumentation', +); +check( + plugin.includes(BUILD_TIME_TRANSFORM_MARKER), + 'sentryWebpackPlugin runs build-time instrumentation (injects the orchestrion banner)', +); + +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-webpack/build.mjs similarity index 54% 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 index 08be7f25a103..82ca226828c4 100644 --- a/dev-packages/e2e-tests/test-applications/node-orchestrion-webpack/build.mjs +++ b/dev-packages/e2e-tests/test-applications/node-webpack/build.mjs @@ -1,17 +1,20 @@ -// 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. +// - `plugin`: with `sentryWebpackPlugin` (build-time instrumentation). +// Only the `plugin` build runs the orchestrion code transform, which injects the "bundler ran" banner +// into the entry chunk. Kept unminified so assert.mjs can match the banner verbatim. 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 +25,8 @@ 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 }, + plugins, }, (err, stats) => { if (err) return reject(err); @@ -40,4 +41,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 }, + }), + ], +); 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 69% 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..9b82d38b838f 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 performs build-time instrumentation", "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'); From 1a7a5adce02dc8308fdf2ef58b3ed1c6ffc53bdf Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 26 Aug 2026 09:47:15 +0200 Subject: [PATCH 2/4] fixes --- .../e2e-tests/test-applications/node-esbuild/assert.mjs | 6 ++++-- .../e2e-tests/test-applications/node-rolldown/assert.mjs | 6 ++++-- .../e2e-tests/test-applications/node-rolldown/package.json | 3 --- .../e2e-tests/test-applications/node-rollup/assert.mjs | 6 ++++-- .../e2e-tests/test-applications/node-vite/assert.mjs | 6 ++++-- .../e2e-tests/test-applications/node-webpack/assert.mjs | 6 ++++-- 6 files changed, 20 insertions(+), 13 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/node-esbuild/assert.mjs b/dev-packages/e2e-tests/test-applications/node-esbuild/assert.mjs index 63fa6e478d87..9e5db41bf893 100644 --- a/dev-packages/e2e-tests/test-applications/node-esbuild/assert.mjs +++ b/dev-packages/e2e-tests/test-applications/node-esbuild/assert.mjs @@ -13,14 +13,16 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); // A distinctive slice of the orchestrion banner that the bundler plugin's build-time code transform // prepends to the entry chunk (see `ORCHESTRION_BUNDLER_MARKER_BANNER` in `@sentry/server-utils`). // It is emitted only when the plugin's build-time instrumentation runs, so it tells a `plugin` build -// apart from a `plain` one. Builds are kept unminified so it survives verbatim. +// apart from a `plain` one. We match against a whitespace-stripped bundle because some bundlers +// (e.g. Rolldown) pretty-print the injected banner rather than emitting it verbatim. const BUILD_TIME_TRANSFORM_MARKER = 'g.bundler=g.bundler||[]'; function bundleText(name) { const dir = join(__dirname, 'dist', name); return readdirSync(dir) .map(f => readFileSync(join(dir, f), 'utf8')) - .join('\n'); + .join('\n') + .replace(/\s+/g, ''); } let failed = false; diff --git a/dev-packages/e2e-tests/test-applications/node-rolldown/assert.mjs b/dev-packages/e2e-tests/test-applications/node-rolldown/assert.mjs index 3f38dde90028..44a0cf99ed4d 100644 --- a/dev-packages/e2e-tests/test-applications/node-rolldown/assert.mjs +++ b/dev-packages/e2e-tests/test-applications/node-rolldown/assert.mjs @@ -14,14 +14,16 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); // A distinctive slice of the orchestrion banner that the bundler plugin's build-time code transform // prepends to the entry chunk (see `ORCHESTRION_BUNDLER_MARKER_BANNER` in `@sentry/server-utils`). // It is emitted only when the plugin's build-time instrumentation runs, so it tells a `plugin` build -// apart from a `plain` one. Builds are kept unminified so it survives verbatim. +// apart from a `plain` one. We match against a whitespace-stripped bundle because some bundlers +// (e.g. Rolldown) pretty-print the injected banner rather than emitting it verbatim. const BUILD_TIME_TRANSFORM_MARKER = 'g.bundler=g.bundler||[]'; function bundleText(name) { const dir = join(__dirname, 'dist', name); return readdirSync(dir) .map(f => readFileSync(join(dir, f), 'utf8')) - .join('\n'); + .join('\n') + .replace(/\s+/g, ''); } let failed = false; diff --git a/dev-packages/e2e-tests/test-applications/node-rolldown/package.json b/dev-packages/e2e-tests/test-applications/node-rolldown/package.json index ad28e67b784b..ea32d98dc0bf 100644 --- a/dev-packages/e2e-tests/test-applications/node-rolldown/package.json +++ b/dev-packages/e2e-tests/test-applications/node-rolldown/package.json @@ -19,8 +19,5 @@ }, "volta": { "extends": "../../package.json" - }, - "sentryTest": { - "skip": true } } diff --git a/dev-packages/e2e-tests/test-applications/node-rollup/assert.mjs b/dev-packages/e2e-tests/test-applications/node-rollup/assert.mjs index cd300d921598..49590e0aecd2 100644 --- a/dev-packages/e2e-tests/test-applications/node-rollup/assert.mjs +++ b/dev-packages/e2e-tests/test-applications/node-rollup/assert.mjs @@ -13,14 +13,16 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); // A distinctive slice of the orchestrion banner that the bundler plugin's build-time code transform // prepends to the entry chunk (see `ORCHESTRION_BUNDLER_MARKER_BANNER` in `@sentry/server-utils`). // It is emitted only when the plugin's build-time instrumentation runs, so it tells a `plugin` build -// apart from a `plain` one. Builds are kept unminified so it survives verbatim. +// apart from a `plain` one. We match against a whitespace-stripped bundle because some bundlers +// (e.g. Rolldown) pretty-print the injected banner rather than emitting it verbatim. const BUILD_TIME_TRANSFORM_MARKER = 'g.bundler=g.bundler||[]'; function bundleText(name) { const dir = join(__dirname, 'dist', name); return readdirSync(dir) .map(f => readFileSync(join(dir, f), 'utf8')) - .join('\n'); + .join('\n') + .replace(/\s+/g, ''); } let failed = false; diff --git a/dev-packages/e2e-tests/test-applications/node-vite/assert.mjs b/dev-packages/e2e-tests/test-applications/node-vite/assert.mjs index f528bb7de92f..a00fd6be1b32 100644 --- a/dev-packages/e2e-tests/test-applications/node-vite/assert.mjs +++ b/dev-packages/e2e-tests/test-applications/node-vite/assert.mjs @@ -13,14 +13,16 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); // A distinctive slice of the orchestrion banner that the bundler plugin's build-time code transform // prepends to the entry chunk (see `ORCHESTRION_BUNDLER_MARKER_BANNER` in `@sentry/server-utils`). // It is emitted only when the plugin's build-time instrumentation runs, so it tells a `plugin` build -// apart from a `plain` one. Builds are kept unminified so it survives verbatim. +// apart from a `plain` one. We match against a whitespace-stripped bundle because some bundlers +// (e.g. Rolldown) pretty-print the injected banner rather than emitting it verbatim. const BUILD_TIME_TRANSFORM_MARKER = 'g.bundler=g.bundler||[]'; function bundleText(name) { const dir = join(__dirname, 'dist', name); return readdirSync(dir) .map(f => readFileSync(join(dir, f), 'utf8')) - .join('\n'); + .join('\n') + .replace(/\s+/g, ''); } let failed = false; 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 7db3de3733cb..4982df4d81ef 100644 --- a/dev-packages/e2e-tests/test-applications/node-webpack/assert.mjs +++ b/dev-packages/e2e-tests/test-applications/node-webpack/assert.mjs @@ -13,14 +13,16 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); // A distinctive slice of the orchestrion banner that the bundler plugin's build-time code transform // prepends to the entry chunk (see `ORCHESTRION_BUNDLER_MARKER_BANNER` in `@sentry/server-utils`). // It is emitted only when the plugin's build-time instrumentation runs, so it tells a `plugin` build -// apart from a `plain` one. Builds are kept unminified so it survives verbatim. +// apart from a `plain` one. We match against a whitespace-stripped bundle because some bundlers +// (e.g. Rolldown) pretty-print the injected banner rather than emitting it verbatim. const BUILD_TIME_TRANSFORM_MARKER = 'g.bundler=g.bundler||[]'; function bundleText(name) { const dir = join(__dirname, 'dist', name); return readdirSync(dir) .map(f => readFileSync(join(dir, f), 'utf8')) - .join('\n'); + .join('\n') + .replace(/\s+/g, ''); } let failed = false; From f0211aac808406489bff07b8e856bc29125cba55 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 26 Aug 2026 11:02:35 +0200 Subject: [PATCH 3/4] fixes --- .../test-applications/node-esbuild/assert.mjs | 22 ++++++++++----- .../test-applications/node-esbuild/build.mjs | 5 ++-- .../node-rolldown/assert.mjs | 27 ++++++++++++------- .../test-applications/node-rolldown/build.mjs | 3 ++- .../test-applications/node-rollup/assert.mjs | 22 ++++++++++----- .../test-applications/node-rollup/build.mjs | 3 ++- .../test-applications/node-vite/assert.mjs | 26 +++++++++++------- .../test-applications/node-vite/build.mjs | 3 ++- .../test-applications/node-webpack/assert.mjs | 22 ++++++++++----- .../test-applications/node-webpack/build.mjs | 3 ++- 10 files changed, 90 insertions(+), 46 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/node-esbuild/assert.mjs b/dev-packages/e2e-tests/test-applications/node-esbuild/assert.mjs index 9e5db41bf893..29c74b84d15a 100644 --- a/dev-packages/e2e-tests/test-applications/node-esbuild/assert.mjs +++ b/dev-packages/e2e-tests/test-applications/node-esbuild/assert.mjs @@ -18,9 +18,20 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const BUILD_TIME_TRANSFORM_MARKER = 'g.bundler=g.bundler||[]'; function bundleText(name) { - const dir = join(__dirname, 'dist', name); - return readdirSync(dir) - .map(f => readFileSync(join(dir, f), 'utf8')) + const files = []; + const walk = dir => { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const full = join(dir, entry.name); + if (entry.isDirectory()) { + walk(full); + } else { + files.push(full); + } + } + }; + walk(join(__dirname, 'dist', name)); + return files + .map(f => readFileSync(f, 'utf8')) .join('\n') .replace(/\s+/g, ''); } @@ -35,10 +46,7 @@ function check(condition, message) { const plain = bundleText('plain'); const plugin = bundleText('plugin'); -check( - !plain.includes(BUILD_TIME_TRANSFORM_MARKER), - 'plain build (no plugin) does not run build-time instrumentation', -); +check(!plain.includes(BUILD_TIME_TRANSFORM_MARKER), 'plain build (no plugin) does not run build-time instrumentation'); check( plugin.includes(BUILD_TIME_TRANSFORM_MARKER), 'sentryEsbuildPlugin runs build-time instrumentation (injects the orchestrion banner)', diff --git a/dev-packages/e2e-tests/test-applications/node-esbuild/build.mjs b/dev-packages/e2e-tests/test-applications/node-esbuild/build.mjs index 67a1c575f2d8..731ba2bf709d 100644 --- a/dev-packages/e2e-tests/test-applications/node-esbuild/build.mjs +++ b/dev-packages/e2e-tests/test-applications/node-esbuild/build.mjs @@ -2,7 +2,8 @@ // - `plain`: no Sentry plugin. // - `plugin`: with `sentryEsbuildPlugin` (build-time instrumentation). // Only the `plugin` build runs the orchestrion code transform, which prepends the "bundler ran" -// banner to the entry chunk. Kept unminified so assert.mjs can match the banner verbatim. +// banner to the entry chunk. Kept unminified so the banner keeps its identifiers (a minifier would +// rename them); assert.mjs matches it whitespace-insensitively. import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { build } from 'esbuild'; @@ -13,7 +14,7 @@ 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'), + outdir: join(__dirname, 'dist', name), bundle: true, platform: 'node', format: 'esm', diff --git a/dev-packages/e2e-tests/test-applications/node-rolldown/assert.mjs b/dev-packages/e2e-tests/test-applications/node-rolldown/assert.mjs index 44a0cf99ed4d..0022135375e0 100644 --- a/dev-packages/e2e-tests/test-applications/node-rolldown/assert.mjs +++ b/dev-packages/e2e-tests/test-applications/node-rolldown/assert.mjs @@ -1,7 +1,6 @@ /** - * Asserts that `sentryRollupPlugin` performs build-time instrumentation when bundling with Rolldown: - * its code transform injects the orchestrion "bundler ran" banner into the entry chunk. A plain build - * (no plugin) does not. + * Asserts that `sentryRollupPlugin` performs build-time instrumentation when bundling with Rolldown: its code transform injects + * the orchestrion "bundler ran" banner into the entry chunk. A plain build (no plugin) does not. * * @module */ @@ -19,9 +18,20 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const BUILD_TIME_TRANSFORM_MARKER = 'g.bundler=g.bundler||[]'; function bundleText(name) { - const dir = join(__dirname, 'dist', name); - return readdirSync(dir) - .map(f => readFileSync(join(dir, f), 'utf8')) + const files = []; + const walk = dir => { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const full = join(dir, entry.name); + if (entry.isDirectory()) { + walk(full); + } else { + files.push(full); + } + } + }; + walk(join(__dirname, 'dist', name)); + return files + .map(f => readFileSync(f, 'utf8')) .join('\n') .replace(/\s+/g, ''); } @@ -36,10 +46,7 @@ function check(condition, message) { const plain = bundleText('plain'); const plugin = bundleText('plugin'); -check( - !plain.includes(BUILD_TIME_TRANSFORM_MARKER), - 'plain build (no plugin) does not run build-time instrumentation', -); +check(!plain.includes(BUILD_TIME_TRANSFORM_MARKER), 'plain build (no plugin) does not run build-time instrumentation'); check( plugin.includes(BUILD_TIME_TRANSFORM_MARKER), 'sentryRollupPlugin runs build-time instrumentation (injects the orchestrion banner)', diff --git a/dev-packages/e2e-tests/test-applications/node-rolldown/build.mjs b/dev-packages/e2e-tests/test-applications/node-rolldown/build.mjs index cfb1f2e3a53e..8cbc1581bae7 100644 --- a/dev-packages/e2e-tests/test-applications/node-rolldown/build.mjs +++ b/dev-packages/e2e-tests/test-applications/node-rolldown/build.mjs @@ -2,7 +2,8 @@ // - `plain`: no Sentry plugin. // - `plugin`: with `sentryRollupPlugin` (build-time instrumentation). // Only the `plugin` build runs the orchestrion code transform, which prepends the "bundler ran" -// banner to the entry chunk. Kept unminified so assert.mjs can match the banner verbatim. +// banner to the entry chunk. Kept unminified so the banner keeps its identifiers (a minifier would +// rename them); assert.mjs matches it whitespace-insensitively. // Rolldown is Rollup API-compatible, so it consumes the same `@sentry/node/rollup` plugin; it also // resolves node modules and CommonJS natively, so no extra resolve/commonjs plugins are needed. import { builtinModules } from 'node:module'; diff --git a/dev-packages/e2e-tests/test-applications/node-rollup/assert.mjs b/dev-packages/e2e-tests/test-applications/node-rollup/assert.mjs index 49590e0aecd2..815b9d8a3389 100644 --- a/dev-packages/e2e-tests/test-applications/node-rollup/assert.mjs +++ b/dev-packages/e2e-tests/test-applications/node-rollup/assert.mjs @@ -18,9 +18,20 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const BUILD_TIME_TRANSFORM_MARKER = 'g.bundler=g.bundler||[]'; function bundleText(name) { - const dir = join(__dirname, 'dist', name); - return readdirSync(dir) - .map(f => readFileSync(join(dir, f), 'utf8')) + const files = []; + const walk = dir => { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const full = join(dir, entry.name); + if (entry.isDirectory()) { + walk(full); + } else { + files.push(full); + } + } + }; + walk(join(__dirname, 'dist', name)); + return files + .map(f => readFileSync(f, 'utf8')) .join('\n') .replace(/\s+/g, ''); } @@ -35,10 +46,7 @@ function check(condition, message) { const plain = bundleText('plain'); const plugin = bundleText('plugin'); -check( - !plain.includes(BUILD_TIME_TRANSFORM_MARKER), - 'plain build (no plugin) does not run build-time instrumentation', -); +check(!plain.includes(BUILD_TIME_TRANSFORM_MARKER), 'plain build (no plugin) does not run build-time instrumentation'); check( plugin.includes(BUILD_TIME_TRANSFORM_MARKER), 'sentryRollupPlugin runs build-time instrumentation (injects the orchestrion banner)', diff --git a/dev-packages/e2e-tests/test-applications/node-rollup/build.mjs b/dev-packages/e2e-tests/test-applications/node-rollup/build.mjs index 9f29551aad7d..ca9695752bc8 100644 --- a/dev-packages/e2e-tests/test-applications/node-rollup/build.mjs +++ b/dev-packages/e2e-tests/test-applications/node-rollup/build.mjs @@ -2,7 +2,8 @@ // - `plain`: no Sentry plugin. // - `plugin`: with `sentryRollupPlugin` (build-time instrumentation). // Only the `plugin` build runs the orchestrion code transform, which prepends the "bundler ran" -// banner to the entry chunk. Kept unminified so assert.mjs can match the banner verbatim. +// banner to the entry chunk. Kept unminified so the banner keeps its identifiers (a minifier would +// rename them); assert.mjs matches it whitespace-insensitively. import { builtinModules } from 'node:module'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; diff --git a/dev-packages/e2e-tests/test-applications/node-vite/assert.mjs b/dev-packages/e2e-tests/test-applications/node-vite/assert.mjs index a00fd6be1b32..da6a795f7914 100644 --- a/dev-packages/e2e-tests/test-applications/node-vite/assert.mjs +++ b/dev-packages/e2e-tests/test-applications/node-vite/assert.mjs @@ -1,6 +1,6 @@ /** - * Asserts that `sentryVitePlugin` performs build-time instrumentation: its code transform injects the - * orchestrion "bundler ran" banner into the entry chunk. A plain build (no plugin) does not. + * Asserts that `sentryVitePlugin` performs build-time instrumentation: its code transform injects + * the orchestrion "bundler ran" banner into the entry chunk. A plain build (no plugin) does not. * * @module */ @@ -18,9 +18,20 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const BUILD_TIME_TRANSFORM_MARKER = 'g.bundler=g.bundler||[]'; function bundleText(name) { - const dir = join(__dirname, 'dist', name); - return readdirSync(dir) - .map(f => readFileSync(join(dir, f), 'utf8')) + const files = []; + const walk = dir => { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const full = join(dir, entry.name); + if (entry.isDirectory()) { + walk(full); + } else { + files.push(full); + } + } + }; + walk(join(__dirname, 'dist', name)); + return files + .map(f => readFileSync(f, 'utf8')) .join('\n') .replace(/\s+/g, ''); } @@ -35,10 +46,7 @@ function check(condition, message) { const plain = bundleText('plain'); const plugin = bundleText('plugin'); -check( - !plain.includes(BUILD_TIME_TRANSFORM_MARKER), - 'plain build (no plugin) does not run build-time instrumentation', -); +check(!plain.includes(BUILD_TIME_TRANSFORM_MARKER), 'plain build (no plugin) does not run build-time instrumentation'); check( plugin.includes(BUILD_TIME_TRANSFORM_MARKER), 'sentryVitePlugin runs build-time instrumentation (injects the orchestrion banner)', 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 840e9663257d..2b62653cd64b 100644 --- a/dev-packages/e2e-tests/test-applications/node-vite/build.mjs +++ b/dev-packages/e2e-tests/test-applications/node-vite/build.mjs @@ -4,7 +4,8 @@ // The Sentry vite plugin's build-time code transform only applies to server builds (it gates itself // on `consumer === 'server'`), so this uses an SSR build rather than a client `lib` build. Only the // `plugin` build then injects the orchestrion "bundler ran" banner into the entry chunk. Kept -// unminified so assert.mjs can match the banner verbatim. +// unminified so the banner keeps its identifiers (a minifier would rename them); assert.mjs matches +// it whitespace-insensitively. import { builtinModules } from 'node:module'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; 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 4982df4d81ef..4ca865f08474 100644 --- a/dev-packages/e2e-tests/test-applications/node-webpack/assert.mjs +++ b/dev-packages/e2e-tests/test-applications/node-webpack/assert.mjs @@ -18,9 +18,20 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const BUILD_TIME_TRANSFORM_MARKER = 'g.bundler=g.bundler||[]'; function bundleText(name) { - const dir = join(__dirname, 'dist', name); - return readdirSync(dir) - .map(f => readFileSync(join(dir, f), 'utf8')) + const files = []; + const walk = dir => { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const full = join(dir, entry.name); + if (entry.isDirectory()) { + walk(full); + } else { + files.push(full); + } + } + }; + walk(join(__dirname, 'dist', name)); + return files + .map(f => readFileSync(f, 'utf8')) .join('\n') .replace(/\s+/g, ''); } @@ -35,10 +46,7 @@ function check(condition, message) { const plain = bundleText('plain'); const plugin = bundleText('plugin'); -check( - !plain.includes(BUILD_TIME_TRANSFORM_MARKER), - 'plain build (no plugin) does not run build-time instrumentation', -); +check(!plain.includes(BUILD_TIME_TRANSFORM_MARKER), 'plain build (no plugin) does not run build-time instrumentation'); check( plugin.includes(BUILD_TIME_TRANSFORM_MARKER), 'sentryWebpackPlugin runs build-time instrumentation (injects the orchestrion banner)', 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 82ca226828c4..eb85a581f05d 100644 --- a/dev-packages/e2e-tests/test-applications/node-webpack/build.mjs +++ b/dev-packages/e2e-tests/test-applications/node-webpack/build.mjs @@ -2,7 +2,8 @@ // - `plain`: no Sentry plugin. // - `plugin`: with `sentryWebpackPlugin` (build-time instrumentation). // Only the `plugin` build runs the orchestrion code transform, which injects the "bundler ran" banner -// into the entry chunk. Kept unminified so assert.mjs can match the banner verbatim. +// into the entry chunk. Kept unminified so the banner keeps its identifiers (a minifier would +// rename them); assert.mjs matches it whitespace-insensitively. import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import webpack from 'webpack'; From 99cf169908acc1a326130fd381857fef8e1554e7 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 27 Aug 2026 10:51:35 +0200 Subject: [PATCH 4/4] test(e2e): Fix build-time banner marker check in node bundler apps The plugin-build assertion searched a whitespace-stripped bundle for `g.bundler=g.bundler||[]`, but `ORCHESTRION_BUNDLER_MARKER_BANNER` initializes the set with `new Set()` (stripped: `newSet()`), so the marker never matched and the `plugin` assertion always failed. Rolldown additionally emits a `/* @__PURE__ */` annotation inside the banner, which whitespace-stripping leaves intact. Match the real banner: strip block comments as well as whitespace before searching, and look for `g.bundler=g.bundler||newSet()`. Verified against the plugin bundles of all five bundlers (webpack, esbuild, vite, rollup, rolldown). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../e2e-tests/test-applications/node-esbuild/assert.mjs | 8 +++++--- .../e2e-tests/test-applications/node-rolldown/assert.mjs | 8 +++++--- .../e2e-tests/test-applications/node-rollup/assert.mjs | 8 +++++--- .../e2e-tests/test-applications/node-vite/assert.mjs | 8 +++++--- .../e2e-tests/test-applications/node-webpack/assert.mjs | 8 +++++--- 5 files changed, 25 insertions(+), 15 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/node-esbuild/assert.mjs b/dev-packages/e2e-tests/test-applications/node-esbuild/assert.mjs index 29c74b84d15a..7d0d77216931 100644 --- a/dev-packages/e2e-tests/test-applications/node-esbuild/assert.mjs +++ b/dev-packages/e2e-tests/test-applications/node-esbuild/assert.mjs @@ -13,9 +13,10 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); // A distinctive slice of the orchestrion banner that the bundler plugin's build-time code transform // prepends to the entry chunk (see `ORCHESTRION_BUNDLER_MARKER_BANNER` in `@sentry/server-utils`). // It is emitted only when the plugin's build-time instrumentation runs, so it tells a `plugin` build -// apart from a `plain` one. We match against a whitespace-stripped bundle because some bundlers -// (e.g. Rolldown) pretty-print the injected banner rather than emitting it verbatim. -const BUILD_TIME_TRANSFORM_MARKER = 'g.bundler=g.bundler||[]'; +// apart from a `plain` one. Before matching we strip block comments and whitespace, because bundlers +// format the injected banner differently — Rolldown pretty-prints it and inserts a `/* @__PURE__ */` +// annotation. The banner initializes the set with `new Set()`, hence the stripped `newSet()` form. +const BUILD_TIME_TRANSFORM_MARKER = 'g.bundler=g.bundler||newSet()'; function bundleText(name) { const files = []; @@ -33,6 +34,7 @@ function bundleText(name) { return files .map(f => readFileSync(f, 'utf8')) .join('\n') + .replace(/\/\*[\s\S]*?\*\//g, '') .replace(/\s+/g, ''); } diff --git a/dev-packages/e2e-tests/test-applications/node-rolldown/assert.mjs b/dev-packages/e2e-tests/test-applications/node-rolldown/assert.mjs index 0022135375e0..28cd7004f32c 100644 --- a/dev-packages/e2e-tests/test-applications/node-rolldown/assert.mjs +++ b/dev-packages/e2e-tests/test-applications/node-rolldown/assert.mjs @@ -13,9 +13,10 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); // A distinctive slice of the orchestrion banner that the bundler plugin's build-time code transform // prepends to the entry chunk (see `ORCHESTRION_BUNDLER_MARKER_BANNER` in `@sentry/server-utils`). // It is emitted only when the plugin's build-time instrumentation runs, so it tells a `plugin` build -// apart from a `plain` one. We match against a whitespace-stripped bundle because some bundlers -// (e.g. Rolldown) pretty-print the injected banner rather than emitting it verbatim. -const BUILD_TIME_TRANSFORM_MARKER = 'g.bundler=g.bundler||[]'; +// apart from a `plain` one. Before matching we strip block comments and whitespace, because bundlers +// format the injected banner differently — Rolldown pretty-prints it and inserts a `/* @__PURE__ */` +// annotation. The banner initializes the set with `new Set()`, hence the stripped `newSet()` form. +const BUILD_TIME_TRANSFORM_MARKER = 'g.bundler=g.bundler||newSet()'; function bundleText(name) { const files = []; @@ -33,6 +34,7 @@ function bundleText(name) { return files .map(f => readFileSync(f, 'utf8')) .join('\n') + .replace(/\/\*[\s\S]*?\*\//g, '') .replace(/\s+/g, ''); } diff --git a/dev-packages/e2e-tests/test-applications/node-rollup/assert.mjs b/dev-packages/e2e-tests/test-applications/node-rollup/assert.mjs index 815b9d8a3389..69f1f0f42a68 100644 --- a/dev-packages/e2e-tests/test-applications/node-rollup/assert.mjs +++ b/dev-packages/e2e-tests/test-applications/node-rollup/assert.mjs @@ -13,9 +13,10 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); // A distinctive slice of the orchestrion banner that the bundler plugin's build-time code transform // prepends to the entry chunk (see `ORCHESTRION_BUNDLER_MARKER_BANNER` in `@sentry/server-utils`). // It is emitted only when the plugin's build-time instrumentation runs, so it tells a `plugin` build -// apart from a `plain` one. We match against a whitespace-stripped bundle because some bundlers -// (e.g. Rolldown) pretty-print the injected banner rather than emitting it verbatim. -const BUILD_TIME_TRANSFORM_MARKER = 'g.bundler=g.bundler||[]'; +// apart from a `plain` one. Before matching we strip block comments and whitespace, because bundlers +// format the injected banner differently — Rolldown pretty-prints it and inserts a `/* @__PURE__ */` +// annotation. The banner initializes the set with `new Set()`, hence the stripped `newSet()` form. +const BUILD_TIME_TRANSFORM_MARKER = 'g.bundler=g.bundler||newSet()'; function bundleText(name) { const files = []; @@ -33,6 +34,7 @@ function bundleText(name) { return files .map(f => readFileSync(f, 'utf8')) .join('\n') + .replace(/\/\*[\s\S]*?\*\//g, '') .replace(/\s+/g, ''); } diff --git a/dev-packages/e2e-tests/test-applications/node-vite/assert.mjs b/dev-packages/e2e-tests/test-applications/node-vite/assert.mjs index da6a795f7914..654faa17c083 100644 --- a/dev-packages/e2e-tests/test-applications/node-vite/assert.mjs +++ b/dev-packages/e2e-tests/test-applications/node-vite/assert.mjs @@ -13,9 +13,10 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); // A distinctive slice of the orchestrion banner that the bundler plugin's build-time code transform // prepends to the entry chunk (see `ORCHESTRION_BUNDLER_MARKER_BANNER` in `@sentry/server-utils`). // It is emitted only when the plugin's build-time instrumentation runs, so it tells a `plugin` build -// apart from a `plain` one. We match against a whitespace-stripped bundle because some bundlers -// (e.g. Rolldown) pretty-print the injected banner rather than emitting it verbatim. -const BUILD_TIME_TRANSFORM_MARKER = 'g.bundler=g.bundler||[]'; +// apart from a `plain` one. Before matching we strip block comments and whitespace, because bundlers +// format the injected banner differently — Rolldown pretty-prints it and inserts a `/* @__PURE__ */` +// annotation. The banner initializes the set with `new Set()`, hence the stripped `newSet()` form. +const BUILD_TIME_TRANSFORM_MARKER = 'g.bundler=g.bundler||newSet()'; function bundleText(name) { const files = []; @@ -33,6 +34,7 @@ function bundleText(name) { return files .map(f => readFileSync(f, 'utf8')) .join('\n') + .replace(/\/\*[\s\S]*?\*\//g, '') .replace(/\s+/g, ''); } 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 4ca865f08474..4f2279c837e2 100644 --- a/dev-packages/e2e-tests/test-applications/node-webpack/assert.mjs +++ b/dev-packages/e2e-tests/test-applications/node-webpack/assert.mjs @@ -13,9 +13,10 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); // A distinctive slice of the orchestrion banner that the bundler plugin's build-time code transform // prepends to the entry chunk (see `ORCHESTRION_BUNDLER_MARKER_BANNER` in `@sentry/server-utils`). // It is emitted only when the plugin's build-time instrumentation runs, so it tells a `plugin` build -// apart from a `plain` one. We match against a whitespace-stripped bundle because some bundlers -// (e.g. Rolldown) pretty-print the injected banner rather than emitting it verbatim. -const BUILD_TIME_TRANSFORM_MARKER = 'g.bundler=g.bundler||[]'; +// apart from a `plain` one. Before matching we strip block comments and whitespace, because bundlers +// format the injected banner differently — Rolldown pretty-prints it and inserts a `/* @__PURE__ */` +// annotation. The banner initializes the set with `new Set()`, hence the stripped `newSet()` form. +const BUILD_TIME_TRANSFORM_MARKER = 'g.bundler=g.bundler||newSet()'; function bundleText(name) { const files = []; @@ -33,6 +34,7 @@ function bundleText(name) { return files .map(f => readFileSync(f, 'utf8')) .join('\n') + .replace(/\/\*[\s\S]*?\*\//g, '') .replace(/\s+/g, ''); }