Skip to content

Commit 0bc7a4d

Browse files
HazATrhcarvalho
andauthored
feat[apm]: Use Performance API (#2474)
* feat: Use performance api * feat: Update ops & descriptions * feat: Handle navigation changes correctly * chore: Code comments * Apply suggestions from code review Co-Authored-By: Rodolfo Carvalho <[email protected]> * chore: CodeReview * ref: Check for performance API Co-authored-by: Rodolfo Carvalho <[email protected]>
1 parent dd7bf92 commit 0bc7a4d

File tree

3 files changed

+201
-3
lines changed

3 files changed

+201
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
66

7+
- [apm] feat: Make use of the `performance` browser API to provide better instrumentation (#2474)
78
- [browser] ref: Move global error handler + unhandled promise rejection to instrument
89

910
## 5.13.2

packages/apm/src/integrations/tracing.ts

Lines changed: 199 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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];

packages/apm/src/span.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ export class Span implements SpanInterface, SpanContext {
108108
/**
109109
* Timestamp when the span was created.
110110
*/
111-
public readonly startTimestamp: number = timestampWithMs();
111+
public startTimestamp: number = timestampWithMs();
112112

113113
/**
114114
* Internal start time tracked with a monotonic clock.

0 commit comments

Comments
 (0)