Repository navigation
Description
Follow-up to #5578. #6090 moved the 1h session cap and the 50ms move debounce onto the monotonic clock. Everything else #5578 listed still reads the wall clock.
What still reads the wall clock
- Segment boundary.
SessionCaptureStrategycuts a segment whenframeTimestamp - segmentTimestampreaches 5s. - Buffer trim.
BufferCaptureStrategykeeps the last 30s withnow - errorReplayDuration. It computes the buffered segment's duration asnow - firstFrame. - Frame windows.
ReplayCache.createVideoOfandrotatewalk epoch-millis windows. Frame files are named by epoch millis. - Gesture timestamps.
ReplayGestureConverterstamps touch start, end, cancel and move events with the wall clock. The gesture batch timer andtimeOffsetdurations are split out to Session Replay gesture batching measures durations with the wall clock #6523, which lands before v9.
What goes wrong
A backward step mid-recording makes new frames look older than the segment they belong to. They fall outside the window, and rotate deletes them until the wall clock catches back up. Breadcrumbs from that stretch are dropped the same way. A forward step cuts a 5s segment of one frozen frame on every captured frame until the segment start catches up, so a 2-minute step sends about 24 frozen segments in 30s. Buffer mode fails too: a backward step makes the buffered duration negative, and a forward step rotates out the whole buffer.
Not a breaking change
Lands on main (decided on 2026-11-10), as a one-off exception to the project's rule that timestamps wait for v9. Today's behavior is a live bug, and the timestamps sent only change after a clock jump.
Checked on main on 2026-11-09:
- Session counts. Replay doesn't start, end or time release-health sessions.
LifecycleWatcherdecides, and replay follows. Crash-free rate and replay counts can't move. - Public API. Every public replay method that carries time takes epoch millis as a
Long. AnAnchoredClockstill produces epoch millis. TheICurrentDateProviderconstructor parameter must stay, because sentry-dart calls the four-argumentReplayIntegrationconstructor. - Metrics. On Android the ticker counts deep sleep, like the wall clock. With no clock step the output is the same. After a step it gets more accurate.
Decision: one anchor per recording that re-anchors on a clock jump
Decided on 2026-11-10.
- Anchor an
AnchoredClockatstart(), which reads the wall clock today, so no value changes there. Only a captured frame re-anchors:resume()reuses the current anchor, and the first frame after resume follows a jump made while paused, like any other jump. Re-anchoring atresume()would make every request queued behind it look like it raced a jump. - On every captured frame, compare a fresh wall-clock reading with the anchored time. If they differ by more than 1s, close the current segment at the old timeline's "now" and start the next segment on a new anchor.
- With no jump, every value sent is the same as today, apart from rounding of up to 1ms.
- After a jump, no frames are lost, and breadcrumbs, errors and spans stay aligned with the video to within one frame. The jump shows up once: a gap after a forward jump, an overlap after a backward one.
Rejected:
- One anchor, never re-anchored: breadcrumbs, errors and spans stay offset for up to an hour.
- New anchor at each segment boundary: changes values even without a jump, and the player freezes briefly every 5s.
- Project only breadcrumbs onto the replay clock: errors and spans stay offset.
- End the replay on a jump: splits one session into two replays.
Also decided:
- After a backward jump the next segment overlaps the previous one, and the player's video lags its timeline until the user seeks. Accepted; the player fix is a separate frontend request.
- A forward jump longer than the 1h session cap stops a session recording, as hitting the cap does. In buffer mode it drops the buffered video from before the jump and keeps recording, so a replay never spans a gap of hours or years.
- Requests already queued when the clock re-anchors (pause, flush, configuration change, error) recompute from the new timeline, so no segment mixes two timelines.
- The jump tolerance is 1s, just under the player's 1.1s "Video Unavailable" marker.
- The jump detector stays internal to
sentry-android-replay. ReplayIntegration'sICurrentDateProviderstays and becomes the anchor's wall clock, so no constructor changes.- After a jump in buffer mode, crash recovery only gets the frames from after the jump.
Done when
- The four sites above take time from the re-anchoring replay clock.
ReplayGestureConvertergets it through theICurrentDateProviderit already takes, so this change doesn't touch that file and doesn't depend on Session Replay gesture batching measures durations with the wall clock #6523. - Frames on disk stay in epoch millis, so crash recovery on the next launch still works.
- Tests cover a backward and a forward step for segments, the buffer trim and gesture timestamps, and a forward jump over 1h stops the recording.
- The
ICurrentDateProviderparameters stay; removing them is Breaking clock changes for v9: span timing and ICurrentDateProvider removal #6506.
Agent notes
Verified on main at 5652060246 (2026-11-09).
- Segment boundary:
SessionCaptureStrategy.kt:144. Frame stamp::117. - Buffer:
BufferCaptureStrategy.kt:138(frame stamp),:143(rotate on frame),:199(onTouchEvent→rotateEvents),:263-269(segment start and duration). ReplayCache.kt::81(file name),:179-189(createVideoOfwindow loop),:220(rotate(until = from + duration)),:260(rotate),:442(fromDiskparses the filename into the timestamp).ReplayGestureConverter.kt::74(move eventtimestamp),:105,:128,:142(touch start, end and cancel timestamps). The batch timer andtimeOffset(:39,:63,:67-68) are Session Replay gesture batching measures durations with the wall clock #6523.- Breadcrumb window filter:
CaptureStrategy.kt:209-212. Gesture event filter::235-236.Breadcrumb()stampsSystem.currentTimeMillis()(Breadcrumb.java:567).maxBreadcrumbsdefaults to 100. TODO [v9]markers:BaseCaptureStrategy.kt:59,ReplayGestureConverter.kt:16.AnchoredClock(sentry/src/main/java/io/sentry/time/AnchoredClock.java) hascreate(EpochClock, MonotonicTicker),startTime(),now(). No drift check.- Clocks:
options.monotonicTicker(Android:elapsedRealtimeNanos) is already passed to both strategies (ReplayIntegration.kt:236-247). Epoch source:options.epochClock. - Public timestamp APIs in
sentry-android-replay.api:ReplayCache.addFrame(File, long, String),ReplayCache.createVideoOf(long, long, …),GeneratedVideo(File, int, long),ReplayIntegration.onScreenshotRecorded(File, long). - Hybrid callers: sentry-dart
SentryFlutterPlugin.kt(around line 293) callsReplayIntegration(ctx, dateProvider = CurrentDateProvider.getInstance(), recorderProvider, replayCacheProvider = null).android_replay_recorder.dartcallsonScreenshotRecorded(bitmap), with no timestamp. sentry-react-native's Android code only importsDefaultReplayBreadcrumbConverter. Unity and KMP were not checked (GitHub code search rate limit). - Gotchas from fix(replay): Measure recording deadlines on a monotonic clock (JAVA-575) #6090: evaluate time checks when the frame is captured, not in the queued
replayExecutortask (SessionCaptureStrategyTest: "executor delay does not trigger a segment boundary"). Gesture tests that step the clock by more than ~500ms hitCAPTURE_MOVE_EVENT_THRESHOLDinstead of the debounce. - Code plan (2026-11-10): new internal
capture/ReplayClock.ktholding a volatileAnchoredClockand a generation counter, withrestart(),nowMillis()andcheckForStep(): Rebase(oldNowMs, newStartMs)?. Session strategy checks at capture (:117) and, on a rebase, closes[segmentTimestamp, oldNow)before storing the frame. Buffer strategy computes the trim cutoff at capture and ages buffered segments on aStopwatch. The gesture converter getsICurrentDateProvider { replayClock.nowMillis() }atBaseCaptureStrategy.kt:72.
Activity
linear-code commentedon Nov 9, 2026
JAVA-829
[-]Session Replay segment, buffer and gesture timing reads the wall clock[/-][+]Session Replay segment, buffer and gesture timestamps read the wall clock[/+]fix(replay): Drop the buffer at too-long jumps and re-read raced segm…