Skip to content

Commit c89e9ca

Browse files
HazATrhcarvalho
authored andcommitted
ref(apm): Always use monotonic clock for time calculations
Also remove offset time calculations since everything is based on timeOrigin
1 parent 268c7e7 commit c89e9ca

File tree

3 files changed

+73
-88
lines changed

3 files changed

+73
-88
lines changed

packages/apm/src/integrations/tracing.ts

Lines changed: 15 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -95,19 +95,6 @@ interface Activity {
9595
const global = getGlobalObject<Window>();
9696
const defaultTracingOrigins = ['localhost', /^\//];
9797

98-
if (global.performance) {
99-
// Polyfill for performance.timeOrigin.
100-
//
101-
// While performance.timing.navigationStart is deprecated in favor of performance.timeOrigin, performance.timeOrigin
102-
// is not as widely supported. Namely, performance.timeOrigin is undefined in Safari as of writing.
103-
// tslint:disable-next-line:strict-type-predicates
104-
if (performance.timeOrigin === undefined) {
105-
// @ts-ignore
106-
// tslint:disable-next-line:deprecation
107-
performance.timeOrigin = performance.timing.navigationStart;
108-
}
109-
}
110-
11198
/**
11299
* Tracing Integration
113100
*/
@@ -297,13 +284,20 @@ export class Tracing implements Integration {
297284
document.addEventListener('visibilitychange', () => {
298285
if (document.hidden && Tracing._activeTransaction) {
299286
logger.log('[Tracing] Discarded active transaction incl. activities since tab moved to the background');
300-
Tracing._activeTransaction = undefined;
301-
Tracing._activities = {};
287+
Tracing._resetActiveTransaction();
302288
}
303289
});
304290
}
305291
}
306292

293+
/**
294+
* Unsets the current active transaction + activities
295+
*/
296+
private static _resetActiveTransaction(): void {
297+
Tracing._activeTransaction = undefined;
298+
Tracing._activities = {};
299+
}
300+
307301
/**
308302
* Registers to History API to detect navigation changes
309303
*/
@@ -415,7 +409,6 @@ export class Tracing implements Integration {
415409
);
416410

417411
Tracing._activeTransaction = span;
418-
Tracing._addOffsetToSpan(span as SpanClass);
419412

420413
// We need to do this workaround here and not use configureScope
421414
// Reason being at the time we start the inital transaction we do not have a client bound on the hub yet
@@ -460,6 +453,7 @@ export class Tracing implements Integration {
460453
logger.log('[Tracing] finishIdleTransaction', active.transaction);
461454
// true = use timestamp of last span
462455
active.finish(true);
456+
Tracing._resetActiveTransaction();
463457
}
464458
}
465459

@@ -542,6 +536,11 @@ export class Tracing implements Integration {
542536
const startTime = Tracing._msToSec(entry.startTime as number);
543537
const duration = Tracing._msToSec(entry.duration as number);
544538

539+
if (transactionSpan.op === 'navigation' && timeOrigin + startTime < transactionSpan.startTimestamp) {
540+
logger.log('[Tracing] Discarded performance entry because of navigation');
541+
return;
542+
}
543+
545544
switch (entry.entryType) {
546545
case 'navigation':
547546
addPerformanceNavigationTiming(transactionSpan, entry, 'unloadEvent');
@@ -630,18 +629,6 @@ export class Tracing implements Integration {
630629
}
631630
}
632631

633-
/**
634-
* Adds offset to the span
635-
*
636-
* @param measureName name of the performance measure
637-
* @param span Span to add data.offset to
638-
*/
639-
private static _addOffsetToSpan(span: SpanClass): void {
640-
if (global.performance) {
641-
span.setData('offset', Tracing._msToSec(performance.now()));
642-
}
643-
}
644-
645632
/**
646633
* Converts from milliseconds to seconds
647634
* @param time time in ms
@@ -681,7 +668,6 @@ export class Tracing implements Integration {
681668
const hub = _getCurrentHub();
682669
if (hub) {
683670
const span = hub.startSpan(spanContext);
684-
Tracing._addOffsetToSpan(span as SpanClass);
685671
Tracing._activities[Tracing._currentIndex] = {
686672
name,
687673
span,
@@ -738,18 +724,6 @@ export class Tracing implements Integration {
738724
});
739725
}
740726
span.finish();
741-
// If there is an offset in data, update timestamps accordingly
742-
if (
743-
global.performance &&
744-
span.data &&
745-
typeof span.data.offset === 'number' &&
746-
typeof span.timestamp === 'number'
747-
) {
748-
const timeOrigin = Tracing._msToSec(performance.timeOrigin);
749-
const duration = span.timestamp - span.startTimestamp;
750-
span.startTimestamp = timeOrigin + span.data.offset;
751-
span.timestamp = timeOrigin + duration;
752-
}
753727
}
754728
// tslint:disable-next-line: no-dynamic-delete
755729
delete Tracing._activities[id];

packages/apm/src/span.ts

Lines changed: 15 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,7 @@
22

33
import { getCurrentHub, Hub } from '@sentry/hub';
44
import { Span as SpanInterface, SpanContext, SpanStatus } from '@sentry/types';
5-
import {
6-
dropUndefinedKeys,
7-
dynamicRequire,
8-
getGlobalObject,
9-
isInstanceOf,
10-
isNodeEnv,
11-
logger,
12-
timestampWithMs,
13-
uuid4,
14-
} from '@sentry/utils';
15-
16-
const INITIAL_TIME = Date.now();
17-
18-
const performanceFallback: Pick<Performance, 'now'> = {
19-
now(): number {
20-
return INITIAL_TIME - Date.now();
21-
},
22-
};
23-
24-
const crossPlatformPerformance: Pick<Performance, 'now'> = (() => {
25-
if (isNodeEnv()) {
26-
try {
27-
const perfHooks = dynamicRequire(module, 'perf_hooks') as { performance: Performance };
28-
return perfHooks.performance;
29-
} catch (_) {
30-
return performanceFallback;
31-
}
32-
}
33-
return getGlobalObject<Window>().performance || performanceFallback;
34-
})();
5+
import { dropUndefinedKeys, isInstanceOf, logger, timestampWithMs, uuid4 } from '@sentry/utils';
356

367
// TODO: Should this be exported?
378
export const TRACEPARENT_REGEXP = new RegExp(
@@ -106,21 +77,10 @@ export class Span implements SpanInterface, SpanContext {
10677
public sampled?: boolean;
10778

10879
/**
109-
* Timestamp when the span was created.
80+
* Timestamp in seconds when the span was created.
11081
*/
11182
public startTimestamp: number = timestampWithMs();
11283

113-
/**
114-
* Internal start time tracked with a monotonic clock.
115-
*
116-
* Works with mostly any browser version released since 2012.
117-
* https://caniuse.com/#search=performance.now
118-
*
119-
* Works with Node.js v8.5.0 or higher.
120-
* https://nodejs.org/api/perf_hooks.html#perf_hooks_performance_now
121-
*/
122-
private readonly _startTimestampMonotonic: number = crossPlatformPerformance.now();
123-
12484
/**
12585
* Finish timestamp of the span.
12686
*/
@@ -301,8 +261,7 @@ export class Span implements SpanInterface, SpanContext {
301261
return undefined;
302262
}
303263

304-
const durationSeconds = (crossPlatformPerformance.now() - this._startTimestampMonotonic) / 1000;
305-
this.timestamp = this.startTimestamp + durationSeconds;
264+
this.timestamp = timestampWithMs();
306265

307266
if (this.spanRecorder === undefined) {
308267
return undefined;
@@ -324,10 +283,21 @@ export class Span implements SpanInterface, SpanContext {
324283
logger.warn('Discarding transaction Span without sampling decision');
325284
return undefined;
326285
}
286+
327287
const finishedSpans = this.spanRecorder ? this.spanRecorder.finishedSpans.filter(s => s !== this) : [];
328288

289+
// The reason we do `useLastSpanTimestamp` is that if we use the Tracing integration with an idle transaction.
290+
// The idea is that after e.g. 500ms idle time, we finish the transaction
291+
// But we don't want the "idle time" taking into account for the end timestamp of the transaction
292+
// instead we search for the timestamp of the last finished span and change the end timestamp to the same
293+
// timestamp of the last finished span
329294
if (useLastSpanTimestamp && finishedSpans.length > 0) {
330-
this.timestamp = finishedSpans[finishedSpans.length - 1].timestamp;
295+
this.timestamp = finishedSpans.reduce((prev: Span, current: Span) => {
296+
if (prev.timestamp && current.timestamp) {
297+
return prev.timestamp > current.timestamp ? prev : current;
298+
}
299+
return prev;
300+
}).timestamp;
331301
}
332302

333303
return this._hub.captureEvent({

packages/utils/src/misc.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -346,11 +346,52 @@ function _htmlElementAsString(el: unknown): string {
346346
return out.join('');
347347
}
348348

349+
const INITIAL_TIME = Date.now();
350+
let prevNow = 0;
351+
352+
const performanceFallback: Pick<Performance, 'now' | 'timeOrigin'> = {
353+
now(): number {
354+
let now = Date.now() - INITIAL_TIME;
355+
if (now < prevNow) {
356+
now = prevNow;
357+
}
358+
prevNow = now;
359+
return now;
360+
},
361+
timeOrigin: INITIAL_TIME,
362+
};
363+
364+
export const crossPlatformPerformance: Pick<Performance, 'now' | 'timeOrigin'> = (() => {
365+
if (isNodeEnv()) {
366+
try {
367+
const perfHooks = dynamicRequire(module, 'perf_hooks') as { performance: Performance };
368+
return perfHooks.performance;
369+
} catch (_) {
370+
return performanceFallback;
371+
}
372+
}
373+
374+
if (getGlobalObject<Window>().performance) {
375+
// Polyfill for performance.timeOrigin.
376+
//
377+
// While performance.timing.navigationStart is deprecated in favor of performance.timeOrigin, performance.timeOrigin
378+
// is not as widely supported. Namely, performance.timeOrigin is undefined in Safari as of writing.
379+
// tslint:disable-next-line:strict-type-predicates
380+
if (performance.timeOrigin === undefined) {
381+
// @ts-ignore
382+
// tslint:disable-next-line:deprecation
383+
performance.timeOrigin = performance.timing.navigationStart;
384+
}
385+
}
386+
387+
return getGlobalObject<Window>().performance || performanceFallback;
388+
})();
389+
349390
/**
350-
* Returns a timestamp in seconds with milliseconds precision.
391+
* Returns a timestamp in seconds with milliseconds precision since the UNIX epoch calculated with the monotonic clock.
351392
*/
352393
export function timestampWithMs(): number {
353-
return Date.now() / 1000;
394+
return (crossPlatformPerformance.timeOrigin + crossPlatformPerformance.now()) / 1000;
354395
}
355396

356397
// https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string

0 commit comments

Comments
 (0)