diff --git a/packages/core/src/utils/time.ts b/packages/core/src/utils/time.ts index ff858a15b0ac..bfed9386f8bb 100644 --- a/packages/core/src/utils/time.ts +++ b/packages/core/src/utils/time.ts @@ -74,22 +74,23 @@ export function timestampInSeconds(): number { /** * Cached result of getBrowserTimeOrigin. */ -let cachedTimeOrigin: [number | undefined, string] | undefined; +let cachedTimeOrigin: number | null | undefined = null; /** * Gets the time origin and the mode used to determine it. + * TODO: move to `@sentry/browser-utils` package. */ -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 +100,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 +116,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; } /** @@ -129,9 +135,9 @@ function getBrowserTimeOrigin(): [number | undefined, string] { * performance API is available. */ export function browserPerformanceTimeOrigin(): number | undefined { - if (!cachedTimeOrigin) { + if (cachedTimeOrigin === null) { cachedTimeOrigin = getBrowserTimeOrigin(); } - return cachedTimeOrigin[0]; + return cachedTimeOrigin; } 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..e40c607cb409 --- /dev/null +++ b/packages/core/test/lib/utils/time.test.ts @@ -0,0 +1,142 @@ +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 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(); + }); + + describe('caching', () => { + it('caches `undefined` result', async () => { + vi.stubGlobal('performance', undefined); + + const timeModule = await import(`../../../src/utils/time?update=${Date.now()}`); + + const result1 = timeModule.browserPerformanceTimeOrigin(); + + expect(result1).toBeUndefined(); + + 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(); + }); + }); +});