Repository navigation
fix(replay): Keep the replay timeline anchored across wall-clock jumps (JAVA-829) - #6542
Draft
runningcode wants to merge 5 commits into
Draft
runningcode wants to merge 5 commits into
runningcode wants to merge 5 commits into
Conversation
Session Replay stamped frames, cut segments, trimmed the buffer and stamped gestures from repeated wall-clock reads. A device clock change mid-recording then dropped frames (backward) or sent one frozen 5s segment per frame until the segment start caught up (forward); buffer mode got negative durations or rotated out the whole buffer. Each recording now stamps on an AnchoredClock (ReplayClock). Every captured frame compares it with the wall clock; past a 1s tolerance the current segment closes where the old timeline ended and the next one starts on a new anchor, so the jump shows once as a gap or an overlap and breadcrumbs, errors and spans stay aligned with the video. Touch events stamped on the old timeline go into the closing segment. A forward jump longer than sessionDuration stops a session recording the way the cap does. Buffered segments age on the timeline they were stamped on. Without a jump, every serialized value matches the wall clock as before. Fixes GH-6522 Fixes JAVA-829 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ent requests Buffer mode now drops what it recorded before a forward wall-clock jump longer than sessionDuration: the pre-jump frames, touch events and already buffered segments. It keeps recording on the new timeline, so an error soon after no longer sends one replay spanning hours or years. Session mode already stops at the same jump. Segment requests (pause, flush, stop, configuration change, error flush) read their times when made, so a busy executor doesn't stretch the segment. If a frame moved the recording onto a new timeline in between, those times and the frames the task finds were on different timelines: a backward jump could produce a frozen segment minutes long, and a buffer flush right after a forward jump could stretch one frame over the whole jump. The strategies now track the timeline their queued work is on, and a request's task reads its times again on that timeline when it differs from the one the request read. Without a jump nothing is re-read, so every value stays what it was. resume() no longer re-anchors the clock. Otherwise every request until its queued task runs would look like it raced a jump. A jump while paused is followed by the first frame after resume, like any other. Refs GH-6522 Refs JAVA-829 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The function references and generateSequence pulled in kotlin.jvm.internal.FunctionReferenceImpl and kotlin.sequences.SequencesKt, which R8 reports as missing in the SDK size app. A lambda and a plain loop keep replay's Kotlin stdlib footprint as it was. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
📲 Install BuildsAndroid
|
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
📜 Description
Session Replay read the wall clock for every frame stamp, segment boundary, buffer trim and gesture stamp, so a device clock change mid-recording broke it:
Each recording now stamps on one
AnchoredClock, held by a new internalReplayClockincapture/. On every captured frame the strategy compares a fresh wall-clock reading with the timeline. Within 1s nothing changes. Past that:The jump shows once in the replay: as a gap when the clock moved forward, as an overlap when it moved back. Stamps stay epoch millis, so the frame cache on disk and crash recovery work as before.
Without a jump, every serialized value is what the wall clock gives today. A value can come out up to 1ms early, because the anchor drops its sub-millisecond part.
Details worth a look
sessionDuration:does not stop replay when the wall clock jumps past the deadlineis replaced by one that expects the stop.SegmentRequestinBaseCaptureStrategykeeps those reads, unless a frame moved the recording onto a new timeline in between. In that case the task reads them again on the timeline its queued work is on.executorTimeline, the timeline the replay executor's work is on. The task of the frame that followed a jump moves it onto the new timeline, as the last thing it does.executorTimelineat request time, or whenexecutorTimelinemoved before the task ran. Without a jump nothing is re-read, so every value stays the request-time read it is today.resume()no longer re-anchors. Otherwise every request made until its queued task ran would look like it raced a jump and be re-read. A jump while paused is followed by the first frame after resume, like any other jump; the segment it closes is empty in a real cache.convert()hands the buffer's timeline to the session strategy. The flushed buffer segments and the session's first segment share one timeline, and a jump no frame has seen yet is still caught by the session's first frame.errorReplayDurationof elapsed time whichever way the clock jumped.Concurrency
replayExecutor.timelineLock(AutoClosableReentrantLockinBaseCaptureStrategy) makes the jump check, the re-anchor and the drain ofcurrentEventsone step. Touch events are stamped and appended under it too. So two frames can't both follow one jump, and each touch lands on exactly one side of it.submit:ReplayExecutorService.submitruns inline on the executor thread, which would mean holding the lock across segment capture. The lock is also never held across an SDK call or the logger.AnchoredClock.Compatibility
sentry-android-replay.apiis unchanged.ICurrentDateProvider(sentry-dart calls the 4-argReplayIntegrationconstructor). A small adapter makes it the anchor's wall clock.TODO [v9]now names what is left: removingICurrentDateProvider(JAVA-827).ReplayGestureConverteris untouched because fix(replay): Measure gesture batch durations on a monotonic clock (JAVA-830) #6538 changes it. ItsICurrentDateProvideris now backed by the replay clock.Not addressed here
onScreenshotRecorded(File, Long)are unchanged.💡 Motivation and Context
📝 Checklist
sendDefaultPIIis enabled.🔮 Next steps
ICurrentDateProviderfrom the replay strategies in v9 (JAVA-827).🤖 Generated with Claude Code