@@ -230,6 +230,81 @@ describe('startIdleSpan', () => {
230230 ) ;
231231 } ) ;
232232
233+ it ( 'Ensures idle span cannot exceed finalTimeout' , ( ) => {
234+ const transactions : Event [ ] = [ ] ;
235+ const beforeSendTransaction = jest . fn ( event => {
236+ transactions . push ( event ) ;
237+ return null ;
238+ } ) ;
239+ const options = getDefaultTestClientOptions ( {
240+ dsn,
241+ tracesSampleRate : 1 ,
242+ beforeSendTransaction,
243+ } ) ;
244+ const client = new TestClient ( options ) ;
245+ setCurrentClient ( client ) ;
246+ client . init ( ) ;
247+
248+ // We want to accomodate a bit of drift there, so we ensure this starts earlier...
249+ const finalTimeout = 99_999 ;
250+ const baseTimeInSeconds = Math . floor ( Date . now ( ) / 1000 ) - 9999 ;
251+
252+ const idleSpan = startIdleSpan ( { name : 'idle span' , startTime : baseTimeInSeconds } , { finalTimeout : finalTimeout } ) ;
253+ expect ( idleSpan ) . toBeDefined ( ) ;
254+
255+ // regular child - should be kept
256+ const regularSpan = startInactiveSpan ( {
257+ name : 'regular span' ,
258+ startTime : baseTimeInSeconds + 2 ,
259+ } ) ;
260+ regularSpan . end ( baseTimeInSeconds + 4 ) ;
261+
262+ // very late ending span
263+ const discardedSpan = startInactiveSpan ( { name : 'discarded span' , startTime : baseTimeInSeconds + 99 } ) ;
264+ discardedSpan . end ( baseTimeInSeconds + finalTimeout + 100 ) ;
265+
266+ // Should be cancelled - will not finish
267+ const cancelledSpan = startInactiveSpan ( {
268+ name : 'cancelled span' ,
269+ startTime : baseTimeInSeconds + 4 ,
270+ } ) ;
271+
272+ jest . runOnlyPendingTimers ( ) ;
273+
274+ expect ( regularSpan . isRecording ( ) ) . toBe ( false ) ;
275+ expect ( idleSpan . isRecording ( ) ) . toBe ( false ) ;
276+ expect ( discardedSpan . isRecording ( ) ) . toBe ( false ) ;
277+ expect ( cancelledSpan . isRecording ( ) ) . toBe ( false ) ;
278+
279+ expect ( beforeSendTransaction ) . toHaveBeenCalledTimes ( 1 ) ;
280+ const transaction = transactions [ 0 ] ;
281+
282+ // End time is based on idle time etc.
283+ const idleSpanEndTime = transaction . timestamp ! ;
284+ expect ( idleSpanEndTime ) . toEqual ( baseTimeInSeconds + finalTimeout / 1000 ) ;
285+
286+ expect ( transaction . spans ) . toHaveLength ( 2 ) ;
287+ expect ( transaction . spans ) . toEqual (
288+ expect . arrayContaining ( [
289+ expect . objectContaining ( {
290+ description : 'regular span' ,
291+ timestamp : baseTimeInSeconds + 4 ,
292+ start_timestamp : baseTimeInSeconds + 2 ,
293+ } ) ,
294+ ] ) ,
295+ ) ;
296+ expect ( transaction . spans ) . toEqual (
297+ expect . arrayContaining ( [
298+ expect . objectContaining ( {
299+ description : 'cancelled span' ,
300+ timestamp : idleSpanEndTime ,
301+ start_timestamp : baseTimeInSeconds + 4 ,
302+ status : 'cancelled' ,
303+ } ) ,
304+ ] ) ,
305+ ) ;
306+ } ) ;
307+
233308 it ( 'emits span hooks' , ( ) => {
234309 const client = getClient ( ) ! ;
235310
@@ -274,6 +349,27 @@ describe('startIdleSpan', () => {
274349 expect ( recordDroppedEventSpy ) . toHaveBeenCalledWith ( 'sample_rate' , 'transaction' ) ;
275350 } ) ;
276351
352+ it ( 'sets finish reason when span is ended manually' , ( ) => {
353+ let transaction : Event | undefined ;
354+ const beforeSendTransaction = jest . fn ( event => {
355+ transaction = event ;
356+ return null ;
357+ } ) ;
358+ const options = getDefaultTestClientOptions ( { dsn, tracesSampleRate : 1 , beforeSendTransaction } ) ;
359+ const client = new TestClient ( options ) ;
360+ setCurrentClient ( client ) ;
361+ client . init ( ) ;
362+
363+ const span = startIdleSpan ( { name : 'foo' } ) ;
364+ span . end ( ) ;
365+ jest . runOnlyPendingTimers ( ) ;
366+
367+ expect ( beforeSendTransaction ) . toHaveBeenCalledTimes ( 1 ) ;
368+ expect ( transaction ?. contexts ?. trace ?. data ?. [ SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON ] ) . toEqual (
369+ 'externalFinish' ,
370+ ) ;
371+ } ) ;
372+
277373 it ( 'sets finish reason when span ends' , ( ) => {
278374 let transaction : Event | undefined ;
279375 const beforeSendTransaction = jest . fn ( event => {
@@ -285,8 +381,7 @@ describe('startIdleSpan', () => {
285381 setCurrentClient ( client ) ;
286382 client . init ( ) ;
287383
288- // This is only set when op === 'ui.action.click'
289- startIdleSpan ( { name : 'foo' , op : 'ui.action.click' } ) ;
384+ startIdleSpan ( { name : 'foo' } ) ;
290385 startSpan ( { name : 'inner' } , ( ) => { } ) ;
291386 jest . runOnlyPendingTimers ( ) ;
292387
@@ -296,6 +391,57 @@ describe('startIdleSpan', () => {
296391 ) ;
297392 } ) ;
298393
394+ it ( 'sets finish reason when span ends via expired heartbeat timeout' , ( ) => {
395+ let transaction : Event | undefined ;
396+ const beforeSendTransaction = jest . fn ( event => {
397+ transaction = event ;
398+ return null ;
399+ } ) ;
400+ const options = getDefaultTestClientOptions ( { dsn, tracesSampleRate : 1 , beforeSendTransaction } ) ;
401+ const client = new TestClient ( options ) ;
402+ setCurrentClient ( client ) ;
403+ client . init ( ) ;
404+
405+ startIdleSpan ( { name : 'foo' } ) ;
406+ startSpanManual ( { name : 'inner' } , ( ) => { } ) ;
407+ jest . runOnlyPendingTimers ( ) ;
408+
409+ expect ( beforeSendTransaction ) . toHaveBeenCalledTimes ( 1 ) ;
410+ expect ( transaction ?. contexts ?. trace ?. data ?. [ SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON ] ) . toEqual (
411+ 'heartbeatFailed' ,
412+ ) ;
413+ } ) ;
414+
415+ it ( 'sets finish reason when span ends via final timeout' , ( ) => {
416+ let transaction : Event | undefined ;
417+ const beforeSendTransaction = jest . fn ( event => {
418+ transaction = event ;
419+ return null ;
420+ } ) ;
421+ const options = getDefaultTestClientOptions ( { dsn, tracesSampleRate : 1 , beforeSendTransaction } ) ;
422+ const client = new TestClient ( options ) ;
423+ setCurrentClient ( client ) ;
424+ client . init ( ) ;
425+
426+ startIdleSpan ( { name : 'foo' } , { finalTimeout : TRACING_DEFAULTS . childSpanTimeout * 2 } ) ;
427+
428+ const span1 = startInactiveSpan ( { name : 'inner' } ) ;
429+ jest . advanceTimersByTime ( TRACING_DEFAULTS . childSpanTimeout - 1 ) ;
430+ span1 . end ( ) ;
431+
432+ const span2 = startInactiveSpan ( { name : 'inner2' } ) ;
433+ jest . advanceTimersByTime ( TRACING_DEFAULTS . childSpanTimeout - 1 ) ;
434+ span2 . end ( ) ;
435+
436+ startInactiveSpan ( { name : 'inner3' } ) ;
437+ jest . runOnlyPendingTimers ( ) ;
438+
439+ expect ( beforeSendTransaction ) . toHaveBeenCalledTimes ( 1 ) ;
440+ expect ( transaction ?. contexts ?. trace ?. data ?. [ SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON ] ) . toEqual (
441+ 'finalTimeout' ,
442+ ) ;
443+ } ) ;
444+
299445 it ( 'uses finish reason set outside when span ends' , ( ) => {
300446 let transaction : Event | undefined ;
301447 const beforeSendTransaction = jest . fn ( event => {
@@ -307,8 +453,7 @@ describe('startIdleSpan', () => {
307453 setCurrentClient ( client ) ;
308454 client . init ( ) ;
309455
310- // This is only set when op === 'ui.action.click'
311- const span = startIdleSpan ( { name : 'foo' , op : 'ui.action.click' } ) ;
456+ const span = startIdleSpan ( { name : 'foo' } ) ;
312457 span . setAttribute ( SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON , 'custom reason' ) ;
313458 startSpan ( { name : 'inner' } , ( ) => { } ) ;
314459 jest . runOnlyPendingTimers ( ) ;
@@ -496,7 +641,7 @@ describe('startIdleSpan', () => {
496641
497642 describe ( 'trim end timestamp' , ( ) => {
498643 it ( 'trims end to highest child span end' , ( ) => {
499- const idleSpan = startIdleSpan ( { name : 'foo' , startTime : 1000 } ) ;
644+ const idleSpan = startIdleSpan ( { name : 'foo' , startTime : 1000 } , { finalTimeout : 99_999_999 } ) ;
500645 expect ( idleSpan ) . toBeDefined ( ) ;
501646
502647 const span1 = startInactiveSpan ( { name : 'span1' , startTime : 1001 } ) ;
@@ -515,8 +660,28 @@ describe('startIdleSpan', () => {
515660 expect ( spanToJSON ( idleSpan ! ) . timestamp ) . toBe ( 1100 ) ;
516661 } ) ;
517662
663+ it ( 'trims end to final timeout' , ( ) => {
664+ const idleSpan = startIdleSpan ( { name : 'foo' , startTime : 1000 } , { finalTimeout : 30_000 } ) ;
665+ expect ( idleSpan ) . toBeDefined ( ) ;
666+
667+ const span1 = startInactiveSpan ( { name : 'span1' , startTime : 1001 } ) ;
668+ span1 ?. end ( 1005 ) ;
669+
670+ const span2 = startInactiveSpan ( { name : 'span2' , startTime : 1002 } ) ;
671+ span2 ?. end ( 1100 ) ;
672+
673+ const span3 = startInactiveSpan ( { name : 'span1' , startTime : 1050 } ) ;
674+ span3 ?. end ( 1060 ) ;
675+
676+ expect ( getActiveSpan ( ) ) . toBe ( idleSpan ) ;
677+
678+ jest . runAllTimers ( ) ;
679+
680+ expect ( spanToJSON ( idleSpan ! ) . timestamp ) . toBe ( 1030 ) ;
681+ } ) ;
682+
518683 it ( 'keeps lower span endTime than highest child span end' , ( ) => {
519- const idleSpan = startIdleSpan ( { name : 'foo' , startTime : 1000 } ) ;
684+ const idleSpan = startIdleSpan ( { name : 'foo' , startTime : 1000 } , { finalTimeout : 99_999_999 } ) ;
520685 expect ( idleSpan ) . toBeDefined ( ) ;
521686
522687 const span1 = startInactiveSpan ( { name : 'span1' , startTime : 999_999_999 } ) ;
0 commit comments