Fix session replay dropping screens inside Throttler window#605
Fix session replay dropping screens inside Throttler window#605tsushanth wants to merge 2 commits into
Conversation
When a draw arrives while isThrottling is true, the existing code silently discards it with no rescheduled snapshot. If the user navigates to a new screen during the throttle window, that screen is completely absent from the replay recording. Add a hasPendingDraw flag. When a draw is discarded (isThrottling=true), set the flag. After the postDelayed snapshot fires, check the flag and call throttle() once more so the latest view tree is always captured. Fixes PostHog#596
|
| // Set to true when a draw arrives while a postDelayed is already in flight. | ||
| // The pending draw is re-captured once the delayed snapshot fires, ensuring | ||
| // that a screen change landing inside the throttle window is not silently lost. | ||
| @Volatile private var hasPendingDraw = false |
There was a problem hiding this comment.
hasPendingDraw uses @Volatile while the existing isThrottling field uses AtomicBoolean, which provides atomic compound operations via getAndSet. The read-then-clear in the postDelayed lambda (val pendingAfter = hasPendingDraw; hasPendingDraw = false) is not atomic. If throttle() is ever called off the main thread (which the AtomicBoolean on isThrottling leaves room for), a draw signal set between the read and the clear would be silently lost. Using AtomicBoolean.getAndSet(false) matches the existing pattern and makes the operation atomic.
| @Volatile private var hasPendingDraw = false | |
| private val hasPendingDraw = AtomicBoolean(false) |
| val pendingAfter = hasPendingDraw | ||
| hasPendingDraw = false | ||
| executeAndReleaseThrottle(runnable) |
There was a problem hiding this comment.
If
hasPendingDraw is changed to AtomicBoolean, this read-then-clear becomes a single atomic getAndSet(false), eliminating the non-atomic window.
| val pendingAfter = hasPendingDraw | |
| hasPendingDraw = false | |
| executeAndReleaseThrottle(runnable) | |
| val pendingAfter = hasPendingDraw.getAndSet(false) | |
| executeAndReleaseThrottle(runnable) |
| } else { | ||
| // A draw arrived while a postDelayed is already scheduled. | ||
| // Mark it so the delayed lambda re-captures the current view tree after it fires. | ||
| hasPendingDraw = true |
There was a problem hiding this comment.
Fixes #596
Problem
When a user navigates to a new screen during the throttle window, the screen is silently dropped from the session replay recording.
Throttler.throttle()schedules apostDelayedfor the first draw that arrives after a snapshot and setsisThrottling = true. Any subsequent draws whileisThrottlingis true hit theelsebranch and are silently discarded — no newpostDelayedis queued. If the user navigates A → B → C within a single throttle window:isThrottlingisfalse→postDelayedscheduled,isThrottling = trueisThrottlingistrue→ all silently droppedpostDelayedfires → snapshots whatever is on screen now (C) → produces an A→C diffScreen B never appears in the recording.
Fix
Add a
hasPendingDrawflag. When a draw is discarded (isThrottling == true), set the flag. After the delayed snapshot fires, check the flag and callthrottle()once more so the latest view tree is always captured.1 file changed.