feat(core)!: Make beforeSendSpan compatible with streamed spans by default - #22643
Conversation
size-limit report 📦
|
a32a10f to
bec33b0
Compare
bec33b0 to
e303625
Compare
a6f845e to
344055e
Compare
beforeSendSpan compatible with streamed spans by default
9f59f8c to
9176c4b
Compare
isaacs
left a comment
There was a problem hiding this comment.
I'm not super familiar with this part of the machinery, so leaving it as a comment. If I'm misreading things or someone else disagrees, I'm happy to be overruled :)
Since span streaming is the direction we're moving, and the goal is to make it the default, moving these checks out of the integration and into the client feels like the right call.
…` and log warnings if used with streaming (#22908) This PR: 1. Deprecates `beforeSendTransaction` and `ignoreTransactions` 2. Logs a console warning (intentionally not gated with `debug: true`) to warn users if they use any of the two options with span streaming enabled I intentionally didn't update the migration guide. this is handled in #22643. more details in #22856 closes #22856 closes #20279
0ec024e to
4910062
Compare
c3ecf63 to
0749d71
Compare
9ef7183 to
f6cfc05
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f6cfc05. Configure here.
9dcfb44 to
6fb09a1
Compare
Make streamed span JSON the default beforeSendSpan contract and provide withStaticSpan for callbacks that still process transaction span JSON. Deprecate withStreamedSpan for removal in version 12. BREAKING CHANGE: beforeSendSpan receives StreamedSpanJSON by default. Fixes #22349 Co-Authored-By: Cursor <cursoragent@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
`withStreamedSpan` no longer marks its callback. Nothing reads `_streamed` since `isStreamedBeforeSendSpanCallback` became "not wrapped with `withStaticSpan`", so the wrapper returns the callback unchanged instead of mutating a function it was handed. Resolve `traceLifecycle` once in the `Client` constructor, where any value other than `'static'` normalizes to the `'stream'` default. Neither span streaming integration writes back to the option anymore, which removes an ordering hazard: integrations that read `hasSpanStreamingEnabled` during their own `setup` could observe the value before or after the mutation depending on integration order. The registration gates now test `!== 'static'` so they agree with the normalized value; previously an unknown value failed the gate but still resolved to `'stream'`, leaving nothing to flush spans. Move the `beforeSendSpan` format check into the constructor as well. A callback is only invoked for the span format matching the trace lifecycle, so a mismatch means it is never called. The client can warn where the integration could not: with `traceLifecycle: 'static'` the streaming integration is never registered, so an unwrapped callback previously went unreported. A `withStaticSpan` callback under `traceLifecycle: 'stream'` no longer downgrades the lifecycle to `'static'`. Opting out of streaming requires setting `traceLifecycle: 'static'` explicitly. Refs #22349 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
9810912 to
4d63933
Compare
| const processedSpan = | ||
| beforeSendSpan && isStreamedBeforeSendSpanCallback(beforeSendSpan) | ||
| // check for traceLifecycle here because in static lifecycle, | ||
| // captureSpan is called for INP spans. If an unmigrated beforeSendSpan | ||
| // callback is run on these spans, it will throw an error. |
There was a problem hiding this comment.
Bug: In production builds, old beforeSendSpan callbacks expecting v1 SpanJSON format are silently invoked with the new v2 StreamedSpanJSON format, leading to incorrect span processing.
Severity: HIGH
Suggested Fix
Implement a runtime check within captureSpan to prevent v1-style beforeSendSpan callbacks from being invoked with v2 StreamedSpanJSON data in production. Either skip these incompatible callbacks or throw an error to make the breaking change explicit, rather than allowing silent data corruption. The current DEBUG_BUILD warnings are insufficient as they don't run in production.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location: packages/core/src/tracing/spans/captureSpan.ts#L79-L82
Potential issue: With the new default `traceLifecycle: 'stream'`, the `captureSpan`
function now invokes `beforeSendSpan` callbacks that are not wrapped with
`withStaticSpan`. If a user has an older callback designed for the v1 `SpanJSON` format
(expecting fields like `description`), it will now be called with the v2
`StreamedSpanJSON` format (with fields like `name` and `end_timestamp`). This mismatch
will cause the callback to silently process incorrect data, leading to failed or
improper span modification. While development builds have warnings, these are stripped
from production builds, making this a silent failure for users who update.
There was a problem hiding this comment.
this is somewhat wrong because we already warn about this and users' callbacks will "break" if they type-check. Another thing we'll fix in a follow-up is to try/catch beforeSendSpan callback invocations, so that the SDK doesn't crash users.

This PR changes
beforeSendSpanto receiveStreamedSpanJSONby default, matching the new trace lifecycle default.Callbacks that intentionally process legacy transaction span JSON need to add the
withStaticSpanwrapper that marks the callback as "static"-compatible and hands users theSpanJSONtype they used in the callback beforehand.The
withStreamedSpanwrapper remains available as a deprecated compatibility helper and is scheduled for removal in version 12.More changes:
beforeSendSpancallbacks no longer lead to switching thetraceLifecycle. Since it now has a default and users need to actively opt out of span streaming, I think it's fair to treat incompatible callbacks as "invalid" and hence skip over them.beforeSendSpancompatibility checks were moved from the integrations into the core client which ensures that they always run now, even if users selected thestaticlife cycle and hencespanStreamingIntegrationdoesn't get added.SpanJSON) spans in favour of always sending them as v2 spans, we now convert aStreamedSpanJsontoSpanJsonincaptureSpan, hand it to the static callback and then convert it back. Not great but I think we need to let users still scrub INP spans.Closes #22349