@@ -109,6 +109,59 @@ export function startIdleSpan(startSpanOptions: StartSpanOptions, options: Parti
109109
110110 const client = getClient ( ) ;
111111
112+ function patchSpanEnd ( span : Span ) : void {
113+ // We patch span.end to ensure we can run some things before the span is ended
114+ // eslint-disable-next-line @typescript-eslint/unbound-method
115+ span . end = new Proxy ( span . end , {
116+ apply ( target , thisArg , args : Parameters < Span [ 'end' ] > ) {
117+ if ( beforeSpanEnd ) {
118+ beforeSpanEnd ( span ) ;
119+ }
120+
121+ // If the span is non-recording, nothing more to do here...
122+ // This is the case if tracing is enabled but this specific span was not sampled
123+ if ( thisArg instanceof SentryNonRecordingSpan ) {
124+ return ;
125+ }
126+
127+ // Just ensuring that this keeps working, even if we ever have more arguments here
128+ const [ definedEndTimestamp , ...rest ] = args ;
129+ const timestamp = definedEndTimestamp || timestampInSeconds ( ) ;
130+ const spanEndTimestamp = spanTimeInputToSeconds ( timestamp ) ;
131+
132+ // Ensure we end with the last span timestamp, if possible
133+ const spans = getSpanDescendants ( span ) . filter ( child => child !== span ) ;
134+
135+ // If we have no spans, we just end, nothing else to do here
136+ if ( ! spans . length ) {
137+ onIdleSpanEnded ( spanEndTimestamp ) ;
138+ return Reflect . apply ( target , thisArg , [ spanEndTimestamp , ...rest ] ) ;
139+ }
140+
141+ const childEndTimestamps = spans
142+ . map ( span => spanToJSON ( span ) . timestamp )
143+ . filter ( timestamp => ! ! timestamp ) as number [ ] ;
144+ const latestSpanEndTimestamp = childEndTimestamps . length ? Math . max ( ...childEndTimestamps ) : undefined ;
145+
146+ // In reality this should always exist here, but type-wise it may be undefined...
147+ const spanStartTimestamp = spanToJSON ( span ) . start_timestamp ;
148+
149+ // The final endTimestamp should:
150+ // * Never be before the span start timestamp
151+ // * Be the latestSpanEndTimestamp, if there is one, and it is smaller than the passed span end timestamp
152+ // * Otherwise be the passed end timestamp
153+ // Final timestamp can never be after finalTimeout
154+ const endTimestamp = Math . min (
155+ spanStartTimestamp ? spanStartTimestamp + finalTimeout / 1000 : Infinity ,
156+ Math . max ( spanStartTimestamp || - Infinity , Math . min ( spanEndTimestamp , latestSpanEndTimestamp || Infinity ) ) ,
157+ ) ;
158+
159+ onIdleSpanEnded ( endTimestamp ) ;
160+ return Reflect . apply ( target , thisArg , [ endTimestamp , ...rest ] ) ;
161+ } ,
162+ } ) ;
163+ }
164+
112165 if ( ! client || ! hasTracingEnabled ( ) ) {
113166 const span = new SentryNonRecordingSpan ( ) ;
114167
@@ -118,64 +171,15 @@ export function startIdleSpan(startSpanOptions: StartSpanOptions, options: Parti
118171 ...getDynamicSamplingContextFromSpan ( span ) ,
119172 } satisfies Partial < DynamicSamplingContext > ;
120173 freezeDscOnSpan ( span , dsc ) ;
121-
174+ patchSpanEnd ( span ) ;
122175 return span ;
123176 }
124177
125178 const scope = getCurrentScope ( ) ;
126179 const previousActiveSpan = getActiveSpan ( ) ;
127180 const span = _startIdleSpan ( startSpanOptions ) ;
128181
129- // We patch span.end to ensure we can run some things before the span is ended
130- // eslint-disable-next-line @typescript-eslint/unbound-method
131- span . end = new Proxy ( span . end , {
132- apply ( target , thisArg , args : Parameters < Span [ 'end' ] > ) {
133- if ( beforeSpanEnd ) {
134- beforeSpanEnd ( span ) ;
135- }
136-
137- // If the span is non-recording, nothing more to do here...
138- // This is the case if tracing is enabled but this specific span was not sampled
139- if ( thisArg instanceof SentryNonRecordingSpan ) {
140- return ;
141- }
142-
143- // Just ensuring that this keeps working, even if we ever have more arguments here
144- const [ definedEndTimestamp , ...rest ] = args ;
145- const timestamp = definedEndTimestamp || timestampInSeconds ( ) ;
146- const spanEndTimestamp = spanTimeInputToSeconds ( timestamp ) ;
147-
148- // Ensure we end with the last span timestamp, if possible
149- const spans = getSpanDescendants ( span ) . filter ( child => child !== span ) ;
150-
151- // If we have no spans, we just end, nothing else to do here
152- if ( ! spans . length ) {
153- onIdleSpanEnded ( spanEndTimestamp ) ;
154- return Reflect . apply ( target , thisArg , [ spanEndTimestamp , ...rest ] ) ;
155- }
156-
157- const childEndTimestamps = spans
158- . map ( span => spanToJSON ( span ) . timestamp )
159- . filter ( timestamp => ! ! timestamp ) as number [ ] ;
160- const latestSpanEndTimestamp = childEndTimestamps . length ? Math . max ( ...childEndTimestamps ) : undefined ;
161-
162- // In reality this should always exist here, but type-wise it may be undefined...
163- const spanStartTimestamp = spanToJSON ( span ) . start_timestamp ;
164-
165- // The final endTimestamp should:
166- // * Never be before the span start timestamp
167- // * Be the latestSpanEndTimestamp, if there is one, and it is smaller than the passed span end timestamp
168- // * Otherwise be the passed end timestamp
169- // Final timestamp can never be after finalTimeout
170- const endTimestamp = Math . min (
171- spanStartTimestamp ? spanStartTimestamp + finalTimeout / 1000 : Infinity ,
172- Math . max ( spanStartTimestamp || - Infinity , Math . min ( spanEndTimestamp , latestSpanEndTimestamp || Infinity ) ) ,
173- ) ;
174-
175- onIdleSpanEnded ( endTimestamp ) ;
176- return Reflect . apply ( target , thisArg , [ endTimestamp , ...rest ] ) ;
177- } ,
178- } ) ;
182+ patchSpanEnd ( span ) ;
179183
180184 /**
181185 * Cancels the existing idle timeout, if there is one.
0 commit comments