From 0417dd6d488a809e4d04782172b3dd9b44cbeef5 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 7 Jan 2026 11:05:01 +0100 Subject: [PATCH 1/6] ref(core): Streamline and test `browserPerformanceTimeOrigin` --- packages/core/src/utils/time.ts | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/packages/core/src/utils/time.ts b/packages/core/src/utils/time.ts index ff858a15b0ac..c9981b352059 100644 --- a/packages/core/src/utils/time.ts +++ b/packages/core/src/utils/time.ts @@ -74,22 +74,22 @@ export function timestampInSeconds(): number { /** * Cached result of getBrowserTimeOrigin. */ -let cachedTimeOrigin: [number | undefined, string] | undefined; +let cachedTimeOrigin: number | undefined; /** * Gets the time origin and the mode used to determine it. */ -function getBrowserTimeOrigin(): [number | undefined, string] { +function getBrowserTimeOrigin(): number | undefined { // Unfortunately browsers may report an inaccurate time origin data, through either performance.timeOrigin or // performance.timing.navigationStart, which results in poor results in performance data. We only treat time origin // data as reliable if they are within a reasonable threshold of the current time. - const { performance } = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window; if (!performance?.now) { - return [undefined, 'none']; + return undefined; } - const threshold = 3600 * 1000; + // TOOD: We should probably set a much tighter threshold here as skew can already happen within just a few minutes. + const threshold = 3_600_000; // 1 hour in milliseconds const performanceNow = performance.now(); const dateNow = Date.now(); @@ -99,6 +99,10 @@ function getBrowserTimeOrigin(): [number | undefined, string] { : threshold; const timeOriginIsReliable = timeOriginDelta < threshold; + // TODO: Remove all code related to `performance.timing.navigationStart` once we drop support for Safari 14. + // `performance.timeSince` is available in Safari 15. + // see: https://caniuse.com/mdn-api_performance_timeorigin + // While performance.timing.navigationStart is deprecated in favor of performance.timeOrigin, performance.timeOrigin // is not as widely supported. Namely, performance.timeOrigin is undefined in Safari as of writing. // Also as of writing, performance.timing is not available in Web Workers in mainstream browsers, so it is not always @@ -111,17 +115,18 @@ function getBrowserTimeOrigin(): [number | undefined, string] { const navigationStartDelta = hasNavigationStart ? Math.abs(navigationStart + performanceNow - dateNow) : threshold; const navigationStartIsReliable = navigationStartDelta < threshold; - if (timeOriginIsReliable || navigationStartIsReliable) { - // Use the more reliable time origin - if (timeOriginDelta <= navigationStartDelta) { - return [performance.timeOrigin, 'timeOrigin']; - } else { - return [navigationStart, 'navigationStart']; - } + // TODO: Since timeOrigin explicitly replaces navigationStart, we should probably remove the navigationStartIsReliable check. + if (timeOriginIsReliable && timeOriginDelta <= navigationStartDelta) { + return performance.timeOrigin; + } + + if (navigationStartIsReliable) { + return navigationStart; } + // TODO: We should probably fall back to Date.now() - performance.now(), since this is still more accurate than just Date.now() (?) // Either both timeOrigin and navigationStart are skewed or neither is available, fallback to Date. - return [dateNow, 'dateNow']; + return dateNow; } /** @@ -133,5 +138,5 @@ export function browserPerformanceTimeOrigin(): number | undefined { cachedTimeOrigin = getBrowserTimeOrigin(); } - return cachedTimeOrigin[0]; + return cachedTimeOrigin; } From e32955f4692fc0aeaf5bdefb22a697d2c364d342 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 7 Jan 2026 11:07:22 +0100 Subject: [PATCH 2/6] add test file --- packages/core/src/utils/time.ts | 1 + packages/core/test/lib/utils/time.test.ts | 105 ++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 packages/core/test/lib/utils/time.test.ts diff --git a/packages/core/src/utils/time.ts b/packages/core/src/utils/time.ts index c9981b352059..15a9448ce2e2 100644 --- a/packages/core/src/utils/time.ts +++ b/packages/core/src/utils/time.ts @@ -78,6 +78,7 @@ let cachedTimeOrigin: number | undefined; /** * Gets the time origin and the mode used to determine it. + * TODO: move to `@sentry/browser-utils` package. */ function getBrowserTimeOrigin(): number | undefined { // Unfortunately browsers may report an inaccurate time origin data, through either performance.timeOrigin or diff --git a/packages/core/test/lib/utils/time.test.ts b/packages/core/test/lib/utils/time.test.ts new file mode 100644 index 000000000000..a7b194dfa288 --- /dev/null +++ b/packages/core/test/lib/utils/time.test.ts @@ -0,0 +1,105 @@ +import { describe, expect, it, vi } from 'vitest'; + +async function getFreshPerformanceTimeOrigin() { + // Adding the query param with the date, forces a fresh import each time this is called + // otherwise, the dynamic import would be cached and thus fall back to the cached value. + const timeModule = await import(`../../../src/utils/time?update=${Date.now()}`); + return timeModule.browserPerformanceTimeOrigin(); +} + +const RELIABLE_THRESHOLD_MS = 3_600_000; + +describe('browserPerformanceTimeOrigin', () => { + it('returns `performance.timeOrigin` if it is available and reliable', async () => { + const timeOrigin = await getFreshPerformanceTimeOrigin(); + expect(timeOrigin).toBeDefined(); + expect(timeOrigin).toBeGreaterThan(0); + expect(timeOrigin).toBeLessThan(Date.now()); + expect(timeOrigin).toBe(performance.timeOrigin); + }); + + it('returns `undefined` if `performance.now` is not available', async () => { + vi.stubGlobal('performance', undefined); + + const timeOrigin = await getFreshPerformanceTimeOrigin(); + expect(timeOrigin).toBeUndefined(); + + vi.unstubAllGlobals(); + }); + + it('returns `Date.now()` if `performance.timeOrigin` is not reliable', async () => { + const currentTimeMs = 1767778040866; + + const unreliableTime = currentTimeMs - RELIABLE_THRESHOLD_MS - 2_000; + + const timeSincePageloadMs = 1_234.789; + + vi.useFakeTimers(); + vi.setSystemTime(new Date(currentTimeMs)); + + vi.stubGlobal('performance', { + timeOrigin: unreliableTime, + timing: { + navigationStart: unreliableTime, + }, + now: () => timeSincePageloadMs, + }); + + const timeOrigin = await getFreshPerformanceTimeOrigin(); + expect(timeOrigin).toBe(1767778040866); + + vi.useRealTimers(); + vi.unstubAllGlobals(); + }); + + it('returns `performance.timing.navigationStart` if `performance.timeOrigin` is not available', async () => { + const currentTimeMs = 1767778040870; + + const navigationStartMs = currentTimeMs - 2_000; + // const unreliableTime = currentTimeMs - RELIABLE_THRESHOLD_MS - 2_000; + + const timeSincePageloadMs = 1_234.789; + + vi.useFakeTimers(); + vi.setSystemTime(new Date(currentTimeMs)); + + vi.stubGlobal('performance', { + timeOrigin: undefined, + timing: { + navigationStart: navigationStartMs, + }, + now: () => timeSincePageloadMs, + }); + + const timeOrigin = await getFreshPerformanceTimeOrigin(); + expect(timeOrigin).toBe(navigationStartMs); + + vi.useRealTimers(); + vi.unstubAllGlobals(); + }); + + it('returns `performance.timing.navigationStart` if `performance.timeOrigin` is less reliable', async () => { + const currentTimeMs = 1767778040874; + + const navigationStartMs = currentTimeMs - 2_000; + + const timeSincePageloadMs = 1_234.789; + + vi.useFakeTimers(); + vi.setSystemTime(new Date(currentTimeMs)); + + vi.stubGlobal('performance', { + timeOrigin: navigationStartMs - 1, + timing: { + navigationStart: navigationStartMs, + }, + now: () => timeSincePageloadMs, + }); + + const timeOrigin = await getFreshPerformanceTimeOrigin(); + expect(timeOrigin).toBe(navigationStartMs); + + vi.useRealTimers(); + vi.unstubAllGlobals(); + }); +}); From 3dca9511b1d1e35ed46891c0324ec1f7fe12aa46 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 7 Jan 2026 11:40:34 +0100 Subject: [PATCH 3/6] fix repeated cache misses when caching `undefined` --- packages/core/src/utils/time.ts | 4 +-- packages/core/test/lib/utils/time.test.ts | 43 +++++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/packages/core/src/utils/time.ts b/packages/core/src/utils/time.ts index 15a9448ce2e2..bfed9386f8bb 100644 --- a/packages/core/src/utils/time.ts +++ b/packages/core/src/utils/time.ts @@ -74,7 +74,7 @@ export function timestampInSeconds(): number { /** * Cached result of getBrowserTimeOrigin. */ -let cachedTimeOrigin: number | undefined; +let cachedTimeOrigin: number | null | undefined = null; /** * Gets the time origin and the mode used to determine it. @@ -135,7 +135,7 @@ function getBrowserTimeOrigin(): number | undefined { * performance API is available. */ export function browserPerformanceTimeOrigin(): number | undefined { - if (!cachedTimeOrigin) { + if (cachedTimeOrigin === null) { cachedTimeOrigin = getBrowserTimeOrigin(); } diff --git a/packages/core/test/lib/utils/time.test.ts b/packages/core/test/lib/utils/time.test.ts index a7b194dfa288..dea657257174 100644 --- a/packages/core/test/lib/utils/time.test.ts +++ b/packages/core/test/lib/utils/time.test.ts @@ -102,4 +102,47 @@ describe('browserPerformanceTimeOrigin', () => { vi.useRealTimers(); vi.unstubAllGlobals(); }); + + describe('caching', () => { + it('caches `undefined` result', async () => { + vi.stubGlobal('performance', undefined); + + // Get a fresh module instance + const timeModule = await import(`../../../src/utils/time?update=${Date.now()}`); + + // Call browserPerformanceTimeOrigin multiple times + const result1 = timeModule.browserPerformanceTimeOrigin(); + + // All should be undefined + expect(result1).toBeUndefined(); + + // Now set performance to a valid object - if caching works, the result should still be undefined + // because the first call should have cached the undefined result + vi.stubGlobal('performance', { + timeOrigin: 1000, + now: () => 100, + }); + + const result2 = timeModule.browserPerformanceTimeOrigin(); + expect(result2).toBeUndefined(); // Should still be undefined due to caching + + vi.unstubAllGlobals(); + }); + + it('caches `number` result', async () => { + const timeModule = await import(`../../../src/utils/time?update=${Date.now()}`); + const result = timeModule.browserPerformanceTimeOrigin(); + const timeOrigin = performance.timeOrigin; + expect(result).toBe(timeOrigin); + + vi.stubGlobal('performance', { + now: undefined, + }); + + const result2 = timeModule.browserPerformanceTimeOrigin(); + expect(result2).toBe(timeOrigin); + + vi.unstubAllGlobals(); + }); + }); }); From 3b6d45ef89f689cd4582747b22b9cecf5133ae03 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 7 Jan 2026 11:43:09 +0100 Subject: [PATCH 4/6] Apply suggestions from code review --- packages/core/test/lib/utils/time.test.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/core/test/lib/utils/time.test.ts b/packages/core/test/lib/utils/time.test.ts index dea657257174..2d96d75226ab 100644 --- a/packages/core/test/lib/utils/time.test.ts +++ b/packages/core/test/lib/utils/time.test.ts @@ -107,17 +107,12 @@ describe('browserPerformanceTimeOrigin', () => { it('caches `undefined` result', async () => { vi.stubGlobal('performance', undefined); - // Get a fresh module instance const timeModule = await import(`../../../src/utils/time?update=${Date.now()}`); - // Call browserPerformanceTimeOrigin multiple times const result1 = timeModule.browserPerformanceTimeOrigin(); - // All should be undefined expect(result1).toBeUndefined(); - - // Now set performance to a valid object - if caching works, the result should still be undefined - // because the first call should have cached the undefined result + vi.stubGlobal('performance', { timeOrigin: 1000, now: () => 100, From faec1fbe4a051c78f870158da030fe36d782bdd3 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 7 Jan 2026 11:44:31 +0100 Subject: [PATCH 5/6] Update packages/core/test/lib/utils/time.test.ts --- packages/core/test/lib/utils/time.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/test/lib/utils/time.test.ts b/packages/core/test/lib/utils/time.test.ts index 2d96d75226ab..051782fb3036 100644 --- a/packages/core/test/lib/utils/time.test.ts +++ b/packages/core/test/lib/utils/time.test.ts @@ -56,7 +56,6 @@ describe('browserPerformanceTimeOrigin', () => { const currentTimeMs = 1767778040870; const navigationStartMs = currentTimeMs - 2_000; - // const unreliableTime = currentTimeMs - RELIABLE_THRESHOLD_MS - 2_000; const timeSincePageloadMs = 1_234.789; From 12da3f01e002b27babce49bc416e04ccc9075ae0 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 7 Jan 2026 11:55:02 +0100 Subject: [PATCH 6/6] format --- packages/core/test/lib/utils/time.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/test/lib/utils/time.test.ts b/packages/core/test/lib/utils/time.test.ts index 051782fb3036..e40c607cb409 100644 --- a/packages/core/test/lib/utils/time.test.ts +++ b/packages/core/test/lib/utils/time.test.ts @@ -111,7 +111,7 @@ describe('browserPerformanceTimeOrigin', () => { const result1 = timeModule.browserPerformanceTimeOrigin(); expect(result1).toBeUndefined(); - + vi.stubGlobal('performance', { timeOrigin: 1000, now: () => 100,