Skip to content

fix(node): Build-time instrument the ESM build of graphql - #23669

Merged
mydea merged 1 commit into
fn/bundler-testsfrom
fn/graphql-esm-instrument
Aug 28, 2026
Merged

fix(node): Build-time instrument the ESM build of graphql#23669
mydea merged 1 commit into
fn/bundler-testsfrom
fn/graphql-esm-instrument

Conversation

@mydea

@mydea mydea commented Aug 27, 2026

Copy link
Copy Markdown
Member

Build-time instrumentation of graphql silently produced no spans when a bundler resolved graphql's ESM build. The orchestrion transform ran, the app worked, but no auto.graphql.diagnostic_channel spans were emitted — the same failure class as orchestrion-treeshake-repro.

Root cause

graphql ships dual CJS/ESM per-file builds (language/parser.js and language/parser.mjs, etc.), and the orchestrion matcher compares filePath exactly. The graphql config only listed the CommonJS paths, so a bundler that resolves graphql's ESM build never matched:

  • Bundlers honor graphql's module field → load index.mjs.mjs files → no match → uninstrumented.
  • Node's native ESM resolver ignores module (graphql has no exports map) → import 'graphql' loads index.js (CJS) → .js files → matched. So the runtime --import path and the existing node-integration-tests (CJS and ESM) were unaffected; this gap was bundler-path-only.

This is why the issue went unnoticed: everything that goes through Node's loader used the CJS build.

Fix

Mirror the dual-path pattern already used by the openai config: emit one entry per built file (.js for require, .mjs for import). No behavior change for the CJS path.

The stacked test PR on top exercises this across webpack/vite/rollup/rolldown (ESM) and esbuild (CJS).

@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 28.57 kB - -
@sentry/browser - with treeshaking flags 26.92 kB - -
@sentry/browser - with treeshaking flags tracing without tracing 26.82 kB - -
@sentry/browser (incl. Tracing) 48.63 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 48.65 kB - -
@sentry/browser (incl. Tracing, Profiling) 51.56 kB - -
@sentry/browser (incl. Tracing, Replay) 88.11 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 77.51 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 92.82 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 105.51 kB - -
@sentry/browser (incl. Feedback) 45.79 kB - -
@sentry/browser (incl. sendFeedback) 33.35 kB - -
@sentry/browser (incl. FeedbackAsync) 38.46 kB - -
@sentry/browser (incl. Metrics) 29.51 kB - -
@sentry/browser (incl. Logs) 29.8 kB - -
@sentry/browser (incl. Metrics & Logs) 30.43 kB - -
@sentry/react 30.31 kB - -
@sentry/react (incl. Tracing) 50.84 kB - -
@sentry/vue 35.69 kB - -
@sentry/vue (incl. Tracing) 50.88 kB - -
@sentry/svelte 28.59 kB - -
CDN Bundle 30.36 kB - -
CDN Bundle (incl. Tracing) 49.12 kB - -
CDN Bundle (incl. Logs, Metrics) 32.56 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 51.01 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 73 kB - -
CDN Bundle (incl. Tracing, Replay) 86.62 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 88.52 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 92.4 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 94.33 kB - -
CDN Bundle - uncompressed 89.97 kB - -
CDN Bundle (incl. Tracing) - uncompressed 146.82 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 96.26 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 152.51 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 225.42 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 266.32 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 272 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 280.01 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 285.68 kB - -
@sentry/nextjs (client) 53.41 kB - -
@sentry/sveltekit (client) 49.08 kB - -
@sentry/core/server 65.3 kB - -
@sentry/core/browser 52.37 kB - -
@sentry/node 122.37 kB +0.04% +41 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 85.23 kB +0.03% +19 B 🔺
@sentry/node - without tracing 87.69 kB +0.06% +46 B 🔺
@sentry/node - without channel injection 102.06 kB +0.04% +32 B 🔺
@sentry/aws-serverless 95.85 kB +0.05% +47 B 🔺
@sentry/cloudflare (withSentry) - minified 199.57 kB - -
@sentry/cloudflare (withSentry) 495.71 kB - -

View base workflow run

The graphql orchestrion config only listed graphql's CommonJS file paths
(`language/parser.js`, ...). graphql ships dual CJS/ESM per-file builds and the
orchestrion matcher compares `filePath` exactly, so bundlers resolving graphql's
ESM build (webpack with `outputModule`, vite, rollup, rolldown) never matched and
left graphql uninstrumented at build time — the app ran but produced no graphql
spans. Node's native ESM resolver ignores the `module` field and loads graphql's
CJS build, so the runtime `--import` path (and the node-integration-tests) were
unaffected; this gap only hit the bundler plugin path.

Mirror the existing dual-path pattern from the `openai` config: emit one entry per
built file (`.js` for `require`, `.mjs` for `import`).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mydea
mydea force-pushed the fn/graphql-esm-instrument branch from 102018c to df48e30 Compare August 27, 2026 09:35

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit df48e30. Configure here.

},
module: { name: 'graphql', versionRange: '>=14.0.0 <17', filePath },
functionQuery: { functionName: 'execute', kind: 'Auto' as const },
})),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix PR lacks regression tests

Medium Severity

This fix PR adds .mjs matcher entries so bundlers that resolve graphql's ESM build emit spans, but the diff includes no unit, integration, or E2E test that would fail without those entries. Review guidelines for fix PRs require a regression test covering the repaired path. I flagged this because it was mentioned in the review rules. The description notes a stacked test PR; landing that coverage with this change would satisfy the requirement.

Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit df48e30. Configure here.

@mydea
mydea marked this pull request as ready for review August 27, 2026 11:20
@mydea
mydea requested a review from a team as a code owner August 27, 2026 11:20
@mydea
mydea requested review from msonnb and stephanie-anderson and removed request for a team August 27, 2026 11:20
@mydea
mydea merged commit d0a11d8 into develop Aug 28, 2026
397 of 403 checks passed
@mydea
mydea deleted the fn/graphql-esm-instrument branch August 28, 2026 09:26
mydea added a commit that referenced this pull request Aug 28, 2026
…time (#23670)

Turns the five node bundler apps (`node-webpack`, `node-vite`,
`node-rollup`, `node-rolldown`, `node-esbuild`) from a static
banner-grep into a real runtime test, in the spirit of
[orchestrion-treeshake-repro](https://github.com/logaretm/orchestrion-treeshake-repro):
actually run the bundled service against a real `graphql` package and
verify what gets injected — across **both** the build-time (bundler
plugin) and runtime (`--import` hook) instrumentation paths.

Each app builds the same `graphql` workload four ways and runs each
bundle:

- `plain` — graphql inlined, no plugin, no `--import`: **no** graphql
spans (negative control),
- `plugin` — graphql inlined, Sentry bundler plugin, no `--import`: one
set of spans, via build-time injection,
- `plain-external` — graphql external, `--import`: one set, via the
runtime hook,
- `plugin-external` — graphql external, plugin, `--import`: one set —
the plugin can't touch an external module, so the runtime hook is the
sole injector and there's no double instrumentation.

"One set" is defined relative to the build-time run, so the count stays
correct across bundlers and graphql versions. This guards the fix
beneath it (#23669) and catches the whole class of "bundle looks
instrumented but emits no/double spans" regressions the static grep
couldn't.

### Shared assertion helper

The per-app `assert.mjs` was ~86 identical lines copied five times. It
now lives once in `@sentry-internal/test-utils` as
`assertBundlerInstrumentation('graphql')`, and each app's `assert.mjs`
is a single call. The helper is parameterized by a small fixture
descriptor (`{ moduleName, origin, sourceMarker, assertResult }`) rather
than hardcoded to graphql, so a future library is one registry entry
plus a `src/app.mjs` workload — everything else (the four-variant
matrix, the "one set" logic, the shape checks) is generic. `entry.mjs`
is likewise library-agnostic: it runs a conventional `runWorkload()` and
ships whatever it returns back as `result`.

### Decisions

- **Control inlining with Vite's SSR knob, not
`rollupOptions.external`.** A Vite SSR build externalizes deps by
default, so `rollupOptions.external` never governed whether graphql was
bundled — the "inlined" variants were silently shipping an external
graphql and not exercising the build-time path at all. The variants now
set `ssr.noExternal` / `ssr.external` explicitly (the latter also wins
over the plugin's own force-bundle, leaving graphql for the runtime
hook).
- **Assert the bundle _shape_, not just the span counts.** The counts
can come out right by accident even when the externalization knob is a
no-op, so each variant is also checked to actually be inlined (carries
graphql's own source, no bare `graphql` import) or external (keeps the
bare import, no inlined source). This scans every emitted chunk in the
output dir, because bundlers split the entry's `await
import('./app.mjs')` into a sibling chunk (webpack) where the marker
lands rather than in `main.*`.
- **Hand the result back through a file, not stdout.**
`console.log(...)` + `process.exit()` can truncate or EPIPE when stdout
is a pipe (the exit lands before the buffered write drains). The entry
`writeFileSync`s its result to `SENTRY_E2E_RESULT_FILE` and the helper
reads it back; a boot crash or a missing file surfaces as an explicit
failure instead of being swallowed.
- **Isolate the build-time path.** The entry sets
`enableRuntimeChannelInjection: false` and the non-`--import` bundles
run with a plain `node`, so the bundler plugin is the only possible
injector — making the `plain` build a true negative rather than
something the runtime hook could rescue.
- **Capture via the `spanEnd` hook** (+ `spanToJSON`) rather than
`beforeSendSpan`, so collection is independent of transport and trace
lifecycle. Transport is a no-op and the DSN fake — nothing hits the
network.
- **Entry is an async function, not top-level await**, so the same
source bundles to both ESM and esbuild's CJS node output.
- **esbuild emits CJS**, the other four emit ESM. esbuild's ESM output
can't perform the CJS `require('node:async_hooks')` that
`@sentry/server-utils` does once inlined (an esbuild CJS-in-ESM interop
limit, the "separate bug" the repro noted); CJS output is the normal
esbuild node target and sidesteps it. This also means esbuild resolves
graphql's CJS build, so the suite covers both the `.js` and `.mjs`
orchestrion paths.
- **Dropped the banner-grep** assertion: the runtime span check is a
strictly stronger proof that build-time injection ran, and the banner's
`new Set()` formatting varies by bundler.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: isaacs <i@izs.me>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants