@@ -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