Skip to content

fix(replay): mask descendants of ph-no-capture containers - #914

Merged
turnipdabeets merged 2 commits into
mainfrom
fix/replay-mask-no-capture-descendants
Oct 30, 2026
Merged

turnipdabeets merged 2 commits into
mainfrom
fix/replay-mask-no-capture-descendants

Conversation

@turnipdabeets

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

In screenshot replay, a ph-no-capture view masked only its own rect and the mask walk stopped there. A child of an unclipped container (clipChildren = false) can draw outside that rect, and that overflow was recorded unmasked, even overflowing text that default maskAllTextInputs would have masked without the tag. Reported in #908 review. It predates #908, but #908 makes screenshots the only mode, so it should land on main first.

The fix follows session-replay-privacy Behavior 4: the wrapper's rect is masked along with its descendants' rects.

  • A ph-no-capture view, and every visible descendant, adds its rect, and the walk keeps descending.
  • A ComposeView inside the subtree is masked whole instead of running the semantics pass. So a Compose postHogUnmask node that overflows a tagged View container stays masked. This was already the case inside the container's bounds, and it errs toward privacy.
  • ph-no-mask on a View descendant still takes precedence, as before.
  • Descendants are now checked like any other view, so a child that is animating or mid-layout makes the verified walk discard that frame. Untagged screens already do this.

💚 How did you test it?

  • New Robolectric test no-capture container masks descendants that overflow its bounds. A tagged, unclipped wrapper has overflowing text, plain, Compose and ph-no-mask children. The test asserts that the wrapper and the first three are masked and the ph-no-mask child isn't. It also checks that the draw-time verification walk isn't misaligned, and keeps an untagged control where only the text is masked. It fails without the fix.
  • ./gradlew :posthog-android:testReleaseUnitTest --tests '*replay*' passes; spotlessCheck is clean.
  • Not tested on a device.

📝 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.
  • No breaking change or entry added to the changelog.

If releasing new changes

  • Ran pnpm changeset to generate a changeset file

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

  • Written with Claude Code, then run through a pre-PR review loop: one review cycle, then triage. Triage kept only the missing changeset.
  • Deferred: skipping descendant rects that fall inside the container rect. This is a perf-only change and hasn't been measured.
  • The test root sets clipChildren = false because getGlobalVisibleRect clips a child to its parent based on the grandparent's flag.

🤖 Generated with Claude Code

A ph-no-capture view only masked its own rect and stopped the walk, so a
child of an unclipped container could draw outside that rect unmasked.
Descendants are now walked and masked too; a ComposeView inside the
subtree is masked whole. ph-no-mask on a descendant still wins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@turnipdabeets turnipdabeets self-assigned this Oct 29, 2026
@turnipdabeets
turnipdabeets requested a review from a team as a code owner October 29, 2026 21:22

@marandaneto marandaneto left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The code looks good; no qualifying findings. Verdict: correct.

@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.

Nothing blocks merging here. The descendant walk lines up with the privacy spec and the new test covers the overflow, the verified re-walk and the untagged control. One thing worth a look: a ComposeView that carries the ph-no-capture tag itself still skips the no-capture branch, which predates this PR and could be a follow-up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@turnipdabeets
turnipdabeets merged commit 3280f6c into main Oct 30, 2026
16 checks passed
@turnipdabeets
turnipdabeets deleted the fix/replay-mask-no-capture-descendants branch October 30, 2026 14:52
turnipdabeets added a commit that referenced this pull request Oct 30, 2026
Moves #914's ph-no-capture descendant masking into the shared classifier as a NO_CAPTURE rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

3 participants