Skip to content

fix(replay): warn when wireframe capture cannot record a Compose window - #760

Open
posthog[bot] wants to merge 4 commits into
mainfrom
posthog-self-driving/fixreplay-warn-when-wireframe-mode-cant-24f8f3
Open

fix(replay): warn when wireframe capture cannot record a Compose window#760
posthog[bot] wants to merge 4 commits into
mainfrom
posthog-self-driving/fixreplay-warn-when-wireframe-mode-cant-24f8f3

Conversation

@posthog

@posthog posthog Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

  • Every Jetpack Compose app that turns on session replay with the shipped defaults records a blank gray screen, and nothing in the SDK says why. The developer's only route to the answer today is a support ticket.
  • sessionReplayConfig.screenshot defaults to false, so capture builds a wireframe. View.toWireframe() only walks classic View types (TextView, ImageView, Spinner, ViewGroup), and a Compose-rooted window contains none of them, so the tree comes back essentially empty.
  • The SDK already detects a Compose root (isComposeView() / containsComposeView() / WindowDrawState.composeRooted), but that detection only fed the screenshot mask path. On the wireframe path it logged nothing and changed nothing.
  // PostHogSessionReplayConfig
- screenshot = false   // wireframe capture: classic Views only -> blank on Compose, silently
+ screenshot = false   // same default, but the SDK now says when it cannot record the screen
Window Capture mode Before After
Classic Views wireframe records records, no new log
Compose wireframe blank, silent blank, one warning naming sessionReplayConfig.screenshot = true
Compose screenshot records records, unchanged

Changes

  • generateSnapshot() now calls warnIfComposeWireframe() on the wireframe branch. It reads the cached Compose-root verdict; when that is unknown it resolves it inline on the main thread, or posts the check without waiting when called from the capture executor and reads the result on the next snapshot. The capture thread never blocks for this log line.
  • The check only runs while debug logging is on (the guard reads logger.isEnabled() first), and a "not Compose" verdict is re-checked after a layout at most once per second, so a classic-View app with debug on pays a small bounded walk and nothing with debug off.
  • The warning fires once per process, guarded by an AtomicBoolean that is read before the Compose check, so a warned process never pays for the check again.
  • Capture behaviour is unchanged. The mode is not switched automatically: screenshots can show content the mask rules do not cover, so that stays an explicit opt-in.
  • KDoc on screenshot, maskAllTextInputs and maskAllImages now states the wireframe limit and that masking applies to both modes.

💚 How did you test it?

  • Two new Robolectric tests in PostHogReplayIntegrationTest: a Compose-rooted window warns exactly once over two snapshots, and a classic-View window stays quiet.
  • Verified the test is real: with the one-line call removed, wireframe capture warns once about a compose rooted window fails; with it restored it passes.
  • Ran the full :posthog-android:testReleaseUnitTest suite and spotlessCheck. Both pass.
  • Not tested on a device or emulator, and no sample app run. The blank-recording symptom itself is not re-proved here; it follows from View.toWireframe() covering only classic View types and is the same failure mode addressed for screenshot mode in fix(replay): keep Compose screenshots by routing them onto the verified mask path #713.

📝 Checklist

  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.

If releasing new changes

  • Ran pnpm changeset to generate a changeset file

🤖 Agent context

Autonomy: Fully autonomous

  • Written by Claude Opus 5 in PostHog Desktop, from an inbox report about blank Compose session recordings. Tools used: shell (gradle, git), file read/edit, GitHub CLI.
  • Considered switching to screenshot capture automatically when a Compose root is found. Rejected it: it changes what leaves the device without the developer asking, and screenshot capture is opt-in for that reason. A warning gives the same information and leaves the choice with the app.
  • Placed the check in generateSnapshot() rather than at install time, because a Compose root can be mounted lazily and is only known from the live view tree.
  • The environment had no JDK and no Android SDK; both were installed to run the test suite.

Created with PostHog Desktop from this inbox report.

Wireframe capture is the default (sessionReplayConfig.screenshot = false) and
View.toWireframe() only walks classic Android View types, so a Jetpack Compose
window produces an almost empty wireframe tree that plays back as a blank gray
screen. The SDK already detects a Compose root for the screenshot mask path, but
said nothing on the wireframe path.

Log one warning per process when a Compose root is found while wireframe capture
is on, naming sessionReplayConfig.screenshot = true as the fix. Also state the
limit and the mask scope in the PostHogSessionReplayConfig KDoc.

Generated-By: PostHog Desktop
Task-Id: 0206a002-16b5-4fb5-822e-2085e838e802
@posthog

posthog Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

🦔 PostHog Review reviewed this pull request

Found 0 must fix, 1 should fix, 1 consider.

Published 2 findings (view the review).

Resolved comments: 2 fixed

@posthog

posthog Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

PostHog Review alpha 🦔 If you find any issues helpful - please reply "valid", "invalid", etc., for evaluation purposes 🙏

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review

Found 1 should fix, 1 consider.

posthog Bot added 2 commits September 3, 2026 19:59
…s on

The once-per-process guard was spent by compareAndSet before config.logger.log
ran, but PostHogAndroidLogger drops the message unless config.debug is true and
debug defaults to false. A developer who called PostHog.debug(true) after the
first Compose snapshot could therefore never see the warning.

Check config.logger.isEnabled() before spending the guard, so the budget is only
consumed on a line that is actually delivered. Reading it first also skips the
Compose root check entirely while debug logging is off, which is the common case.

Generated-By: PostHog Desktop
Task-Id: ff7ca759-b632-4447-9bda-c11574bd8e0b
Compose detection matches an AndroidComposeView at any depth, not just at the
window root, so a hybrid screen with one interop ComposeView inside an XML
layout trips the warning. On that screen View.toWireframe() still recurses over
every ViewGroup child and emits real nodes for TextView, ImageView, ProgressBar,
RatingBar, Switch and WebView, so only the Compose subtree comes back as an
empty box. Saying "the recording will be blank" overstated it.

Reword the line to say Compose content records blank, and that this is the whole
recording only when Compose draws the whole screen. The screenshot recommendation
is unchanged. No behaviour change.

Generated-By: PostHog Desktop
Task-Id: ff7ca759-b632-4447-9bda-c11574bd8e0b
@ioannisj ioannisj self-assigned this Sep 4, 2026
@ioannisj
ioannisj marked this pull request as ready for review September 4, 2026 12:46
@ioannisj
ioannisj requested a review from a team as a code owner September 4, 2026 12:46
@marandaneto

Copy link
Copy Markdown
Member

if our goal is to deprecate wireframes, is it worth doing this? maybe a single warning if screenshot is disabled and its a jetpack compose app check would be enough?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants