From 25c1b289aba80004085736aedeaa54bc6edc1914 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 5 Aug 2026 18:30:27 +0200 Subject: [PATCH] fix(browser): Use drift-corrected time origin for INP, replay and profiling `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) --- .../src/metrics/webVitalSpans.ts | 5 +- .../test/metrics/webVitalSpans.test.ts | 3 + packages/browser/src/profiling/utils.ts | 6 +- packages/core/src/shared-exports.ts | 7 +- packages/core/src/utils/time.ts | 24 +++++- packages/core/test/lib/utils/time.test.ts | 81 ++++++++++++++++++- .../src/util/createPerformanceEntries.ts | 6 +- 7 files changed, 119 insertions(+), 13 deletions(-) diff --git a/packages/browser-utils/src/metrics/webVitalSpans.ts b/packages/browser-utils/src/metrics/webVitalSpans.ts index 9c4f82249874..89024148865c 100644 --- a/packages/browser-utils/src/metrics/webVitalSpans.ts +++ b/packages/browser-utils/src/metrics/webVitalSpans.ts @@ -1,6 +1,7 @@ import type { Client, Integration, Span, SpanAttributes } from '@sentry/core'; import { browserPerformanceTimeOrigin, + correctedPerformanceTimeOrigin, debug, getActiveSpan, getClient, @@ -329,7 +330,9 @@ export function trackInpAsSpan(client: Client): void { export function _sendInpSpan(inpValue: number, entry: PerformanceEventTiming, standalone = false): void { DEBUG_BUILD && debug.log(`Sending INP span (${inpValue})`); - const startTime = msToSec((browserPerformanceTimeOrigin() as number) + entry.startTime); + // INP reports on pagehide, potentially hours after the origin cached at init, so the corrected origin is used to stay + // on the same timeline as span and event timestamps. + const startTime = msToSec((correctedPerformanceTimeOrigin() as number) + entry.startTime); const duration = msToSec(inpValue); const interactionType = INP_ENTRY_MAP[entry.name]; diff --git a/packages/browser-utils/test/metrics/webVitalSpans.test.ts b/packages/browser-utils/test/metrics/webVitalSpans.test.ts index ef938d7017f0..95fc1494447e 100644 --- a/packages/browser-utils/test/metrics/webVitalSpans.test.ts +++ b/packages/browser-utils/test/metrics/webVitalSpans.test.ts @@ -17,6 +17,7 @@ vi.mock('@sentry/core', async () => { return { ...actual, browserPerformanceTimeOrigin: vi.fn(), + correctedPerformanceTimeOrigin: vi.fn(), timestampInSeconds: vi.fn(), getCurrentScope: vi.fn(), getClient: vi.fn(), @@ -475,6 +476,7 @@ describe('_sendInpSpan', () => { beforeEach(() => { vi.mocked(SentryCore.getCurrentScope).mockReturnValue(mockScope as any); vi.mocked(SentryCore.browserPerformanceTimeOrigin).mockReturnValue(1000); + vi.mocked(SentryCore.correctedPerformanceTimeOrigin).mockReturnValue(1000); vi.mocked(htmlTreeAsString).mockReturnValue('