fix(replay): capture Jetpack Compose content in wireframe mode - #665
Draft
posthog[bot] wants to merge 1 commit into
Draft
fix(replay): capture Jetpack Compose content in wireframe mode#665posthog[bot] wants to merge 1 commit into
posthog[bot] wants to merge 1 commit into
Conversation
Compose draws its whole UI into a single AndroidComposeView that has no child Views, so the wireframe walk in `toWireframe()` bottomed out at the decor view and Compose screens recorded as a blank screen — with nothing in the logs to explain it, even with `debug = true`. Screenshot mode was the only mode that could see Compose, and it ships off by default. Compose content is now read from the semantics tree (the only structural description available without capturing pixels) and emitted as text, input and image wireframes, giving Compose apps the same wireframe experience View-based apps already get. Masking follows the existing rules: `postHogUnmask` wins, then `postHogMask` and passwords force masking, then `maskAllTextInputs`. Semantics can only be read on the main thread, so every Compose root under the decor view is read in a single hop per capture rather than one hop per root, and the mapping from semantics to wireframes runs on the capture thread. Screenshot mode still gives the highest fidelity, so the SDK now also logs that recommendation once when it detects Compose in wireframe mode. Generated-By: PostHog Code Task-Id: 537208f8-bc65-49f5-9463-e30d3aa58623
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
💡 Motivation and Context
A customer reported that many of their Android session recordings render as blank screens. The cause is not app-specific: Jetpack Compose draws its entire UI into a single
AndroidComposeViewthat has no child Views, andView.toWireframe()only recurses intoViewGroupchildren. In the default wireframe mode the tree therefore bottomed out at the decor view plus a background colour, which the player renders as an empty screen. Screenshot mode is the only mode that could see Compose, and it ships off by default — so any Compose app that turns on session replay and doesn't also flipscreenshot = truegot recordings that look broken, with nothing in the logs to point at the cause even withdebug = true.Compose content is now built from the semantics tree — the only structural description of the UI available without capturing pixels — so Compose apps get the same wireframe experience View-based apps already get, with no change to the privacy posture of wireframe mode (still no pixels, and existing masking rules apply). Screenshot mode remains the highest-fidelity option, and the SDK now logs that recommendation once when it detects Compose in wireframe mode, closing the silent-failure gap.
Masking follows the same precedence as the screenshot path (
findMaskableComposeWidgets) and the View path:postHogUnmaskwins over everything, thenpostHogMaskand passwords force masking, thenmaskAllTextInputsapplies. Images need no rule of their own — semantics carries no pixels, so a Compose image is always the player's placeholder.Semantics can only be read on the main thread, so every Compose root under the decor view is read in one hop per capture (a screen can hold many roots, e.g. a
ComposeViewper list row); the mapping from semantics to wireframes then runs on the capture thread.💚 How did you test it?
Unit tests. 16 tests over the semantics-to-wireframe mapping (masking precedence, roles, density conversion, node skipping, id stability) and 4 end-to-end tests driving
generateSnapshot()under Robolectric: content appears in the emittedRRFullSnapshotEvent, an empty read reproduces the blank screen this fixes, ids stay stable across snapshots so an unchanged tree emits no mutations, and two Compose roots in one window don't collide. Full suite,lintDebugandapiCheckpass.Before/after on real snapshot payloads. No emulator was available in this environment, so instead of a screen recording I captured the actual wireframes the player receives, end to end through
generateSnapshot(), for a Compose login screen. Before, the whole payload is four nested empty divs and a window background — nothing to draw. After:Rendering both payloads side by side (an approximation of the player's transformer, not the player itself) goes from an empty screen to a recognisable masked login screen — the positions above are absolute, so the layout survives.
Not manually tested on a device or in the real player: no emulator/KVM was available here, so a device-level demo and a check in the real player are worth doing before release.
📝 Checklist
If releasing new changes
pnpm changesetto generate a changeset file🤖 Agent context
Autonomy: Fully autonomous
Written by Claude Code (Opus) from a PostHog Signals inbox report about blank Android recordings, using the repo's Gradle/Robolectric setup for verification.
Two alternatives were considered and rejected. Auto-promoting Compose screens to screenshot mode would fix the blank screens with full fidelity, but it silently changes what leaves the device: wireframe mode omits anything the SDK doesn't understand, whereas screenshot mode ships those pixels. That is a privacy-posture change that shouldn't be flipped without a human decision, and it would also affect mixed View/Compose windows. Logging a warning only was the report's suggested first step, but on its own it leaves the customer's recordings blank. Reading the semantics tree fixes the symptom while staying inside wireframe mode's "only ship what we understand" model — and PostHog already walks that same tree for screenshot masking.
Fidelity is deliberately bounded: no colours or typography (semantics doesn't carry them), and content with no semantics at all (custom
Canvasdrawing, icons without acontentDescription) stays invisible. That's why the one-time log still recommends screenshot mode. Reviewers may want to weigh in on whether the docs should also gain an Android Compose note, mirroring the existing iOS/SwiftUI guidance.Compose access is routed through a single internal seam (
composeSemanticsReader) because the Compose dependency iscompileOnly— its classes are absent from the unit-test classpath, and the existing test file deliberately avoids resolving Compose-referencing methods. The seam keeps that constraint in one place and lets the tests drive the mapping without a Compose runtime.Created with PostHog Desktop from this inbox report.