Skip to content

fix(replay): Measure recording deadlines on a monotonic clock (JAVA-575) - #6090

Draft
runningcode wants to merge 3 commits into
mainfrom
no/java-575-replay-monotonic-intervals
Draft

fix(replay): Measure recording deadlines on a monotonic clock (JAVA-575)#6090
runningcode wants to merge 3 commits into
mainfrom
no/java-575-replay-monotonic-intervals

Conversation

@runningcode

Copy link
Copy Markdown
Contributor

📜 Description

Session Replay derives every interval and window from the wall clock. Two of those intervals are pure control flow, and this PR moves them onto io.sentry.time.Deadline, backed by the options' MonotonicTicker:

Site Before After
1h sessionDuration cap frameTimestamp - replayStartTimestamp >= sessionDuration Deadline.after(ticker, sessionDuration, MILLISECONDS)
50 ms ACTION_MOVE debounce lastCapturedMoveEvent + 50 > now Deadline.after(ticker, 50, MILLISECONDS)

On Android the ticker is SystemClock.elapsedRealtimeNanos(), which advances across device suspend exactly as the wall clock does — so the conversion preserves today's semantics and only removes the sensitivity to a clock step.

The cap is still evaluated when a frame is captured rather than when its queued task runs, so a busy replay executor still cannot trip it (covered by the existing executor delay does not trigger a segment boundary test).

Deadline also retires two 0-means-unset sentinels (replayStartTimestamp, lastCapturedMoveEvent) in favor of a nullable field, which is what Deadline's own javadoc argues for.

What this PR deliberately does not change

Everything else JAVA-575 lists stays on the wall clock:

  • segment timestamps and durations (SessionCaptureStrategy, BufferCaptureStrategy)
  • frame filenames and the ReplayCache.createVideoOf / rotate windows
  • gesture timeOffsets

Those values are serialized, and fixing them means re-basing every emitted replay timestamp onto an AnchoredClock — a change to what customers receive, and therefore v9. A "wall clock for serialization, ticks for windows" hybrid is not an option either: the replay cache survives process death and is replayed on the next launch, and a monotonic tick is meaningless across processes, so frame timestamps must stay epoch on disk.

// TODO [MAJOR] markers at both ICurrentDateProvider injection sites record what changes at v9 and why.

💡 Motivation and Context

A backward wall-clock step mid-recording (NTP correction, carrier time, user change — all realistic on phones) makes frames look "newer than now"; a forward step makes a recording look older than it is. For the two sites converted here that means:

  • Forward step past the cap → a healthy recording is stopped on the very next frame with Session replay deadline exceeded (1h).
  • Backward step → the recording never reaches its cap, and every move event is suppressed until the wall clock catches back up.

Audit finding B3 from JAVA-557.

💚 How did you test it?

./gradlew :sentry-android-replay:testReleaseUnitTest :sentry-android-core:testReleaseUnitTest

Four new regression tests, each of which fails on the arithmetic being replaced:

  • onScreenshotRecorded does not stop replay when the wall clock jumps past the deadline
  • onScreenshotRecorded stops replay when the wall clock steps backwards past the deadline
  • wall clock stepping backwards does not suppress move events
  • wall clock stepping forwards does not lift the debounce early

They drive a TestMonotonicTicker and the fake wall clock independently. The existing tests now advance both together, which is what normal operation looks like.

📝 Checklist

  • I added GH Issue ID & Linear ID
  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • Review from the native team if needed.
  • No breaking change or entry added to the changelog.
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs.
  • Public API changes reviewed by another Mobile SDK team member or implemented according to the develop docs spec.

No .api diff: every type touched is internal Kotlin, and ReplayIntegration's public constructors are unchanged — the ticker is read from SentryOptions where the strategies are built.

🔮 Next steps

The serialized half of JAVA-575 wants one AnchoredClock per recording, replacing ICurrentDateProvider throughout the replay module, so that every emitted epoch is projected from a single anchor and all internal diffs become tick diffs. That belongs in the v9 bucket alongside JAVA-572 / JAVA-577 / JAVA-642 / JAVA-578.

🤖 Generated with Claude Code

Session Replay derives every interval from the wall clock. Two of those
intervals are pure control flow, and a clock step corrupts both:

- The 1h `sessionDuration` cap was `frameTimestamp - replayStartTimestamp`.
  A forward step past the cap kills a healthy recording on the next frame;
  a backward step lets one run indefinitely.
- The 50ms ACTION_MOVE debounce was `lastCapturedMoveEvent + 50 > now`.
  A backward step suppresses every move event until the wall clock catches
  back up.

Both now use `io.sentry.time.Deadline` on the options' `MonotonicTicker`,
which is `SystemClock.elapsedRealtimeNanos()` on Android. That clock
advances across device suspend just as the wall clock does, so the
conversion preserves today's semantics and only removes the step.

The cap is still evaluated when a frame is captured rather than when its
queued task runs, so a busy replay executor cannot trip it.

Everything else JAVA-575 lists — segment timestamps and durations, frame
filenames, buffer cutoffs, gesture `timeOffset`s — stays on the wall clock:
those values are serialized, and fixing them means re-basing them onto an
`AnchoredClock`, which is a v9 change. `// TODO [MAJOR]` markers record that
at both `ICurrentDateProvider` injection sites.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@linear-code

linear-code Bot commented Sep 10, 2026

Copy link
Copy Markdown

JAVA-575

@sentry

sentry Bot commented Sep 10, 2026

Copy link
Copy Markdown

📲 Install Builds

Android

🔗 App Name App ID Version Configuration
SDK Size io.sentry.tests.size 8.56.0 (1) release

⚙️ sentry-android Build Distribution Settings

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Session Replay drives interval/window math from the wall clock

1 participant