feat(replay): mask canvas session replay recordings on Flutter web#499
Draft
turnipdabeets wants to merge 1 commit into
Draft
feat(replay): mask canvas session replay recordings on Flutter web#499turnipdabeets wants to merge 1 commit into
turnipdabeets wants to merge 1 commit into
Conversation
Contributor
posthog-flutter Compliance ReportDate: 2026-07-25 16:09:07 UTC ✅ All Tests Passed!45/45 tests passed Capture Tests✅ 29/29 tests passed View Details
Feature_Flags Tests✅ 16/16 tests passed View Details
|
turnipdabeets
force-pushed
the
feat/web-canvas-masking
branch
2 times, most recently
from
July 24, 2026 20:08
9216aee to
4d8f8d1
Compare
On Flutter web (CanvasKit) session replay is recorded by posthog-js canvas capture, and DOM-based masking cannot reach text painted into the canvas — sessionReplayConfig masking options were silent no-ops (#496). setup() now registers a mask-region provider with posthog-js (session_recording.captureCanvas.canvasMaskRegionsFn): widget-tree rects are computed with the same selection logic mobile uses (maskAllTexts / maskAllImages / PostHogMaskWidget / obscured text fields), converted to canvas-relative CSS pixels (transform-aware, 1px outset, per-Flutter-frame cache), and painted black inside the posthog-js capture pipeline before frames are encoded. Fails closed: a failed widget-tree walk yields a full-canvas mask, and with requireMaskProvider set in the posthog.init HTML config, frames captured before Flutter registers are blacked out. The flt-semantics accessibility tree (which mirrors widget text into recordable DOM) is excluded via blockSelector, and an in-flight recording is restarted once so start-time options apply. Registration retries with backoff until posthog-js is available — covering both the snippet stub being replaced by the real instance and posthog-js loading after Flutter entirely. Requires posthog-js with captureCanvas.canvasMaskRegionsFn support and config.sessionReplay = true. Fixes #496 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
turnipdabeets
force-pushed
the
feat/web-canvas-masking
branch
from
July 25, 2026 16:05
4d8f8d1 to
89d7af3
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
💡 Motivation and Context
Fixes #496 — on Flutter web, session replay is posthog-js canvas capture, and the entire app (including PII text) is pixels inside the CanvasKit canvas. DOM-based masking can't see it, and
sessionReplayConfig.maskAllTexts/maskAllImageswere silent no-ops on web.setup()(web, whenconfig.sessionReplayis true) registers a mask-region provider with posthog-js viaset_config:PostHogMaskController.getMaskElements, new single-walk method) with the exact selection rules mobile uses:maskAllTexts/maskAllImagesgate the full text/image sets;PostHogMaskWidgetand obscured text fields always mask. Rects are transform-aware (AABB for rotated/scaled widgets), outset 1px, converted to canvas-relative CSS px against theflutter-viewhost rect (correct for embedded/offset views), and cached per Flutter frame (recompute only when a frame was painted — idle cost ≈0.01ms/call, measured).PostHogWidgetrequirement); withrequireMaskProviderset in theposthog.initHTML config, frames captured before Flutter boots are blacked out; non-Flutter canvases get no regions (identity-checked via the flutter-view host chain).flt-semanticsDOM nodes as plain text, which rrweb records; the provider forwardsblockSelector: 'flt-semantics-host'(merged with any user selector). Reproduced and verified gone.blockSelector/requireMaskProvideronly at rrweb record() start, so an in-flight recording is restarted once after registration (same session id; sampling decision persists). Registration retries with backoff (250ms→4s, 2min cap) until posthog-js is available — covering both the snippet stub being replaced by the real instance and posthog-js loading after Flutter entirely.No new public Dart API (package barrel diff is zero lines). Existing config options and
PostHogMaskWidgetsimply start working on web, with mobile-identical semantics.💚 How did you test it?
flutter analyzeclean; VM tests 7/7 (geometry incl. rotation AABB + degenerate rects; single-walk equivalence vs the two-walk union); browser tests 11/11 (--platform chrome, wired into CI): set_config merge preserving user config, fail-closed full-cover for a flutter-view canvas without PostHogWidget, warn-once on persistent walk failure, foreign-canvas exclusion, semantics blockSelector with maskAllTexts=false, deferred registration + stub-replacement convergence (incl. posthog-js absent entirely at setup), restart-exactly-once.before_send(all text masked incl. scaled/rotated, images honoringmaskAllImages; wrapper-only tier verified — exactly the wrapped/obscured widgets masked), and real recordings played + backward-scrub-tested in the production PostHog player.Known limitations (documented in CHANGELOG; docs PR to follow):
CustomPainter-drawn text isn't in the widget tree → not masked (same as mobile;PostHogMaskWidgetis the escape hatch); requiresconfig.sessionReplay = trueandPostHogWidgetwrapping the app — without the widget, masking fails closed (black frames) by design.📝 Checklist
If releasing new changes
## Nextentry added (repo convention).🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Built with Claude Code from the investigation of #496 (transferred from PostHog/posthog#66291). The provider deliberately reuses the mobile masking walk rather than a parallel implementation, so web and mobile masking semantics cannot drift. Notable findings fixed during pre-PR review loops (several with executed repros): posthog-js replaces the snippet stub rather than upgrading it (retry must re-read
window.posthog),set_configagainst the stub would wipe usersession_recordingconfig, start-time-only options require the one-time recording restart, and the a11y semantics tree leaks widget text as DOM text nodes. E2E verification was performed against the sibling posthog-js branch with wire-level frame inspection and the production replay player.🤖 Generated with Claude Code