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

@andreiborzaandreiborza 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 threadpackages/core/src/tracing/sentrySpan.ts Outdated
@github-actions

github-actionsBot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize% ChangeChange
@sentry/browser27.59 kB--
@sentry/browser - with treeshaking flags26.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 flags75.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/react29.38 kB--
@sentry/react (incl. Tracing)48.48 kB+0.28%+132 B 🔺
@sentry/vue32.97 kB+0.47%+152 B 🔺
@sentry/vue (incl. Tracing)48.07 kB+0.36%+172 B 🔺
@sentry/svelte27.61 kB--
CDN Bundle30 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 - uncompressed89.35 kB--
CDN Bundle (incl. Tracing) - uncompressed145.75 kB+0.33%+477 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed94.05 kB--
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed149.72 kB+0.32%+477 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed218.59 kB--
CDN Bundle (incl. Tracing, Replay) - uncompressed264.76 kB+0.19%+477 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed268.72 kB+0.18%+477 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed278.46 kB+0.18%+479 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed282.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/server78.24 kB+0.67%+515 B 🔺
@sentry/core/browser64.58 kB+0.83%+531 B 🔺
@sentry/node-core62.65 kB+0.44%+272 B 🔺
@sentry/node121.47 kB+0.21%+252 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection)69.95 kB--
@sentry/node/light50.64 kB+0.36%+178 B 🔺
@sentry/node - without tracing72.98 kB+0.41%+297 B 🔺
@sentry/aws-serverless83.82 kB+0.31%+253 B 🔺
@sentry/cloudflare (withSentry) - minified181.21 kB+0.33%+595 B 🔺
@sentry/cloudflare (withSentry)448.43 kB+0.36%+1.6 kB 🔺

View base workflow run

Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
@andreiborzaandreiborza changed the title feat(core): Add deferred segment-span transaction capture, orphan emission, and provider-span sealingfeat(core): Add deferred segment-span transaction captureJun 29, 2026
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
@andreiborza
andreiborza requested a review from a team as a code ownerJune 29, 2026 15:46
@andreiborza
andreiborza requested review from JPeer264 and mydea and removed request for a teamJune 29, 2026 15:46
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 29ce501 to c741940CompareJune 29, 2026 15:46
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from c741940 to 4b3fc03CompareJune 30, 2026 08:13
Comment threadpackages/opentelemetry/src/tracer.ts
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 4b3fc03 to 14cb421CompareJune 30, 2026 08:48
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 14cb421 to 6b8db6eCompareJune 30, 2026 09:56
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 6b8db6e to 2df53adCompareJune 30, 2026 15:17
Comment threadpackages/core/test/lib/tracing/sentrySpan.test.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
@andreiborza

Copy link
Copy Markdown
MemberAuthor

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
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 51859a2 to f25bf84CompareJuly 1, 2026 11:40
Comment threadpackages/core/src/tracing/deferSegmentSpanCapture.ts Outdated
@andreiborza
andreiborza marked this pull request as ready for review July 1, 2026 13:16
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from c11b10c to e7f6447CompareJuly 1, 2026 14:02
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 6de76e6 to f2439e8CompareJuly 1, 2026 18:41
Comment threadpackages/core/src/tracing/deferSegmentSpanCapture.ts
Comment threadpackages/core/src/tracing/deferSegmentSpanCapture.ts Outdated
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch 3 times, most recently from 220ade4 to d93efbaCompareJuly 1, 2026 23:09

@cursorcursorBot 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 threadpackages/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
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from d93efba to 22abc5cCompareJuly 2, 2026 11:25

@JPeer264JPeer264 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 developJul 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 freeto 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

@andreiborza@JPeer264@nicohrubec
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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

@andreiborzaandreiborza 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 threadpackages/core/src/tracing/sentrySpan.ts Outdated
@github-actions

github-actionsBot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize% ChangeChange
@sentry/browser27.59 kB--
@sentry/browser - with treeshaking flags26.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 flags75.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/react29.38 kB--
@sentry/react (incl. Tracing)48.48 kB+0.28%+132 B 🔺
@sentry/vue32.97 kB+0.47%+152 B 🔺
@sentry/vue (incl. Tracing)48.07 kB+0.36%+172 B 🔺
@sentry/svelte27.61 kB--
CDN Bundle30 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 - uncompressed89.35 kB--
CDN Bundle (incl. Tracing) - uncompressed145.75 kB+0.33%+477 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed94.05 kB--
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed149.72 kB+0.32%+477 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed218.59 kB--
CDN Bundle (incl. Tracing, Replay) - uncompressed264.76 kB+0.19%+477 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed268.72 kB+0.18%+477 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed278.46 kB+0.18%+479 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed282.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/server78.24 kB+0.67%+515 B 🔺
@sentry/core/browser64.58 kB+0.83%+531 B 🔺
@sentry/node-core62.65 kB+0.44%+272 B 🔺
@sentry/node121.47 kB+0.21%+252 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection)69.95 kB--
@sentry/node/light50.64 kB+0.36%+178 B 🔺
@sentry/node - without tracing72.98 kB+0.41%+297 B 🔺
@sentry/aws-serverless83.82 kB+0.31%+253 B 🔺
@sentry/cloudflare (withSentry) - minified181.21 kB+0.33%+595 B 🔺
@sentry/cloudflare (withSentry)448.43 kB+0.36%+1.6 kB 🔺

View base workflow run

Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
@andreiborzaandreiborza changed the title feat(core): Add deferred segment-span transaction capture, orphan emission, and provider-span sealingfeat(core): Add deferred segment-span transaction captureJun 29, 2026
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
@andreiborza
andreiborza requested a review from a team as a code ownerJune 29, 2026 15:46
@andreiborza
andreiborza requested review from JPeer264 and mydea and removed request for a teamJune 29, 2026 15:46
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 29ce501 to c741940CompareJune 29, 2026 15:46
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from c741940 to 4b3fc03CompareJune 30, 2026 08:13
Comment threadpackages/opentelemetry/src/tracer.ts
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 4b3fc03 to 14cb421CompareJune 30, 2026 08:48
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 14cb421 to 6b8db6eCompareJune 30, 2026 09:56
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 6b8db6e to 2df53adCompareJune 30, 2026 15:17
Comment threadpackages/core/test/lib/tracing/sentrySpan.test.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
@andreiborza

Copy link
Copy Markdown
MemberAuthor

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
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 51859a2 to f25bf84CompareJuly 1, 2026 11:40
Comment threadpackages/core/src/tracing/deferSegmentSpanCapture.ts Outdated
@andreiborza
andreiborza marked this pull request as ready for review July 1, 2026 13:16
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from c11b10c to e7f6447CompareJuly 1, 2026 14:02
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 6de76e6 to f2439e8CompareJuly 1, 2026 18:41
Comment threadpackages/core/src/tracing/deferSegmentSpanCapture.ts
Comment threadpackages/core/src/tracing/deferSegmentSpanCapture.ts Outdated
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch 3 times, most recently from 220ade4 to d93efbaCompareJuly 1, 2026 23:09

@cursorcursorBot 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 threadpackages/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
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from d93efba to 22abc5cCompareJuly 2, 2026 11:25

@JPeer264JPeer264 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 developJul 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 freeto 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

@andreiborza@JPeer264@nicohrubec
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@andreiborzaandreiborza 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 threadpackages/core/src/tracing/sentrySpan.ts Outdated
@github-actions

github-actionsBot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize% ChangeChange
@sentry/browser27.59 kB--
@sentry/browser - with treeshaking flags26.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 flags75.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/react29.38 kB--
@sentry/react (incl. Tracing)48.48 kB+0.28%+132 B 🔺
@sentry/vue32.97 kB+0.47%+152 B 🔺
@sentry/vue (incl. Tracing)48.07 kB+0.36%+172 B 🔺
@sentry/svelte27.61 kB--
CDN Bundle30 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 - uncompressed89.35 kB--
CDN Bundle (incl. Tracing) - uncompressed145.75 kB+0.33%+477 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed94.05 kB--
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed149.72 kB+0.32%+477 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed218.59 kB--
CDN Bundle (incl. Tracing, Replay) - uncompressed264.76 kB+0.19%+477 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed268.72 kB+0.18%+477 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed278.46 kB+0.18%+479 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed282.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/server78.24 kB+0.67%+515 B 🔺
@sentry/core/browser64.58 kB+0.83%+531 B 🔺
@sentry/node-core62.65 kB+0.44%+272 B 🔺
@sentry/node121.47 kB+0.21%+252 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection)69.95 kB--
@sentry/node/light50.64 kB+0.36%+178 B 🔺
@sentry/node - without tracing72.98 kB+0.41%+297 B 🔺
@sentry/aws-serverless83.82 kB+0.31%+253 B 🔺
@sentry/cloudflare (withSentry) - minified181.21 kB+0.33%+595 B 🔺
@sentry/cloudflare (withSentry)448.43 kB+0.36%+1.6 kB 🔺

View base workflow run

Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
@andreiborzaandreiborza changed the title feat(core): Add deferred segment-span transaction capture, orphan emission, and provider-span sealingfeat(core): Add deferred segment-span transaction captureJun 29, 2026
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
@andreiborza
andreiborza requested a review from a team as a code ownerJune 29, 2026 15:46
@andreiborza
andreiborza requested review from JPeer264 and mydea and removed request for a teamJune 29, 2026 15:46
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 29ce501 to c741940CompareJune 29, 2026 15:46
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from c741940 to 4b3fc03CompareJune 30, 2026 08:13
Comment threadpackages/opentelemetry/src/tracer.ts
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 4b3fc03 to 14cb421CompareJune 30, 2026 08:48
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 14cb421 to 6b8db6eCompareJune 30, 2026 09:56
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 6b8db6e to 2df53adCompareJune 30, 2026 15:17
Comment threadpackages/core/test/lib/tracing/sentrySpan.test.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
@andreiborza

Copy link
Copy Markdown
MemberAuthor

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
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 51859a2 to f25bf84CompareJuly 1, 2026 11:40
Comment threadpackages/core/src/tracing/deferSegmentSpanCapture.ts Outdated
@andreiborza
andreiborza marked this pull request as ready for review July 1, 2026 13:16
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from c11b10c to e7f6447CompareJuly 1, 2026 14:02
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 6de76e6 to f2439e8CompareJuly 1, 2026 18:41
Comment threadpackages/core/src/tracing/deferSegmentSpanCapture.ts
Comment threadpackages/core/src/tracing/deferSegmentSpanCapture.ts Outdated
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch 3 times, most recently from 220ade4 to d93efbaCompareJuly 1, 2026 23:09

@cursorcursorBot 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 threadpackages/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
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from d93efba to 22abc5cCompareJuly 2, 2026 11:25

@JPeer264JPeer264 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 developJul 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 freeto 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

@andreiborza@JPeer264@nicohrubec
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@andreiborzaandreiborza 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 threadpackages/core/src/tracing/sentrySpan.ts Outdated
@github-actions

github-actionsBot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize% ChangeChange
@sentry/browser27.59 kB--
@sentry/browser - with treeshaking flags26.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 flags75.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/react29.38 kB--
@sentry/react (incl. Tracing)48.48 kB+0.28%+132 B 🔺
@sentry/vue32.97 kB+0.47%+152 B 🔺
@sentry/vue (incl. Tracing)48.07 kB+0.36%+172 B 🔺
@sentry/svelte27.61 kB--
CDN Bundle30 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 - uncompressed89.35 kB--
CDN Bundle (incl. Tracing) - uncompressed145.75 kB+0.33%+477 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed94.05 kB--
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed149.72 kB+0.32%+477 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed218.59 kB--
CDN Bundle (incl. Tracing, Replay) - uncompressed264.76 kB+0.19%+477 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed268.72 kB+0.18%+477 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed278.46 kB+0.18%+479 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed282.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/server78.24 kB+0.67%+515 B 🔺
@sentry/core/browser64.58 kB+0.83%+531 B 🔺
@sentry/node-core62.65 kB+0.44%+272 B 🔺
@sentry/node121.47 kB+0.21%+252 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection)69.95 kB--
@sentry/node/light50.64 kB+0.36%+178 B 🔺
@sentry/node - without tracing72.98 kB+0.41%+297 B 🔺
@sentry/aws-serverless83.82 kB+0.31%+253 B 🔺
@sentry/cloudflare (withSentry) - minified181.21 kB+0.33%+595 B 🔺
@sentry/cloudflare (withSentry)448.43 kB+0.36%+1.6 kB 🔺

View base workflow run

Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
@andreiborzaandreiborza changed the title feat(core): Add deferred segment-span transaction capture, orphan emission, and provider-span sealingfeat(core): Add deferred segment-span transaction captureJun 29, 2026
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
@andreiborza
andreiborza requested a review from a team as a code ownerJune 29, 2026 15:46
@andreiborza
andreiborza requested review from JPeer264 and mydea and removed request for a teamJune 29, 2026 15:46
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 29ce501 to c741940CompareJune 29, 2026 15:46
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from c741940 to 4b3fc03CompareJune 30, 2026 08:13
Comment threadpackages/opentelemetry/src/tracer.ts
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 4b3fc03 to 14cb421CompareJune 30, 2026 08:48
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 14cb421 to 6b8db6eCompareJune 30, 2026 09:56
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 6b8db6e to 2df53adCompareJune 30, 2026 15:17
Comment threadpackages/core/test/lib/tracing/sentrySpan.test.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
@andreiborza

Copy link
Copy Markdown
MemberAuthor

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
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 51859a2 to f25bf84CompareJuly 1, 2026 11:40
Comment threadpackages/core/src/tracing/deferSegmentSpanCapture.ts Outdated
@andreiborza
andreiborza marked this pull request as ready for review July 1, 2026 13:16
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from c11b10c to e7f6447CompareJuly 1, 2026 14:02
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 6de76e6 to f2439e8CompareJuly 1, 2026 18:41
Comment threadpackages/core/src/tracing/deferSegmentSpanCapture.ts
Comment threadpackages/core/src/tracing/deferSegmentSpanCapture.ts Outdated
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch 3 times, most recently from 220ade4 to d93efbaCompareJuly 1, 2026 23:09

@cursorcursorBot 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 threadpackages/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
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from d93efba to 22abc5cCompareJuly 2, 2026 11:25

@JPeer264JPeer264 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 developJul 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 freeto 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

@andreiborza@JPeer264@nicohrubec
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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

@andreiborzaandreiborza 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 threadpackages/core/src/tracing/sentrySpan.ts Outdated
@github-actions

github-actionsBot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize% ChangeChange
@sentry/browser27.59 kB--
@sentry/browser - with treeshaking flags26.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 flags75.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/react29.38 kB--
@sentry/react (incl. Tracing)48.48 kB+0.28%+132 B 🔺
@sentry/vue32.97 kB+0.47%+152 B 🔺
@sentry/vue (incl. Tracing)48.07 kB+0.36%+172 B 🔺
@sentry/svelte27.61 kB--
CDN Bundle30 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 - uncompressed89.35 kB--
CDN Bundle (incl. Tracing) - uncompressed145.75 kB+0.33%+477 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed94.05 kB--
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed149.72 kB+0.32%+477 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed218.59 kB--
CDN Bundle (incl. Tracing, Replay) - uncompressed264.76 kB+0.19%+477 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed268.72 kB+0.18%+477 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed278.46 kB+0.18%+479 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed282.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/server78.24 kB+0.67%+515 B 🔺
@sentry/core/browser64.58 kB+0.83%+531 B 🔺
@sentry/node-core62.65 kB+0.44%+272 B 🔺
@sentry/node121.47 kB+0.21%+252 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection)69.95 kB--
@sentry/node/light50.64 kB+0.36%+178 B 🔺
@sentry/node - without tracing72.98 kB+0.41%+297 B 🔺
@sentry/aws-serverless83.82 kB+0.31%+253 B 🔺
@sentry/cloudflare (withSentry) - minified181.21 kB+0.33%+595 B 🔺
@sentry/cloudflare (withSentry)448.43 kB+0.36%+1.6 kB 🔺

View base workflow run

Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
@andreiborzaandreiborza changed the title feat(core): Add deferred segment-span transaction capture, orphan emission, and provider-span sealingfeat(core): Add deferred segment-span transaction captureJun 29, 2026
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
@andreiborza
andreiborza requested a review from a team as a code ownerJune 29, 2026 15:46
@andreiborza
andreiborza requested review from JPeer264 and mydea and removed request for a teamJune 29, 2026 15:46
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 29ce501 to c741940CompareJune 29, 2026 15:46
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from c741940 to 4b3fc03CompareJune 30, 2026 08:13
Comment threadpackages/opentelemetry/src/tracer.ts
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 4b3fc03 to 14cb421CompareJune 30, 2026 08:48
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 14cb421 to 6b8db6eCompareJune 30, 2026 09:56
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 6b8db6e to 2df53adCompareJune 30, 2026 15:17
Comment threadpackages/core/test/lib/tracing/sentrySpan.test.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
@andreiborza

Copy link
Copy Markdown
MemberAuthor

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
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 51859a2 to f25bf84CompareJuly 1, 2026 11:40
Comment threadpackages/core/src/tracing/deferSegmentSpanCapture.ts Outdated
@andreiborza
andreiborza marked this pull request as ready for review July 1, 2026 13:16
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from c11b10c to e7f6447CompareJuly 1, 2026 14:02
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 6de76e6 to f2439e8CompareJuly 1, 2026 18:41
Comment threadpackages/core/src/tracing/deferSegmentSpanCapture.ts
Comment threadpackages/core/src/tracing/deferSegmentSpanCapture.ts Outdated
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch 3 times, most recently from 220ade4 to d93efbaCompareJuly 1, 2026 23:09

@cursorcursorBot 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 threadpackages/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
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from d93efba to 22abc5cCompareJuly 2, 2026 11:25

@JPeer264JPeer264 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 developJul 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 freeto 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

@andreiborza@JPeer264@nicohrubec
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@andreiborzaandreiborza 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 threadpackages/core/src/tracing/sentrySpan.ts Outdated
@github-actions

github-actionsBot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize% ChangeChange
@sentry/browser27.59 kB--
@sentry/browser - with treeshaking flags26.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 flags75.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/react29.38 kB--
@sentry/react (incl. Tracing)48.48 kB+0.28%+132 B 🔺
@sentry/vue32.97 kB+0.47%+152 B 🔺
@sentry/vue (incl. Tracing)48.07 kB+0.36%+172 B 🔺
@sentry/svelte27.61 kB--
CDN Bundle30 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 - uncompressed89.35 kB--
CDN Bundle (incl. Tracing) - uncompressed145.75 kB+0.33%+477 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed94.05 kB--
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed149.72 kB+0.32%+477 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed218.59 kB--
CDN Bundle (incl. Tracing, Replay) - uncompressed264.76 kB+0.19%+477 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed268.72 kB+0.18%+477 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed278.46 kB+0.18%+479 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed282.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/server78.24 kB+0.67%+515 B 🔺
@sentry/core/browser64.58 kB+0.83%+531 B 🔺
@sentry/node-core62.65 kB+0.44%+272 B 🔺
@sentry/node121.47 kB+0.21%+252 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection)69.95 kB--
@sentry/node/light50.64 kB+0.36%+178 B 🔺
@sentry/node - without tracing72.98 kB+0.41%+297 B 🔺
@sentry/aws-serverless83.82 kB+0.31%+253 B 🔺
@sentry/cloudflare (withSentry) - minified181.21 kB+0.33%+595 B 🔺
@sentry/cloudflare (withSentry)448.43 kB+0.36%+1.6 kB 🔺

View base workflow run

Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
@andreiborzaandreiborza changed the title feat(core): Add deferred segment-span transaction capture, orphan emission, and provider-span sealingfeat(core): Add deferred segment-span transaction captureJun 29, 2026
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
@andreiborza
andreiborza requested a review from a team as a code ownerJune 29, 2026 15:46
@andreiborza
andreiborza requested review from JPeer264 and mydea and removed request for a teamJune 29, 2026 15:46
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 29ce501 to c741940CompareJune 29, 2026 15:46
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from c741940 to 4b3fc03CompareJune 30, 2026 08:13
Comment threadpackages/opentelemetry/src/tracer.ts
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 4b3fc03 to 14cb421CompareJune 30, 2026 08:48
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 14cb421 to 6b8db6eCompareJune 30, 2026 09:56
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 6b8db6e to 2df53adCompareJune 30, 2026 15:17
Comment threadpackages/core/test/lib/tracing/sentrySpan.test.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
@andreiborza

Copy link
Copy Markdown
MemberAuthor

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
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 51859a2 to f25bf84CompareJuly 1, 2026 11:40
Comment threadpackages/core/src/tracing/deferSegmentSpanCapture.ts Outdated
@andreiborza
andreiborza marked this pull request as ready for review July 1, 2026 13:16
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from c11b10c to e7f6447CompareJuly 1, 2026 14:02
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 6de76e6 to f2439e8CompareJuly 1, 2026 18:41
Comment threadpackages/core/src/tracing/deferSegmentSpanCapture.ts
Comment threadpackages/core/src/tracing/deferSegmentSpanCapture.ts Outdated
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch 3 times, most recently from 220ade4 to d93efbaCompareJuly 1, 2026 23:09

@cursorcursorBot 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 threadpackages/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
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from d93efba to 22abc5cCompareJuly 2, 2026 11:25

@JPeer264JPeer264 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 developJul 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 freeto 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

@andreiborza@JPeer264@nicohrubec
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@andreiborzaandreiborza 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 threadpackages/core/src/tracing/sentrySpan.ts Outdated
@github-actions

github-actionsBot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize% ChangeChange
@sentry/browser27.59 kB--
@sentry/browser - with treeshaking flags26.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 flags75.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/react29.38 kB--
@sentry/react (incl. Tracing)48.48 kB+0.28%+132 B 🔺
@sentry/vue32.97 kB+0.47%+152 B 🔺
@sentry/vue (incl. Tracing)48.07 kB+0.36%+172 B 🔺
@sentry/svelte27.61 kB--
CDN Bundle30 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 - uncompressed89.35 kB--
CDN Bundle (incl. Tracing) - uncompressed145.75 kB+0.33%+477 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed94.05 kB--
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed149.72 kB+0.32%+477 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed218.59 kB--
CDN Bundle (incl. Tracing, Replay) - uncompressed264.76 kB+0.19%+477 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed268.72 kB+0.18%+477 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed278.46 kB+0.18%+479 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed282.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/server78.24 kB+0.67%+515 B 🔺
@sentry/core/browser64.58 kB+0.83%+531 B 🔺
@sentry/node-core62.65 kB+0.44%+272 B 🔺
@sentry/node121.47 kB+0.21%+252 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection)69.95 kB--
@sentry/node/light50.64 kB+0.36%+178 B 🔺
@sentry/node - without tracing72.98 kB+0.41%+297 B 🔺
@sentry/aws-serverless83.82 kB+0.31%+253 B 🔺
@sentry/cloudflare (withSentry) - minified181.21 kB+0.33%+595 B 🔺
@sentry/cloudflare (withSentry)448.43 kB+0.36%+1.6 kB 🔺

View base workflow run

Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
@andreiborzaandreiborza changed the title feat(core): Add deferred segment-span transaction capture, orphan emission, and provider-span sealingfeat(core): Add deferred segment-span transaction captureJun 29, 2026
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
@andreiborza
andreiborza requested a review from a team as a code ownerJune 29, 2026 15:46
@andreiborza
andreiborza requested review from JPeer264 and mydea and removed request for a teamJune 29, 2026 15:46
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 29ce501 to c741940CompareJune 29, 2026 15:46
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from c741940 to 4b3fc03CompareJune 30, 2026 08:13
Comment threadpackages/opentelemetry/src/tracer.ts
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 4b3fc03 to 14cb421CompareJune 30, 2026 08:48
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 14cb421 to 6b8db6eCompareJune 30, 2026 09:56
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 6b8db6e to 2df53adCompareJune 30, 2026 15:17
Comment threadpackages/core/test/lib/tracing/sentrySpan.test.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
@andreiborza

Copy link
Copy Markdown
MemberAuthor

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
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 51859a2 to f25bf84CompareJuly 1, 2026 11:40
Comment threadpackages/core/src/tracing/deferSegmentSpanCapture.ts Outdated
@andreiborza
andreiborza marked this pull request as ready for review July 1, 2026 13:16
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from c11b10c to e7f6447CompareJuly 1, 2026 14:02
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 6de76e6 to f2439e8CompareJuly 1, 2026 18:41
Comment threadpackages/core/src/tracing/deferSegmentSpanCapture.ts
Comment threadpackages/core/src/tracing/deferSegmentSpanCapture.ts Outdated
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch 3 times, most recently from 220ade4 to d93efbaCompareJuly 1, 2026 23:09

@cursorcursorBot 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 threadpackages/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
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from d93efba to 22abc5cCompareJuly 2, 2026 11:25

@JPeer264JPeer264 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 developJul 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 freeto 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

@andreiborza@JPeer264@nicohrubec
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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

@andreiborzaandreiborza 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 threadpackages/core/src/tracing/sentrySpan.ts Outdated
@github-actions

github-actionsBot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize% ChangeChange
@sentry/browser27.59 kB--
@sentry/browser - with treeshaking flags26.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 flags75.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/react29.38 kB--
@sentry/react (incl. Tracing)48.48 kB+0.28%+132 B 🔺
@sentry/vue32.97 kB+0.47%+152 B 🔺
@sentry/vue (incl. Tracing)48.07 kB+0.36%+172 B 🔺
@sentry/svelte27.61 kB--
CDN Bundle30 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 - uncompressed89.35 kB--
CDN Bundle (incl. Tracing) - uncompressed145.75 kB+0.33%+477 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed94.05 kB--
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed149.72 kB+0.32%+477 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed218.59 kB--
CDN Bundle (incl. Tracing, Replay) - uncompressed264.76 kB+0.19%+477 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed268.72 kB+0.18%+477 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed278.46 kB+0.18%+479 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed282.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/server78.24 kB+0.67%+515 B 🔺
@sentry/core/browser64.58 kB+0.83%+531 B 🔺
@sentry/node-core62.65 kB+0.44%+272 B 🔺
@sentry/node121.47 kB+0.21%+252 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection)69.95 kB--
@sentry/node/light50.64 kB+0.36%+178 B 🔺
@sentry/node - without tracing72.98 kB+0.41%+297 B 🔺
@sentry/aws-serverless83.82 kB+0.31%+253 B 🔺
@sentry/cloudflare (withSentry) - minified181.21 kB+0.33%+595 B 🔺
@sentry/cloudflare (withSentry)448.43 kB+0.36%+1.6 kB 🔺

View base workflow run

Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
@andreiborzaandreiborza changed the title feat(core): Add deferred segment-span transaction capture, orphan emission, and provider-span sealingfeat(core): Add deferred segment-span transaction captureJun 29, 2026
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
@andreiborza
andreiborza requested a review from a team as a code ownerJune 29, 2026 15:46
@andreiborza
andreiborza requested review from JPeer264 and mydea and removed request for a teamJune 29, 2026 15:46
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 29ce501 to c741940CompareJune 29, 2026 15:46
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from c741940 to 4b3fc03CompareJune 30, 2026 08:13
Comment threadpackages/opentelemetry/src/tracer.ts
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 4b3fc03 to 14cb421CompareJune 30, 2026 08:48
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 14cb421 to 6b8db6eCompareJune 30, 2026 09:56
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 6b8db6e to 2df53adCompareJune 30, 2026 15:17
Comment threadpackages/core/test/lib/tracing/sentrySpan.test.ts Outdated
Comment threadpackages/core/src/tracing/sentrySpan.ts Outdated
@andreiborza

Copy link
Copy Markdown
MemberAuthor

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
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 51859a2 to f25bf84CompareJuly 1, 2026 11:40
Comment threadpackages/core/src/tracing/deferSegmentSpanCapture.ts Outdated
@andreiborza
andreiborza marked this pull request as ready for review July 1, 2026 13:16
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from c11b10c to e7f6447CompareJuly 1, 2026 14:02
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from 6de76e6 to f2439e8CompareJuly 1, 2026 18:41
Comment threadpackages/core/src/tracing/deferSegmentSpanCapture.ts
Comment threadpackages/core/src/tracing/deferSegmentSpanCapture.ts Outdated
@andreiborza
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch 3 times, most recently from 220ade4 to d93efbaCompareJuly 1, 2026 23:09

@cursorcursorBot 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 threadpackages/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
andreiborzaforce-pushed the ab/sentry-trace-provider-core-capture branch from d93efba to 22abc5cCompareJuly 2, 2026 11:25

@JPeer264JPeer264 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 developJul 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 freeto 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

@andreiborza@JPeer264@nicohrubec