React Native Session Replay Across Flutter and WebViews
React Native session replay, Flutter replay, and WebView replay all work — but what they capture depends entirely on how each framework paints its UI. React Native records through the native view hierarchy, where masking is bridge- and architecture-dependent. Flutter reconstructs a widget tree, so native maps and platform views fall out of frame. WebViews in hybrid apps need their own instrumentation or they double-record. Default masking is aggressive but brittle, and every stack needs its own pre-ship check.
The replay that recorded nothing
Watch what the QA lead does at 0:14. She's scrubbing a Flutter Web replay of a checkout, coffee going cold, trying to work out why one user abandoned. The cursor glides across the screen. A click lands where the email field should be. Another click, lower, where the card field lives. And the whole time, behind that cursor, there's nothing. A blank white rectangle where the entire form should render. She rewinds. Plays it again. Same white void.
She wasn't watching a broken user. She was watching a broken recording.
I've hit this exact white-screen replay on every Flutter Web project I audited before canvas capture got switched on: same cursor, same phantom clicks, same missing form. The user did everything right. The tooling captured her intent and threw away her context. That gap between "the SDK is installed" and "the SDK sees what the user sees" is what this whole piece is about.
Failure modes mirror the rendering pipeline
Here's the mental model that makes the rest of this predictable. Replay breaks in the shape of how the framework draws to the screen. If you know how a framework produces pixels, you can guess in advance where the recording will lie to you.
Three architectures, three capture strategies, three distinct blind spots. React Native draws through a native view hierarchy, so replay walks that tree of real UIViews and Android Views. Flutter draws everything itself onto a surface, so there's no native hierarchy to read — the tool rebuilds a description of the widget tree instead. Hybrid WebViews embed a full browser inside the app, which means two recording systems that, by default, don't know the other exists.
None of these are bugs. They're consequences of a design choice. Once that clicks, the vendor docs stop reading like trivia and start reading like a map of where your masking quietly fails.
The capture-technology matrix
This is the table I keep open when someone asks "does session replay even work with Flutter?" or "will it mask our card field on React Native?" The honest answer is: it depends on this grid.
| Stack | How it renders | Capture method | Can see | Cannot see | Masking model |
|---|---|---|---|---|---|
| React Native | Native view hierarchy (UIView / Android View) | Walk native views; PixelCopy on Android, or Canvas fallback | Native text, inputs, images as real views | Nothing structural — but masking config can be overridden | Config-driven under PixelCopy; Canvas force-masks everything |
| Flutter (native) | Flutter paints its own surface | Reconstruct widget tree from selected frames | Standard Flutter widgets | Platform views, native maps, custom RenderObjects (shown as rectangles) | Applied to reconstructed tree |
| Flutter Web | Skia into a browser <canvas> |
Dedicated canvas capture required | Full UI only if canvas mode is on | Everything (blank white screen) on generic DOM tools | Canvas masking, version-gated |
| Hybrid WebView | Browser engine inside native shell | Browser SDK inside WebView + native webview tracking | Web DOM content | Web content entirely, if only the native SDK is instrumented | Must be applied on the web side |
Read that "cannot see" column twice. It's the part that decides whether your replay is a debugging tool or a false sense of security.
React Native: masking that doesn't survive the bridge
React Native looks like the easy case, because there's a real native hierarchy to walk. It's also where I've watched masking evaporate three separate ways.
The first is strategy-dependence. Per Sentry's React Native privacy docs, masking options "only apply when using the default PixelCopy strategy on Android. If you set the strategy to Canvas, those options will be ignored and all sensitive content (texts, inputs, images) will be always masked." Force-masking everything sounds safe, and for privacy it usually is. The trap is the reverse assumption — teams who set maskAllText:false for a debugging build and expect it to hold.
That leads to the second failure mode, and it's a nasty one. When you switch on React Native's New Architecture (Fabric), the SDK can flip to Canvas capture on its own. A Sentry GitHub issue quotes the v7.5 changelog directly: "If this strategy is used, all text and images will be masked, regardless of any masking configuration." So maskAllText:false isn't rejected loudly. It's silently ignored, because the entire capture strategy changed underneath you when you upgraded architectures.
The third one I call the silent-zero. Amplitude's React Native replay ships with a sample rate you have to opt into. Their React Native SDK docs state it plainly: "By default, Session Replay captures 0% of sessions for replay." Install it, ship it, open the dashboard three weeks later, and find nothing. Not broken. Just never switched on.
The QA test I run: build a throwaway screen with one known-sensitive text block and one field you expect to stay visible. Record it on both the old architecture and the New Architecture. Confirm the masks land as configured on each, and confirm sessions actually show up at all. On my last three RN audits, this ten-minute test caught a masking regression on two of them.
If you're checking masks on form inputs specifically, our notes on the fields that quietly kill conversions pair well with this exercise.
Flutter: the tree it rebuilds, the pixels it discards
Flutter can't walk a native hierarchy because it barely has one. It paints its own surface, so replay tools rebuild a description of the widget tree from selected frames instead of grabbing pixels. That works beautifully for standard widgets and fails predictably at the edges.
Contentsquare is refreshingly blunt about those edges. Their Flutter replay docs note that "content rendered outside the Flutter view (such as platform views and native maps) is not collected; Webviews require specific implementation steps to be properly reflected in Session Replay; custom shapes are sent as rectangles." So your Google Map delivery picker becomes a gray box. Your hand-rolled custom-painted chart flattens to a rectangle. The user tapped a pin, and you see them tapping a void.
Flutter Web is the sub-case that produced our cold-open white screen. Because Flutter renders through a browser canvas rather than the DOM, generic DOM-based tools capture only cursor and click tracking over a blank frame. A Flutter GitHub issue sums up what every generic tool sees: "the tools only capture a white screen with cursor movement and click tracking. The actual UI content is not visible in the recordings." PostHog's answer is an explicit setting. Their Flutter docs tell you to "also enable the Canvas capture setting... required as Flutter renders your app using a browser canvas element."
One more gotcha earns its own sentence, because it's the kind that leaks data. PostHog's canvas masking carries hard version floors, and their troubleshooting docs explain that on an older posthog-js "frames record unmasked and the plugin logs a console warning telling you to upgrade." Unmasked recording, with nothing but a console line nobody reads standing between you and a compliance incident.
The QA test: open a screen with a native map or platform view and confirm it renders in replay instead of a gray rectangle. On Flutter Web, verify canvas capture is on and that your SDK versions clear the masking floor. Don't trust a failure mode whose only signal is a console warning.
Hybrid WebViews and the double-recording trap
Hybrid apps hide a second rendering engine inside the first. A native shell wraps a WebView, and that WebView is a full browser with its own DOM. Two worlds — and by default, neither records the other.
Consolidated web-plus-native replay means instrumenting the Browser SDK inside the WebView, switching on webview tracking on the mobile side, and turning replay on in both places. Datadog's setup docs describe the handoff: "The Session Replay is recorded through the Browser SDK, then the Mobile SDK handles the batching and uploading of the webview recording." Get that wiring wrong and you land in one of two ditches. Either both SDKs record the same WebView and you double-count and double-store, or the web content records unmasked because you applied masking on the native side, which never touches the DOM inside the browser.
The QA test: load a WebView screen, confirm it records exactly once, and confirm masking is applied on the web side where the actual inputs live. This is the check I see teams skip most, because "it shows up in replay" feels like success right up until you notice it shows up twice.
Worked example: a masking-survival scorecard
Take one hypothetical checkout screen and run it through every stack. Four elements: an email field, a card field, a native map for delivery selection, and an embedded WebView promo banner. I mark each as captured, masked, dropped, or double-recorded, using only the documented behaviors above.
| Element | React Native (New Arch) | Flutter (native) | Flutter Web (canvas off) | Hybrid WebView (native-only masking) |
|---|---|---|---|---|
| Email field | Captured, force-masked (Canvas) | Captured, masked | Dropped (white screen) | Captured, unmasked (wrong side) |
| Card field | Captured, force-masked (Canvas) | Captured, masked | Dropped (white screen) | Captured, unmasked (wrong side) |
| Native delivery map | Captured as native view | Dropped → rectangle | Dropped (white screen) | Captured as native view |
| WebView promo banner | Needs separate instrumentation | Needs separate steps | Dropped (white screen) | Double-recorded or missing |
The point isn't the specific cells. It's that one screen produces four completely different truth-tables depending on where it renders. Copy this table, swap in your real elements, and fill it during QA. That's your pre-ship checklist in a single artifact. For the checkout context specifically, it sits well beside checkout abandonment analysis with session replay.
Where the tools stand today
Same rubric for everyone, and every vendor gets a "what it doesn't handle" column. The competitor rows carry the failure modes their own docs describe. Kixo gets an honest caveat: I have no independent cross-framework failure-mode test for it, so it's scored on documented capabilities, not on a broken replay I reproduced myself.
| Vendor | Native capture | RN architecture handling | Flutter platform-view visibility | WebView consolidation | Not handled / caveat |
|---|---|---|---|---|---|
| Sentry | Native views; PixelCopy or Canvas | Canvas force-masks under Fabric | — | — | maskAllText:false silently ignored on New Arch |
| Amplitude | RN replay plugin | Documented plugin | — | — | Defaults to 0% capture until you set a sample rate |
| Contentsquare | Flutter tree reconstruction | — | Platform views/native maps not collected | WebView needs specific steps | Custom shapes reduced to rectangles |
| PostHog | rrweb web; Flutter tree | — | Canvas mode required (Flutter Web) | — | Canvas masking version-gated; older js records unmasked |
| Datadog | Native + browser SDK | — | — | Browser SDK in WebView + mobile tracking | Misconfig → double-record or unmasked web |
| Kixo | Native iOS/Android + web via rrweb | Not independently tested here | Not independently tested here | Not independently tested here | Cross-framework failure modes not verified in this audit; fact-sheet capabilities only |
Kixo's documented capabilities list native iOS/Android capture, web replay via rrweb, heatmaps, and privacy masking, alongside a chat-first angle where you ask questions in plain language and get charts back with a visible reasoning trail. That's the honest boundary of what I can say. I haven't put its masking through the RN-bridge or Flutter-Web tests the way I have the others, so treat that row as a starting point for your own QA rather than a tested result. If you're building a broader shortlist, our twelve real criteria for choosing a replay tool covers the rest of the rubric.
The pre-ship checklist
Run these in order before you trust a single cross-platform replay.
- Confirm the sample rate isn't zero. Especially on Amplitude's RN plugin, which captures 0% by default. No sessions means nothing else matters.
- Confirm masks survive the RN bridge and architecture. Record on both old and New Architecture, and verify the Canvas fallback didn't quietly override your config.
- Confirm Flutter platform views render. Open a native map or platform view and check it isn't a gray rectangle.
- Confirm the canvas masking version floor. On Flutter Web, verify canvas capture is on and your SDK versions clear the masking requirement. Don't accept a console-warning-only pass.
- Confirm the WebView records exactly once, masked on the web side. Instrument the Browser SDK inside the WebView, enable mobile webview tracking, and check for double-recording.
Here's the field note I keep coming back to. Somewhere there's a confused user — the kind I have a permanent soft spot for — who mistyped her card number twice and finally got the order through. She'll never know that a mask failed and her digits sat in a replay unmasked because the WebView masking got applied on the wrong side. She can't audit that. You can. Replay is a privacy surface before it's ever a debugging one, and the job is making sure the recording protects her as reliably as it helps you. That responsibility doesn't transfer automatically between frameworks. You re-earn it on every stack.
For the retention side of that same responsibility, our data retention policy for cost and privacy is the next thing to read.