@@ -132,12 +132,17 @@ export class Tracing implements Integration {
132132
133133 private readonly _emitOptionsWarning : boolean = false ;
134134
135+ private static _performanceCursor : number = 0 ;
136+
135137 /**
136138 * Constructor for Tracing
137139 *
138140 * @param _options TracingOptions
139141 */
140142 public constructor ( _options ?: Partial < TracingOptions > ) {
143+ if ( global . performance ) {
144+ global . performance . mark ( 'sentry-tracing-init' ) ;
145+ }
141146 const defaults = {
142147 discardBackgroundSpans : true ,
143148 idleTimeout : 500 ,
@@ -257,6 +262,7 @@ export class Tracing implements Integration {
257262 event . timestamp - event . start_timestamp < 0 ) ;
258263
259264 if ( Tracing . options . maxTransactionDuration !== 0 && event . type === 'transaction' && isOutdatedTransaction ) {
265+ logger . log ( '[Tracing] Discarded transaction since it maxed out maxTransactionDuration' ) ;
260266 return null ;
261267 }
262268 }
@@ -316,6 +322,7 @@ export class Tracing implements Integration {
316322 ) ;
317323
318324 Tracing . _activeTransaction = span ;
325+ Tracing . _addOffsetToSpan ( `idleTransactionStarted-${ Tracing . _currentIndex } ` , span as SpanClass ) ;
319326
320327 // We need to do this workaround here and not use configureScope
321328 // 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 {
357364 const active = Tracing . _activeTransaction as SpanClass ;
358365 if ( active ) {
359366 logger . log ( '[Tracing] finishIdleTransaction' , active . transaction ) ;
367+ Tracing . _addPerformanceEntries ( active ) ;
360368 // true = use timestamp of last span
361369 active . finish ( true ) ;
362370 }
363371 }
364372
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+
365529 /**
366530 * Sets the status of the current active transaction (if there is one)
367531 */
@@ -373,6 +537,32 @@ export class Tracing implements Integration {
373537 }
374538 }
375539
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+
376566 /**
377567 * Starts tracking for a specifc activity
378568 *
@@ -403,9 +593,11 @@ export class Tracing implements Integration {
403593 if ( spanContext && _getCurrentHub ) {
404594 const hub = _getCurrentHub ( ) ;
405595 if ( hub ) {
596+ const span = hub . startSpan ( spanContext ) ;
597+ Tracing . _addOffsetToSpan ( `${ name } ${ Tracing . _currentIndex } ` , span as SpanClass ) ;
406598 Tracing . _activities [ Tracing . _currentIndex ] = {
407599 name,
408- span : hub . startSpan ( spanContext ) ,
600+ span,
409601 } ;
410602 }
411603 } else {
@@ -445,7 +637,7 @@ export class Tracing implements Integration {
445637
446638 if ( activity ) {
447639 logger . log ( `[Tracing] popActivity ${ activity . name } #${ id } ` ) ;
448- const span = activity . span ;
640+ const span = activity . span as SpanClass ;
449641 if ( span ) {
450642 if ( spanData ) {
451643 Object . keys ( spanData ) . forEach ( ( key : string ) => {
@@ -459,6 +651,11 @@ export class Tracing implements Integration {
459651 } ) ;
460652 }
461653 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+ }
462659 }
463660 // tslint:disable-next-line: no-dynamic-delete
464661 delete Tracing . _activities [ id ] ;
0 commit comments