Skip to content

feat: capture platform views in session replay screenshots#393

Draft
marcorizza wants to merge 2 commits into
PostHog:mainfrom
marcorizza:issue/151
Draft

feat: capture platform views in session replay screenshots#393
marcorizza wants to merge 2 commits into
PostHog:mainfrom
marcorizza:issue/151

Conversation

@marcorizza
Copy link
Copy Markdown

💡 Motivation and Context

Fixes #151.

Session Replay in Flutter was only capturing the Flutter render tree, so native platform views rendered on top of Flutter content were missing from recordings. In practice, this meant native overlays such as paywalls, maps, or web views could appear blank or invisible in session replay snapshots.

This PR adds an opt-in sessionReplayConfig.capturePlatformViews flag for session replay. When enabled, Flutter requests a screenshot from the native view hierarchy on iOS and Android and uses that image for replay snapshots, allowing native platform views to be included in recordings.

The option defaults to false because native platform view content cannot be automatically masked using Flutter widget masking rules.

💚 How did you test it?

  • Added a unit test to verify that sessionReplayConfig.capturePlatformViews is disabled by default and correctly serialized in the replay config map.
  • Manually verified on iOS and Android that native platform views are captured in session replay when sessionReplayConfig.capturePlatformViews is enabled.
  • Confirmed the existing Flutter-only behavior is preserved when the option is left disabled.

📝 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

@marcorizza marcorizza requested a review from a team as a code owner May 23, 2026 10:41
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 23, 2026

Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
posthog_flutter/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt:572-606
**Soft-canvas fallback silently misses GPU-composited platform views**

`View.draw()` renders using a software `Canvas`, which cannot read content from hardware-accelerated surfaces like `SurfaceView`, `TextureView`, or Metal/Vulkan-backed views. This is the exact content type `capturePlatformViews` is meant to capture. On pre-API 26 devices, or whenever `PixelCopy` fails, this fallback will produce blank areas for the native platform views the user opted into capturing. A brief inline comment explaining this limitation would prevent confusion when developers debug unexpected blank regions on older API levels.

### Issue 2 of 2
posthog_flutter/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt:608-613
**PNG compression runs on the main UI thread**

`bitmapToPng` is called directly inside the `PixelCopy` completion callback, which is dispatched to `Handler(Looper.getMainLooper())`. PNG encoding of a full-screen `ARGB_8888` bitmap (`Bitmap.CompressFormat.PNG, 100`) can take 100–300 ms on a typical device, blocking the main thread for the duration of every session-replay snapshot when `capturePlatformViews` is enabled. The same applies to the `captureNativeScreenshotFallback` path. Consider offloading the compression to a background thread (e.g. `Executors.newSingleThreadExecutor()`) and marshalling the byte-array result back to the main thread before calling `result.success()`.

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

Comment on lines +572 to +606
private fun captureNativeScreenshotFallback(
contentView: View,
cropLeft: Int,
cropTop: Int,
cropRight: Int,
cropBottom: Int,
logicalWidth: Int,
logicalHeight: Int,
result: Result,
) {
val contentBitmap =
Bitmap.createBitmap(contentView.width, contentView.height, Bitmap.Config.ARGB_8888)
contentView.draw(android.graphics.Canvas(contentBitmap))

val croppedBitmap =
Bitmap.createBitmap(
contentBitmap,
cropLeft,
cropTop,
cropRight - cropLeft,
cropBottom - cropTop,
)
contentBitmap.recycle()

val outputBitmap =
if (croppedBitmap.width == logicalWidth && croppedBitmap.height == logicalHeight) {
croppedBitmap
} else {
Bitmap.createScaledBitmap(croppedBitmap, logicalWidth, logicalHeight, true).also {
croppedBitmap.recycle()
}
}

result.success(bitmapToPng(outputBitmap))
}
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 Soft-canvas fallback silently misses GPU-composited platform views

View.draw() renders using a software Canvas, which cannot read content from hardware-accelerated surfaces like SurfaceView, TextureView, or Metal/Vulkan-backed views. This is the exact content type capturePlatformViews is meant to capture. On pre-API 26 devices, or whenever PixelCopy fails, this fallback will produce blank areas for the native platform views the user opted into capturing. A brief inline comment explaining this limitation would prevent confusion when developers debug unexpected blank regions on older API levels.

Prompt To Fix With AI
This is a comment left during a code review.
Path: posthog_flutter/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt
Line: 572-606

Comment:
**Soft-canvas fallback silently misses GPU-composited platform views**

`View.draw()` renders using a software `Canvas`, which cannot read content from hardware-accelerated surfaces like `SurfaceView`, `TextureView`, or Metal/Vulkan-backed views. This is the exact content type `capturePlatformViews` is meant to capture. On pre-API 26 devices, or whenever `PixelCopy` fails, this fallback will produce blank areas for the native platform views the user opted into capturing. A brief inline comment explaining this limitation would prevent confusion when developers debug unexpected blank regions on older API levels.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +608 to +613
private fun bitmapToPng(bitmap: Bitmap): ByteArray {
val outputStream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
bitmap.recycle()
return outputStream.toByteArray()
}
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 PNG compression runs on the main UI thread

bitmapToPng is called directly inside the PixelCopy completion callback, which is dispatched to Handler(Looper.getMainLooper()). PNG encoding of a full-screen ARGB_8888 bitmap (Bitmap.CompressFormat.PNG, 100) can take 100–300 ms on a typical device, blocking the main thread for the duration of every session-replay snapshot when capturePlatformViews is enabled. The same applies to the captureNativeScreenshotFallback path. Consider offloading the compression to a background thread (e.g. Executors.newSingleThreadExecutor()) and marshalling the byte-array result back to the main thread before calling result.success().

Prompt To Fix With AI
This is a comment left during a code review.
Path: posthog_flutter/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt
Line: 608-613

Comment:
**PNG compression runs on the main UI thread**

`bitmapToPng` is called directly inside the `PixelCopy` completion callback, which is dispatched to `Handler(Looper.getMainLooper())`. PNG encoding of a full-screen `ARGB_8888` bitmap (`Bitmap.CompressFormat.PNG, 100`) can take 100–300 ms on a typical device, blocking the main thread for the duration of every session-replay snapshot when `capturePlatformViews` is enabled. The same applies to the `captureNativeScreenshotFallback` path. Consider offloading the compression to a background thread (e.g. `Executors.newSingleThreadExecutor()`) and marshalling the byte-array result back to the main thread before calling `result.success()`.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor

Hey, thank you for the work here!

I'll have a deeper look soon but at first glance my biggest concern is PII leaking from platform views when the option is enabled.

Can PostHogMaskWidget be used for those cases, so at least developers can have control over opt-in masking of platform views?

Also I wonder if somehow exposing findMaskableWidgets could help, since this will return masking rects from native. Or maybe just grab a screenshot directly from native sdks with masking already applied (both require refactor of posthog-ios and posthog-android)

Curious to hear thoughts from @marandaneto @turnipdabeets as well

@marandaneto
Copy link
Copy Markdown
Member

i think completely replacing the flutter approach with a native approach isn't great because of masking, afaik its not possible to mask on the native side since we dont have access to the maskable widgets

imo a better approach is:

stitching recordings, the flutter and the native one

theres no automatic way to find out if a screen is Flutter or native (via platform views or pure native)
the only way is with a manual API where the user would tell -> hey here starts a platform view and/or a pure native view
so we can stitch and continue a recording from the native side

eg

posthog.startSessionRecording() // usually starts automatically

// user calls - pseudo code
posthog.stopSessionRecording()
posthog.startNativeSessionRecording(resume: true)
showPlatformViewUI()

closePlatformViewUI()
posthog.stopNativeSessionRecording()
posthog.startSessionRecording(resume: true)

so both recordings, from both sides (flutter and native), carry the same session id and are a single playable recording

@marandaneto marandaneto marked this pull request as draft May 27, 2026 07:48
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.

Session Recordings: Platform Views (Native Views)

3 participants