From 4552922b181140aadc94ba8a64452e28bf06f535 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 22 Jul 2026 12:17:13 +0200 Subject: [PATCH 1/2] fix(core): Account for clock drift in initial `timestampInSeconds()` call --- packages/core/src/utils/time.ts | 48 ++++++++++++++++---- packages/core/test/lib/utils/time.test.ts | 54 +++++++++++++++++++++-- 2 files changed, 91 insertions(+), 11 deletions(-) diff --git a/packages/core/src/utils/time.ts b/packages/core/src/utils/time.ts index 4d260069fae6..df88b350e4ae 100644 --- a/packages/core/src/utils/time.ts +++ b/packages/core/src/utils/time.ts @@ -40,6 +40,11 @@ function createUnixTimestampInSecondsFunc(): () => number { } const timeOrigin = performance.timeOrigin; + const performanceNow = withRandomSafeContext(() => performance.now()); + const dateNow = safeDateNow(); + if (!hasAccuratePerformanceTimestamp(timeOrigin, performanceNow, dateNow)) { + return dateTimestampInSeconds; + } // performance.now() is a monotonic clock, which means it starts at 0 when the process begins. To get the current // wall clock time (actual UNIX timestamp), we need to add the starting time origin and the current time elapsed. @@ -92,16 +97,12 @@ function getBrowserTimeOrigin(): number | undefined { return undefined; } - const threshold = 300_000; // 5 minutes in milliseconds const performanceNow = withRandomSafeContext(() => performance.now()); const dateNow = safeDateNow(); - const timeOrigin = performance.timeOrigin; - if (typeof timeOrigin === 'number') { - const timeOriginDelta = Math.abs(timeOrigin + performanceNow - dateNow); - if (timeOriginDelta < threshold) { - return timeOrigin; - } + + if (hasAccuratePerformanceTimestamp(timeOrigin, performanceNow, dateNow)) { + return timeOrigin; } // TODO: Remove all code related to `performance.timing.navigationStart` once we drop support for Safari 14. @@ -117,7 +118,7 @@ function getBrowserTimeOrigin(): number | undefined { const navigationStart = performance.timing?.navigationStart; if (typeof navigationStart === 'number') { const navigationStartDelta = Math.abs(navigationStart + performanceNow - dateNow); - if (navigationStartDelta < threshold) { + if (navigationStartDelta < WALL_CLOCK_DRIFT_THESHOLD) { return navigationStart; } } @@ -138,3 +139,34 @@ export function browserPerformanceTimeOrigin(): number | undefined { return cachedTimeOrigin; } + +const WALL_CLOCK_DRIFT_THESHOLD = 300_000; // 5 minutes in milliseconds + +/** + * Check if the current timestamp calculated from the Performance API from + * `performance.timeOrigin + performance.now()` is within a reasonable threshold of + * the current time calculated from the Date API from `Date.now()`. + * + * The `performance.now()` clock is a monotonic clock, which means it starts at 0 when the process starts. + * Unfortunately, it is known to drift behind the actual wall clock, for example when a device hibernates, + * or tabs are suspended / inactive for a long period of time. + * + * Our threshold is 5 minutes, which is a reasonable compromise between accuracy and performance. + * + * @param performanceTimeOrigin - The time origin reported by the Performance API. + * @param performanceNow - The current time reported by the Performance API. + * @param dateNow - The current time reported by the Date API. + * + * @returns `true` if the current performance API-derive time is considered accurate, `false` otherwise. + */ +function hasAccuratePerformanceTimestamp( + performanceTimeOrigin: number, + performanceNow: number, + dateNow: number, +): boolean { + if (typeof performanceTimeOrigin === 'number') { + const timeOriginDelta = Math.abs(performanceTimeOrigin + performanceNow - dateNow); + return timeOriginDelta < WALL_CLOCK_DRIFT_THESHOLD; + } + return false; +} diff --git a/packages/core/test/lib/utils/time.test.ts b/packages/core/test/lib/utils/time.test.ts index a1d537df5862..33b4927fffa3 100644 --- a/packages/core/test/lib/utils/time.test.ts +++ b/packages/core/test/lib/utils/time.test.ts @@ -1,13 +1,61 @@ -import { describe, expect, it, vi } from 'vitest'; +import { beforeEach } from 'node:test'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +let freshImportId = 0; + +async function getFreshTimeModule() { + return import(`../../../src/utils/time?update=${freshImportId++}`); +} async function getFreshPerformanceTimeOrigin() { - // Adding the query param with the date, forces a fresh import each time this is called + // Adding a query param 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()}`); + const timeModule = await getFreshTimeModule(); return timeModule.browserPerformanceTimeOrigin(); } const RELIABLE_THRESHOLD_MS = 300_000; +const DAY_MS = 24 * 60 * 60 * 1_000; + +afterEach(() => { + vi.useRealTimers(); + vi.unstubAllGlobals(); +}); + +describe('timestampInSeconds', () => { + it('uses the Date API when the performance timestamp is inaccurate on initialization', async () => { + const currentTimeMs = 1_800_000_000_000; + + vi.useFakeTimers(); + vi.setSystemTime(currentTimeMs); + vi.stubGlobal('performance', { + timeOrigin: currentTimeMs - 10 * DAY_MS, + now: () => 3 * DAY_MS, + }); + const timeModule = await getFreshTimeModule(); + + expect(timeModule.timestampInSeconds()).toBe(currentTimeMs / 1_000); + }); + + it.fails('uses the Date API when the performance clock drifts after initialization', async () => { + const initialTimeMs = 1_800_000_000_000; + let performanceNow = 0; + + vi.useFakeTimers(); + vi.setSystemTime(initialTimeMs); + vi.stubGlobal('performance', { + timeOrigin: initialTimeMs, + now: () => performanceNow, + }); + const timeModule = await getFreshTimeModule(); + timeModule.timestampInSeconds(); + + vi.setSystemTime(initialTimeMs + 10 * DAY_MS); + performanceNow += 3 * DAY_MS; + + expect(timeModule.timestampInSeconds()).toBe((initialTimeMs + 10 * DAY_MS) / 1_000); + }); +}); describe('browserPerformanceTimeOrigin', () => { it('returns `performance.timeOrigin` if it is available and reliable', async () => { From b4b663a8406bd2deeca4491735a64c122c33e313 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 22 Jul 2026 12:18:00 +0200 Subject: [PATCH 2/2] clean --- 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 33b4927fffa3..58598f00d281 100644 --- a/packages/core/test/lib/utils/time.test.ts +++ b/packages/core/test/lib/utils/time.test.ts @@ -1,4 +1,3 @@ -import { beforeEach } from 'node:test'; import { afterEach, describe, expect, it, vi } from 'vitest'; let freshImportId = 0;