Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 21 additions & 15 deletions packages/core/src/utils/time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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
Expand All @@ -111,27 +116,28 @@ 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;
}

/**
* The number of milliseconds since the UNIX epoch. This value is only usable in a browser, and only when the
* performance API is available.
*/
export function browserPerformanceTimeOrigin(): number | undefined {
if (!cachedTimeOrigin) {
if (cachedTimeOrigin === null) {
cachedTimeOrigin = getBrowserTimeOrigin();
}

return cachedTimeOrigin[0];
return cachedTimeOrigin;
}
142 changes: 142 additions & 0 deletions packages/core/test/lib/utils/time.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
Loading