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..7d0d77216931 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-esbuild/assert.mjs @@ -0,0 +1,61 @@ +/** + * 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. 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 = []; + 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\S]*?\*\//g, '') + .replace(/\s+/g, ''); +} + +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..731ba2bf709d --- /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. +// - `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 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'; +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')], + outdir: join(__dirname, 'dist', name), + 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..28cd7004f32c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-rolldown/assert.mjs @@ -0,0 +1,61 @@ +/** + * 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. 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 = []; + 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\S]*?\*\//g, '') + .replace(/\s+/g, ''); +} + +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..8cbc1581bae7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-rolldown/build.mjs @@ -0,0 +1,42 @@ +// 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 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'; +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..ea32d98dc0bf --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-rolldown/package.json @@ -0,0 +1,23 @@ +{ + "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" + } +} 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..69f1f0f42a68 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-rollup/assert.mjs @@ -0,0 +1,61 @@ +/** + * 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. 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 = []; + 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\S]*?\*\//g, '') + .replace(/\s+/g, ''); +} + +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..ca9695752bc8 --- /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. +// - `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 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'; +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..654faa17c083 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-vite/assert.mjs @@ -0,0 +1,61 @@ +/** + * 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. 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 = []; + 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\S]*?\*\//g, '') + .replace(/\s+/g, ''); +} + +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..2b62653cd64b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-vite/build.mjs @@ -0,0 +1,52 @@ +// 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 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'; +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..4f2279c837e2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-webpack/assert.mjs @@ -0,0 +1,61 @@ +/** + * 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. 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 = []; + 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\S]*?\*\//g, '') + .replace(/\s+/g, ''); +} + +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 52% 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..eb85a581f05d 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,21 @@ -// 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 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'; +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 +26,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 +42,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');