Skip to content

Commit a6a4f7b

Browse files
authored
ref(core): Streamline and test browserPerformanceTimeOrigin (#18708)
- Remove a couple of bytes, by no longer storing the source of the timeOrigin. While this would be arguably valuable, we never used this information, so right now it was just wasted space. If we ever need it, we can bring it back of course) - Add tests for the time origin determination, reliability testing and fallback logic - Add TODOs for future improvements Blocked on major: We can remove an entire branch of this code, if we decide to drop Safari 14 support (opened #18707 to track)
1 parent 314babc commit a6a4f7b

File tree

2 files changed

+163
-15
lines changed

2 files changed

+163
-15
lines changed

packages/core/src/utils/time.ts

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -74,22 +74,23 @@ export function timestampInSeconds(): number {
7474
/**
7575
* Cached result of getBrowserTimeOrigin.
7676
*/
77-
let cachedTimeOrigin: [number | undefined, string] | undefined;
77+
let cachedTimeOrigin: number | null | undefined = null;
7878

7979
/**
8080
* Gets the time origin and the mode used to determine it.
81+
* TODO: move to `@sentry/browser-utils` package.
8182
*/
82-
function getBrowserTimeOrigin(): [number | undefined, string] {
83+
function getBrowserTimeOrigin(): number | undefined {
8384
// Unfortunately browsers may report an inaccurate time origin data, through either performance.timeOrigin or
8485
// performance.timing.navigationStart, which results in poor results in performance data. We only treat time origin
8586
// data as reliable if they are within a reasonable threshold of the current time.
86-
8787
const { performance } = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window;
8888
if (!performance?.now) {
89-
return [undefined, 'none'];
89+
return undefined;
9090
}
9191

92-
const threshold = 3600 * 1000;
92+
// TOOD: We should probably set a much tighter threshold here as skew can already happen within just a few minutes.
93+
const threshold = 3_600_000; // 1 hour in milliseconds
9394
const performanceNow = performance.now();
9495
const dateNow = Date.now();
9596

@@ -99,6 +100,10 @@ function getBrowserTimeOrigin(): [number | undefined, string] {
99100
: threshold;
100101
const timeOriginIsReliable = timeOriginDelta < threshold;
101102

103+
// TODO: Remove all code related to `performance.timing.navigationStart` once we drop support for Safari 14.
104+
// `performance.timeSince` is available in Safari 15.
105+
// see: https://caniuse.com/mdn-api_performance_timeorigin
106+
102107
// While performance.timing.navigationStart is deprecated in favor of performance.timeOrigin, performance.timeOrigin
103108
// is not as widely supported. Namely, performance.timeOrigin is undefined in Safari as of writing.
104109
// Also as of writing, performance.timing is not available in Web Workers in mainstream browsers, so it is not always
@@ -111,27 +116,28 @@ function getBrowserTimeOrigin(): [number | undefined, string] {
111116
const navigationStartDelta = hasNavigationStart ? Math.abs(navigationStart + performanceNow - dateNow) : threshold;
112117
const navigationStartIsReliable = navigationStartDelta < threshold;
113118

114-
if (timeOriginIsReliable || navigationStartIsReliable) {
115-
// Use the more reliable time origin
116-
if (timeOriginDelta <= navigationStartDelta) {
117-
return [performance.timeOrigin, 'timeOrigin'];
118-
} else {
119-
return [navigationStart, 'navigationStart'];
120-
}
119+
// TODO: Since timeOrigin explicitly replaces navigationStart, we should probably remove the navigationStartIsReliable check.
120+
if (timeOriginIsReliable && timeOriginDelta <= navigationStartDelta) {
121+
return performance.timeOrigin;
122+
}
123+
124+
if (navigationStartIsReliable) {
125+
return navigationStart;
121126
}
122127

128+
// TODO: We should probably fall back to Date.now() - performance.now(), since this is still more accurate than just Date.now() (?)
123129
// Either both timeOrigin and navigationStart are skewed or neither is available, fallback to Date.
124-
return [dateNow, 'dateNow'];
130+
return dateNow;
125131
}
126132

127133
/**
128134
* The number of milliseconds since the UNIX epoch. This value is only usable in a browser, and only when the
129135
* performance API is available.
130136
*/
131137
export function browserPerformanceTimeOrigin(): number | undefined {
132-
if (!cachedTimeOrigin) {
138+
if (cachedTimeOrigin === null) {
133139
cachedTimeOrigin = getBrowserTimeOrigin();
134140
}
135141

136-
return cachedTimeOrigin[0];
142+
return cachedTimeOrigin;
137143
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { describe, expect, it, vi } from 'vitest';
2+
3+
async function getFreshPerformanceTimeOrigin() {
4+
// Adding the query param with the date, forces a fresh import each time this is called
5+
// otherwise, the dynamic import would be cached and thus fall back to the cached value.
6+
const timeModule = await import(`../../../src/utils/time?update=${Date.now()}`);
7+
return timeModule.browserPerformanceTimeOrigin();
8+
}
9+
10+
const RELIABLE_THRESHOLD_MS = 3_600_000;
11+
12+
describe('browserPerformanceTimeOrigin', () => {
13+
it('returns `performance.timeOrigin` if it is available and reliable', async () => {
14+
const timeOrigin = await getFreshPerformanceTimeOrigin();
15+
expect(timeOrigin).toBeDefined();
16+
expect(timeOrigin).toBeGreaterThan(0);
17+
expect(timeOrigin).toBeLessThan(Date.now());
18+
expect(timeOrigin).toBe(performance.timeOrigin);
19+
});
20+
21+
it('returns `undefined` if `performance.now` is not available', async () => {
22+
vi.stubGlobal('performance', undefined);
23+
24+
const timeOrigin = await getFreshPerformanceTimeOrigin();
25+
expect(timeOrigin).toBeUndefined();
26+
27+
vi.unstubAllGlobals();
28+
});
29+
30+
it('returns `Date.now()` if `performance.timeOrigin` is not reliable', async () => {
31+
const currentTimeMs = 1767778040866;
32+
33+
const unreliableTime = currentTimeMs - RELIABLE_THRESHOLD_MS - 2_000;
34+
35+
const timeSincePageloadMs = 1_234.789;
36+
37+
vi.useFakeTimers();
38+
vi.setSystemTime(new Date(currentTimeMs));
39+
40+
vi.stubGlobal('performance', {
41+
timeOrigin: unreliableTime,
42+
timing: {
43+
navigationStart: unreliableTime,
44+
},
45+
now: () => timeSincePageloadMs,
46+
});
47+
48+
const timeOrigin = await getFreshPerformanceTimeOrigin();
49+
expect(timeOrigin).toBe(1767778040866);
50+
51+
vi.useRealTimers();
52+
vi.unstubAllGlobals();
53+
});
54+
55+
it('returns `performance.timing.navigationStart` if `performance.timeOrigin` is not available', async () => {
56+
const currentTimeMs = 1767778040870;
57+
58+
const navigationStartMs = currentTimeMs - 2_000;
59+
60+
const timeSincePageloadMs = 1_234.789;
61+
62+
vi.useFakeTimers();
63+
vi.setSystemTime(new Date(currentTimeMs));
64+
65+
vi.stubGlobal('performance', {
66+
timeOrigin: undefined,
67+
timing: {
68+
navigationStart: navigationStartMs,
69+
},
70+
now: () => timeSincePageloadMs,
71+
});
72+
73+
const timeOrigin = await getFreshPerformanceTimeOrigin();
74+
expect(timeOrigin).toBe(navigationStartMs);
75+
76+
vi.useRealTimers();
77+
vi.unstubAllGlobals();
78+
});
79+
80+
it('returns `performance.timing.navigationStart` if `performance.timeOrigin` is less reliable', async () => {
81+
const currentTimeMs = 1767778040874;
82+
83+
const navigationStartMs = currentTimeMs - 2_000;
84+
85+
const timeSincePageloadMs = 1_234.789;
86+
87+
vi.useFakeTimers();
88+
vi.setSystemTime(new Date(currentTimeMs));
89+
90+
vi.stubGlobal('performance', {
91+
timeOrigin: navigationStartMs - 1,
92+
timing: {
93+
navigationStart: navigationStartMs,
94+
},
95+
now: () => timeSincePageloadMs,
96+
});
97+
98+
const timeOrigin = await getFreshPerformanceTimeOrigin();
99+
expect(timeOrigin).toBe(navigationStartMs);
100+
101+
vi.useRealTimers();
102+
vi.unstubAllGlobals();
103+
});
104+
105+
describe('caching', () => {
106+
it('caches `undefined` result', async () => {
107+
vi.stubGlobal('performance', undefined);
108+
109+
const timeModule = await import(`../../../src/utils/time?update=${Date.now()}`);
110+
111+
const result1 = timeModule.browserPerformanceTimeOrigin();
112+
113+
expect(result1).toBeUndefined();
114+
115+
vi.stubGlobal('performance', {
116+
timeOrigin: 1000,
117+
now: () => 100,
118+
});
119+
120+
const result2 = timeModule.browserPerformanceTimeOrigin();
121+
expect(result2).toBeUndefined(); // Should still be undefined due to caching
122+
123+
vi.unstubAllGlobals();
124+
});
125+
126+
it('caches `number` result', async () => {
127+
const timeModule = await import(`../../../src/utils/time?update=${Date.now()}`);
128+
const result = timeModule.browserPerformanceTimeOrigin();
129+
const timeOrigin = performance.timeOrigin;
130+
expect(result).toBe(timeOrigin);
131+
132+
vi.stubGlobal('performance', {
133+
now: undefined,
134+
});
135+
136+
const result2 = timeModule.browserPerformanceTimeOrigin();
137+
expect(result2).toBe(timeOrigin);
138+
139+
vi.unstubAllGlobals();
140+
});
141+
});
142+
});

0 commit comments

Comments
 (0)