Skip to content

fix(core): Don't emit spanStart when child span is SentryNonRecordingSpan#21349

Closed
JPeer264 wants to merge 3 commits into
developfrom
jp/span-start-call
Closed

fix(core): Don't emit spanStart when child span is SentryNonRecordingSpan#21349
JPeer264 wants to merge 3 commits into
developfrom
jp/span-start-call

Conversation

@JPeer264
Copy link
Copy Markdown
Member

@JPeer264 JPeer264 commented Jun 5, 2026

follow up to #21197

Apparently when setting tracesSampleRate: 0 (or any value between 0 and 1), there are SentryNonRecordingSpans. When they are getting generrated spanStart is being emitted and the Cloudflare client was listening to it and added these spans to _pendingSpans. However, spanEnd was never being called and that is why _pendingSpans where never getting less.

With the fix of #21197 the flushLock was now finally waiting for these spans to be sent, but they were never being sent and would cause another leak (only under very high load).

Open Question

I wonder if it was on purpose that spanStart was actually being emitted with SentryNonRecoringSpans, or if there is some edge case I can't think of.

@JPeer264 JPeer264 requested a review from a team June 5, 2026 12:45
@JPeer264 JPeer264 self-assigned this Jun 5, 2026
@JPeer264 JPeer264 requested a review from a team as a code owner June 5, 2026 12:45
@JPeer264 JPeer264 requested review from andreiborza, logaretm and mydea and removed request for a team June 5, 2026 12:45
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 5, 2026

size-limit report 📦

Path Size % Change Change
@sentry/browser 27.21 kB - -
@sentry/browser - with treeshaking flags 25.65 kB - -
@sentry/browser (incl. Tracing) 45.49 kB -0.03% -13 B 🔽
@sentry/browser (incl. Tracing + Span Streaming) 47.72 kB -0.03% -13 B 🔽
@sentry/browser (incl. Tracing, Profiling) 50.27 kB -0.03% -14 B 🔽
@sentry/browser (incl. Tracing, Replay) 84.7 kB -0.02% -15 B 🔽
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 74.33 kB -0.02% -14 B 🔽
@sentry/browser (incl. Tracing, Replay with Canvas) 89.39 kB -0.02% -15 B 🔽
@sentry/browser (incl. Tracing, Replay, Feedback) 102.09 kB -0.02% -16 B 🔽
@sentry/browser (incl. Feedback) 44.37 kB - -
@sentry/browser (incl. sendFeedback) 32.03 kB - -
@sentry/browser (incl. FeedbackAsync) 37.12 kB - -
@sentry/browser (incl. Metrics) 28.28 kB - -
@sentry/browser (incl. Logs) 28.53 kB - -
@sentry/browser (incl. Metrics & Logs) 29.22 kB - -
@sentry/react 29.02 kB - -
@sentry/react (incl. Tracing) 47.76 kB -0.03% -12 B 🔽
@sentry/vue 32.21 kB -0.04% -11 B 🔽
@sentry/vue (incl. Tracing) 47.39 kB -0.03% -11 B 🔽
@sentry/svelte 27.23 kB - -
CDN Bundle 29.58 kB - -
CDN Bundle (incl. Tracing) 47.94 kB -0.03% -12 B 🔽
CDN Bundle (incl. Logs, Metrics) 31.09 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 49.19 kB -0.03% -13 B 🔽
CDN Bundle (incl. Replay, Logs, Metrics) 70.37 kB - -
CDN Bundle (incl. Tracing, Replay) 85.32 kB -0.02% -15 B 🔽
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 86.5 kB -0.02% -12 B 🔽
CDN Bundle (incl. Tracing, Replay, Feedback) 91.14 kB -0.02% -15 B 🔽
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 92.33 kB -0.02% -13 B 🔽
CDN Bundle - uncompressed 87.78 kB - -
CDN Bundle (incl. Tracing) - uncompressed 144.94 kB -0.04% -46 B 🔽
CDN Bundle (incl. Logs, Metrics) - uncompressed 92.27 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 148.69 kB -0.04% -46 B 🔽
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 217.08 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 263.8 kB -0.02% -46 B 🔽
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 267.55 kB -0.02% -46 B 🔽
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 277.5 kB -0.02% -46 B 🔽
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 281.24 kB -0.02% -46 B 🔽
@sentry/nextjs (client) 50.25 kB -0.03% -11 B 🔽
@sentry/sveltekit (client) 45.91 kB -0.03% -11 B 🔽
@sentry/core/server 75.94 kB -0.02% -13 B 🔽
@sentry/core/browser 63.08 kB -0.03% -13 B 🔽
@sentry/node-core 61.71 kB -0.02% -12 B 🔽
@sentry/node 130.41 kB -0.01% -11 B 🔽
@sentry/node - without tracing 74.09 kB -0.02% -11 B 🔽
@sentry/aws-serverless 86.28 kB -0.02% -10 B 🔽
@sentry/cloudflare (withSentry) - minified 173.64 kB -0.03% -46 B 🔽
@sentry/cloudflare (withSentry) 433.71 kB -0.02% -76 B 🔽

View base workflow run

@JPeer264 JPeer264 requested a review from Lms24 June 5, 2026 13:14
Copy link
Copy Markdown
Member

@logaretm logaretm left a comment

Choose a reason for hiding this comment

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

Not sure if we should do this across all core, could we do a narrower fix scoped to the cloudflare's client? Like skip adding to the set if the span is non recording.

WDYT?

Comment thread packages/core/src/tracing/trace.ts Outdated
Comment on lines 557 to 564
if (childSpan.isRecording()) {
client.emit('spanStart', childSpan);
// If it has an endTimestamp, it's already ended
if (spanArguments.endTimestamp) {
client.emit('spanEnd', childSpan);
client.emit('afterSpanEnd', childSpan);
}
}
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.

I think spanEnd still fires from the constructor? So it means you get span ends for spans that never triggered a start?

Copy link
Copy Markdown
Member

@Lms24 Lms24 Jun 8, 2026

Choose a reason for hiding this comment

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

I checked this. The funny thing is, the constructor endTimestamp check for SentrySpan is basically dead code (I think). The user-facing start*Span* APIs don't allow you to pass an endTimestamp. We then convert the user-facing StartSpanOptions into SpanArguments in parseSentrySpanArguments. But in there, we don't set an endTimestamp either. So there's no typed user-facing way how a started span can be ended immediately.

There is one exception though: users still pass in endTimeStamp despite this not being typed out. It still works because we spread the options into the SpanArguments. I'd argue this is undefined behaviour and the public contract is that users always have to explicitly call .end().

I think we should clean this up in the long-term but it's fine for this PR.

@JPeer264
Copy link
Copy Markdown
Member Author

JPeer264 commented Jun 7, 2026

I first had it only scoped to Cloudflare, but moved it to core.

It is kinda odd that spanStart is getting emitted, but spanEnd is not. It feels wrong IMO.

Copy link
Copy Markdown
Member

@Lms24 Lms24 left a comment

Choose a reason for hiding this comment

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

It is kinda odd that spanStart is getting emitted, but spanEnd is not. It feels wrong IMO.

Agreed, this sounds like a bug we should fix. Quickly talked about this offline with Sigrid and Charly and we'd slightly tend towards not emitting hooks for non-recording spans. We just need to check that this doesn't break any "side effect" behaviour our callbacks depend on.

Comment thread packages/core/src/tracing/trace.ts Outdated
@Lms24 Lms24 changed the title fix(core,cloudflare): Do not call spanStart when span is not recording fix(core): Do not call spanStart when child span is not recording Jun 8, 2026
@Lms24
Copy link
Copy Markdown
Member

Lms24 commented Jun 8, 2026

Ok, so I ended up making two changes to this PR to fix an issue we overlooked:

  • we can't reliably use span.isRecording() to guard the call because it also returns false on already ended spans. Which is something we apparently care about but we shouldn't need to (see fix(core): Don't emit spanStart when child span is SentryNonRecordingSpan #21349 (comment)).
  • I removed the spanEnd emissions from startChildSpan because this indeed handled in the constructor as pointed out by @logaretm. Again, this is rather "undefined behaviour" but for now, we can at least get rid of some bytes.

We currently also live with a bit of an inconsistency:

  • negatively sampled root spans still emit -Start/-End events because they're always a SentrySpan
  • OTel-emitted spans also always emit -Start/-End events regardless of sampling state (or if the span is recording/nonRecording)

For the future, I have a couple of improvements in mind:

  • I don't really like that we emit spanStart and spanEnd events in multiple places. Specifically, I think we should move them all into the SentrySpan class because we kinda have to emit the spanEnd event in there anyway. We likely want to save the client reference while in the constructor and reuse that on end. For OTel, we already use only the SentrySpanProcessor which is fine.
  • We should remove the endTimestamp parameter from SentrySpanArguments because to me this seems like dead code.
  • I think with this PR we establish start/end consistency but we likely still need to rethink if we want to emit these hooks at all or not. It's probably easier to always emit these events.

@Lms24 Lms24 force-pushed the jp/span-start-call branch from 06318ab to 8c867f6 Compare June 8, 2026 13:22
@Lms24 Lms24 changed the title fix(core): Do not call spanStart when child span is not recording fix(core): Don't emit spanStart when child span is SentryNonRecordingSpan Jun 8, 2026
Copy link
Copy Markdown

@cursor cursor Bot left a comment

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 3 potential issues.

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 8c867f6. Configure here.

Comment thread packages/core/src/tracing/trace.ts
Comment thread packages/core/test/lib/tracing/idleSpan.test.ts Outdated
Comment thread packages/cloudflare/test/client.test.ts
Comment thread packages/core/src/tracing/trace.ts
Comment thread packages/core/src/tracing/trace.ts
Copy link
Copy Markdown
Member

@Lms24 Lms24 left a comment

Choose a reason for hiding this comment

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

Gonna shamelessly semi self-approve :D

Copy link
Copy Markdown
Member

logaretm commented Jun 8, 2026

Those changes make sense but I think changing lifecycle hooks behavior, particularly in this case (skipping them entirely, but also "sometimes") could be a breaking behavior.

So I suggest we should narrow the fix to cloudflare only for now. Later we can review how the hooks should emit for the different kinds of spans we have and ship that for v11, which will also give us a chance to document, and write tests for these cases.

I don't mind shipping this either, but I feel the changes here are a bit of a scope creep on the intended fix for the issue.

@Lms24
Copy link
Copy Markdown
Member

Lms24 commented Jun 8, 2026

Yeah, it's a good point. Especially given the inconsistencies left after this PR, it probably makes sense to just not ship this and fix it for cloudflare. Then we follow up with a proper fix. Good call!

Fwiw, we shouldn't consider client hooks (behaviour) public API. We don't document them for users. That being said, we have hybrid SDKs (react native) that might subscribe, so we shouldn't break them xD.

Lms24 added a commit that referenced this pull request Jun 8, 2026
supersedes #21349 for
the time being. This does not fix the case where a `spanStart` event is
emitted not a `spanEnd` event isn't. It only locally fixes an occurance
of the symptom. We're gonna merge this first to resolve the cloudflare
SDK bug but we'll need a more reliable fix than the one I hacked together in #21349.

See
#21349 (comment)
@Lms24
Copy link
Copy Markdown
Member

Lms24 commented Jun 8, 2026

Gonna close this in favour of #21367. I'll follow up with a proper fix once I've had more time to think about the various combinations of Sentry(NonRecording)?Span and OTel spans. Tracking in #21368

@Lms24 Lms24 closed this Jun 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants