Skip to content

Commit 1be9b73

Browse files
HazATrhcarvalho
andauthored
ref(apm): Always use monotonic clock for time calculations (#2485)
* ref(apm): Always use monotonic clock for time calculations Also remove offset time calculations since everything is based on timeOrigin * ref: Changelog * doc: Document trimming of transaction end time * doc(apm): Document argument to Span.finish inline Document the argument using the same format used in microsoft/TypeScript code base. * Update packages/apm/src/span.ts Co-Authored-By: Rodolfo Carvalho <[email protected]> * ref: Remove empty blocking test * Remove logging for discarded entries We may reintroduce this later on-demand. For now, reduce noise. Co-authored-by: Rodolfo Carvalho <[email protected]> Co-authored-by: Rodolfo Carvalho <[email protected]>
1 parent 57fe8c6 commit 1be9b73

File tree

5 files changed

+77
-155
lines changed

5 files changed

+77
-155
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- [apm] feat: Add a simple heartbeat check, if activities don't change in 3 beats, finish the transaction (#2478)
88
- [apm] feat: Make use of the `performance` browser API to provide better instrumentation (#2474)
99
- [browser] ref: Move global error handler + unhandled promise rejection to instrument (#2475)
10+
- [apm] ref: Always use monotonic clock for time calculations (#2485)
1011
- [apm] fix: Add trace context to all events (#2486)
1112

1213
## 5.13.2

packages/apm/src/integrations/tracing.ts

Lines changed: 15 additions & 43 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
@@ -458,8 +451,8 @@ export class Tracing implements Integration {
458451
if (active) {
459452
Tracing._addPerformanceEntries(active);
460453
logger.log('[Tracing] finishIdleTransaction', active.transaction);
461-
// true = use timestamp of last span
462-
active.finish(true);
454+
active.finish(/*trimEnd*/ true);
455+
Tracing._resetActiveTransaction();
463456
}
464457
}
465458

@@ -542,6 +535,10 @@ export class Tracing implements Integration {
542535
const startTime = Tracing._msToSec(entry.startTime as number);
543536
const duration = Tracing._msToSec(entry.duration as number);
544537

538+
if (transactionSpan.op === 'navigation' && timeOrigin + startTime < transactionSpan.startTimestamp) {
539+
return;
540+
}
541+
545542
switch (entry.entryType) {
546543
case 'navigation':
547544
addPerformanceNavigationTiming(transactionSpan, entry, 'unloadEvent');
@@ -630,18 +627,6 @@ export class Tracing implements Integration {
630627
}
631628
}
632629

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-
645630
/**
646631
* Converts from milliseconds to seconds
647632
* @param time time in ms
@@ -681,7 +666,6 @@ export class Tracing implements Integration {
681666
const hub = _getCurrentHub();
682667
if (hub) {
683668
const span = hub.startSpan(spanContext);
684-
Tracing._addOffsetToSpan(span as SpanClass);
685669
Tracing._activities[Tracing._currentIndex] = {
686670
name,
687671
span,
@@ -738,18 +722,6 @@ export class Tracing implements Integration {
738722
});
739723
}
740724
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-
}
753725
}
754726
// tslint:disable-next-line: no-dynamic-delete
755727
delete Tracing._activities[id];

packages/apm/src/span.ts

Lines changed: 18 additions & 49 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,23 +77,12 @@ 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

11384
/**
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-
124-
/**
125-
* Finish timestamp of the span.
85+
* Timestamp in seconds when the span ended.
12686
*/
12787
public timestamp?: number;
12888

@@ -293,16 +253,19 @@ export class Span implements SpanInterface, SpanContext {
293253
}
294254

295255
/**
296-
* Sets the finish timestamp on the current span
256+
* Sets the finish timestamp on the current span.
257+
* @param trimEnd If true, sets the end timestamp of the transaction to the highest timestamp of child spans, trimming
258+
* the duration of the transaction span. This is useful to discard extra time in the transaction span that is not
259+
* accounted for in child spans, like what happens in the idle transaction Tracing integration, where we finish the
260+
* transaction after a given "idle time" and we don't want this "idle time" to be part of the transaction.
297261
*/
298-
public finish(useLastSpanTimestamp: boolean = false): string | undefined {
262+
public finish(trimEnd: boolean = false): string | undefined {
299263
// This transaction is already finished, so we should not flush it again.
300264
if (this.timestamp !== undefined) {
301265
return undefined;
302266
}
303267

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

307270
if (this.spanRecorder === undefined) {
308271
return undefined;
@@ -324,10 +287,16 @@ export class Span implements SpanInterface, SpanContext {
324287
logger.warn('Discarding transaction Span without sampling decision');
325288
return undefined;
326289
}
290+
327291
const finishedSpans = this.spanRecorder ? this.spanRecorder.finishedSpans.filter(s => s !== this) : [];
328292

329-
if (useLastSpanTimestamp && finishedSpans.length > 0) {
330-
this.timestamp = finishedSpans[finishedSpans.length - 1].timestamp;
293+
if (trimEnd && finishedSpans.length > 0) {
294+
this.timestamp = finishedSpans.reduce((prev: Span, current: Span) => {
295+
if (prev.timestamp && current.timestamp) {
296+
return prev.timestamp > current.timestamp ? prev : current;
297+
}
298+
return prev;
299+
}).timestamp;
331300
}
332301

333302
return this._hub.captureEvent({

packages/apm/test/tracing.test.ts

Lines changed: 0 additions & 61 deletions
This file was deleted.

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)