Skip to content

chore!: remove wireframe session replay capture - #908

Merged
turnipdabeets merged 18 commits into
chore/v4-remove-deprecatedfrom
chore/v4-remove-replay-wireframe
Nov 3, 2026
Merged

turnipdabeets merged 18 commits into
chore/v4-remove-deprecatedfrom
chore/v4-remove-replay-wireframe

Conversation

@turnipdabeets

@turnipdabeets turnipdabeets commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

Part of the 4.0 stack, based on #871 (chore/v4-remove-deprecated). Ioannis asked to drop wireframe on Android too, matching posthog-ios 4.0 (PostHog/posthog-ios#907, then PostHog/posthog-ios#973 removed screenshotMode outright).

Android session replay has two capture modes. Wireframe is the default (screenshot = false). It serializes the View tree to JSON. Screenshot captures masked images. This PR removes wireframe, so replay always captures masked screenshots.

Why:

  • Wireframe leaks ph-no-capture containers. sdk-specs compliance/posthog-android.md n26 marks this: toWireframe() never checks isNoCapture() for a tagged container, so its children's text is still serialized in the default mode. The screenshot path masks a tagged view as one rect.
  • Wireframe doesn't see Compose. A ComposeView was one box with a background color. The screenshot path masks Compose through semantics.
  • One capture path instead of two. The recent privacy and alignment work (mask verification, keyboard pan, discard handling) only ever covered the screenshot path.

What changes

Before (default config) After
View-based screen wireframe tree (childWireframes, style, text masked with *) one screenshot wireframe with masked base64, the same payload screenshot = true already sent
Compose screen an empty box recorded as screenshots
PostHogSessionReplayConfig.screenshot selects the mode removed
drawableConverter / PostHogDrawableConverter used by wireframe image capture removed; nothing else read them
$sdk_debug_replay_capture_mode wireframe / screenshot always screenshot

The full snapshot and meta events are the same as what screenshot mode sends today, and the meta href (window title) is unchanged. The player already handles both formats, so the server doesn't need any change.

Public API: removing the option, not deprecating it as a no-op, follows PostHog/posthog-ios#973. An app that set screenshot = false to keep pixels out of recordings now gets a compile error instead of silently uploading screenshots, so it has to choose: mask what's sensitive (ph-no-capture, Modifier.postHogMask(), maskAllImages / maskAllTextInputs) or turn replay off.

Core RRWireframe trimmed (16126b22): it keeps only id, x, y, width, height, type and base64, matching iOS 4.0, and RRStyle is deleted. Each window sends one screenshot node, so the child-flattening tree diff becomes a single-node compare (update when the image or size changes). Screenshot frames no longer send an empty style: {}, the same shape iOS 4.0 sends. posthog-flutter's SnapshotSender passed style = RRStyle(), so it needs PostHog/posthog-flutter#684.

Deleted as orphaned: toWireframe() and its helpers (Drawable.base64/toRGBColor/toBitmap/copy, String.mask(), the theme background lookup) and the forceScreenshot parameter on generateSnapshot.

⚠️ Privacy / behavior notes for reviewers (also in the changeset)

For standard widgets the masking decision is unchanged: both modes used the same shouldMask* / isTextInputSensitive helpers. What changes is everything else:

  • Custom-drawn content can now appear. Wireframe sent a box for any view it didn't recognize (custom onDraw, maps, video, charts). Screenshots render the pixels, and masking only covers recognized widgets plus ph-no-capture / Modifier.postHogMask().
  • Compose apps start recording real content. That's quota and a consent/disclosure change for apps that had replay on but saw empty boxes.
  • Cost: more bandwidth and CPU per frame (PixelCopy + WebP encode).

Downstream PRs

Follow-ups (not in this PR)

💚 How did you test it?

  • :posthog-android:testDebugUnitTest and testWithoutCompose: pass. PostHogReplayIntegrationTest is 133/133.
    • Tests that set screenshot = true now get the same thing from the default config.
    • The race test that used a wireframe capture as the second, unguarded visibility caller now uses a second screenshot capture. Its name says so.
    • The capture-mode debugProperties tests are now one test, which checks that the default config reports screenshot.
    • The batch-request.json event-shape snapshot changes only on that one property.
  • spotlessCheck, apiCheck and the sample app build pass.
  • Published posthog 7.0.0 / posthog-android 4.0.0 from this branch to a local Maven repo and built KMP's downstream PR against it: config tests pass, the sample builds.
  • Not run: on-device manual replay.

📝 Checklist

  • All commits are signed (see Commit signing)
  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed. (draft docs PRs above; sdk-specs follow-up)
  • No breaking change or entry added to the changelog. (Breaking; added to the 4.0 changeset.)

If releasing new changes

  • Ran pnpm changeset to generate a changeset file (added a bullet to the existing .changeset/android-v4-major.md)

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

  • Written with Claude Code (Opus 5.5) at the maintainer's request, as a follow-up on the 4.0 stack that mirrors the iOS wireframe removal.
  • Also removed drawableConverter, which was never deprecated, because without wireframe it does nothing. Easy to put back as a deprecated no-op if we'd rather.

🤖 Generated with Claude Code

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

PostHog Review · Standard · reviewed 697f9c0: 1 Should fix.

This is the only automatic review while this pull request is a draft. Comment @posthog review to review new changes sooner. Otherwise, a review of the new changes runs when you mark it ready for review.

See this review and its findings in PostHog Review. Comment @posthog review --deep for a Deep review. A Deep review takes longer and looks deeper than a Standard review.

@veria-ai

veria-ai Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

PR overview

All previously flagged issues have been addressed. No open security concerns remain on this pull request.

Security review

No open security issues remain on this pull request.

Fixed/addressed: 3 · PR risk: 0/10

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

PostHog Review · Standard · reviewed e28cf3d: 1 Must fix.

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

PostHog Review · Standard · reviewed 37df5a2: 1 Must fix.

This is review round 3 on this pull request. When findings keep coming back in the same code, the approach is usually the problem. Before the next fix, check whether a different design would remove the whole class of findings.

@dustinbyrne dustinbyrne left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The animated-view and idle surface-scan fixes are verified. Two default screenshot privacy gaps remain: overflowing descendants of masked containers and Compose images without descriptions. Details are inline and in the existing Compose-image discussion.

AI-assisted review.

turnipdabeets and others added 4 commits October 30, 2026 11:06
Session replay always records masked screenshots, the mode
`screenshot = true` already used. Remove `PostHogSessionReplayConfig.screenshot`,
and `drawableConverter` / `PostHogDrawableConverter`, which only the
wireframe builder read.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
With wireframe capture gone, each window sends a single screenshot node,
so the tree fields, RRStyle and the child-flattening diff are unused.
Screenshot frames no longer carry an empty `style`, matching iOS 4.0.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@turnipdabeets
turnipdabeets force-pushed the chore/v4-remove-replay-wireframe branch from 37df5a2 to 9856753 Compare October 30, 2026 15:15
turnipdabeets and others added 3 commits October 30, 2026 13:25
# Conflicts:
#	posthog-android/src/main/java/com/posthog/android/replay/PostHogReplayIntegration.kt
#	posthog-android/src/main/java/com/posthog/android/replay/PostHogSessionReplayConfig.kt
#	posthog-android/src/test/java/com/posthog/android/replay/PostHogReplayIntegrationTest.kt
Co-Authored-By: Claude Opus 5.5 <[email protected]>

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

PostHog Review

Found 3 must fix.

This pull request is large, so the review ran in 4 parts with less depth than usual.

This is review round 4 on this pull request. When findings keep coming back in the same code, the approach is usually the problem. Before the next fix, check whether a different design would remove the whole class of findings.

Findings on the changed lines

posthog-android/src/main/java/com/posthog/android/replay/PostHogReplayIntegration.kt:789

P1 · Mask clipped text inputs before making screenshots the default

With default replay, an EditText partially clipped by a scrolling parent now uploads visible characters. addTextAreaGlobalVisibleRect() adds compound padding to the already-clipped visible rect, leaving rendered text outside the mask; wireframe capture previously serialized masked text. Compute text bounds before clipping, or mask the full visible rect.

posthog-android/src/main/java/com/posthog/android/replay/PostHogReplayIntegration.kt:789

P1 · Cover the entire sensitive rectangle before capturing screenshots by default

With default replay settings, masked images now expose corner pixels: paintScreenshotMasks() uses drawRoundRect with an inward radius, leaving pixels inside the sensitive rectangle untouched. Wireframe capture previously omitted those images entirely. Small images and text near view corners therefore leak into uploaded screenshots; paint opaque rectangular masks.

posthog-android/src/main/java/com/posthog/android/replay/PostHogReplayIntegration.kt:1122

P1 · Traverse unclipped containers whose own bounds are offscreen

When a stable container has offscreen bounds and clipChildren = false, its descendants can still draw onscreen. isVisible() rejects the container, while isRenderedButUnplaceable() returns false, so the walk skips its sensitive descendants. Default screenshot capture now uploads pixels wireframes omitted; traverse unclipped descendants despite the parent's invisible rect.

… HEAD

# Conflicts:
#	.changeset/android-v4-major.md

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

PostHog Review · Standard · reviewed fc6bbaa: 1 Must fix.

This is review round 5 on this pull request. When findings keep coming back in the same code, the approach is usually the problem. Before the next fix, check whether a different design would remove the whole class of findings.

Merged semantics promote a child's postHogUnmask to its clickable parent,
which skipped masking for every sibling. Read the unmerged tree instead.

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

PostHog Review · Standard · reviewed 885380c: 2 Must fix.

This is review round 6 on this pull request. When findings keep coming back in the same code, the approach is usually the problem. Before the next fix, check whether a different design would remove the whole class of findings.

Text with TextOverflow.Visible draws past its layout box, and
clearAndSetSemantics removes the Text metadata the walk relies on. Both
left text unmasked in screenshots.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

PostHog Review · Standard · reviewed 92916ae: 2 Must fix, 1 Should fix.

This is review round 7 on this pull request. When findings keep coming back in the same code, the approach is usually the problem. Before the next fix, check whether a different design would remove the whole class of findings.

… shadows

Overflow bounds ignored graphicsLayer transforms, and text shadows could
paint readable copies outside the mask.
Text(String) rebuilds its paragraph on every GetTextLayoutResult call,
which the verification and draw-sample walks repeated on the main thread.

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

PostHog Review · Standard · reviewed 788b3a8: 2 Must fix, 1 Should fix. This pull request is large, so the review ran in 4 parts with less depth than usual.

This is review round 8 on this pull request. When findings keep coming back in the same code, the approach is usually the problem. Before the next fix, check whether a different design would remove the whole class of findings.

turnipdabeets and others added 3 commits November 2, 2026 13:17
…er walk

A text style change is invisible until the layout is re-read, so any draw now drops the cached bounds. A walk over several ComposeViews also evicted earlier roots by clearing the shared cache for each one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Cleared semantics wipe the text semantics on the same layout node, so its overflow was masked only to the layout box. Read them from the text's modifier and reuse the painted-bounds measurement. Discard the frame when they can't be read, including on legacy walks.
Comment thread docs/migration-3.x-to-4.0.md Resolved

@ioannisj ioannisj left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 An AI agent wrote this on behalf of @ioannisj.

Just one small nit on the legacy discard logging, nothing in the way of merging from my side.

@turnipdabeets
turnipdabeets merged commit 265ec84 into chore/v4-remove-deprecated Nov 3, 2026
11 checks passed
@turnipdabeets
turnipdabeets deleted the chore/v4-remove-replay-wireframe branch November 3, 2026 15:30
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.

4 participants