feat(android): compose screenshot windows in session replay - #823
dustinbyrne wants to merge 4 commits into
Conversation
|
[Medium risk] Adds window composition logic to session replay screenshots. This PR is not safe to merge until the dialog privacy-mask alignment and unthrottled layout capture path are addressed. Reviews (1) · Last reviewed commit: "feat(android): prototype screenshot-mode..." |
| ) { | ||
| cached | ||
| } else { | ||
| layer.view.toScreenshotWireframe(layer.window, layer.status.drawState, forceAlpha = true) |
There was a problem hiding this comment.
On API 26, PixelCopy can capture a dialog with a surface offset from its decor coordinates. This path paints masks using decor coordinates without validating that offset; the reported API 26 fixture leaves tagged dialog text visible outside its mask in the replay image. Align and validate the masks against the captured pixels, or omit overlays that cannot be aligned safely.
How this was verified: Dialog layers reach PixelCopy while their masks are painted from decor coordinates without a surface-origin adjustment.
Prompt To Fix With AI
This is a comment left during a code review.
Path: posthog-android/src/main/java/com/posthog/android/replay/PostHogReplayIntegration.kt
Line: 568
Comment:
**Dialog masks can miss text**
On API 26, PixelCopy can capture a dialog with a surface offset from its decor coordinates. This path paints masks using decor coordinates without validating that offset; the reported API 26 fixture leaves tagged dialog text visible outside its mask in the replay image. Align and validate the masks against the captured pixels, or omit overlays that cannot be aligned safely.
**How this was verified:** Dialog layers reach PixelCopy while their masks are painted from decor coordinates without a surface-origin adjustment.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| ViewTreeObserver.OnGlobalLayoutListener { | ||
| drawState.recordLayout() | ||
| if (isActive() && isNativeSdk && config.sessionReplayConfig.screenshot) { | ||
| requestSceneCapture(decorView) | ||
| } |
There was a problem hiding this comment.
Layout captures bypass throttling
On a screen that repeatedly lays out, this listener requests scene captures without the draw listener's throttle. It can drive repeated full-window PixelCopy and encoding work, increasing device load and replay payload. Coalesce or throttle layout-triggered captures with draw-triggered captures.
Prompt To Fix With AI
This is a comment left during a code review.
Path: posthog-android/src/main/java/com/posthog/android/replay/PostHogReplayIntegration.kt
Line: 393-397
Comment:
**Layout captures bypass throttling**
On a screen that repeatedly lays out, this listener requests scene captures without the draw listener's throttle. It can drive repeated full-window PixelCopy and encoding work, increasing device load and replay payload. Coalesce or throttle layout-triggered captures with draw-triggered captures.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| val attributes = window.attributes | ||
| layers.add( | ||
| SceneLayer( | ||
| view, | ||
| window, | ||
| status, | ||
| position[0], | ||
| position[1], | ||
| view.width, | ||
| view.height, | ||
| if (attributes.flags and WindowManager.LayoutParams.FLAG_DIM_BEHIND != 0) attributes.dimAmount else 0f, | ||
| ), |
There was a problem hiding this comment.
Composition reads the dialog's dim amount but not its window alpha. For a translucent dialog, such as the added proof scene with alpha set to 0.5, the replay draws the captured layer without that opacity and no longer matches what the user saw. Carry window opacity into the composed layer.
Prompt To Fix With AI
This is a comment left during a code review.
Path: posthog-android/src/main/java/com/posthog/android/replay/PostHogReplayIntegration.kt
Line: 527-538
Comment:
**Window opacity is ignored**
Composition reads the dialog's dim amount but not its window alpha. For a translucent dialog, such as the added proof scene with alpha set to 0.5, the replay draws the captured layer without that opacity and no longer matches what the user saw. Carry window opacity into the composed layer.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.|
|
||
| @Test | ||
| fun `dialog belongs to its activity scene even when window tokens differ`() { | ||
| val first = Robolectric.buildActivity(Activity::class.java).setup() | ||
| val second = Robolectric.buildActivity(Activity::class.java).setup() | ||
| try { | ||
| val sut = getSut() | ||
| val baseToken = Binder() | ||
| val dialogParams = WindowManager.LayoutParams().apply { token = Binder() } | ||
| assertTrue(sut.isSceneWindow(baseToken, first.get(), dialogParams, Dialog(first.get()).window!!)) | ||
| assertFalse(sut.isSceneWindow(baseToken, first.get(), dialogParams, Dialog(second.get()).window!!)) | ||
| assertFalse(sut.isSceneWindow(baseToken, null, dialogParams, Dialog(first.get()).window!!)) | ||
| dialogParams.token = baseToken | ||
| assertTrue(sut.isSceneWindow(baseToken, null, dialogParams, Dialog(second.get()).window!!)) | ||
| } finally { | ||
| second.pause().stop().destroy() | ||
| first.pause().stop().destroy() |
There was a problem hiding this comment.
Scene output lacks regression coverage
This test checks dialog ownership but never captures a scene or checks its emitted snapshot. Layer order, dialog removal, and masked pixels can therefore regress while it stays green. Add a scene-level test for those behaviors.
Prompt To Fix With AI
This is a comment left during a code review.
Path: posthog-android/src/test/java/com/posthog/android/replay/PostHogReplayIntegrationTest.kt
Line: 319-335
Comment:
**Scene output lacks regression coverage**
This test checks dialog ownership but never captures a scene or checks its emitted snapshot. Layer order, dialog removal, and masked pixels can therefore regress while it stays green. Add a scene-level test for those behaviors.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.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!
marandaneto
left a comment
There was a problem hiding this comment.
Automated advisory code review. The blocking layout-throttling issue is already covered by an existing inline comment; posting the remaining independently reproduced blocking finding.
| return false | ||
| } | ||
| val result = mutableListOf<RREvent>() | ||
| if (sceneViewport != viewport) { |
There was a problem hiding this comment.
blocking: Emit metadata when the base activity changes — Metadata is now emitted only when viewport dimensions change. Switching between same-sized activities updates the screenshot but leaves the previous activity's href in replay; the previous per-decor sentMetaEvent state emitted metadata for each new activity. Track the base activity/title as well as viewport dimensions when deciding whether to emit RRMetaEvent. Reproduction: reproduced — ./gradlew :posthog-android:testDebugUnitTest --tests 'com.posthog.android.replay.PostHogReplayIntegrationTest.*review *' fails on the reviewed head because the snapshot after CheckoutActivity contains no metadata for ReceiptActivity; the equivalent merge-base producer test passes.
💡 Motivation and Context
Screenshot-mode Android replay currently records the Activity and its dialog in separate window snapshots, so a bottom sheet can play over a blank canvas instead of the checkout beneath it. This draft composes attached Activity and dialog layers into one scene, retaining transparent pixels and the dim layer, and sends complete snapshots under the session window ID. It includes a debug-only synthetic checkout and window probes for reproduction.
Shared replay comparison
Both recordings show the synthetic Northstar Market checkout and delivery-options bottom sheet:
Before this can be merged
💚 How did you test it?
:posthog-android:testDebugUnitTest --tests 'com.posthog.android.replay.PostHogReplayIntegrationTest': 127 passing tests, including differing Activity/dialog window-token ownership.:posthog-samples:posthog-android-sample:assembleDebug,spotlessKotlinCheck, andspotlessKotlinGradleCheckpassed.📝 Checklist
If releasing new changes
pnpm changesetto generate a changeset file.🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Pi coding agent assisted with Kotlin changes, emulator fixtures, Gradle checks, mock ingest, and Chromium rrweb playback. The session transcript is not linked because it contains a test-project credential. This draft deliberately keeps screenshot-mode composition in the Android SDK and leaves player production work and the older-API masking fix for follow-up; human review is required before merge.