Skip to content

fix(web): forward maskAllTexts/maskAllImages to posthog-js maskCanvas on Flutter web#454

Open
tsushanth wants to merge 1 commit into
PostHog:mainfrom
tsushanth:fix/flutter-web-canvas-masking-66291
Open

fix(web): forward maskAllTexts/maskAllImages to posthog-js maskCanvas on Flutter web#454
tsushanth wants to merge 1 commit into
PostHog:mainfrom
tsushanth:fix/flutter-web-canvas-masking-66291

Conversation

@tsushanth

Copy link
Copy Markdown

Problem

On Flutter web with the CanvasKit renderer, maskAllTexts = true and maskAllImages = true in PostHogSessionReplayConfig were silently ignored. Session replay there is handled entirely by posthog-js recording the raw <canvas> element — the Dart widget-tree masking pipeline (painting black rectangles over RenderParagraph / RenderImage nodes before screenshot) is never reached because sendMetaEvent and sendFullSnapshot are no-ops on web. As a result, PII painted to the canvas by Text widgets remained visible in recorded replays regardless of the masking config.

Reported in PostHog/posthog#66291.

Fix

During setup() on web, when sessionReplay is enabled and either masking flag is true, the SDK now calls:

posthog.set_config({ session_recording: { maskCanvas: true } })

rrweb then replaces the entire canvas content with a solid colour before encoding each snapshot frame — which is the closest equivalent to per-widget masking that the CanvasKit architecture allows, since all content exists as opaque pixels rather than DOM text nodes.

The tradeoff (whole-canvas replacement vs per-widget bounds) is intrinsic to how CanvasKit works. The PostHogSessionReplayConfig docstring now explains the platform difference and points users to posthog.init() session_recording.maskCanvas for direct control if they need to decouple the flags.

Changes

  • posthog_flutter_web_handler.dart — expose set_config on the PostHog JS interop extension
  • posthog_flutter_web.dart — call set_config with maskCanvas: true during setup() when either masking flag is enabled
  • posthog_config.dart — document per-platform masking behaviour on PostHogSessionReplayConfig, maskAllTexts, and maskAllImages
  • posthog_flutter_web_handler_test.dart — four browser-only tests covering all flag combinations (maskAllTexts only, maskAllImages only, both false, sessionReplay disabled)

Testing

flutter test --platform chrome test/posthog_flutter_web_handler_test.dart

All four new tests plus the existing suite pass.

… on Flutter web

On Flutter web with the CanvasKit renderer, session replay is handled by
posthog-js recording the raw <canvas> element.  The Dart widget-tree masking
pipeline (RenderParagraph / RenderImage rectangles) is never reached because
sendMetaEvent and sendFullSnapshot are no-ops on web, so maskAllTexts and
maskAllImages were silently ignored and PII painted to the canvas remained
visible in replays.

Fix: during setup() on web, when sessionReplay is enabled and either
masking flag is true, call posthog.set_config({ session_recording: { maskCanvas:
true } }).  rrweb then replaces the entire canvas content with a solid colour
before encoding each snapshot frame, matching the intent of the masking flags.

The tradeoff — whole-canvas masking instead of per-widget bounds — is
fundamental to how CanvasKit works (pixels, no DOM text nodes) and is
documented on PostHogSessionReplayConfig with guidance to configure
session_recording.maskCanvas directly in posthog.init() for more control.

Adds four browser-only tests covering the four flag combinations.

Fixes PostHog/posthog#66291
@tsushanth tsushanth requested a review from a team as a code owner June 26, 2026 13:58
@greptile-apps

greptile-apps Bot commented Jun 26, 2026

Copy link
Copy Markdown

Reviews (1): Last reviewed commit: "fix(web): forward maskAllTexts/maskAllIm..." | Re-trigger Greptile

Comment on lines +189 to +243
test('calls set_config maskCanvas when maskAllTexts is true', () async {
final config = PostHogConfig('test-token')
..sessionReplay = true
..sessionReplayConfig = (PostHogSessionReplayConfig()
..maskAllTexts = true
..maskAllImages = false);

await PosthogFlutterWeb().setup(config);

expect(setConfigCalled, isTrue);
final cfg = capturedConfig!.dartify()! as Map<Object?, Object?>;
final recording = cfg['session_recording'] as Map<Object?, Object?>;
expect(recording['maskCanvas'], isTrue);
});

test('calls set_config maskCanvas when maskAllImages is true', () async {
final config = PostHogConfig('test-token')
..sessionReplay = true
..sessionReplayConfig = (PostHogSessionReplayConfig()
..maskAllTexts = false
..maskAllImages = true);

await PosthogFlutterWeb().setup(config);

expect(setConfigCalled, isTrue);
final cfg = capturedConfig!.dartify()! as Map<Object?, Object?>;
final recording = cfg['session_recording'] as Map<Object?, Object?>;
expect(recording['maskCanvas'], isTrue);
});

test('does not call set_config maskCanvas when both masking flags are false',
() async {
final config = PostHogConfig('test-token')
..sessionReplay = true
..sessionReplayConfig = (PostHogSessionReplayConfig()
..maskAllTexts = false
..maskAllImages = false);

await PosthogFlutterWeb().setup(config);

expect(setConfigCalled, isFalse);
});

test('does not call set_config maskCanvas when sessionReplay is disabled',
() async {
final config = PostHogConfig('test-token')
..sessionReplay = false
..sessionReplayConfig = (PostHogSessionReplayConfig()
..maskAllTexts = true
..maskAllImages = true);

await PosthogFlutterWeb().setup(config);

expect(setConfigCalled, isFalse);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Prefer parameterised tests for the masking flag combinations

The four new tests are structurally identical — each creates a PostHogConfig with a specific pair of (maskAllTexts, maskAllImages, sessionReplay) values and asserts whether set_config was called. Per the project's coding conventions, this pattern should be expressed as a single parameterised test (a for loop over a table of cases) rather than four near-duplicate test bodies. The "both flags true + sessionReplay enabled" combination is also absent from the positive-call cases, which a table format would make obvious to add.

Context Used: Do not attempt to comment on incorrect alphabetica... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@turnipdabeets

turnipdabeets commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

@tsushanth IIRC maskCanvas isn't a posthog-js session_recording option, so it gets dropped — posthog-js only forwards an allow-listed set of keys to rrweb, and maskCanvas isn't one of them.

Could you please validate against an actual recorded replay, since the current tests only assert the SDK calls its own mock? A short public video of a replay with the canvas masked attached to the PR would be super helpful for confirming it end to end. 🙏

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