1- import type { Span , StartSpanOptions } from '@sentry/types' ;
1+ import type { Span , SpanAttributes , StartSpanOptions } from '@sentry/types' ;
22import { logger , timestampInSeconds } from '@sentry/utils' ;
33import { getClient , getCurrentScope } from '../currentScopes' ;
44
55import { DEBUG_BUILD } from '../debug-build' ;
6+ import { SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON } from '../semanticAttributes' ;
67import { hasTracingEnabled } from '../utils/hasTracingEnabled' ;
7- import { getSpanDescendants , removeChildSpanFromSpan , spanToJSON } from '../utils/spanUtils' ;
8+ import { getSpanDescendants , removeChildSpanFromSpan , spanTimeInputToSeconds , spanToJSON } from '../utils/spanUtils' ;
89import { SentryNonRecordingSpan } from './sentryNonRecordingSpan' ;
910import { SPAN_STATUS_ERROR } from './spanstatus' ;
1011import { startInactiveSpan } from './trace' ;
11- import { getActiveSpan } from './utils' ;
12+ import { getActiveSpan , getCapturedScopesOnSpan } from './utils' ;
1213
1314export const TRACING_DEFAULTS = {
1415 idleTimeout : 1_000 ,
@@ -108,6 +109,20 @@ export function startIdleSpan(startSpanOptions: StartSpanOptions, options: Parti
108109 const previousActiveSpan = getActiveSpan ( ) ;
109110 const span = _startIdleSpan ( startSpanOptions ) ;
110111
112+ function _endSpan ( timestamp : number = timestampInSeconds ( ) ) : void {
113+ // Ensure we end with the last span timestamp, if possible
114+ const spans = getSpanDescendants ( span ) . filter ( child => child !== span ) ;
115+
116+ const latestEndTimestamp = spans . length ? Math . max ( ...spans . map ( span => spanToJSON ( span ) . timestamp || 0 ) ) : 0 ;
117+
118+ // If the timestamp is smaller than the latest end timestamp of a span, we still want to use it
119+ const endTimestamp = latestEndTimestamp
120+ ? Math . min ( spanTimeInputToSeconds ( timestamp ) , latestEndTimestamp )
121+ : timestamp ;
122+
123+ span . end ( endTimestamp ) ;
124+ }
125+
111126 /**
112127 * Cancels the existing idle timeout, if there is one.
113128 */
@@ -136,7 +151,7 @@ export function startIdleSpan(startSpanOptions: StartSpanOptions, options: Parti
136151 _idleTimeoutID = setTimeout ( ( ) => {
137152 if ( ! _finished && activities . size === 0 && _autoFinishAllowed ) {
138153 _finishReason = FINISH_REASON_IDLE_TIMEOUT ;
139- span . end ( endTimestamp ) ;
154+ _endSpan ( endTimestamp ) ;
140155 }
141156 } , idleTimeout ) ;
142157 }
@@ -149,7 +164,7 @@ export function startIdleSpan(startSpanOptions: StartSpanOptions, options: Parti
149164 _idleTimeoutID = setTimeout ( ( ) => {
150165 if ( ! _finished && _autoFinishAllowed ) {
151166 _finishReason = FINISH_REASON_HEARTBEAT_FAILED ;
152- span . end ( endTimestamp ) ;
167+ _endSpan ( endTimestamp ) ;
153168 }
154169 } , childSpanTimeout ) ;
155170 }
@@ -190,7 +205,7 @@ export function startIdleSpan(startSpanOptions: StartSpanOptions, options: Parti
190205 }
191206 }
192207
193- function endIdleSpan ( ) : void {
208+ function onIdleSpanEnded ( ) : void {
194209 _finished = true ;
195210 activities . clear ( ) ;
196211
@@ -209,9 +224,25 @@ export function startIdleSpan(startSpanOptions: StartSpanOptions, options: Parti
209224 return ;
210225 }
211226
212- const attributes = spanJSON . data || { } ;
213- if ( spanJSON . op === 'ui.action.click' && ! attributes [ FINISH_REASON_TAG ] ) {
214- span . setAttribute ( FINISH_REASON_TAG , _finishReason ) ;
227+ const attributes : SpanAttributes = spanJSON . data || { } ;
228+ if ( spanJSON . op === 'ui.action.click' && ! attributes [ SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON ] ) {
229+ span . setAttribute ( SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON , _finishReason ) ;
230+ }
231+
232+ // Save finish reason as tag, in addition to attribute, to maintain backwards compatibility
233+ const scopes = getCapturedScopesOnSpan ( span ) ;
234+
235+ // Make sure to fetch up-to-date data here, as it may have been updated above
236+ const finalAttributes : SpanAttributes = spanToJSON ( span ) . data || { } ;
237+ const finalFinishReason = finalAttributes [ SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON ] ;
238+ if ( scopes . scope && typeof finalFinishReason === 'string' ) {
239+ // We only want to add the tag to the transaction event itself
240+ scopes . scope . addEventProcessor ( event => {
241+ if ( event . type === 'transaction' ) {
242+ event . tags = { ...event . tags , [ FINISH_REASON_TAG ] : finalFinishReason } ;
243+ }
244+ return event ;
245+ } ) ;
215246 }
216247
217248 DEBUG_BUILD &&
@@ -279,7 +310,7 @@ export function startIdleSpan(startSpanOptions: StartSpanOptions, options: Parti
279310 _popActivity ( endedSpan . spanContext ( ) . spanId ) ;
280311
281312 if ( endedSpan === span ) {
282- endIdleSpan ( ) ;
313+ onIdleSpanEnded ( ) ;
283314 }
284315 } ) ;
285316
@@ -303,7 +334,7 @@ export function startIdleSpan(startSpanOptions: StartSpanOptions, options: Parti
303334 if ( ! _finished ) {
304335 span . setStatus ( { code : SPAN_STATUS_ERROR , message : 'deadline_exceeded' } ) ;
305336 _finishReason = FINISH_REASON_FINAL_TIMEOUT ;
306- span . end ( ) ;
337+ _endSpan ( ) ;
307338 }
308339 } , finalTimeout ) ;
309340
0 commit comments