diff --git a/.changeset/fix-throttler-dropped-screens.md b/.changeset/fix-throttler-dropped-screens.md new file mode 100644 index 00000000..bb2054ac --- /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. 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 69d87d0a..d7d762a1 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 } } }