-
Notifications
You must be signed in to change notification settings - Fork 43
Fix session replay dropping screens inside Throttler window #605
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. |
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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) | ||||||||||||
|
Comment on lines
+40
to
42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
| // 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 | ||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Comment on lines
+40
to
+52
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi @tsushanth, thanks for the pull request. An agent found the following finding, it's worth verifying: retry runs after the skipped screen is gone Test sketch (not executed): at t=1000 draw A; t=1100 draw B; t=1200 draw C; run delayed work at t=2000 and t=3000. Expected a sequence containing B; current code snapshots current state C at both deadlines, producing A,C,C. Trigger: ordinary replay with two state changes inside the default 1s throttle window. Entry: Fix: capture/retain immutable pending state before it is replaced, or narrow the claim and issue closure. Add state-based A→B→C, single-pending-draw, continuous-draw, and no-pending-draw tests. |
||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hasPendingDrawuses@Volatilewhile the existingisThrottlingfield usesAtomicBoolean, which provides atomic compound operations viagetAndSet. The read-then-clear in thepostDelayedlambda (val pendingAfter = hasPendingDraw; hasPendingDraw = false) is not atomic. Ifthrottle()is ever called off the main thread (which theAtomicBooleanonisThrottlingleaves room for), a draw signal set between the read and the clear would be silently lost. UsingAtomicBoolean.getAndSet(false)matches the existing pattern and makes the operation atomic.