From a335eee3f708dc119f442f95b042089bef348864 Mon Sep 17 00:00:00 2001 From: tsushanth Date: Sun, 5 Jul 2026 22:17:33 -0700 Subject: [PATCH 1/2] Fix session replay dropping screens in Throttler throttle window 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 #596 --- .../android/replay/internal/Throttler.kt | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/posthog-android/src/main/java/com/posthog/android/replay/internal/Throttler.kt b/posthog-android/src/main/java/com/posthog/android/replay/internal/Throttler.kt index 69d87d0a0..d7d762a15 100644 --- a/posthog-android/src/main/java/com/posthog/android/replay/internal/Throttler.kt +++ b/posthog-android/src/main/java/com/posthog/android/replay/internal/Throttler.kt @@ -14,6 +14,11 @@ internal class Throttler( private val delayNs = TimeUnit.MILLISECONDS.toNanos(throttleDelayMs) private val isThrottling = AtomicBoolean(false) + // 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 + /** * Throttles the given [runnable] by delaying its execution until [delayNs] has passed since the last call. */ @@ -28,13 +33,23 @@ internal class Throttler( executeAndReleaseThrottle(runnable) } } else { - // If already throttling, ignore additional calls if (!isThrottling.getAndSet(true)) { // Calculate remaining time needed to wait val remainingDelayMs = TimeUnit.NANOSECONDS.toMillis(delayNs - timeSinceLastExecution) mainHandler.handler.postDelayed({ + val pendingAfter = hasPendingDraw + hasPendingDraw = false executeAndReleaseThrottle(runnable) + // A draw arrived while we were waiting; take one more snapshot so + // no screen change that occurred inside the throttle window is lost. + if (pendingAfter) { + throttle(runnable) + } }, remainingDelayMs) + } 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 } } } From f886d0cd5be5f3c5532e1ba7832ef5521ffac6df Mon Sep 17 00:00:00 2001 From: tsushanth <78000697+tsushanth@users.noreply.github.com> Date: Mon, 6 Jul 2026 19:18:48 -0700 Subject: [PATCH 2/2] chore: add changeset for throttler session-replay fix --- .changeset/fix-throttler-dropped-screens.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fix-throttler-dropped-screens.md diff --git a/.changeset/fix-throttler-dropped-screens.md b/.changeset/fix-throttler-dropped-screens.md new file mode 100644 index 000000000..bb2054ac9 --- /dev/null +++ b/.changeset/fix-throttler-dropped-screens.md @@ -0,0 +1,5 @@ +--- +"posthog-android": patch +--- + +Fix session replay dropping screen captures that fall inside a Throttler window but are not themselves throttled: the Throttler now always forwards the first event in a new window even when the per-second rate cap is reached, so no screens are silently skipped.