Skip to content

feat: capture platform views in session replay screenshots#453

Open
turnipdabeets wants to merge 1 commit into
mainfrom
pr-393-platform-views
Open

feat: capture platform views in session replay screenshots#453
turnipdabeets wants to merge 1 commit into
mainfrom
pr-393-platform-views

Conversation

@turnipdabeets

@turnipdabeets turnipdabeets commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

Platform views (WebView, Google Maps, etc.) rendered by Flutter are composited outside the Flutter layer tree. Without explicit handling, session replay either silently skips them (leaving blank gaps) or could inadvertently capture sensitive content. This PR adds first-class support: views are masked by default (black box), with opt-in capture on a per-view or global basis.

Closes #393.
Addresses only part 1 of #151

Note I'll address fully native views in a separate PR, that will require iOS and android SDK work as well.

💚 How did you test it?

  • iOS simulator (iPhone 17, iOS 26.4): ran all 8 example app test flows — masked views appear as black boxes in PostHog replay, captured WebViews show actual content
  • Android device (Pixel 4, Android 13): same flows verified
  • flutter test in posthog_flutter/ — all tests pass
  • Confirmed 5-second timeout fires correctly when native channel does not respond

See videos:

Android: https://us.posthog.com/shared/AYnlFG5M-DGczYUYyn27g1EC55HvDg?t=1
https://us.posthog.com/shared/kWlqsDn5LUynHgynC0Mp1t_wnAfK5w?t=2

iOS: https://us.posthog.com/shared/2G_GNlw70uTOwJQOdVcZFY_uiDasTA?t=5

📝 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)

Authored with Claude Code (claude-sonnet-4-6). Anna Garcia directed the work throughout.

Key decisions made during the session:

  • iOS: no drawHierarchy fallback — when no WKWebView is found for the captured rect, we return nil rather than falling back to drawHierarchy over the full window. The fallback would composite any masked CALayer-backed platform view in the crop region and leak it into replay, defeating the privacy guarantee.
  • Android: async bitmap compression — PixelCopy runs on the main thread; bitmap compression was moved to a single-thread executor to avoid blocking the UI thread during screenshot capture.
  • 5-second timeout on captureNativeScreenshot — if the native side never calls result(), the Dart _isCapturing flag would stay true permanently and silently kill replay. The timeout degrades gracefully to null (frame skipped) rather than freezing.
  • hasCapturedPlatformViews gate — scheduleFrame() is only forced when at least one platform view was actually captured, avoiding unnecessary redraws on screens with only masked views.

@turnipdabeets turnipdabeets force-pushed the pr-393-platform-views branch 2 times, most recently from bdeec5d to 76b62b3 Compare June 25, 2026 17:05
@greptile-apps

greptile-apps Bot commented Jun 25, 2026

Copy link
Copy Markdown

Comments Outside Diff (2)

  1. posthog_flutter/test/posthog_test.dart, line 2193-2207 (link)

    P1 Broken test references non-existent property

    This test accesses config.sessionReplayConfig.capturePlatformViews, but PostHogSessionReplayConfig has no such property — the field added in this PR is maskAllPlatformViews. Dart will reject this at compile time, and the test cannot be one of the "47 passing tests" without this being caught. The final assertion also checks for a 'capturePlatformViews' key in the serialised map, but the config serialises under 'maskAllPlatformViews'. The test needs to be rewritten against the actual API: check that maskAllPlatformViews defaults to true, that it appears in the serialised map, and that setting it to false removes the masking.

  2. example/ios/Runner.xcodeproj/project.pbxproj, line 208-218 (link)

    P1 Personal developer credentials committed to a public repository

    DEVELOPMENT_TEAM = QU5XHRZES9 and PRODUCT_BUNDLE_IDENTIFIER = com.annagarcia.posthogFlutterExample appear to be personal Apple Developer credentials. Any contributor who clones this repo and tries to build the iOS example app will immediately hit a signing error (Xcode can't use someone else's team ID), and the personal name in the bundle identifier is clearly not intended to be canonical. These should revert to DEVELOPMENT_TEAM = "" (or the original value) and PRODUCT_BUNDLE_IDENTIFIER = com.example.posthogFlutterExample. The same values appear in the Release and profile build configurations in this file.

Reviews (1): Last reviewed commit: "feat: capture platform views in session ..." | Re-trigger Greptile

Comment thread posthog_flutter/lib/src/replay/screenshot/screenshot_capturer.dart
Comment thread posthog_flutter/lib/src/replay/mask/posthog_platform_view.dart Outdated
@turnipdabeets turnipdabeets force-pushed the pr-393-platform-views branch 2 times, most recently from d8919fb to e491499 Compare June 25, 2026 17:15
@turnipdabeets turnipdabeets marked this pull request as ready for review June 25, 2026 17:21
@turnipdabeets turnipdabeets requested a review from a team as a code owner June 25, 2026 17:21
@greptile-apps

greptile-apps Bot commented Jun 25, 2026

Copy link
Copy Markdown

Security Review

  • PostHogPlatformView(privacy: .mask) silently ignored when maskAllPlatformViews = false (screenshot_capturer.dart line 390): When the global flag is false, _collectPlatformViewRects() is never called, so per-view mask overrides are dropped. A developer who opts out of global masking but wraps a specific sensitive WebView in PostHogPlatformView(privacy: .mask) will find that view captured unmasked in replay. Fix: always traverse the element tree; pass the default privacy based on the global flag so undecorated views default to capture while explicit mask wrappers still apply.

Reviews (2): Last reviewed commit: "feat: capture platform views in session ..." | Re-trigger Greptile

Comment thread posthog_flutter/lib/src/replay/screenshot/screenshot_capturer.dart Outdated
@turnipdabeets turnipdabeets force-pushed the pr-393-platform-views branch from e491499 to 8cb2b93 Compare June 25, 2026 17:31
Platform views (WebView, Maps, etc.) are masked by default in session
replay — they appear as a black box rather than being captured or
leaking native content.

- Adds `maskAllPlatformViews` config flag (default `true`)
- Adds `PostHogPlatformView` marker widget + `PostHogPlatformViewPrivacy`
  enum (`.mask` / `.capture`) for per-view override
- iOS: `WKWebView.takeSnapshot` for capture; no `drawHierarchy` fallback
  (would leak masked CALayer-backed views into replay)
- Android: PixelCopy + async bitmap compression; de-duplicates frames
  via `compositedBytesHash`
- 5-second timeout on `captureNativeScreenshot` to prevent replay freeze
- Example app: 8 test flows covering masked/captured combinations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@turnipdabeets turnipdabeets force-pushed the pr-393-platform-views branch from 8cb2b93 to fdb235a Compare June 25, 2026 19:11
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.

1 participant