-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
test(e2e): Verify bundler plugins instrument a bundled graphql at runtime #23670
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
656420c
c6a89f2
a08c3dd
ad04c2d
2f16810
8b67446
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,61 +1,7 @@ | ||
| /** | ||
| * 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'; | ||
| import { assertBundlerInstrumentation } from '@sentry-internal/test-utils'; | ||
|
|
||
| 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.'); | ||
| // Drives the four built bundles (plain / plugin / plain-external / plugin-external) across the | ||
| // build-time and runtime instrumentation paths and asserts exactly one set of graphql spans in each | ||
| // instrumented scenario, plus the inlined-vs-external bundle shape. See `assertBundlerInstrumentation` | ||
| // in `@sentry-internal/test-utils` for the full matrix. | ||
| assertBundlerInstrumentation('graphql'); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,41 +1,50 @@ | ||
| // 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. | ||
| // Bundles the entrypoint with esbuild four ways, each a directly-runnable CJS bundle: | ||
| // - `plain` / `plugin`: graphql inlined. Only `plugin` (with `sentryEsbuildPlugin`) | ||
| // build-time instruments it. Run without `--import`. | ||
| // - `plain-external` / `plugin-external`: graphql kept external so the runtime `--import` hook can | ||
| // intercept it at load time. Run with `--import`. | ||
| // esbuild emits CJS (not ESM): its ESM output can't perform the CJS `require('node:async_hooks')` that | ||
| // `@sentry/server-utils` does once inlined, and CJS is the normal esbuild node target. `assert.mjs` | ||
| // runs all four and checks the query works and that exactly one set of graphql spans is emitted in | ||
| // each instrumented scenario. Kept unminified so the injected snippet keeps its identifiers. | ||
| import { rmSync } from 'node:fs'; | ||
| 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) { | ||
| rmSync(join(__dirname, 'dist'), { recursive: true, force: true }); | ||
|
|
||
| // No auth/release/telemetry — we only care about the build-time transforms and defines. | ||
| const makeSentryPlugin = () => | ||
| sentryEsbuildPlugin({ | ||
| telemetry: false, | ||
| sourcemaps: { disable: true }, | ||
| release: { create: false, finalize: false, inject: false }, | ||
| }); | ||
|
|
||
| function run(name, { external, plugins }) { | ||
| return build({ | ||
| entryPoints: [join(__dirname, 'src', 'entry.mjs')], | ||
| outdir: join(__dirname, 'dist', name), | ||
| outfile: join(__dirname, 'dist', name, 'main.cjs'), | ||
| bundle: true, | ||
| platform: 'node', | ||
| format: 'esm', | ||
| format: 'cjs', | ||
| // The `*-external` variants keep graphql out of the bundle, so it is resolved from node_modules at | ||
| // runtime and the `--import` hook can transform it as it loads. | ||
| external: external ? ['graphql'] : [], | ||
| 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 }, | ||
| }), | ||
| ], | ||
| ); | ||
| await run('plain', { external: false, plugins: [] }); | ||
| await run('plugin', { external: false, plugins: [makeSentryPlugin()] }); | ||
| await run('plain-external', { external: true, plugins: [] }); | ||
| await run('plugin-external', { external: true, plugins: [makeSentryPlugin()] }); | ||
|
|
||
| // eslint-disable-next-line no-console | ||
| console.log('built plain + plugin with esbuild'); | ||
| console.log('built plain + plugin (inlined) and plain-external + plugin-external with esbuild'); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,12 @@ | ||
| // eslint-disable-next-line no-console | ||
| console.log('this is the application'); | ||
| // The real workload the bundle instruments: a `graphql` query. Whether `graphql` is inlined into the | ||
| // bundle or kept external is decided per-variant by `build.mjs`; the `plugin` build's orchestrion | ||
| // transform rewrites the inlined copy. graphql 16.x sits in the supported orchestrion range | ||
| // (`>=14.0.0 <17`). The conventional `runWorkload` export lets the shared `entry.mjs` stay | ||
| // library-agnostic. | ||
| import { buildSchema, graphql } from 'graphql'; | ||
|
|
||
| const schema = buildSchema('type Query { hello: String }'); | ||
|
|
||
| export async function runWorkload() { | ||
| return graphql({ schema, source: '{ hello }', rootValue: { hello: () => 'world' } }); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,48 @@ | ||
| // Bundled entrypoint, run directly with `node` (no `--import` runtime hook). `Sentry.init` runs | ||
| // first so the instrumentation's channel subscriber is ready, then the workload is imported and run. | ||
| // Spans are collected via the `spanEnd` hook (transport- and trace-lifecycle-independent) and written | ||
| // to the file named by `SENTRY_E2E_RESULT_FILE` for `assert.mjs` to read back. The workload's return | ||
| // value rides along as `result` so the assertion can check it without knowing what the workload does. | ||
| // | ||
| // The body is an async function rather than top-level await so the same source bundles to both ESM | ||
| // and CommonJS (esbuild emits CJS for a node target, which disallows top-level await). | ||
| import { writeFileSync } from 'node:fs'; | ||
| import * as Sentry from '@sentry/node'; | ||
|
|
||
| Sentry.init({ | ||
| traceLifecycle: 'static', | ||
| dsn: 'https://public@dsn.ingest.sentry.io/1337', | ||
| tracesSampleRate: 1, | ||
| }); | ||
| async function main() { | ||
| Sentry.init({ | ||
| dsn: 'https://public@dsn.ingest.sentry.io/1337', | ||
| tracesSampleRate: 1, | ||
| // Isolate the build-time path: with the runtime hook off, the bundler plugin is the only possible | ||
| // injector, so a `plain` (no-plugin) build is a true negative. | ||
| enableRuntimeChannelInjection: false, | ||
| // Hermetic — never hit the network. | ||
| transport: () => ({ send: () => Promise.resolve({}), flush: () => Promise.resolve(true) }), | ||
| }); | ||
|
|
||
| await import('./app.mjs'); | ||
| const spans = []; | ||
| Sentry.getClient()?.on('spanEnd', span => { | ||
| const json = Sentry.spanToJSON(span); | ||
| spans.push({ name: json.name, origin: json.attributes?.['sentry.origin'] }); | ||
| }); | ||
|
|
||
| const { runWorkload } = await import('./app.mjs'); | ||
|
|
||
| let result; | ||
| await Sentry.startSpan({ name: 'workload' }, async () => { | ||
| result = await runWorkload(); | ||
| }); | ||
|
|
||
| await Sentry.flush(2000); | ||
|
|
||
| const resultFile = process.env.SENTRY_E2E_RESULT_FILE; | ||
| if (!resultFile) { | ||
| throw new Error('SENTRY_E2E_RESULT_FILE is required (assertBundlerInstrumentation sets it).'); | ||
| } | ||
| // Write synchronously so the payload is fully flushed before `process.exit`. `console.log` + exit | ||
| // can truncate or EPIPE when stdout is a pipe (the exit lands before the buffered write drains). | ||
| writeFileSync(resultFile, JSON.stringify({ result, spans })); | ||
| process.exit(0); | ||
| } | ||
|
|
||
| void main(); | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,61 +1,7 @@ | ||
| /** | ||
| * 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'; | ||
| import { assertBundlerInstrumentation } from '@sentry-internal/test-utils'; | ||
|
|
||
| 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, ''); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The banner test was kind of irrelevant, and the whitespace stripping a bit brittle, so removing this is good. However, now nothing asserts the shape of the build anymore, which is presumably how graphql was not actually being inlined as expected. This patch would add a check that it's being inlined or kept external as expected: https://gist.github.com/isaacs/2d9f123a663771811c04178f1089b7f9 |
||
| } | ||
|
|
||
| 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.'); | ||
| // Drives the four built bundles (plain / plugin / plain-external / plugin-external) across the | ||
| // build-time and runtime instrumentation paths and asserts exactly one set of graphql spans in each | ||
| // instrumented scenario, plus the inlined-vs-external bundle shape. See `assertBundlerInstrumentation` | ||
| // in `@sentry-internal/test-utils` for the full matrix. | ||
| assertBundlerInstrumentation('graphql'); | ||
Uh oh!
There was an error while loading. Please reload this page.