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

@Lms24Lms24 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

@cursorcursorBot 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 CursorFix in Web

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

@github-actions

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize% ChangeChange
@sentry/browser30.16 kB+0.11%+33 B 🔺
@sentry/browser - with treeshaking flags28.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 flags76.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/react31.97 kB+0.11%+34 B 🔺
@sentry/react (incl. Tracing)49.82 kB+0.05%+22 B 🔺
@sentry/vue35.24 kB+0.11%+36 B 🔺
@sentry/vue (incl. Tracing)49.55 kB+0.07%+30 B 🔺
@sentry/svelte30.18 kB+0.11%+33 B 🔺
CDN Bundle32.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 - uncompressed95.37 kB+0.07%+59 B 🔺
CDN Bundle (incl. Tracing) - uncompressed142.88 kB+0.06%+74 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed100 kB+0.08%+74 B 🔺
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed146.86 kB+0.06%+74 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed224.7 kB+0.04%+74 B 🔺
CDN Bundle (incl. Tracing, Replay) - uncompressed262.14 kB+0.03%+74 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed266.11 kB+0.03%+74 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed275.85 kB+0.03%+74 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed279.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/server65.59 kB+0.07%+42 B 🔺
@sentry/core/browser51.89 kB+0.1%+47 B 🔺
@sentry/node120.14 kB+0.03%+28 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection)0 Baddedadded
@sentry/node - without tracing83.77 kB+0.04%+31 B 🔺
@sentry/aws-serverless92.26 kB+0.04%+29 B 🔺
@sentry/cloudflare (withSentry) - minified218.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
MemberAuthor

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

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.

@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!

@plgrazon

Copy link
Copy Markdown

Getting the same issue, has this been closed permanently?

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Lms24@bezata@plgrazon
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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

@Lms24Lms24 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

@cursorcursorBot 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 CursorFix in Web

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

@github-actions

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize% ChangeChange
@sentry/browser30.16 kB+0.11%+33 B 🔺
@sentry/browser - with treeshaking flags28.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 flags76.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/react31.97 kB+0.11%+34 B 🔺
@sentry/react (incl. Tracing)49.82 kB+0.05%+22 B 🔺
@sentry/vue35.24 kB+0.11%+36 B 🔺
@sentry/vue (incl. Tracing)49.55 kB+0.07%+30 B 🔺
@sentry/svelte30.18 kB+0.11%+33 B 🔺
CDN Bundle32.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 - uncompressed95.37 kB+0.07%+59 B 🔺
CDN Bundle (incl. Tracing) - uncompressed142.88 kB+0.06%+74 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed100 kB+0.08%+74 B 🔺
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed146.86 kB+0.06%+74 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed224.7 kB+0.04%+74 B 🔺
CDN Bundle (incl. Tracing, Replay) - uncompressed262.14 kB+0.03%+74 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed266.11 kB+0.03%+74 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed275.85 kB+0.03%+74 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed279.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/server65.59 kB+0.07%+42 B 🔺
@sentry/core/browser51.89 kB+0.1%+47 B 🔺
@sentry/node120.14 kB+0.03%+28 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection)0 Baddedadded
@sentry/node - without tracing83.77 kB+0.04%+31 B 🔺
@sentry/aws-serverless92.26 kB+0.04%+29 B 🔺
@sentry/cloudflare (withSentry) - minified218.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
MemberAuthor

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

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.

@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!

@plgrazon

Copy link
Copy Markdown

Getting the same issue, has this been closed permanently?

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Lms24@bezata@plgrazon
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@Lms24Lms24 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

@cursorcursorBot 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 CursorFix in Web

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

@github-actions

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize% ChangeChange
@sentry/browser30.16 kB+0.11%+33 B 🔺
@sentry/browser - with treeshaking flags28.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 flags76.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/react31.97 kB+0.11%+34 B 🔺
@sentry/react (incl. Tracing)49.82 kB+0.05%+22 B 🔺
@sentry/vue35.24 kB+0.11%+36 B 🔺
@sentry/vue (incl. Tracing)49.55 kB+0.07%+30 B 🔺
@sentry/svelte30.18 kB+0.11%+33 B 🔺
CDN Bundle32.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 - uncompressed95.37 kB+0.07%+59 B 🔺
CDN Bundle (incl. Tracing) - uncompressed142.88 kB+0.06%+74 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed100 kB+0.08%+74 B 🔺
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed146.86 kB+0.06%+74 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed224.7 kB+0.04%+74 B 🔺
CDN Bundle (incl. Tracing, Replay) - uncompressed262.14 kB+0.03%+74 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed266.11 kB+0.03%+74 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed275.85 kB+0.03%+74 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed279.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/server65.59 kB+0.07%+42 B 🔺
@sentry/core/browser51.89 kB+0.1%+47 B 🔺
@sentry/node120.14 kB+0.03%+28 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection)0 Baddedadded
@sentry/node - without tracing83.77 kB+0.04%+31 B 🔺
@sentry/aws-serverless92.26 kB+0.04%+29 B 🔺
@sentry/cloudflare (withSentry) - minified218.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
MemberAuthor

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

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.

@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!

@plgrazon

Copy link
Copy Markdown

Getting the same issue, has this been closed permanently?

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Lms24@bezata@plgrazon
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@Lms24Lms24 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

@cursorcursorBot 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 CursorFix in Web

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

@github-actions

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize% ChangeChange
@sentry/browser30.16 kB+0.11%+33 B 🔺
@sentry/browser - with treeshaking flags28.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 flags76.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/react31.97 kB+0.11%+34 B 🔺
@sentry/react (incl. Tracing)49.82 kB+0.05%+22 B 🔺
@sentry/vue35.24 kB+0.11%+36 B 🔺
@sentry/vue (incl. Tracing)49.55 kB+0.07%+30 B 🔺
@sentry/svelte30.18 kB+0.11%+33 B 🔺
CDN Bundle32.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 - uncompressed95.37 kB+0.07%+59 B 🔺
CDN Bundle (incl. Tracing) - uncompressed142.88 kB+0.06%+74 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed100 kB+0.08%+74 B 🔺
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed146.86 kB+0.06%+74 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed224.7 kB+0.04%+74 B 🔺
CDN Bundle (incl. Tracing, Replay) - uncompressed262.14 kB+0.03%+74 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed266.11 kB+0.03%+74 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed275.85 kB+0.03%+74 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed279.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/server65.59 kB+0.07%+42 B 🔺
@sentry/core/browser51.89 kB+0.1%+47 B 🔺
@sentry/node120.14 kB+0.03%+28 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection)0 Baddedadded
@sentry/node - without tracing83.77 kB+0.04%+31 B 🔺
@sentry/aws-serverless92.26 kB+0.04%+29 B 🔺
@sentry/cloudflare (withSentry) - minified218.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
MemberAuthor

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

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.

@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!

@plgrazon

Copy link
Copy Markdown

Getting the same issue, has this been closed permanently?

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Lms24@bezata@plgrazon
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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

@Lms24Lms24 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

@cursorcursorBot 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 CursorFix in Web

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

@github-actions

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize% ChangeChange
@sentry/browser30.16 kB+0.11%+33 B 🔺
@sentry/browser - with treeshaking flags28.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 flags76.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/react31.97 kB+0.11%+34 B 🔺
@sentry/react (incl. Tracing)49.82 kB+0.05%+22 B 🔺
@sentry/vue35.24 kB+0.11%+36 B 🔺
@sentry/vue (incl. Tracing)49.55 kB+0.07%+30 B 🔺
@sentry/svelte30.18 kB+0.11%+33 B 🔺
CDN Bundle32.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 - uncompressed95.37 kB+0.07%+59 B 🔺
CDN Bundle (incl. Tracing) - uncompressed142.88 kB+0.06%+74 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed100 kB+0.08%+74 B 🔺
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed146.86 kB+0.06%+74 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed224.7 kB+0.04%+74 B 🔺
CDN Bundle (incl. Tracing, Replay) - uncompressed262.14 kB+0.03%+74 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed266.11 kB+0.03%+74 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed275.85 kB+0.03%+74 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed279.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/server65.59 kB+0.07%+42 B 🔺
@sentry/core/browser51.89 kB+0.1%+47 B 🔺
@sentry/node120.14 kB+0.03%+28 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection)0 Baddedadded
@sentry/node - without tracing83.77 kB+0.04%+31 B 🔺
@sentry/aws-serverless92.26 kB+0.04%+29 B 🔺
@sentry/cloudflare (withSentry) - minified218.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
MemberAuthor

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

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.

@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!

@plgrazon

Copy link
Copy Markdown

Getting the same issue, has this been closed permanently?

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Lms24@bezata@plgrazon
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@Lms24Lms24 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

@cursorcursorBot 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 CursorFix in Web

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

@github-actions

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize% ChangeChange
@sentry/browser30.16 kB+0.11%+33 B 🔺
@sentry/browser - with treeshaking flags28.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 flags76.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/react31.97 kB+0.11%+34 B 🔺
@sentry/react (incl. Tracing)49.82 kB+0.05%+22 B 🔺
@sentry/vue35.24 kB+0.11%+36 B 🔺
@sentry/vue (incl. Tracing)49.55 kB+0.07%+30 B 🔺
@sentry/svelte30.18 kB+0.11%+33 B 🔺
CDN Bundle32.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 - uncompressed95.37 kB+0.07%+59 B 🔺
CDN Bundle (incl. Tracing) - uncompressed142.88 kB+0.06%+74 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed100 kB+0.08%+74 B 🔺
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed146.86 kB+0.06%+74 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed224.7 kB+0.04%+74 B 🔺
CDN Bundle (incl. Tracing, Replay) - uncompressed262.14 kB+0.03%+74 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed266.11 kB+0.03%+74 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed275.85 kB+0.03%+74 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed279.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/server65.59 kB+0.07%+42 B 🔺
@sentry/core/browser51.89 kB+0.1%+47 B 🔺
@sentry/node120.14 kB+0.03%+28 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection)0 Baddedadded
@sentry/node - without tracing83.77 kB+0.04%+31 B 🔺
@sentry/aws-serverless92.26 kB+0.04%+29 B 🔺
@sentry/cloudflare (withSentry) - minified218.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
MemberAuthor

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

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.

@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!

@plgrazon

Copy link
Copy Markdown

Getting the same issue, has this been closed permanently?

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Lms24@bezata@plgrazon
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@Lms24Lms24 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

@cursorcursorBot 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 CursorFix in Web

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

@github-actions

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize% ChangeChange
@sentry/browser30.16 kB+0.11%+33 B 🔺
@sentry/browser - with treeshaking flags28.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 flags76.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/react31.97 kB+0.11%+34 B 🔺
@sentry/react (incl. Tracing)49.82 kB+0.05%+22 B 🔺
@sentry/vue35.24 kB+0.11%+36 B 🔺
@sentry/vue (incl. Tracing)49.55 kB+0.07%+30 B 🔺
@sentry/svelte30.18 kB+0.11%+33 B 🔺
CDN Bundle32.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 - uncompressed95.37 kB+0.07%+59 B 🔺
CDN Bundle (incl. Tracing) - uncompressed142.88 kB+0.06%+74 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed100 kB+0.08%+74 B 🔺
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed146.86 kB+0.06%+74 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed224.7 kB+0.04%+74 B 🔺
CDN Bundle (incl. Tracing, Replay) - uncompressed262.14 kB+0.03%+74 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed266.11 kB+0.03%+74 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed275.85 kB+0.03%+74 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed279.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/server65.59 kB+0.07%+42 B 🔺
@sentry/core/browser51.89 kB+0.1%+47 B 🔺
@sentry/node120.14 kB+0.03%+28 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection)0 Baddedadded
@sentry/node - without tracing83.77 kB+0.04%+31 B 🔺
@sentry/aws-serverless92.26 kB+0.04%+29 B 🔺
@sentry/cloudflare (withSentry) - minified218.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
MemberAuthor

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

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.

@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!

@plgrazon

Copy link
Copy Markdown

Getting the same issue, has this been closed permanently?

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Lms24@bezata@plgrazon
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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

@Lms24Lms24 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

@cursorcursorBot 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 CursorFix in Web

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

@github-actions

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize% ChangeChange
@sentry/browser30.16 kB+0.11%+33 B 🔺
@sentry/browser - with treeshaking flags28.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 flags76.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/react31.97 kB+0.11%+34 B 🔺
@sentry/react (incl. Tracing)49.82 kB+0.05%+22 B 🔺
@sentry/vue35.24 kB+0.11%+36 B 🔺
@sentry/vue (incl. Tracing)49.55 kB+0.07%+30 B 🔺
@sentry/svelte30.18 kB+0.11%+33 B 🔺
CDN Bundle32.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 - uncompressed95.37 kB+0.07%+59 B 🔺
CDN Bundle (incl. Tracing) - uncompressed142.88 kB+0.06%+74 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed100 kB+0.08%+74 B 🔺
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed146.86 kB+0.06%+74 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed224.7 kB+0.04%+74 B 🔺
CDN Bundle (incl. Tracing, Replay) - uncompressed262.14 kB+0.03%+74 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed266.11 kB+0.03%+74 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed275.85 kB+0.03%+74 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed279.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/server65.59 kB+0.07%+42 B 🔺
@sentry/core/browser51.89 kB+0.1%+47 B 🔺
@sentry/node120.14 kB+0.03%+28 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection)0 Baddedadded
@sentry/node - without tracing83.77 kB+0.04%+31 B 🔺
@sentry/aws-serverless92.26 kB+0.04%+29 B 🔺
@sentry/cloudflare (withSentry) - minified218.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
MemberAuthor

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

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.

@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!

@plgrazon

Copy link
Copy Markdown

Getting the same issue, has this been closed permanently?

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Lms24@bezata@plgrazon