-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
fix: Reimplement timestamp computation #2947
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,98 +1,148 @@ | ||
import { getGlobalObject } from './misc'; | ||
import { dynamicRequire, isNodeEnv } from './node'; | ||
|
||
const INITIAL_TIME = Date.now(); | ||
/** | ||
* An object that can return the current timestamp in seconds since the UNIX epoch. | ||
*/ | ||
interface TimestampSource { | ||
nowSeconds(): number; | ||
} | ||
|
||
/** | ||
* A TimestampSource implementation for environments that do not support the Performance Web API natively. | ||
* | ||
* Note that this TimestampSource does not use a monotonic clock. A call to `nowSeconds` may return a timestamp earlier | ||
* than a previously returned value. We do not try to emulate a monotonic behavior in order to facilitate debugging. It | ||
* is more obvious to explain "why does my span have negative duration" than "why my spans have zero duration". | ||
*/ | ||
const dateTimestampSource: TimestampSource = { | ||
nowSeconds: () => Date.now() / 1000, | ||
}; | ||
|
||
/** | ||
* Cross platform compatible partial performance implementation | ||
* A partial definition of the [Performance Web API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Performance} | ||
* for accessing a high resolution monotonic clock. | ||
*/ | ||
interface CrossPlatformPerformance { | ||
interface Performance { | ||
/** | ||
* The millisecond timestamp at which measurement began, measured in Unix time. | ||
*/ | ||
timeOrigin: number; | ||
/** | ||
* Returns the current timestamp in ms | ||
* Returns the current millisecond timestamp, where 0 represents the start of measurement. | ||
*/ | ||
now(): number; | ||
} | ||
|
||
let prevNow = 0; | ||
|
||
const performanceFallback: CrossPlatformPerformance = { | ||
now(): number { | ||
let now = Date.now() - INITIAL_TIME; | ||
if (now < prevNow) { | ||
now = prevNow; | ||
} | ||
prevNow = now; | ||
return now; | ||
}, | ||
timeOrigin: INITIAL_TIME, | ||
}; | ||
|
||
const crossPlatformPerformance: CrossPlatformPerformance = ((): CrossPlatformPerformance => { | ||
// React Native's performance.now() starts with a gigantic offset, so we need to wrap it. | ||
if (isReactNative()) { | ||
return getReactNativePerformanceWrapper(); | ||
} | ||
|
||
if (isNodeEnv()) { | ||
try { | ||
const perfHooks = dynamicRequire(module, 'perf_hooks') as { performance: CrossPlatformPerformance }; | ||
return perfHooks.performance; | ||
} catch (_) { | ||
return performanceFallback; | ||
} | ||
} | ||
|
||
/** | ||
* Returns a wrapper around the native Performance API browser implementation, or undefined for browsers that do not | ||
* support the API. | ||
* | ||
* Wrapping the native API works around differences in behavior from different browsers. | ||
*/ | ||
function getBrowserPerformance(): Performance | undefined { | ||
const { performance } = getGlobalObject<Window>(); | ||
|
||
if (!performance || !performance.now) { | ||
return performanceFallback; | ||
return undefined; | ||
} | ||
|
||
// Polyfill for performance.timeOrigin. | ||
// Replace performance.timeOrigin with our own timeOrigin based on Date.now(). | ||
// | ||
// 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. | ||
if (performance.timeOrigin === undefined) { | ||
// As of writing, performance.timing is not available in Web Workers in mainstream browsers, so it is not always a | ||
// valid fallback. In the absence of a initial time provided by the browser, fallback to INITIAL_TIME. | ||
// @ts-ignore ignored because timeOrigin is a readonly property but we want to override | ||
// eslint-disable-next-line deprecation/deprecation | ||
performance.timeOrigin = (performance.timing && performance.timing.navigationStart) || INITIAL_TIME; | ||
} | ||
// This is a partial workaround for browsers reporting performance.timeOrigin such that performance.timeOrigin + | ||
// performance.now() gives a date arbitrarily in the past. | ||
// | ||
// Additionally, computing timeOrigin in this way fills the gap for browsers where performance.timeOrigin is | ||
// undefined. | ||
// | ||
// The assumption that performance.timeOrigin + performance.now() ~= Date.now() is flawed, but we depend on it to | ||
// interact with data coming out of performance entries. | ||
// | ||
// Note that despite recommendations against it in the spec, browsers implement the Performance API with a clock that | ||
// might stop when the computer is asleep (and perhaps under other circumstances). Such behavior causes | ||
// performance.timeOrigin + performance.now() to have an arbitrary skew over Date.now(). In laptop computers, we have | ||
// observed skews that can be as long as days, weeks or months. | ||
// | ||
// See https://github.com/getsentry/sentry-javascript/issues/2590. | ||
// | ||
// BUG: despite our best intentions, this workaround has its limitations. It mostly addresses timings of pageload | ||
// transactions, but ignores the skew built up over time that can aversely affect timestamps of navigation | ||
// transactions of long-lived web pages. | ||
const timeOrigin = Date.now() - performance.now(); | ||
|
||
return performance; | ||
})(); | ||
return { | ||
now: () => performance.now(), | ||
timeOrigin, | ||
}; | ||
} | ||
|
||
/** | ||
* Returns a timestamp in seconds with milliseconds precision since the UNIX epoch calculated with the monotonic clock. | ||
* Returns the native Performance API implementation from Node.js. Returns undefined in old Node.js versions that don't | ||
* implement the API. | ||
*/ | ||
export function timestampWithMs(): number { | ||
return (crossPlatformPerformance.timeOrigin + crossPlatformPerformance.now()) / 1000; | ||
function getNodePerformance(): Performance | undefined { | ||
try { | ||
const perfHooks = dynamicRequire(module, 'perf_hooks') as { performance: Performance }; | ||
return perfHooks.performance; | ||
} catch (_) { | ||
return undefined; | ||
} | ||
} | ||
|
||
/** | ||
* Determines if running in react native | ||
* The Performance API implementation for the current platform, if available. | ||
*/ | ||
function isReactNative(): boolean { | ||
return getGlobalObject<Window>().navigator?.product === 'ReactNative'; | ||
} | ||
const platformPerformance: Performance | undefined = isNodeEnv() ? getNodePerformance() : getBrowserPerformance(); | ||
|
||
const timestampSource: TimestampSource = | ||
platformPerformance === undefined | ||
? dateTimestampSource | ||
: { | ||
nowSeconds: () => (platformPerformance.timeOrigin + platformPerformance.now()) / 1000, | ||
}; | ||
|
||
/** | ||
* Performance wrapper for react native as performance.now() has been found to start off with an unusual offset. | ||
* Returns a timestamp in seconds since the UNIX epoch using the Date API. | ||
*/ | ||
function getReactNativePerformanceWrapper(): CrossPlatformPerformance { | ||
// Performance only available >= RN 0.63 | ||
const { performance } = getGlobalObject<Window>(); | ||
if (performance && typeof performance.now === 'function') { | ||
const INITIAL_OFFSET = performance.now(); | ||
export const dateTimestampInSeconds = dateTimestampSource.nowSeconds.bind(dateTimestampSource); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see any gains from using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Functions would of course work. I started this with documentation, and a common interface seemed like a reasonable way to explain that there are going to be multiple ways to generate a timestamp. When exporting from |
||
|
||
/** | ||
* Returns a timestamp in seconds since the UNIX epoch using either the Performance or Date APIs, depending on the | ||
* availability of the Performance API. | ||
* | ||
* See `usingPerformanceAPI` to test whether the Performance API is used. | ||
* | ||
* BUG: Note that because of how browsers implement the Performance API, the clock might stop when the computer is | ||
* asleep. This creates a skew between `dateTimestampInSeconds` and `timestampInSeconds`. The | ||
* skew can grow to arbitrary amounts like days, weeks or months. | ||
* See https://github.com/getsentry/sentry-javascript/issues/2590. | ||
*/ | ||
export const timestampInSeconds = timestampSource.nowSeconds.bind(timestampSource); | ||
|
||
return { | ||
now(): number { | ||
return performance.now() - INITIAL_OFFSET; | ||
}, | ||
timeOrigin: INITIAL_TIME, | ||
}; | ||
// Re-exported with an old name for backwards-compatibility. | ||
export const timestampWithMs = timestampInSeconds; | ||
|
||
/** | ||
* A boolean that is true when timestampInSeconds uses the Performance API to produce monotonic timestamps. | ||
*/ | ||
export const usingPerformanceAPI = platformPerformance !== undefined; | ||
|
||
/** | ||
* 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 const browserPerformanceTimeOrigin = ((): number | undefined => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure how I feel about this tbh. Imports should imo be always deterministic, and here they are not. import { something } from 'utils';
if (something) {
// wait, why it can be undefined? I imported it.
} On the other hand, we'll end up with the code like: import { getSometing } from 'utils';
const something = getSometing();
if (something) {
// wait, why it can be undefined? I imported it.
} I'm not against or pro any of these options, so you can decide. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I agree. Here, this value is only The alternative was to repeat the fallback to If you have yet another idea how to deal with this, I'm happy to update. |
||
const { performance } = getGlobalObject<Window>(); | ||
if (!performance) { | ||
return undefined; | ||
} | ||
return performanceFallback; | ||
} | ||
if (performance.timeOrigin) { | ||
return 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 | ||
// a valid fallback. In the absence of an initial time provided by the browser, fallback to the current time from the | ||
// Date API. | ||
// eslint-disable-next-line deprecation/deprecation | ||
return (performance.timing && performance.timing.navigationStart) || Date.now(); | ||
})(); |
Uh oh!
There was an error while loading. Please reload this page.