PostHog / PostHog/posthog-flutter

iOS session replay records a Mapbox `UiKitView` as a black box, even with `maskAllPlatformViews = false`

Open
#593 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
Dart
Stars
97
Forks
85
Avg merge
1d 8h
Merged PRs (30d)
31

Description

Version

5.44.2

Steps to Reproduce

Environment: posthog_flutter 5.44.2, mapbox_maps_flutter 2.24.3, Flutter stable, iOS 27.0 simulator (iPhone 18 Pro).

  1. Add a mapbox_maps_flutter MapWidget to a screen. On iOS that plugin renders a plain UiKitView — the only iOS branch, with no texture-mode option (lib/src/mapbox_maps_platform.dart:80):
} else if (defaultTargetPlatform == TargetPlatform.iOS) {
  return UiKitView(
    viewType: 'plugins.flutter.io/mapbox_maps',
    ...
  1. Enable session replay with every mask off:
await Posthog().setup(
  PostHogConfig('<token>')
    ..sessionReplay = true
    ..sessionReplayConfig = (PostHogSessionReplayConfig()
      ..maskAllTexts = false
      ..maskAllImages = false
      ..maskAllPlatformViews = false
      ..captureNativeScreens = true),
);
  1. Wrap the app in PostHogWidget, run on an iOS simulator, and open the screen with the map.
  2. Watch the resulting replay.

Wrapping the map in PostHogPlatformView(privacy: PostHogPlatformViewPrivacy.capture) makes no difference.

Related

Related to #151, which tracked platform view recording and was closed in July 2026 after PR #453 and #473. On iOS the reveal path only covers WKWebView, so the Mapbox case from that thread still reproduces — a maintainer had linked the "Flutter iOS Google Maps does not visible in session replay" question there as the same problem.

Questions

  1. Is non-WebView platform view capture planned for iOS, or is WebView-only the intended scope? If the latter, maskAllPlatformViews should document it.
  2. Could drawHierarchy(in:afterScreenUpdates:) be gated on no masked platform view intersecting the crop rect, keeping the leak protection for mixed screens while letting the fully-unmasked case work?
Expected Result

The map appears in the replay, since platform view masking is disabled globally and maskAllPlatformViews is documented as a global opt-out with no iOS caveat.

Actual Result

The map is a solid black rectangle exactly its own size, while the debug log claims the opposite:

Image
flutter: Found 0 masked and 1 captured platform view rect(s)

That mismatch is what makes this hard to diagnose from the app side: the SDK reports the view as captured, and the replay shows it masked.

Cause. Dart classifies the UiKitView as captured and asks native to screenshot the rect (screenshot_capturer.dart:610). captureOneNative searches that rect for a WKWebView, finds none, and returns nil (darwin/.../PosthogFlutterPlugin.swift:744):

// No WKWebView found for the captured rect. Returning nil here
// keeps this safe: drawHierarchy over the full window would
// include any masked CALayer-backed platform view overlapping
// the crop region and leak it into replay.
onResult(nil)

Dart reads nil as "capture failed" and paints the mask anyway (screenshot_capturer.dart:419):

if (bytes == null) {
  _imageMaskPainter.drawMaskedImage(canvas, [fallbackMask], pixelRatio);
  return;
}

So the view round-trips captured → declined → re-masked, with the log line emitted before the decline.

I have only tested Mapbox, so I can't speak to other plugins, but the WebView-only check is not Mapbox-specific. I have not tested Android either, though its captureOneNative composites platform-view SurfaceViews rather than restricting to web views (android/.../PosthogFlutterPlugin.kt:1216-1221), so the platforms appear to differ.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with screenshot_capturer.dart at lines 419 and 610, then inspect darwin/.../PosthogFlutterPlugin.swift around captureOneNative at line 744. Reproduce the Mapbox UiKitView case and trace the captured-to-nil-to-mask path. Done means an unmasked iOS platform view appears in replay while masked platform views remain protected.

Written by the indexing model from the issue text.

Assessment

Tech stack
dart, flutter, ios, swift
Domain
mobile, observability
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.