Skip to content

Fix session replay dropping screens inside Throttler window#605

Open
tsushanth wants to merge 2 commits into
PostHog:mainfrom
tsushanth:fix/596-throttler-dropped-screens
Open

Fix session replay dropping screens inside Throttler window#605
tsushanth wants to merge 2 commits into
PostHog:mainfrom
tsushanth:fix/596-throttler-dropped-screens

Conversation

@tsushanth

Copy link
Copy Markdown

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 a postDelayed for the first draw that arrives after a snapshot and sets isThrottling = true. Any subsequent draws while isThrottling is true hit the else branch and are silently discarded — no new postDelayed is queued. If the user navigates A → B → C within a single throttle window:

  1. B's first draw: isThrottling is falsepostDelayed scheduled, isThrottling = true
  2. C's draws: isThrottling is true → all silently dropped
  3. postDelayed fires → snapshots whatever is on screen now (C) → produces an A→C diff

Screen B never appears in the recording.

Fix

Add a hasPendingDraw flag. When a draw is discarded (isThrottling == true), set the flag. After the delayed snapshot fires, check the flag and call throttle() once more so the latest view tree is always captured.

@Volatile private var hasPendingDraw = false

// in postDelayed lambda:
val pendingAfter = hasPendingDraw
hasPendingDraw = false
executeAndReleaseThrottle(runnable)
if (pendingAfter) { throttle(runnable) }

// in else branch (was: silently dropped):
hasPendingDraw = true

1 file changed.

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
@tsushanth tsushanth requested a review from a team as a code owner July 6, 2026 05:18
@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown

Comments Outside Diff (1)

  1. posthog-android/src/main/java/com/posthog/android/replay/internal/Throttler.kt, line 25-55 (link)

    P2 No tests for the new pending-draw behavior. The PR description neatly describes the A→B→C scenario (navigate to B, then C, within a single throttle window), which would translate directly into a parameterised unit test verifying that the runnable is invoked a second time after the delayed snapshot fires. There are currently no tests for Throttler at all, so this is a good opportunity to add coverage before the edge-case logic grows further.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Reviews (1): Last reviewed commit: "Fix session replay dropping screens in T..." | Re-trigger Greptile

// 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Suggested change
@Volatile private var hasPendingDraw = false
private val hasPendingDraw = AtomicBoolean(false)

Comment on lines +40 to 42
val pendingAfter = hasPendingDraw
hasPendingDraw = false
executeAndReleaseThrottle(runnable)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 If hasPendingDraw is changed to AtomicBoolean, this read-then-clear becomes a single atomic getAndSet(false), eliminating the non-atomic window.

Suggested change
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Companion to the AtomicBoolean change: use set(true) for consistency.

Suggested change
hasPendingDraw = true
hasPendingDraw.set(true)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@ioannisj ioannisj left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a changeset for this. Greptile comments look reasonable to me. Also, I'm not sure if this fixes #596 since completely but maybe partially addresses it.

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.

Some screens appear to be dropped in session replay recordings

3 participants