@@ -132,12 +132,17 @@ export class Tracing implements Integration {
132
132
133
133
private readonly _emitOptionsWarning : boolean = false ;
134
134
135
+ private static _performanceCursor : number = 0 ;
136
+
135
137
/**
136
138
* Constructor for Tracing
137
139
*
138
140
* @param _options TracingOptions
139
141
*/
140
142
public constructor ( _options ?: Partial < TracingOptions > ) {
143
+ if ( global . performance ) {
144
+ global . performance . mark ( 'sentry-tracing-init' ) ;
145
+ }
141
146
const defaults = {
142
147
discardBackgroundSpans : true ,
143
148
idleTimeout : 500 ,
@@ -257,6 +262,7 @@ export class Tracing implements Integration {
257
262
event . timestamp - event . start_timestamp < 0 ) ;
258
263
259
264
if ( Tracing . options . maxTransactionDuration !== 0 && event . type === 'transaction' && isOutdatedTransaction ) {
265
+ logger . log ( '[Tracing] Discarded transaction since it maxed out maxTransactionDuration' ) ;
260
266
return null ;
261
267
}
262
268
}
@@ -316,6 +322,7 @@ export class Tracing implements Integration {
316
322
) ;
317
323
318
324
Tracing . _activeTransaction = span ;
325
+ Tracing . _addOffsetToSpan ( `idleTransactionStarted-${ Tracing . _currentIndex } ` , span as SpanClass ) ;
319
326
320
327
// We need to do this workaround here and not use configureScope
321
328
// Reason being at the time we start the inital transaction we do not have a client bound on the hub yet
@@ -357,11 +364,168 @@ export class Tracing implements Integration {
357
364
const active = Tracing . _activeTransaction as SpanClass ;
358
365
if ( active ) {
359
366
logger . log ( '[Tracing] finishIdleTransaction' , active . transaction ) ;
367
+ Tracing . _addPerformanceEntries ( active ) ;
360
368
// true = use timestamp of last span
361
369
active . finish ( true ) ;
362
370
}
363
371
}
364
372
373
+ /**
374
+ * This uses `performance.getEntries()` to add additional spans to the active transaction.
375
+ * Also, we update our timings since we consider the timings in this API to be more correct than our manual
376
+ * measurements.
377
+ *
378
+ * @param transactionSpan The transaction span
379
+ */
380
+ private static _addPerformanceEntries ( transactionSpan : SpanClass ) : void {
381
+ if ( ! global . performance ) {
382
+ // Gatekeeper if performance API not available
383
+ return ;
384
+ }
385
+ logger . log ( '[Tracing] Adding & adjusting spans using Performance API' ) ;
386
+ let navigationOffset = 0 ;
387
+ if (
388
+ transactionSpan . op === 'navigation' &&
389
+ transactionSpan . data &&
390
+ typeof transactionSpan . data . offset === 'number'
391
+ ) {
392
+ navigationOffset = transactionSpan . data . offset ;
393
+ }
394
+ // tslint:disable-next-line: completed-docs
395
+ function addSpan ( span : SpanClass ) : void {
396
+ if ( transactionSpan . spanRecorder ) {
397
+ transactionSpan . spanRecorder . finishSpan ( span ) ;
398
+ }
399
+ }
400
+
401
+ // tslint:disable-next-line: completed-docs
402
+ function addPerformanceNavigationTiming ( parent : SpanClass , entry : { [ key : string ] : number } , event : string ) : void {
403
+ const span = parent . child ( {
404
+ description : event ,
405
+ op : 'browser' ,
406
+ } ) ;
407
+ span . startTimestamp = parent . startTimestamp + Tracing . _msToSec ( entry [ `${ event } Start` ] ) ;
408
+ span . timestamp = parent . startTimestamp + Tracing . _msToSec ( entry [ `${ event } End` ] ) ;
409
+ addSpan ( span ) ;
410
+ }
411
+
412
+ // tslint:disable-next-line: completed-docs
413
+ function addRequest ( parent : SpanClass , entry : { [ key : string ] : number } ) : void {
414
+ const request = parent . child ( {
415
+ description : 'request' ,
416
+ op : 'browser' ,
417
+ } ) ;
418
+ request . startTimestamp = parent . startTimestamp + Tracing . _msToSec ( entry . requestStart ) ;
419
+ request . timestamp = parent . startTimestamp + Tracing . _msToSec ( entry . responseEnd ) ;
420
+ addSpan ( request ) ;
421
+ const response = parent . child ( {
422
+ description : 'response' ,
423
+ op : 'browser' ,
424
+ } ) ;
425
+ response . startTimestamp = parent . startTimestamp + Tracing . _msToSec ( entry . responseStart ) ;
426
+ response . timestamp = parent . startTimestamp + Tracing . _msToSec ( entry . responseEnd ) ;
427
+ addSpan ( response ) ;
428
+ }
429
+
430
+ let entryScriptSrc : string | undefined ;
431
+
432
+ if ( global . document ) {
433
+ // tslint:disable-next-line: prefer-for-of
434
+ for ( let i = 0 ; i < document . scripts . length ; i ++ ) {
435
+ // We go through all scripts on the page and look for 'data-entry'
436
+ // We remember the name and measure the time between this script finished loading and
437
+ // our mark 'sentry-tracing-init'
438
+ if ( document . scripts [ i ] . dataset . entry === 'true' ) {
439
+ entryScriptSrc = document . scripts [ i ] . src ;
440
+ break ;
441
+ }
442
+ }
443
+ }
444
+
445
+ let entryScriptStartEndTime : number | undefined ;
446
+ let tracingInitMarkStartTime : number | undefined ;
447
+
448
+ // tslint:disable: no-unsafe-any
449
+ performance
450
+ . getEntries ( )
451
+ . slice ( Tracing . _performanceCursor )
452
+ . forEach ( ( entry : any ) => {
453
+ const startTime = Tracing . _msToSec ( entry . startTime as number ) ;
454
+ const duration = Tracing . _msToSec ( entry . duration as number ) ;
455
+
456
+ switch ( entry . entryType ) {
457
+ case 'navigation' :
458
+ addPerformanceNavigationTiming ( transactionSpan , entry , 'unloadEvent' ) ;
459
+ addPerformanceNavigationTiming ( transactionSpan , entry , 'domContentLoadedEvent' ) ;
460
+ addPerformanceNavigationTiming ( transactionSpan , entry , 'loadEvent' ) ;
461
+ addPerformanceNavigationTiming ( transactionSpan , entry , 'connect' ) ;
462
+ addPerformanceNavigationTiming ( transactionSpan , entry , 'domainLookup' ) ;
463
+ addRequest ( transactionSpan , entry ) ;
464
+ break ;
465
+ case 'mark' :
466
+ case 'paint' :
467
+ case 'measure' :
468
+ const mark = transactionSpan . child ( {
469
+ description : `${ entry . entryType } ${ entry . name } ` ,
470
+ op : 'mark' ,
471
+ } ) ;
472
+ mark . startTimestamp = transactionSpan . startTimestamp + startTime - navigationOffset ;
473
+ mark . timestamp = mark . startTimestamp + duration ;
474
+ if ( tracingInitMarkStartTime === undefined && entry . name === 'sentry-tracing-init' ) {
475
+ tracingInitMarkStartTime = mark . startTimestamp ;
476
+ }
477
+ addSpan ( mark ) ;
478
+ break ;
479
+ case 'resource' :
480
+ const resourceName = entry . name . replace ( window . location . origin , '' ) ;
481
+ if ( entry . initiatorType === 'xmlhttprequest' || entry . initiatorType === 'fetch' ) {
482
+ // We need to update existing spans with new timing info
483
+ if ( transactionSpan . spanRecorder ) {
484
+ transactionSpan . spanRecorder . finishedSpans . map ( ( finishedSpan : SpanClass ) => {
485
+ if ( finishedSpan . description && finishedSpan . description . indexOf ( resourceName ) !== - 1 ) {
486
+ finishedSpan . startTimestamp = transactionSpan . startTimestamp + startTime - navigationOffset ;
487
+ finishedSpan . timestamp = finishedSpan . startTimestamp + duration ;
488
+ }
489
+ } ) ;
490
+ }
491
+ } else {
492
+ const resource = transactionSpan . child ( {
493
+ description : `${ entry . initiatorType } ${ resourceName } ` ,
494
+ op : `resource` ,
495
+ } ) ;
496
+ resource . startTimestamp = transactionSpan . startTimestamp + startTime - navigationOffset ;
497
+ resource . timestamp = resource . startTimestamp + duration ;
498
+ // We remember the entry script end time to calculate the difference to the first init mark
499
+ if ( entryScriptStartEndTime === undefined && ( entryScriptSrc || '' ) . includes ( resourceName ) ) {
500
+ entryScriptStartEndTime = resource . timestamp ;
501
+ }
502
+ addSpan ( resource ) ;
503
+ }
504
+ break ;
505
+ default :
506
+ // Ignore other entry types.
507
+ }
508
+ } ) ;
509
+
510
+ if ( entryScriptStartEndTime !== undefined && tracingInitMarkStartTime !== undefined ) {
511
+ const evaluation = transactionSpan . child ( {
512
+ description : 'evaluation' ,
513
+ op : `script` ,
514
+ } ) ;
515
+ evaluation . startTimestamp = entryScriptStartEndTime ;
516
+ evaluation . timestamp = tracingInitMarkStartTime ;
517
+ addSpan ( evaluation ) ;
518
+ }
519
+
520
+ logger . log ( '[Tracing] Clearing most performance marks' ) ;
521
+ performance . clearMarks ( ) ;
522
+ performance . clearMeasures ( ) ;
523
+ performance . clearResourceTimings ( ) ;
524
+ Tracing . _performanceCursor = Math . max ( performance . getEntries ( ) . length - 1 , 0 ) ;
525
+
526
+ // tslint:enable: no-unsafe-any
527
+ }
528
+
365
529
/**
366
530
* Sets the status of the current active transaction (if there is one)
367
531
*/
@@ -373,6 +537,32 @@ export class Tracing implements Integration {
373
537
}
374
538
}
375
539
540
+ /**
541
+ * Adds offset to the span
542
+ *
543
+ * @param measureName name of the performance measure
544
+ * @param span Span to add data.offset to
545
+ */
546
+ private static _addOffsetToSpan ( measureName : string , span : SpanClass ) : void {
547
+ if ( global . performance ) {
548
+ const name = `#sentry-${ measureName } ` ;
549
+ performance . measure ( name ) ;
550
+ const measure = performance . getEntriesByName ( name ) . pop ( ) ;
551
+ if ( measure ) {
552
+ span . setData ( 'offset' , Tracing . _msToSec ( measure . duration ) ) ;
553
+ }
554
+ performance . clearMeasures ( name ) ;
555
+ }
556
+ }
557
+
558
+ /**
559
+ * Converts from milliseconds to seconds
560
+ * @param time time in ms
561
+ */
562
+ private static _msToSec ( time : number ) : number {
563
+ return time / 1000 ;
564
+ }
565
+
376
566
/**
377
567
* Starts tracking for a specifc activity
378
568
*
@@ -403,9 +593,11 @@ export class Tracing implements Integration {
403
593
if ( spanContext && _getCurrentHub ) {
404
594
const hub = _getCurrentHub ( ) ;
405
595
if ( hub ) {
596
+ const span = hub . startSpan ( spanContext ) ;
597
+ Tracing . _addOffsetToSpan ( `${ name } ${ Tracing . _currentIndex } ` , span as SpanClass ) ;
406
598
Tracing . _activities [ Tracing . _currentIndex ] = {
407
599
name,
408
- span : hub . startSpan ( spanContext ) ,
600
+ span,
409
601
} ;
410
602
}
411
603
} else {
@@ -445,7 +637,7 @@ export class Tracing implements Integration {
445
637
446
638
if ( activity ) {
447
639
logger . log ( `[Tracing] popActivity ${ activity . name } #${ id } ` ) ;
448
- const span = activity . span ;
640
+ const span = activity . span as SpanClass ;
449
641
if ( span ) {
450
642
if ( spanData ) {
451
643
Object . keys ( spanData ) . forEach ( ( key : string ) => {
@@ -459,6 +651,11 @@ export class Tracing implements Integration {
459
651
} ) ;
460
652
}
461
653
span . finish ( ) ;
654
+ // If there is an offset in data, we need to shift timestamps towards it
655
+ if ( span . data && typeof span . data . offset === 'number' && typeof span . timestamp === 'number' ) {
656
+ span . startTimestamp += span . data . offset ;
657
+ span . timestamp += span . data . offset ;
658
+ }
462
659
}
463
660
// tslint:disable-next-line: no-dynamic-delete
464
661
delete Tracing . _activities [ id ] ;
0 commit comments