Skip to content

feat(core): Add deferred segment-span transaction capture#21839

Merged
andreiborza merged 9 commits into
ab/sentry-trace-provider-otelfrom
ab/sentry-trace-provider-core-capture
Jul 2, 2026
Merged

feat(core): Add deferred segment-span transaction capture#21839
andreiborza merged 9 commits into
ab/sentry-trace-provider-otelfrom
ab/sentry-trace-provider-core-capture

Conversation

@andreiborza

@andreiborza andreiborza commented Jun 29, 2026

Copy link
Copy Markdown
Member

What

Adds the ability to defer the assembly of transactions to avoid dropping spans from transactions that end shortly after the segment span itself. Additionally, it also handles children that end after the debounce fired and transactions have already been sent. Spans that don't quite make it will end up as their own transaction in the same trace instead of being dropped .

This mimics what is already done today in the span exporter (a buffer + debounced flush).

Why

SentrySpan assembles a transaction synchronously from the span tree the instant the segment span ends. But some child spans are closed by their instrumentation after the root ends.

For example:

  • Same tick: diagnostics-channel instrumentations (HTTP, undici) end the child in an asyncEnd/response callback that runs after the root handler returns.
  • Later tick: some instrumentations replay spans asynchronously, notably @prisma/instrumentation emits its engine spans on a later tick once it receives the engine trace data.

Without deferral those children aren't in the tree yet at root-end, so they're silently dropped from the transaction. With the OTel SDK, this never happened because the SentrySpanExporter already buffers finished spans and flushes on a debounced timer. The SentryTracerProvider has no exporter, so defer reinstates that buffering window so late-ending children land before the snapshot.

  • Orphan emission handles the tail: a child that ends after the debounce fired and the transaction was already sent can't join it, so it is emitted as its own transaction in the same trace instead of being dropped (mirroring the exporter).

This is used and tested in #21680's integration/e2e tests.

Comment thread packages/core/src/tracing/sentrySpan.ts Outdated
@github-actions

github-actions Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 27.59 kB - -
@sentry/browser - with treeshaking flags 26.03 kB - -
@sentry/browser (incl. Tracing) 46.19 kB +0.34% +152 B 🔺
@sentry/browser (incl. Tracing + Span Streaming) 47.93 kB +0.34% +159 B 🔺
@sentry/browser (incl. Tracing, Profiling) 50.97 kB +0.32% +160 B 🔺
@sentry/browser (incl. Tracing, Replay) 85.44 kB +0.21% +174 B 🔺
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 75.05 kB +0.23% +171 B 🔺
@sentry/browser (incl. Tracing, Replay with Canvas) 90.13 kB +0.21% +181 B 🔺
@sentry/browser (incl. Tracing, Replay, Feedback) 102.79 kB +0.17% +170 B 🔺
@sentry/browser (incl. Feedback) 44.76 kB - -
@sentry/browser (incl. sendFeedback) 32.38 kB - -
@sentry/browser (incl. FeedbackAsync) 37.51 kB - -
@sentry/browser (incl. Metrics) 28.67 kB - -
@sentry/browser (incl. Logs) 28.91 kB - -
@sentry/browser (incl. Metrics & Logs) 29.59 kB - -
@sentry/react 29.38 kB - -
@sentry/react (incl. Tracing) 48.48 kB +0.28% +132 B 🔺
@sentry/vue 32.97 kB +0.47% +152 B 🔺
@sentry/vue (incl. Tracing) 48.07 kB +0.36% +172 B 🔺
@sentry/svelte 27.61 kB - -
CDN Bundle 30 kB - -
CDN Bundle (incl. Tracing) 48.17 kB +0.34% +161 B 🔺
CDN Bundle (incl. Logs, Metrics) 31.57 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 49.47 kB +0.27% +133 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) 70.77 kB - -
CDN Bundle (incl. Tracing, Replay) 85.65 kB +0.21% +174 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 86.92 kB +0.19% +157 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) 91.46 kB +0.18% +159 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 92.7 kB +0.17% +154 B 🔺
CDN Bundle - uncompressed 89.35 kB - -
CDN Bundle (incl. Tracing) - uncompressed 145.75 kB +0.33% +477 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed 94.05 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 149.72 kB +0.32% +477 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 218.59 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 264.76 kB +0.19% +477 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 268.72 kB +0.18% +477 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 278.46 kB +0.18% +479 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 282.41 kB +0.17% +479 B 🔺
@sentry/nextjs (client) 50.88 kB +0.31% +153 B 🔺
@sentry/sveltekit (client) 46.59 kB +0.36% +163 B 🔺
@sentry/core/server 78.24 kB +0.67% +515 B 🔺
@sentry/core/browser 64.58 kB +0.83% +531 B 🔺
@sentry/node-core 62.65 kB +0.44% +272 B 🔺
@sentry/node 121.47 kB +0.21% +252 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 69.95 kB - -
@sentry/node/light 50.64 kB +0.36% +178 B 🔺
@sentry/node - without tracing 72.98 kB +0.41% +297 B 🔺
@sentry/aws-serverless 83.82 kB +0.31% +253 B 🔺
@sentry/cloudflare (withSentry) - minified 181.21 kB +0.33% +595 B 🔺
@sentry/cloudflare (withSentry) 448.43 kB +0.36% +1.6 kB 🔺

View base workflow run

Comment thread packages/core/src/tracing/sentrySpan.ts Outdated
Comment thread packages/core/src/tracing/sentrySpan.ts Outdated
@andreiborza andreiborza changed the title feat(core): Add deferred segment-span transaction capture, orphan emission, and provider-span sealing feat(core): Add deferred segment-span transaction capture Jun 29, 2026
Comment thread packages/core/src/tracing/sentrySpan.ts Outdated
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-otel branch from a1613c2 to 6bd79ac Compare June 29, 2026 15:46
@andreiborza andreiborza requested a review from a team as a code owner June 29, 2026 15:46
@andreiborza andreiborza requested review from JPeer264 and mydea and removed request for a team June 29, 2026 15:46
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-core-capture branch from 29ce501 to c741940 Compare June 29, 2026 15:46
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-otel branch 2 times, most recently from e005612 to 924ce11 Compare June 30, 2026 08:07
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-core-capture branch from c741940 to 4b3fc03 Compare June 30, 2026 08:13
Comment thread packages/opentelemetry/src/tracer.ts
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-otel branch from 924ce11 to 630bb67 Compare June 30, 2026 08:44
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-core-capture branch from 4b3fc03 to 14cb421 Compare June 30, 2026 08:48
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-otel branch from 630bb67 to 15154ed Compare June 30, 2026 09:56
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-core-capture branch from 14cb421 to 6b8db6e Compare June 30, 2026 09:56
Comment thread packages/core/src/tracing/sentrySpan.ts Outdated
Comment thread packages/core/src/tracing/sentrySpan.ts Outdated
Comment thread packages/core/src/tracing/sentrySpan.ts Outdated
Comment thread packages/core/src/tracing/sentrySpan.ts Outdated
Comment thread packages/core/src/tracing/sentrySpan.ts Outdated
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-otel branch from 15154ed to 7837eb8 Compare June 30, 2026 15:17
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-core-capture branch from 6b8db6e to 2df53ad Compare June 30, 2026 15:17
Comment thread packages/core/test/lib/tracing/sentrySpan.test.ts Outdated
Comment thread packages/core/src/tracing/sentrySpan.ts Outdated
@andreiborza

Copy link
Copy Markdown
Member Author

I'm going to rework this slightly so it has no impact on browser SDKs.

@andreiborza andreiborza marked this pull request as draft July 1, 2026 07:22
Comment thread packages/core/src/tracing/deferSegmentSpanCapture.ts Outdated
Comment thread packages/core/test/lib/tracing/deferSegmentSpanCapture.test.ts
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-core-capture branch from 51859a2 to f25bf84 Compare July 1, 2026 11:40
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-otel branch from 7837eb8 to 97e8266 Compare July 1, 2026 11:40
Comment thread packages/core/src/tracing/deferSegmentSpanCapture.ts Outdated
@andreiborza andreiborza requested a review from nicohrubec July 1, 2026 13:16
@andreiborza andreiborza marked this pull request as ready for review July 1, 2026 13:16
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-core-capture branch from c11b10c to e7f6447 Compare July 1, 2026 14:02
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-otel branch 2 times, most recently from cfbfb8b to 1c93e34 Compare July 1, 2026 18:41
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-core-capture branch from 6de76e6 to f2439e8 Compare July 1, 2026 18:41
Comment thread packages/core/src/tracing/deferSegmentSpanCapture.ts
Comment thread packages/core/src/tracing/deferSegmentSpanCapture.ts Outdated
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-core-capture branch 3 times, most recently from 220ade4 to d93efba Compare July 1, 2026 23:09

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

Comment thread packages/core/src/tracing/deferSegmentSpanCapture.ts
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.
…pture

Drops the CAPTURED_SPAN_CLIENTS routing map and the scope/client params threaded
through the strategy. Each client gets one debounced queue (mirroring the OpenTelemetry
span exporter's per-instance buffer); the capturing client is bound when the span ends
and used at drain, so a deferred transaction always lands on the client that created
the span. The strategy interface is now just the convert callback.
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-otel branch from 98255e6 to 298b3a4 Compare July 2, 2026 11:25
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-core-capture branch from d93efba to 22abc5c Compare July 2, 2026 11:25

@JPeer264 JPeer264 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM. I tried it locally with the Cloudflare SDK and executed spans after the root span was finished and it works like a charm: https://sentry-sdks.sentry.io/explore/traces/trace/6ba87226f389461e94a30088900a5924/

@andreiborza andreiborza merged commit 756d4c5 into develop Jul 2, 2026
568 of 599 checks passed
@andreiborza andreiborza deleted the ab/sentry-trace-provider-core-capture branch July 2, 2026 12:12
andreiborza added a commit that referenced this pull request Jul 2, 2026
…ey end (#21842)

### What

Seal spans created by the `SentryTracerProvider` once they end. After
`SentrySpan.end()` finishes its end-of-span processing, every mutator
no-ops, gated on the `spanIsTracerProviderSpan` brand:

- `setAttribute` / `setAttributes`
- `setStatus`
- `updateName`
- `updateStartTime`
- `addLink` / `addLinks`
- `addEvent`

Spans created directly through core (e.g. the browser SDK) are never
branded, so they stay mutable.

### Why

OpenTelemetry SDK spans are immutable after `end()` (setters no-op). The
`SentryTracerProvider` hands native `SentrySpan`s to OTel
instrumentations as OTel spans, so they must honor that contract. Some
instrumentations write to a span after `end()` (e.g. Next.js sets a
status on a render error); without sealing, those late writes overwrite
the finalized values.

This matters most once segment-span capture is deferred (#21839): the
transaction snapshot is then taken on a later tick, so any late
post-`end()` write (status, attribute, name, start time, link, or event)
would be serialized into it. Sealing the span on `end()` keeps the
finalized values intact.

### Notes

- The seal applies only to provider-branded spans. The brand is set
exclusively by the OTel `SentryTracer`, never by the browser SDK, so
browser spans (including web-vitals start-time adjustments via
`updateStartTime`) are unaffected.
- Tests in `sentrySpan.test.ts` cover both directions: a branded span
where all mutators no-op after `end()`, and a non-branded span that
stays mutable.

Stacked on #21839 (deferred capture / orphan emission) and #21666 (which
provides the `spanIsTracerProviderSpan` brand). Dormant until #21680
wires the provider; no provider-branded spans exist before then.
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.

3 participants