[WIP] fix(node): Capture Prisma v5 engine spans under the SentryTracerProvider#21867
Draft
andreiborza wants to merge 47 commits into
Draft
[WIP] fix(node): Capture Prisma v5 engine spans under the SentryTracerProvider#21867andreiborza wants to merge 47 commits into
andreiborza wants to merge 47 commits into
Conversation
Contributor
size-limit report 📦
|
ab6fac9 to
db893dc
Compare
f67d8bd to
ddfd706
Compare
db893dc to
e7deffe
Compare
ddfd706 to
6f37f91
Compare
e7deffe to
c2ffa6d
Compare
34472ac to
00bddb6
Compare
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.
aab7745 to
d506cee
Compare
00bddb6 to
c635e74
Compare
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>
bbac34a to
78daa6e
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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 }, | ||
| ); |
There was a problem hiding this comment.
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.
Triggered by project rule: PR Review Guidelines for Cursor Bot
Reviewed by Cursor Bugbot for commit 78daa6e. Configure here.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Prisma v5 engine spans (
prisma:engine:*, which carry the SQLdb.statement) were dropped under theSentryTracerProvider, leaving only theprisma: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
_idGeneratorto stamp the engine's exact span/trace ids, then relied on the SDK exporter regrouping spans byparent_span_idat flush. TheSentryTracerProvider's tracer has no_idGenerator(so the shim bailed out and dropped every engine span), and it assembles transactions from the live_childrentree rather than regrouping a flat list.Fix
A bounded
spanId -> Spanregistry. Client spans register onspanStart; each v5 engine span is created under the parent it references by id viastartInactiveSpan. 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 flatparent_span_idregrouping for the provider path, and removes the fragile_idGeneratorhack.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 /openTelemetryBasicTracerProviderpath still needs validating there).