Skip to content

[WIP] fix(node): Capture Prisma v5 engine spans under the SentryTracerProvider#21867

Draft
andreiborza wants to merge 47 commits into
ab/sentry-trace-provider-nodefrom
ab/sentry-trace-provider-prisma-v5
Draft

[WIP] fix(node): Capture Prisma v5 engine spans under the SentryTracerProvider#21867
andreiborza wants to merge 47 commits into
ab/sentry-trace-provider-nodefrom
ab/sentry-trace-provider-prisma-v5

Conversation

@andreiborza

Copy link
Copy Markdown
Member

Prisma v5 engine spans (prisma:engine:*, which carry the SQL db.statement) were dropped under the SentryTracerProvider, leaving only the prisma:client:* spans in the transaction. This re-enables the Prisma v5 integration test and fixes the regression.

Root cause

The v5 compatibility shim minted engine spans by hijacking the OTel SDK tracer's private _idGenerator to stamp the engine's exact span/trace ids, then relied on the SDK exporter regrouping spans by parent_span_id at flush. The SentryTracerProvider's tracer has no _idGenerator (so the shim bailed out and dropped every engine span), and it assembles transactions from the live _children tree rather than regrouping a flat list.

Fix

A bounded spanId -> Span registry. Client spans register on spanStart; each v5 engine span is created under the parent it references by id via startInactiveSpan. Because v5 dispatches engine spans detached and out of order (a child can arrive before its parent), a span whose parent isn't registered yet waits in a pending buffer until a later batch registers it. This reproduces the exporter's flat parent_span_id regrouping for the provider path, and removes the fragile _idGenerator hack.


WIP / draft: stacked on #21680 to validate on CI. To be reworked into a standalone PR against develop (the fix is provider-agnostic; the OTel SDK / openTelemetryBasicTracerProvider path still needs validating there).

Comment thread packages/node/src/integrations/tracing/prisma/index.ts
Comment thread packages/node/src/integrations/tracing/prisma/index.ts
Comment thread packages/node/src/integrations/tracing/prisma/index.ts
@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 27.59 kB added added
@sentry/browser - with treeshaking flags 26.03 kB added added
@sentry/browser (incl. Tracing) 46.25 kB added added
@sentry/browser (incl. Tracing + Span Streaming) 47.99 kB added added
@sentry/browser (incl. Tracing, Profiling) 51.01 kB added added
@sentry/browser (incl. Tracing, Replay) 85.49 kB added added
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 75.09 kB added added
@sentry/browser (incl. Tracing, Replay with Canvas) 90.18 kB added added
@sentry/browser (incl. Tracing, Replay, Feedback) 102.84 kB added added
@sentry/browser (incl. Feedback) 44.77 kB added added
@sentry/browser (incl. sendFeedback) 32.39 kB added added
@sentry/browser (incl. FeedbackAsync) 37.52 kB added added
@sentry/browser (incl. Metrics) 28.67 kB added added
@sentry/browser (incl. Logs) 28.91 kB added added
@sentry/browser (incl. Metrics & Logs) 29.6 kB added added
@sentry/react 29.38 kB added added
@sentry/react (incl. Tracing) 48.51 kB added added
@sentry/vue 33.02 kB added added
@sentry/vue (incl. Tracing) 48.11 kB added added
@sentry/svelte 27.61 kB added added
CDN Bundle 30 kB added added
CDN Bundle (incl. Tracing) 48.21 kB added added
CDN Bundle (incl. Logs, Metrics) 31.57 kB added added
CDN Bundle (incl. Tracing, Logs, Metrics) 49.51 kB added added
CDN Bundle (incl. Replay, Logs, Metrics) 70.77 kB added added
CDN Bundle (incl. Tracing, Replay) 85.69 kB added added
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 86.97 kB added added
CDN Bundle (incl. Tracing, Replay, Feedback) 91.49 kB added added
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 92.73 kB added added
CDN Bundle - uncompressed 89.35 kB added added
CDN Bundle (incl. Tracing) - uncompressed 145.88 kB added added
CDN Bundle (incl. Logs, Metrics) - uncompressed 94.05 kB added added
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 149.85 kB added added
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 218.59 kB added added
CDN Bundle (incl. Tracing, Replay) - uncompressed 264.9 kB added added
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 268.86 kB added added
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 278.6 kB added added
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 282.55 kB added added
@sentry/nextjs (client) 50.94 kB added added
@sentry/sveltekit (client) 46.64 kB added added
@sentry/core/server 78.25 kB added added
@sentry/core/browser 64.59 kB added added
@sentry/node-core 63.14 kB added added
@sentry/node 122.91 kB added added
@sentry/node/import (ESM hook with diagnostics-channel injection) 69.95 kB added added
@sentry/node/light 50.69 kB added added
@sentry/node - without tracing 74.17 kB added added
@sentry/aws-serverless 84.94 kB added added
@sentry/cloudflare (withSentry) - minified 181.48 kB added added
@sentry/cloudflare (withSentry) 449.1 kB added added

@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-node branch from ab6fac9 to db893dc Compare July 1, 2026 11:46
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-prisma-v5 branch from f67d8bd to ddfd706 Compare July 1, 2026 11:46
Comment thread packages/node/src/integrations/tracing/prisma/index.ts
Comment thread packages/node/src/integrations/tracing/prisma/index.ts
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-node branch from db893dc to e7deffe Compare July 1, 2026 12:05
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-prisma-v5 branch from ddfd706 to 6f37f91 Compare July 1, 2026 12:05
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-node branch from e7deffe to c2ffa6d Compare July 1, 2026 12:43
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-prisma-v5 branch 2 times, most recently from 34472ac to 00bddb6 Compare July 1, 2026 13:51
Comment thread packages/node/src/integrations/tracing/prisma/index.ts
Add a minimal OpenTelemetry `TracerProvider` that creates native Sentry spans
instead of bridging through the full OTel SDK.
A root span with no parent and no remote (incoming) parent previously continued
the scope's propagation context, so manually-started parallel root spans in the
same scope all collapsed into a single shared trace. The OpenTelemetry SDK
instead mints a fresh trace id per such root span.

Wrap the no-parent branch of `_startSentrySpan` in `startNewTrace` (matching the
existing `options.root` branch) so each parentless root span gets its own trace.
Incoming traces are unaffected, since `continueTrace` sets a remote parent and
takes the `_startRootSpanWithRemoteParent` branch instead.
…race

When `SentryTracer` continues a remote trace whose incoming headers carried no
baggage, `_startRootSpanWithRemoteParent` froze a derived-but-incomplete dynamic
sampling context (missing `sample_rand` and `transaction`) onto the span, which
then propagated downstream.

Only freeze the DSC when the remote parent actually carried one (its trace state
has the `sentry.dsc` key); otherwise leave it unset so it is derived dynamically
from the span, matching the OpenTelemetry SDK path, which never freezes the DSC
there and resolves it lazily (picking up `transaction` and `sample_rand`).
Add per-client deferral of the segment-span transaction capture. The transaction is
otherwise assembled synchronously from the live span tree when the root span ends,
dropping child spans whose instrumentation closes them after it - in the same tick
(diagnostics-channel `asyncEnd`) or on a later tick (e.g. prisma engine spans). When a
client opts in via `_INTERNAL_setDeferSegmentSpanCapture`, a debounced timer (the one the
OpenTelemetry span exporter uses) delays the snapshot so those children land first, and
drains on the client `flush` hook so `Sentry.flush()` / `close()` stays safe. The browser
keeps its synchronous capture.

The opt-in call is wired separately (the Node SDK enables it on the SentryTracerProvider path).
Extract the defer/orphan machinery (per-client queues, debounced drain, flush
wiring, orphan detection, the CAPTURED_SPANS set) out of SentrySpan into a
node-only deferSegmentSpanCapture module, registered through a carrier-based
strategy seam that mirrors set/getAsyncContextStrategy. SentrySpan reads the
seam and captures synchronously when none is registered, so browser bundles that
never register the strategy tree-shake the machinery away.
…sion, flush draining

Covers the three behaviors behind the strategy, driven through SentrySpan.end()
with fake timers: a child ending before the debounce fires lands in the deferred
transaction; a child ending after the snapshot is emitted as its own orphan
transaction tagged sentry.parent_span_already_sent; and pending captures drain
synchronously on the client's flush hook.
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-node branch from aab7745 to d506cee Compare July 1, 2026 14:02
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-prisma-v5 branch from 00bddb6 to c635e74 Compare July 1, 2026 14:02
andreiborza and others added 28 commits July 1, 2026 20:44
Add the SentryTracerProvider under an experimental `useSentryTracerProvider`
flag and update the node setup path to register the new TracerProvider and its
async context strategy instead of the full OTel SDK tracer provider when
enabled.
Outside of span streaming, an outgoing fetch (`http.client`) span with no local
parent is no longer recorded as a standalone transaction — the downstream
sampling decision is left to the server. This is enforced via `onlyIfParent`,
which still creates a non-recording span so trace propagation headers are
injected.

This rule already lives in `SentrySampler`, but that only runs when an
OpenTelemetry SDK tracer provider is set up. Enforcing it in the instrumentation
makes it hold for the `SentryTracerProvider` and for SDKs that don't use an
OpenTelemetry tracer provider at all. The sampler rule is kept for OpenTelemetry
SDK / custom OpenTelemetry setups.
The transaction is assembled synchronously from the live span tree when the root
span ends, dropping child spans whose instrumentation closes them after it - in the
same tick (diagnostics-channel `asyncEnd`) or on a later tick (e.g. prisma engine
spans). A per-client debounced timer (the one the OpenTelemetry span exporter uses)
delays the snapshot so those children land first, and drains on the client `flush`
hook so `Sentry.flush()` / `close()` stays safe.

Enabled on the NodeClient rather than the SentryTracerProvider so it applies with or
without a tracer provider; the browser keeps its synchronous capture.
Under the SentryTracerProvider, streamed spans carry `sentry.origin` as a
first-class attribute including the default `manual` value, whereas the
OpenTelemetry SDK path omits the `manual` default. The `mysql` (v1) db spans
and the `pg.connect` span set no explicit origin, so they surface as
`manual` here.

Assert it for now. When those instrumentations are reworked to set an
explicit `auto.db.otel.*` origin (e.g. #21568 for mysql), these expectations
will be updated to the real origin then.
These assert prisma's engine spans (replayed asynchronously by
`@prisma/instrumentation`), which the SentryTracerProvider drops because it
assembles transactions synchronously on root-span end with no SpanExporter
buffer to wait for late children. They pass on the OpenTelemetry SDK
(`BasicTracerProvider`) path. Skip them here until the general
"complete span-tree capture without a SpanExporter" follow-up lands; v7 is
left enabled as it currently captures the engine spans in time.
Re-enabled now that the streamlined fastify integration (#21706) names spans at
creation instead of renaming via updateName(), so the SentryTracerProvider no
longer stamps sentry.source: 'custom'. Verified locally via e2e (11/11 pass each).
Moves the _INTERNAL_setDeferSegmentSpanCapture call out of initOtel (which only runs
on Sentry.init and only wires the first client) into the NodeClient constructor, which
runs for every client — first, second, or manually constructed — so each defers correctly.
Prisma v5 engine spans (`prisma:engine:*`, which carry the SQL `db.statement`)
were minted by hijacking the OTel SDK tracer's private `_idGenerator`. The
SentryTracerProvider's tracer has no `_idGenerator`, so the shim bailed and
dropped every engine span, leaving only the `prisma:client:*` spans.

Replace the hack with a span registry: client spans register by their span id
on `spanStart`, and each v5 engine span is created under the parent it
references by id via `startInactiveSpan`. Engine spans whose parent hasn't been
seen yet wait in a pending buffer until a later batch registers it, reproducing
the flat `parent_span_id` regrouping the OTel SDK exporter used to do.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-prisma-v5 branch from bbac34a to 78daa6e Compare July 1, 2026 18:45

@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 78daa6e. Configure here.

},
1,
{ maxWait: 100 },
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Debounce timer keeps process alive

Medium Severity

Deferred segment capture uses debounce with setTimeout, but those timers are never unref'd (or passed through safeUnref). On Node, that can keep short-lived scripts or CLI processes from exiting until the debounce/maxWait window finishes.

Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit 78daa6e. Configure here.

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.

1 participant