getsentry / getsentry/sentry-javascript

Span timestamps trust performance.timeOrigin without the drift guard from #2590 — drifting monotonic clocks produce past-dated (dropped) transactions

未关闭
#22,375 4 条评论 2 个 reaction 已指派 1 人 已被 @Lms24 认领 在 GitHub 查看
Bug Component: Performance Core Spans Traces
主要语言
TypeScript
星标
8.7k
派生
1.8k
平均合并
1 天 17 小时
30 天内合并 PR
515

描述

## Summary

`createUnixTimestampInSecondsFunc()` in `@sentry/core` (`packages/core/src/utils/time.ts`) builds the span-timestamp clock as:

```ts
const timeOrigin = performance.timeOrigin;
return () => (timeOrigin + withRandomSafeContext(() => performance.now())) / ONE_SECOND_IN_MS;
```

It trusts `performance.timeOrigin` **unconditionally**, with a standing `// TODO: This does not account for the case where the monotonic clock that powers performance.now() drifts from the wall clock ...`.

Meanwhile `getBrowserTimeOrigin()` (added for #2590 / PR #3356) **does** validate the origin against `Date.now()` with a 5-minute threshold before trusting it — but that guarded value feeds `browserPerformanceTimeOrigin`, **not** the span-timestamp function above. So the class of bug #2590 set out to fix still reaches production through the span-timestamp path. (Still the case on `develop` as of this writing.)

## Impact

When `performance.timeOrigin + performance.now()` drifts from `Date.now()` — the monotonic clock stops while the device/computer sleeps, long-lived process, backgrounded tab (the scenarios #2590 itself described) — **every span/transaction is timestamped in the past**. Ingest accepts the envelope (HTTP 200) and the backend then silently drops/hides it, so tracing appears "broken" with no client-side error. Errors and sessions (which use `dateTimestampInSeconds` / `Date.now()`) are unaffected, which makes this especially hard to diagnose.

## Real-world reproduction (React Native 0.86)

On React Native 0.86 (iOS), `performance.now()` advances only while the process/host is awake while `performance.timeOrigin` stays fixed wall-clock, so on a long-lived process the sum drifts behind `Date.now()` by the accumulated sleep. Measured in-app:

```
performance.timeOrigin ≈ 2026-07-06 (fixed)
performance.now() ≈ 63 h
timeOrigin + performance.now() ≈ 2026-07-10 ← ~7 days behind
Date.now() ≈ 2026-07-17
```

Every navigation / HTTP / app-start transaction was stamped ~7 days in the past and dropped, while errors were fine. Versions: `@sentry/react-native` 8.7.0, `@sentry/core` 10.47.0.

We worked around it app-side by forcing `@sentry/core` onto its `Date.now()` fallback (shadowing `performance.timeOrigin` to `0` when drift is detected). The underlying RN clock behavior is tracked at react/react-native#57595 — but the SDK could be made resilient to *any* drifting platform clock, which is what this issue is about.

## Suggested fix

Apply the drift check that `getBrowserTimeOrigin()` already implements to the span-timestamp path: in `createUnixTimestampInSecondsFunc()`, if `|timeOrigin + performance.now() - Date.now()|` exceeds a threshold, fall back to `dateTimestampInSeconds` (or re-anchor the origin). That closes the gap left open after #2590 / #3356 and makes span timestamps robust to sleep/drift on every platform, not just RN.

## Environment

- `@sentry/core` 10.47.0 (via `@sentry/react-native` 8.7.0); the span-timestamp path is unchanged on `develop`.
- Trigger platform here: React Native 0.86 / iOS — but not RN-specific in principle; any runtime whose monotonic clock drifts from wall time is affected.

Related: #2590, #3356; react/react-native#57595.

贡献指南

打开贡献指南

评估

这个 Issue 还没有评估数据。

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。