feat: capture platform views in session replay screenshots#393
feat: capture platform views in session replay screenshots#393marcorizza wants to merge 2 commits into
Conversation
Prompt To Fix All With AIFix 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 |
| 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)) | ||
| } |
There was a problem hiding this 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.
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.| private fun bitmapToPng(bitmap: Bitmap): ByteArray { | ||
| val outputStream = ByteArrayOutputStream() | ||
| bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream) | ||
| bitmap.recycle() | ||
| return outputStream.toByteArray() | ||
| } |
There was a problem hiding this 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().
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.
💡 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.capturePlatformViewsflag 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
falsebecause native platform view content cannot be automatically masked using Flutter widget masking rules.💚 How did you test it?
sessionReplayConfig.capturePlatformViewsis disabled by default and correctly serialized in the replay config map.sessionReplayConfig.capturePlatformViewsis enabled.📝 Checklist
If releasing new changes
pnpm changesetto generate a changeset file