Skip to content

fix(replay): honor changed masking flags on iOS/Android across setup() calls - #507

Merged
turnipdabeets merged 4 commits into
mainfrom
fix/refresh-parsers-shared-setup
Jul 30, 2026
Merged

fix(replay): honor changed masking flags on iOS/Android across setup() calls#507
turnipdabeets merged 4 commits into
mainfrom
fix/refresh-parsers-shared-setup

Conversation

@turnipdabeets

@turnipdabeets turnipdabeets commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

Follow-up to the review note on #499 (thread): refreshParsers() was only wired into the web setup path, so on iOS/Android a second setup() with changed maskAllTexts/maskAllImages kept masking replay screenshots from the stale parser map built at first singleton access — e.g. maskAllImages: false → true would leave images unmasked (a narrow PII-leak direction). Pre-existing on main, not a regression from #499.

Two changes close it:

  1. Shared setup() refreshes the parser mapPostHogMaskController.instance.refreshParsers(config.sessionReplayConfig) now runs on every platform. The web-side call in WebCanvasMaskProvider.register() stays: it is idempotent, and keeps the provider correct on its own if its entry point ever changes.
  2. ScreenshotCapturer resolves the live config at capture time (Posthog().config ?? _config). A device test of change 1 alone showed masking still stuck: the capturer is built once per widget lifecycle and gated the mask walk on the maskAllTexts/maskAllImages flags of the config object captured at construction, so a second setup()'s new config never reached it — the parser map refreshed, but the walk was skipped before consulting it.

#499 merged while this was being opened, so this targets main directly (rebased to a single commit on top of the #499 squash merge).

💚 How did you test it?

  • New tests: a second setup() with different masking flags rebuilds the parser map, and the capturer resolves the config of the latest setup() (both fail without their fix — verified by reverting each and re-running).
  • Full suite: 249 tests pass, flutter analyze and dart format clean.
  • Device E2E on an Android emulator: recorded a session with maskAll*: false, triggered an in-app second setup() with maskAll*: true, and confirmed in the PostHog replay that frames flip from readable to fully masked at the re-setup point. The same run with only change 1 left phase 2 unmasked, which is how change 2 was found.

📝 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: Human-driven (agent-assisted)

@turnipdabeets
turnipdabeets requested a review from a team as a code owner July 30, 2026 15:16
@turnipdabeets turnipdabeets self-assigned this Jul 30, 2026
Base automatically changed from feat/web-canvas-masking to main July 30, 2026 15:16
@turnipdabeets
turnipdabeets force-pushed the fix/refresh-parsers-shared-setup branch from 9457bab to 5e6f650 Compare July 30, 2026 15:18
@greptile-apps

greptile-apps Bot commented Jul 30, 2026

Copy link
Copy Markdown

Security Review

An existing application-provided canvas masking callback is overwritten rather than composed, potentially exposing sensitive regions that only the application callback masked.

Comments Outside Diff (1)

  1. posthog_flutter/lib/src/replay/web/web_canvas_mask_provider.dart, line 286-289 (link)

    P1 security Existing mask callback overwritten

    When an application supplies canvasCapture.maskRegionsFn for sensitive regions outside the tracked Flutter tree, this assignment replaces that callback with _computeMaskRegions, causing those application-defined regions to be recorded unmasked.

    How this was verified: The existing callback is copied and used as the opt-in signal before being unconditionally replaced by a callback that derives regions only from the Flutter widget tree.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: posthog_flutter/lib/src/replay/web/web_canvas_mask_provider.dart
    Line: 286-289
    
    Comment:
    **Existing mask callback overwritten**
    
    When an application supplies `canvasCapture.maskRegionsFn` for sensitive regions outside the tracked Flutter tree, this assignment replaces that callback with `_computeMaskRegions`, causing those application-defined regions to be recorded unmasked.
    
    **How this was verified:** The existing callback is copied and used as the opt-in signal before being unconditionally replaced by a callback that derives regions only from the Flutter widget tree.
    
    ---
    
    For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
Prompt To Fix All With AI
### Issue 1
posthog_flutter/lib/src/replay/web/web_canvas_mask_provider.dart:286-289
**Existing mask callback overwritten**

When an application supplies `canvasCapture.maskRegionsFn` for sensitive regions outside the tracked Flutter tree, this assignment replaces that callback with `_computeMaskRegions`, causing those application-defined regions to be recorded unmasked.

**How this was verified:** The existing callback is copied and used as the opt-in signal before being unconditionally replaced by a callback that derives regions only from the Flutter widget tree.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: 9457bab | Re-trigger Greptile

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

posthog-flutter Compliance Report

Date: 2026-07-30 19:00:31 UTC
Duration: 96895ms

✅ All Tests Passed!

45/45 tests passed


Capture Tests

29/29 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields 145ms
Format Validation.Event Has Uuid 120ms
Format Validation.Event Has Lib Properties 119ms
Format Validation.Distinct Id Is String 117ms
Format Validation.Token Is Present 116ms
Format Validation.Custom Properties Preserved 119ms
Format Validation.Event Has Timestamp 119ms
Retry Behavior.Retries On 503 5333ms
Retry Behavior.Does Not Retry On 400 2118ms
Retry Behavior.Does Not Retry On 401 2118ms
Retry Behavior.Respects Retry After Header 8128ms
Retry Behavior.Implements Backoff 15445ms
Retry Behavior.Retries On 500 5226ms
Retry Behavior.Retries On 502 5226ms
Retry Behavior.Retries On 504 5225ms
Retry Behavior.Max Retries Respected 15446ms
Deduplication.Generates Unique Uuids 127ms
Deduplication.Preserves Uuid On Retry 5225ms
Deduplication.Preserves Uuid And Timestamp On Retry 10337ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry 5233ms
Deduplication.No Duplicate Events In Batch 125ms
Deduplication.Different Events Have Different Uuids 118ms
Compression.Sends Gzip When Enabled 117ms
Batch Format.Uses Proper Batch Structure 115ms
Batch Format.Flush With No Events Sends Nothing 109ms
Batch Format.Multiple Events Batched Together 126ms
Error Handling.Does Not Retry On 403 2117ms
Error Handling.Does Not Retry On 413 2118ms
Error Handling.Retries On 408 5228ms

Feature_Flags Tests

16/16 tests passed

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

@posthog

posthog Bot commented Jul 30, 2026

Copy link
Copy Markdown

🦔 ReviewHog reviewed this pull request

Nothing worth raising this time, so here's a calming picture instead:

Someone relaxing in a sunny garden

Comment on lines +68 to +73
await Posthog().setup(first);
final capturer = ScreenshotCapturer(first);
expect(capturer.effectiveConfig, same(first));

final second = PostHogConfig('test_project_token');
await Posthog().setup(second);

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.

fwiw, i wouldn't be surprised if we had more gaps with duplicate setup calls, this looks like a really fringe scenario, and probably enters undefined behavior territory (unless i'm missing something)

i'm surprised you can initialize twice - imo the second should be a no-op

@turnipdabeets
turnipdabeets enabled auto-merge (squash) July 30, 2026 18:59
@turnipdabeets
turnipdabeets merged commit 1cb3a7c into main Jul 30, 2026
27 checks passed
@turnipdabeets
turnipdabeets deleted the fix/refresh-parsers-shared-setup branch July 30, 2026 19:03
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