@@ -20,17 +20,32 @@ import type { Span, SpanV2JSON } from '../types-hoist/span';
2020import { mergeScopeData } from '../utils/applyScopeDataToEvent' ;
2121import { debug } from '../utils/debug-logger' ;
2222import { INTERNAL_getSegmentSpan , spanToV2JSON } from '../utils/spanUtils' ;
23- import { safeSetSpanAttributes } from './spanFirstUtils' ;
23+ import { safeSetSpanJSONAttributes } from './spanFirstUtils' ;
2424
2525/**
26- * Captures a span and returns it to the caller, to be enqueued for sending.
26+ * A SpanV2JSON with an attached reference to the segment span.
27+ * This reference is used to compute dynamic sampling context before sending.
28+ * The reference MUST be removed before sending the span envelope.
2729 */
28- export function captureSpan ( span : Span , client = getClient ( ) ) : Span | void {
30+ export interface SpanV2JSONWithSegmentRef extends SpanV2JSON {
31+ _segmentSpan : Span ;
32+ }
33+
34+ /**
35+ * Captures a span and returns a JSON representation to be enqueued for sending.
36+ *
37+ * IMPORTANT: This function converts the span to JSON immediately to avoid writing
38+ * to an already-ended OTel span instance (which is blocked by the OTel Span class).
39+ */
40+ export function captureSpan ( span : Span , client = getClient ( ) ) : SpanV2JSONWithSegmentRef | void {
2941 if ( ! client ) {
3042 DEBUG_BUILD && debug . warn ( 'No client available to capture span.' ) ;
3143 return ;
3244 }
3345
46+ // Convert to JSON FIRST - we cannot write to an already-ended span
47+ const spanJSON = spanToV2JSON ( span ) as SpanV2JSONWithSegmentRef ;
48+
3449 const segmentSpan = INTERNAL_getSegmentSpan ( span ) ;
3550 const serializedSegmentSpan = spanToV2JSON ( segmentSpan ) ;
3651
@@ -39,44 +54,39 @@ export function captureSpan(span: Span, client = getClient()): Span | void {
3954
4055 const originalAttributes = serializedSegmentSpan . attributes ?? { } ;
4156
42- applyCommonSpanAttributes ( span , serializedSegmentSpan , client , finalScopeData , originalAttributes ) ;
57+ applyCommonSpanAttributes ( spanJSON , serializedSegmentSpan , client , finalScopeData , originalAttributes ) ;
4358
4459 if ( span === segmentSpan ) {
45- applyScopeToSegmentSpan ( span , finalScopeData , originalAttributes ) ;
60+ applyScopeToSegmentSpan ( spanJSON , finalScopeData , originalAttributes ) ;
4661 }
4762
48- // Allow integrations to add additional data to span. Pass in a serialized
49- // span to avoid having to potentially serialize the span in every integration
50- // (for improved performance).
51- client . emit ( 'processSpan' , span , { readOnlySpan : spanToV2JSON ( span ) } ) ;
52-
53- // Wondering where we apply the beforeSendSpan callback?
54- // We apply it directly before sending the span,
55- // so whenever the buffer this span gets enqueued in is being flushed.
56- // Why? Because we have to enqueue the span instance itself, not a JSON object.
57- // We could temporarily convert to JSON here but this means that we'd then again
58- // have to mutate the `span` instance (doesn't work for every kind of object mutation)
59- // or construct a fully new span object. The latter is risky because users (or we) could hold
60- // references to the original span instance.
61- client . emit ( 'enqueueSpan' , span ) ;
62-
63- return span ;
63+ // Attach segment span reference for DSC generation at send time
64+ spanJSON . _segmentSpan = segmentSpan ;
65+
66+ // Allow integrations to add additional data to the span JSON
67+ client . emit ( 'processSpan' , spanJSON , { readOnlySpan : span } ) ;
68+
69+ // Enqueue the JSON representation for sending
70+ // Note: We now enqueue JSON instead of the span instance to avoid mutating ended spans
71+ client . emit ( 'enqueueSpan' , spanJSON ) ;
72+
73+ return spanJSON ;
6474}
6575
6676function applyScopeToSegmentSpan (
67- segmentSpan : Span ,
77+ segmentSpanJSON : SpanV2JSON ,
6878 scopeData : ScopeData ,
6979 originalAttributes : SerializedAttributes ,
7080) : void {
7181 // TODO: Apply all scope data from auto instrumentation (contexts, request) to segment span
7282 const { attributes } = scopeData ;
7383 if ( attributes ) {
74- safeSetSpanAttributes ( segmentSpan , attributes , originalAttributes ) ;
84+ safeSetSpanJSONAttributes ( segmentSpanJSON , attributes , originalAttributes ) ;
7585 }
7686}
7787
7888function applyCommonSpanAttributes (
79- span : Span ,
89+ spanJSON : SpanV2JSON ,
8090 serializedSegmentSpan : SpanV2JSON ,
8191 client : Client ,
8292 scopeData : ScopeData ,
@@ -86,8 +96,8 @@ function applyCommonSpanAttributes(
8696 const { release, environment, sendDefaultPii } = client . getOptions ( ) ;
8797
8898 // avoid overwriting any previously set attributes (from users or potentially our SDK instrumentation)
89- safeSetSpanAttributes (
90- span ,
99+ safeSetSpanJSONAttributes (
100+ spanJSON ,
91101 {
92102 [ SEMANTIC_ATTRIBUTE_SENTRY_RELEASE ] : release ,
93103 [ SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT ] : environment ,
0 commit comments