@@ -230,6 +230,81 @@ describe('startIdleSpan', () => {
230
230
) ;
231
231
} ) ;
232
232
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
+
233
308
it ( 'emits span hooks' , ( ) => {
234
309
const client = getClient ( ) ! ;
235
310
@@ -274,6 +349,27 @@ describe('startIdleSpan', () => {
274
349
expect ( recordDroppedEventSpy ) . toHaveBeenCalledWith ( 'sample_rate' , 'transaction' ) ;
275
350
} ) ;
276
351
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
+
277
373
it ( 'sets finish reason when span ends' , ( ) => {
278
374
let transaction : Event | undefined ;
279
375
const beforeSendTransaction = jest . fn ( event => {
@@ -285,8 +381,7 @@ describe('startIdleSpan', () => {
285
381
setCurrentClient ( client ) ;
286
382
client . init ( ) ;
287
383
288
- // This is only set when op === 'ui.action.click'
289
- startIdleSpan ( { name : 'foo' , op : 'ui.action.click' } ) ;
384
+ startIdleSpan ( { name : 'foo' } ) ;
290
385
startSpan ( { name : 'inner' } , ( ) => { } ) ;
291
386
jest . runOnlyPendingTimers ( ) ;
292
387
@@ -296,6 +391,57 @@ describe('startIdleSpan', () => {
296
391
) ;
297
392
} ) ;
298
393
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
+
299
445
it ( 'uses finish reason set outside when span ends' , ( ) => {
300
446
let transaction : Event | undefined ;
301
447
const beforeSendTransaction = jest . fn ( event => {
@@ -307,8 +453,7 @@ describe('startIdleSpan', () => {
307
453
setCurrentClient ( client ) ;
308
454
client . init ( ) ;
309
455
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' } ) ;
312
457
span . setAttribute ( SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON , 'custom reason' ) ;
313
458
startSpan ( { name : 'inner' } , ( ) => { } ) ;
314
459
jest . runOnlyPendingTimers ( ) ;
@@ -496,7 +641,7 @@ describe('startIdleSpan', () => {
496
641
497
642
describe ( 'trim end timestamp' , ( ) => {
498
643
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 } ) ;
500
645
expect ( idleSpan ) . toBeDefined ( ) ;
501
646
502
647
const span1 = startInactiveSpan ( { name : 'span1' , startTime : 1001 } ) ;
@@ -515,8 +660,28 @@ describe('startIdleSpan', () => {
515
660
expect ( spanToJSON ( idleSpan ! ) . timestamp ) . toBe ( 1100 ) ;
516
661
} ) ;
517
662
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
+
518
683
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 } ) ;
520
685
expect ( idleSpan ) . toBeDefined ( ) ;
521
686
522
687
const span1 = startInactiveSpan ( { name : 'span1' , startTime : 999_999_999 } ) ;
0 commit comments