Skip to content

feat(replay): add selective unmasking and protect sensitive inputs - #574

Open
marandaneto wants to merge 2 commits into
mainfrom
fix/session-replay-selective-unmask
Open

feat(replay): add selective unmasking and protect sensitive inputs#574
marandaneto wants to merge 2 commits into
mainfrom
fix/session-replay-selective-unmask

Conversation

@marandaneto

@marandaneto marandaneto commented Sep 11, 2026

Copy link
Copy Markdown
Member

💡 Motivation and Context

Fixes #569.

Flutter apps cannot currently reveal selected safe content while keeping global replay masking enabled. Disabling maskAllTexts also leaves card-number, expiration-date, security-code, OTP, and password autofill fields without automatic SDK masking. Material fields with obscureText: true are already protected, but Cupertino and direct EditableText fields do not have the same protection.

This adds PostHogUnmaskWidget for known-safe Flutter text and images:

// Keep global text and image masking enabled.
PostHogUnmaskWidget(child: Text('Try again'))

Explicit PostHogMaskWidget masks and sensitive inputs remain masked regardless of nesting. Sensitive inputs include obscured fields, password keyboard types, and password, new-password, card-number, expiration-date (including day, month, and year), security-code, and OTP autofill hints. The mask also covers scaled text that extends beyond a dense field's layout bounds.

Flutter web still requires canvas masking to be enabled. The new widget does not remove masks from overlapping widgets or change native platform views or captured native screens. API documentation, the public API snapshot, and a minor changeset are included.

💚 How did you test it?

  • Restored the pre-fix masking code temporarily and reproduced 62 failures in the new regression suite on both the VM and an Android 16 arm64 emulator. The browser regression also failed before the fix.
  • Added 40 expiration-field regression tests in response to review. All 40 failed because masks were missing before the follow-up fix, then passed after adding the four expiration hints. Coverage includes Material, Cupertino, form, and direct editable inputs inside and outside unmask wrappers.
  • With the final fix, all 449 VM tests and 52 Chrome tests passed.
  • All 110 tests passed on the Android emulator through a temporary integration-test app. These include the 40 expiration regressions, captured-pixel comparisons using the production mask painter at 1x and 2x resolution, and an interaction test that types synthetic values and toggles password visibility and unmasking.
  • Verified all four expiration hints exist in Flutter 3.27.0, the minimum supported version.
  • Static analysis, formatting, the public API check, and git diff --check passed.
  • Ran autoreview --mode branch --base origin/main on commit 56fd14cba20ea185989e11d53b6bfd712b4a8ebe. It reported no actionable findings.

The tests use a fake SDK platform and a posthog-js stub. They do not upload recordings or verify backend ingestion. No iOS device run was performed.

📝 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

  • Added a changeset file.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Implemented with Pi using Git, Flutter, and GitHub CLI. The work was done in a dedicated worktree with reproduction-first tests and a separate Pi autoreview pass. Explicit masks and sensitive inputs take precedence over unmasking to prevent an overly broad wrapper from revealing them. Human review is required.

@marandaneto marandaneto self-assigned this Sep 11, 2026
@marandaneto
marandaneto marked this pull request as ready for review September 11, 2026 10:30
@marandaneto
marandaneto requested a review from a team as a code owner September 11, 2026 10:30
Comment thread posthog_flutter/lib/src/replay/mask/sensitive_text_input.dart
@veria-ai

veria-ai Bot commented Sep 11, 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: 1 · PR risk: 0/10

@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

posthog-flutter Compliance Report

Date: 2026-09-11 10:48:26 UTC
Duration: 96821ms

✅ All Tests Passed!

45/45 tests passed


Capture Tests

29/29 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields 142ms
Format Validation.Event Has Uuid 120ms
Format Validation.Event Has Lib Properties 117ms
Format Validation.Distinct Id Is String 116ms
Format Validation.Token Is Present 115ms
Format Validation.Custom Properties Preserved 117ms
Format Validation.Event Has Timestamp 116ms
Retry Behavior.Retries On 503 5329ms
Retry Behavior.Does Not Retry On 400 2118ms
Retry Behavior.Does Not Retry On 401 2119ms
Retry Behavior.Respects Retry After Header 8122ms
Retry Behavior.Implements Backoff 15446ms
Retry Behavior.Retries On 500 5225ms
Retry Behavior.Retries On 502 5224ms
Retry Behavior.Retries On 504 5224ms
Retry Behavior.Max Retries Respected 15442ms
Deduplication.Generates Unique Uuids 124ms
Deduplication.Preserves Uuid On Retry 5222ms
Deduplication.Preserves Uuid And Timestamp On Retry 10332ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry 5229ms
Deduplication.No Duplicate Events In Batch 124ms
Deduplication.Different Events Have Different Uuids 114ms
Compression.Sends Gzip When Enabled 116ms
Batch Format.Uses Proper Batch Structure 113ms
Batch Format.Flush With No Events Sends Nothing 107ms
Batch Format.Multiple Events Batched Together 122ms
Error Handling.Does Not Retry On 403 2116ms
Error Handling.Does Not Retry On 413 2116ms
Error Handling.Retries On 408 5224ms

Feature_Flags Tests

16/16 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id 13ms
Request Payload.Flags Request Uses V2 Query Param 10ms
Request Payload.Flags Request Hits Flags Path Not Decide 9ms
Request Payload.Flags Request Omits Authorization Header 9ms
Request Payload.Token In Flags Body Matches Init 9ms
Request Payload.Groups Round Trip 10ms
Request Payload.Groups Default To Empty Object 10ms
Request Payload.Person Properties Distinct Id Auto Populated When Caller Omits It 9ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False 9ms
Request Payload.Disable Geoip Omitted Defaults To False 9ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key 10ms
Request Lifecycle.No Flags Request On Init Alone 4ms
Request Lifecycle.No Flags Request On Normal Capture 112ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests 15ms
Request Lifecycle.Mock Response Value Is Returned To Caller 10ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event 114ms

@greptile-apps

greptile-apps Bot commented Sep 11, 2026

Copy link
Copy Markdown

Reviews (1): Last reviewed commit: "feat(replay): add selective unmasking an..." | Re-trigger Greptile

@marandaneto

Copy link
Copy Markdown
Member Author

Android emulator validation

Validated commit 9a6d0e9ef77d6171155ba19c32f5d420727c98b2 on an Android 16 arm64 emulator (API 36), using Flutter 3.47.3.

All 70 tests passed: the 69 regression tests plus a temporary interaction test that typed synthetic values into fields, enabled “show password,” and toggled the unmask wrapper off and on.

Pixel assertions and inspection of the captured source and masked images confirmed:

  • The safe error message remained visible inside PostHogUnmaskWidget.
  • Passwords, card numbers, and OTPs remained masked. The password stayed masked in the capture even when “show password” revealed it on screen.
  • Explicit masks and the private sibling outside the unmask wrapper remained masked.
  • Removing the unmask wrapper masked the safe content again. Restoring it revealed only the safe content.

The regression tests also checked captured pixels at 1x and 2x resolution. The earlier before/after run reproduced 62 failures against the pre-fix masking code on the emulator.

The temporary integration app used the SDK from this PR's worktree, a fake SDK platform, and the production mask painter. This validates Flutter rendering and captured replay pixels, not recording uploads or backend ingestion. No iOS device run was performed. No further SDK changes were needed.

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.

Session replay: no unmask primitive on Flutter, and masking ignores obscureText / autofillHints

1 participant