Skip to content

fix(core): Account for clock drift on every timestampInSeconds call - #23054

Draft
Lms24 wants to merge 1 commit into
developfrom
lms/fix-core-browser-timestampInSeconds-offset-clockdrift
Draft

fix(core): Account for clock drift on every timestampInSeconds call#23054
Lms24 wants to merge 1 commit into
developfrom
lms/fix-core-browser-timestampInSeconds-offset-clockdrift

Conversation

@Lms24

@Lms24 Lms24 commented Aug 5, 2026

Copy link
Copy Markdown
Member

This PR addresses clock drift observed in spans, logs, metrics and events caused by relying on performance.now() after it became unreliable.

My initial attempt to fix this in #22488 was flawed because it only checked for click drift on the first timestampInSeconds call. Which is pretty useless given that clock drift likely occurs after the initial pageload (and hence SDK init). More specifically, when devices are put to sleep or browser tabs get suspended in the background.

This fix therefore trades performance for more accurate time stamps by checking for clock drift on every timestampInSeconds call. Once drift is detected, the timeOrigin is corrected with Date.now().

Why not only use Date.now()?

  • performance.now() has sub-ms precision
  • performance.now() is guaranteed to be monotonic. Date.now() can go backwards, or speed up/slow down its seconds (e.g. via NTP or user adjustments)

Limitations:

  • Spans' (or anything where we measure durations/more than one timestamp) durations can become inaccurate if a clock drift durations happens while a span is started but not yet ended. We could look into this as well as a follow-up, though if clock drift occurs while a span is active, we likely won't get a correct duration as well, for example if a device is asleep with an active span.

supersedes #22488
supersedes #22585

@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 456dd81. Configure here.

// See: https://dev.to/noamr/when-a-millisecond-is-not-a-millisecond-3h6
if (Math.abs(timeOrigin + performanceNow - dateNow) > CLOCK_DRIFT_THRESHOLD_MS) {
timeOrigin = dateNow - performanceNow;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Rebased timestamps leave performance entries behind

Medium Severity

timestampInSeconds() updates only its private timeOrigin, while browserPerformanceTimeOrigin() retains its cached pre-drift origin. After sleep, spans use the corrected timeline but performance entries and profiles remain offset, causing post-wake telemetry to be dropped or assigned incorrect times.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 456dd81. Configure here.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 30.16 kB +0.11% +33 B 🔺
@sentry/browser - with treeshaking flags 28.36 kB +0.11% +30 B 🔺
@sentry/browser (incl. Tracing) 47.57 kB +0.07% +31 B 🔺
@sentry/browser (incl. Tracing + Span Streaming) 47.59 kB +0.08% +38 B 🔺
@sentry/browser (incl. Tracing, Profiling) 52.33 kB +0.07% +33 B 🔺
@sentry/browser (incl. Tracing, Replay) 86.96 kB +0.05% +37 B 🔺
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 76.36 kB +0.04% +26 B 🔺
@sentry/browser (incl. Tracing, Replay with Canvas) 91.64 kB +0.05% +40 B 🔺
@sentry/browser (incl. Tracing, Replay, Feedback) 104.31 kB +0.06% +57 B 🔺
@sentry/browser (incl. Feedback) 47.49 kB +0.1% +43 B 🔺
@sentry/browser (incl. sendFeedback) 35 kB +0.1% +34 B 🔺
@sentry/browser (incl. FeedbackAsync) 40.15 kB +0.11% +42 B 🔺
@sentry/browser (incl. Metrics) 31.24 kB +0.11% +33 B 🔺
@sentry/browser (incl. Logs) 31.47 kB +0.16% +48 B 🔺
@sentry/browser (incl. Metrics & Logs) 32.14 kB +0.1% +29 B 🔺
@sentry/react 31.97 kB +0.11% +34 B 🔺
@sentry/react (incl. Tracing) 49.82 kB +0.05% +22 B 🔺
@sentry/vue 35.24 kB +0.11% +36 B 🔺
@sentry/vue (incl. Tracing) 49.55 kB +0.07% +30 B 🔺
@sentry/svelte 30.18 kB +0.11% +33 B 🔺
CDN Bundle 32.17 kB +0.1% +32 B 🔺
CDN Bundle (incl. Tracing) 47.85 kB +0.08% +35 B 🔺
CDN Bundle (incl. Logs, Metrics) 33.72 kB +0.14% +46 B 🔺
CDN Bundle (incl. Tracing, Logs, Metrics) 49.22 kB +0.07% +32 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) 73.06 kB +0.06% +41 B 🔺
CDN Bundle (incl. Tracing, Replay) 85.49 kB +0.04% +32 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 86.79 kB +0.02% +17 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) 91.3 kB +0.04% +29 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 92.62 kB +0.03% +27 B 🔺
CDN Bundle - uncompressed 95.37 kB +0.07% +59 B 🔺
CDN Bundle (incl. Tracing) - uncompressed 142.88 kB +0.06% +74 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed 100 kB +0.08% +74 B 🔺
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 146.86 kB +0.06% +74 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 224.7 kB +0.04% +74 B 🔺
CDN Bundle (incl. Tracing, Replay) - uncompressed 262.14 kB +0.03% +74 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 266.11 kB +0.03% +74 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 275.85 kB +0.03% +74 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 279.8 kB +0.03% +74 B 🔺
@sentry/nextjs (client) 52.4 kB +0.07% +35 B 🔺
@sentry/sveltekit (client) 48.03 kB +0.09% +40 B 🔺
@sentry/core/server 65.59 kB +0.07% +42 B 🔺
@sentry/core/browser 51.89 kB +0.1% +47 B 🔺
@sentry/node 120.14 kB +0.03% +28 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 0 B added added
@sentry/node - without tracing 83.77 kB +0.04% +31 B 🔺
@sentry/aws-serverless 92.26 kB +0.04% +29 B 🔺
@sentry/cloudflare (withSentry) - minified 218.66 kB +0.03% +65 B 🔺
@sentry/cloudflare (withSentry) 538.8 kB +0.06% +278 B 🔺

View base workflow run

@Lms24

Lms24 commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

The two-origin divergence Bugbot flagged is real: timestampInSeconds corrects its origin while browserPerformanceTimeOrigin keeps the one cached at init.

Fixed in #23067, stacked on top of this PR, which exposes the corrected origin and switches over the consumers where the divergence persists (INP, replay, and profiling's adjustForOriginChange, which was compensating against the stale value). #23068 then handles the case where replay observes an entry before a correction but converts it after.

Keeping it out of this PR so each change stays independently reviewable — this one is limited to how timestampInSeconds itself behaves.

@bezata

bezata commented Aug 5, 2026

Copy link
Copy Markdown

Confirmed on a real iPhone running React Native 0.86 with @sentry/core@10.67.0.

One structured log embedded its emission wall time in the message body:

  • embedded Date.now(): 1785945903901 (2026-08-05T16:05:03.901Z)
  • Sentry-stored log timestamp: 2026-08-03T07:19:17Z
  • displacement: approximately 204,346,901 ms (2.365 days)

The full structured-log stream was present under the older window with the same displacement, while error events remained wall-clock-correct. This matches the React Native Apple clock change in react-native#55977 and the symptom reported in getsentry/sentry-react-native#6510.

We applied the same per-call re-anchoring shape downstream as a version-pinned patch. Package-level regression tests against the real @sentry/core package cover an already-skewed first call, drift re-accumulating after initialization, the sub-threshold path, and no-thrash behavior. Live post-patch device verification is still pending, so I am not claiming field recovery yet.

One potentially useful addition to this PR's test suite: the current sleep test starts with an aligned first call and then accumulates drift. Our device also exercised the other entry condition, where timeOrigin + performance.now() was already about 2.37 days behind Date.now() before the first observed timestampInSeconds() call. A first-call pre-existing-skew case would pin that production shape directly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants