Skip to content

fix(browser): Use drift-corrected time origin for INP, replay and profiling - #23067

Closed
Lms24 wants to merge 1 commit into
lms/fix-core-browser-timestampInSeconds-offset-clockdriftfrom
lms/fix-browser-live-performance-time-origin
Closed

Lms24 wants to merge 1 commit into
lms/fix-core-browser-timestampInSeconds-offset-clockdriftfrom
lms/fix-browser-live-performance-time-origin

Conversation

@Lms24

@Lms24 Lms24 commented Aug 5, 2026 •

Copy link
Copy Markdown
Member

No description provided.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 25c1b28. Configure here.

Comment thread packages/browser/src/profiling/utils.ts Outdated
@github-actions

github-actions Bot commented Aug 5, 2026 •

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 29.28 kB +0.16% +45 B 🔺
@sentry/browser - with treeshaking flags 27.55 kB +0.16% +43 B 🔺
@sentry/browser - with treeshaking flags tracing without tracing 27.44 kB +0.15% +40 B 🔺
@sentry/browser (incl. Tracing) 51.22 kB +0.14% +68 B 🔺
@sentry/browser (incl. Tracing + Span Streaming) 51.23 kB +0.12% +61 B 🔺
@sentry/browser (incl. Tracing, Profiling) 54.22 kB +0.08% +41 B 🔺
@sentry/browser (incl. Tracing, Replay) 90.81 kB +0.06% +51 B 🔺
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 79.9 kB +0.05% +36 B 🔺
@sentry/browser (incl. Tracing, Replay with Canvas) 95.5 kB +0.04% +37 B 🔺
@sentry/browser (incl. Tracing, Replay, Feedback) 108.46 kB +0.05% +49 B 🔺
@sentry/browser (incl. Feedback) 46.8 kB +0.08% +36 B 🔺
@sentry/browser (incl. sendFeedback) 34.35 kB +0.14% +48 B 🔺
@sentry/browser (incl. FeedbackAsync) 39.45 kB +0.1% +36 B 🔺
@sentry/browser (incl. Metrics) 30.29 kB +0.13% +38 B 🔺
@sentry/browser (incl. Logs) 30.56 kB +0.15% +45 B 🔺
@sentry/browser (incl. Metrics & Logs) 31.22 kB +0.13% +40 B 🔺
@sentry/react 31.12 kB +0.12% +35 B 🔺
@sentry/react (incl. Tracing) 53.59 kB +0.11% +55 B 🔺
@sentry/vue 36.78 kB +0.13% +47 B 🔺
@sentry/vue (incl. Tracing) 53.75 kB +0.1% +51 B 🔺
@sentry/svelte 29.31 kB +0.16% +45 B 🔺
CDN Bundle 31.05 kB +0.11% +33 B 🔺
CDN Bundle (incl. Tracing) 51.82 kB +0.09% +44 B 🔺
CDN Bundle (incl. Logs, Metrics) 33.31 kB +0.07% +23 B 🔺
CDN Bundle (incl. Tracing, Logs, Metrics) 53.81 kB +0.13% +67 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) 74.07 kB +0.1% +72 B 🔺
CDN Bundle (incl. Tracing, Replay) 89.43 kB +0.07% +61 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 91.37 kB +0.04% +35 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) 95.57 kB +0.04% +37 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 97.53 kB +0.04% +32 B 🔺
CDN Bundle - uncompressed 91.72 kB +0.07% +59 B 🔺
CDN Bundle (incl. Tracing) - uncompressed 154.12 kB +0.06% +88 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed 98.31 kB +0.08% +74 B 🔺
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 160.07 kB +0.06% +88 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 227.87 kB +0.03% +63 B 🔺
CDN Bundle (incl. Tracing, Replay) - uncompressed 273.87 kB +0.05% +112 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 279.81 kB +0.05% +112 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 287.57 kB +0.04% +112 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 293.5 kB +0.04% +112 B 🔺
@sentry/nextjs (client) 55.84 kB +0.11% +57 B 🔺
@sentry/sveltekit (client) 51.64 kB +0.08% +41 B 🔺
@sentry/core/server 40.03 kB +0.11% +41 B 🔺
@sentry/core/browser 13.66 kB +0.25% +33 B 🔺
@sentry/node 141.94 kB +0.05% +58 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 82.92 kB +0.05% +41 B 🔺
@sentry/node - without tracing 90.94 kB +0.07% +56 B 🔺
@sentry/node - without channel injection 120.33 kB +0.06% +66 B 🔺
@sentry/aws-serverless 99.2 kB +0.07% +62 B 🔺
@sentry/cloudflare (withSentry) - minified 206.69 kB +0.04% +78 B 🔺
@sentry/cloudflare (withSentry) 514.37 kB +0.07% +350 B 🔺

View base workflow run

Comment on lines +3 to +4
browserPerformanceTimeOrigin,
correctedPerformanceTimeOrigin,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need both? I don't think we would like to use a non-accurate time origin anywhere, or maybe I misunderstood?

@github-actions

Copy link
Copy Markdown
Contributor

This pull request has gone three weeks without activity. In another week, I will close it.

But! If you comment or otherwise update it, I will reset the clock, and if you apply the label PR: no-auto-close I will leave it alone ... forever!

…filing

`timestampInSeconds` re-derives its time origin when it detects clock drift,
but `browserPerformanceTimeOrigin` caches the origin resolved at SDK init and
never revisits it. Consumers that convert a `PerformanceEntry`'s monotonic
`startTime` to wall clock time therefore end up on a different timeline than
span and event timestamps once a correction has happened.

Exposes the corrected origin as `correctedPerformanceTimeOrigin` and uses it
for the consumers that outlive a span timeout: INP (reports on pagehide) and
replay (sessions run up to an hour). Profiling already compensated for the SDK
changing its time origin, but computed the adjustment against the stale cached
value.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Lms24
Lms24 force-pushed the lms/fix-browser-live-performance-time-origin branch from 25c1b28 to fd7ccbb Compare September 28, 2026 13:10
@Lms24
Lms24 force-pushed the lms/fix-core-browser-timestampInSeconds-offset-clockdrift branch from 456dd81 to ded4f36 Compare September 28, 2026 13:10
@Lms24
Lms24 removed this pull request from stack #23069 September 28, 2026 14:25
@Lms24 Lms24 closed this Sep 28, 2026
Lms24 added a commit that referenced this pull request Oct 7, 2026
This PR fixes* clock drift that occurs when devices go to sleep. After
devices sleep for a few seconds, the browser's monotonic clock (accessed
via `performance.now()`) stops counting. Once the sleep stops, the
monotonic clock resumes right where it left off before the sleep,
causing significant discrepancies between the actual time (wall clock)
and the browser's monotonic clock time. Or in other words, there are two
ways to get an absolute timestamp:

- Using the monotonic clock via `peformance.timeOrigin +
performance.now()` - has high, sub-millisecond precision and a guarantee
that there are no time jumps or adjustments. But suffers from the sleep
problem described above
- Using the wall clock via `Date.now()` - lower millisecond precision,
is known to be corrected (NTP or via users) and can even cause back
jumps. But no sleep problem.

With this fix, we make the following adjustments, to kinda get the best
of both worlds:

- Every `timestampInSecondsCall` checks the monotonic against the wall
clock. If a threshold of difference is enocuntered, it corrects the
`timeOrigin` so that we can keep using the precise monotonic clock, but
anchor its relative time to a corrected time origin.
- On every time origin correction, we remember the previous origin (up
to 30 corrections). Needed for performance entries
- Makes `browserPerformanceTimeOrigin` a time origin corrected helper
function, where you pass in a relative time that comes from monotonic
clock relative timestamps and it returns the time origin that was most
accurate at that relative time point.
- All spans and other telemetry we create from browsers
`PerformanceEntry` objects carry relative times which are completely
sleep-drift uncorrected. We use `browserPerformanceTimeOrigin` to return
a corrected time stamp so that we can create an absolute timestamp and
bring the performance entries to their respective actual time.

### FAQ

If you think this sounds complicated, I agree. So let me answer the most
obvious questions, because I asked myself these a lot, too:

**Why not just always use `Date.now()` and avoid the complicated click
drift detection and correction logic?**

The main issue with this is that we still need to rely on relative
monotonic clock timestamps for performance entries. We cannot correct
them just with `Date.now()` alone but we still need to anchor them. So
either we rely on the original `window.timeOrigin` and therefore have
all performance entry telemetry happen much "earlier" than its
surrounding telemetry relying on `Date.now()`, or we keep the time
origin correction logic. But even in the second case (where we already
pay the tax for drift detection and correction), telemetry would still
have two different anchors: `Date.now()` for regular telemetry and the
corrected time origin + monotonic time for performance entry telemetry.

Another reason is that the monotonic clock gives us sun-millisecond
precision while the wall clock stops at a millisecond resolution.

**Why the reduction from a drift detection threshold of 5 minutes to
just one second?**

Because we keep everything centered around detection and origin offset
correction, these timestamps need to be as accurate as possible. On
phones, short frequent sleeps are very likely to happen. A lot of sleeps
can accumulate until that 5 minutes threshold is reached. So it's really
important we make these timestamps as accurate as possible.

Fwiw, I reproduced this locally and the drift is already noticeable
after just a few seconds of sleep. The 5 minutes threshold was arguably
far too big beforehand.


**Is there precedence for all of this stuff?**

Somewhat, but I'd argue we're doing a bit more: OTel uses a mixture of
`Date.now()` and `performance.now`:
- Spans start at `Date.now()` and at start time, they record a
`performance.now()` timestamp. On span end, they also take
`performance.now()`, compute the diff and convert that to an end
timestamp based on the start timestamp + diff. This is neat because it
guarantees monotony in span durations. I stole this in #24903

In other places, OTel relies purely on Date.now(), and for performance
entries, they simply take uncorrected timestamps. So our fix is more
complete.

\* **So... we're good now?**

Well, not perfectly and we never will. With this choice we make another
commitment (which we already did previously in less obvious cases) to
`Date.now()`. This value can drift as well, just not for sleeps:

- NTP adjustments: Can make hard correction of the wall clock, or make
the clock tick just a bit faster or slower until the device wall clock
synced with the network time.
- User adjustments: Users can adjust the device time at any time into
any direction.

I think we'll have to live with both and I'm not particularly worried
about them. My main objective is getting rid of the sleep drift.

Fixes #2590
Supersedes #22488, #22585, #23067, #23068

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants