Skip to content

fix(core): Bound child span tracking on long-lived spans - #22916

Open
andreiborza wants to merge 8 commits into
developfrom
ab/fix-child-span-retention
Open

fix(core): Bound child span tracking on long-lived spans#22916
andreiborza wants to merge 8 commits into
developfrom
ab/fix-child-span-retention

Conversation

@andreiborza

@andreiborza andreiborza commented Jul 31, 2026

Copy link
Copy Markdown
Member

What

Bounds the child spans a span keeps a reference to. A child is no longer tracked when it could never show up in what that span sends:

  • the parent is unsampled, so its tree is never read
  • the parent is a segment span that is sampled but no longer recording, so what it sends has already been assembled

Late children still resolve their root span, so they keep being sent on their own. Both conditions are read off the parent, so this holds for every capture path, including span streaming and deferred capture.

Why

A span that outlives its children retains every one of them. In NestJS the Create Nest App span stays the active parent of anything a setInterval from a provider constructor starts, so those spans are never released and the process leaks around 80 MB/h.

Reproduction: https://github.com/andreiborza/sentry-nestjs-span-retention-repro (60 s at 20 rps, 12000 spans, post-GC heap snapshots). Children retained on the boot span and heap node growth over the run, both measured on this branch with and without the fix:

retained node growth
before, span streaming (the default) 12000 +126.4%
before, transactions 12000 +126.6%
after, span streaming 0 -2.4%
after, transactions 0 -1.8%

Neither trace lifecycle avoids it, and published @sentry/nestjs@10.69.0 leaks the same way on both, so this is not new in v11.

Closes: #22860

A span keeps a strong reference to every child started under it, so a span that
outlives its children retains all of them. In NestJS the `Create Nest App` span
stays the active parent of anything a `setInterval` from a provider constructor
starts, which retains every span for the lifetime of the process.

Children are no longer tracked on an unsampled span, on a segment span whose
tree has already been serialized, or past the 1000 spans a transaction can
carry. Every child still records its root span, so late children are re-emitted
as their own transaction as before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment thread packages/core/src/tracing/sentrySpan.ts Outdated
@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 29.91 kB - -
@sentry/browser - with treeshaking flags 28.11 kB - -
@sentry/browser (incl. Tracing) 47.37 kB +0.03% +13 B 🔺
@sentry/browser (incl. Tracing + Span Streaming) 47.37 kB +0.03% +13 B 🔺
@sentry/browser (incl. Tracing, Profiling) 52.14 kB +0.03% +13 B 🔺
@sentry/browser (incl. Tracing, Replay) 86.71 kB +0.02% +15 B 🔺
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 76.14 kB +0.03% +16 B 🔺
@sentry/browser (incl. Tracing, Replay with Canvas) 91.42 kB +0.02% +15 B 🔺
@sentry/browser (incl. Tracing, Replay, Feedback) 104.06 kB +0.02% +14 B 🔺
@sentry/browser (incl. Feedback) 47.22 kB - -
@sentry/browser (incl. sendFeedback) 34.76 kB - -
@sentry/browser (incl. FeedbackAsync) 39.86 kB - -
@sentry/browser (incl. Metrics) 30.99 kB - -
@sentry/browser (incl. Logs) 31.22 kB - -
@sentry/browser (incl. Metrics & Logs) 31.89 kB - -
@sentry/react 31.73 kB - -
@sentry/react (incl. Tracing) 49.62 kB +0.03% +12 B 🔺
@sentry/vue 34.99 kB +0.05% +15 B 🔺
@sentry/vue (incl. Tracing) 49.35 kB +0.03% +12 B 🔺
@sentry/svelte 29.94 kB - -
CDN Bundle 32 kB - -
CDN Bundle (incl. Tracing) 47.73 kB +0.03% +14 B 🔺
CDN Bundle (incl. Logs, Metrics) 33.55 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 49.09 kB +0.03% +12 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) 72.89 kB - -
CDN Bundle (incl. Tracing, Replay) 85.36 kB +0.02% +12 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 86.66 kB +0.02% +11 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) 91.15 kB +0.02% +11 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 92.46 kB +0.02% +13 B 🔺
CDN Bundle - uncompressed 95.5 kB - -
CDN Bundle (incl. Tracing) - uncompressed 143.12 kB +0.03% +35 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed 100.21 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 147.09 kB +0.03% +35 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 224.97 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 262.37 kB +0.02% +35 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 266.34 kB +0.02% +35 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 276.08 kB +0.02% +35 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 280.03 kB +0.02% +35 B 🔺
@sentry/nextjs (client) 52.22 kB +0.03% +12 B 🔺
@sentry/sveltekit (client) 47.81 kB +0.03% +13 B 🔺
@sentry/core/server 80.02 kB +0.02% +12 B 🔺
@sentry/core/browser 51.97 kB +0.03% +12 B 🔺
@sentry/node 120.9 kB +0.02% +14 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 0 B added added
@sentry/node - without tracing 84.28 kB +0.03% +17 B 🔺
@sentry/aws-serverless 92.94 kB +0.02% +14 B 🔺
@sentry/cloudflare (withSentry) - minified 219.38 kB +0.02% +35 B 🔺
@sentry/cloudflare (withSentry) 540.55 kB +0.03% +112 B 🔺

View base workflow run

Comment thread packages/core/src/utils/spanUtils.ts Outdated
@andreiborza
andreiborza marked this pull request as ready for review July 31, 2026 15:19
A segment span that is sampled but no longer recording has already had its
tree read, so `addChildSpanToSpan` can decide on its own that a child
arriving now will never be sent. That drops the `_sentryChildSpansSealed`
field, the seal helper and its call sites, and the hard cap: there is no
capture path left that can forget to seal.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Fix All in Cursor

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

Reviewed by Cursor Bugbot for commit 4fb00f7. Configure here.

// context (e.g. a framework boot span captured by a queue consumer) is the rest of the process.
if (rootSpan === span && !span.isRecording()) {
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Late children dropped under deferred capture

Medium Severity

The new segment-span guard treats !isRecording() as “tree already assembled,” but under deferred capture the snapshot runs later. A child that starts on that ended segment during the debounce window is not tracked, and onChildSpanEnded will not orphan it until the root is in CAPTURED_SPANS, so a child that also ends in that window is never sent. This hits Node’s static/transactions path, which always registers deferred capture.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4fb00f7. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant