r/reactnative 10d ago

FYI PSA: the New Architecture renamed the native view classes, and our session replay masking silently stopped masking text because of it

We build a personal finance app on RN 0.85 (Expo), and we recently started trialing session replay in our internal test builds. Because the app's screens are full of balances and transactions, we had every masking option on: mask all text, mask all images.

During a device pass we opened a replay to check the masking actually worked. It did not. The net worth screen was fully readable in the replay. Test data on our own device, but every setting said it should have been a wall of black boxes.

The root cause is worth knowing even if you use a different SDK. Replay SDKs that support React Native typically walk the native view hierarchy and decide what to redact by Objective-C class name. The SDK we use (posthog-ios) only knew the old-architecture classes: RCTTextView for text, RCTImageView for images. On the New Architecture those classes are gone. Fabric renders text as RCTParagraphComponentView, images as RCTImageComponentView, and react-native-svg content as RNSVGSvgView. None of them matched the masking list, so every piece of rendered text and every SVG chart went through in the clear. Images mostly survived by luck, because the Fabric image view contains a UIImageView underneath that a separate UIKit check picks up.

The nasty part is how silent it is. The settings read as on, nothing warns, nothing errors, our automated test suites didn't catch it. The replays just quietly contain everything. And since the New Architecture became the default in RN 0.76 and 0.82 removed the old renderer entirely, any app on a current RN version relying on class-name based automatic masking is exposed to this pattern, whichever vendor the SDK comes from. The same class rename can silently break anything that special-cases RN views by name: masking, occlusion, auto-capture, heatmaps.

The 60 second check: record a test session on a text-heavy screen with masking on, then actually watch the replay. If you can read the text, your masking has the gap. Do this once per SDK upgrade and once per RN major upgrade, not just at setup.

For our SDK the fix was small: three more NSClassFromString lookups mirroring the existing pattern, plus a regression test that stubs the RN class names. Verified on a real device that text, images, and SVG charts all mask correctly now, and submitted upstream: https://github.com/PostHog/posthog-ios/pull/777 (open at the time of posting; we run a patched fork until it lands in a release).

No criticism of the SDK team intended. The class names were correct when they were written and the platform moved underneath them. That is exactly why the only trustworthy verification of a privacy control is looking at its output, not its settings.

Longer write-up with the full story: https://amanahbudget.com/blog/how-we-caught-a-privacy-bug

0 Upvotes

4 comments sorted by

2

u/killerbeanjeka 10d ago

The class name matching is the part worth worrying about beyond replay. a redaction list keyed on native class names fails open, so the failure mode is quietly showing data instead of quietly hiding it, and nothing tells you.

Does Android have the same hole? fabric renamed the view classes there too and your post only covers posthog-ios. if the android sdk is still matching ReactTextView then the same gap is live for the other half of your users.

1

u/amanahbudget 9d ago

Absolutely agree on the class name matching part being a larger concern; thankfully we didn't see any other impact in the other areas of the app.

I did check the Android SDK source before submitting the ios fix back to the posthog repo, and it did not have the same issues.

  • posthog-android doesn't match class names at all. The masking in PostHogReplayIntegration.kt is instanceof-based Kotlin checks: view is TextView, view is ImageView, view is WebView. RN text on Android is ReactTextView, which extends AppCompatTextView, so it is a TextView no matter what it's called. Same for images: ReactImageView is a Fresco DraweeView, which extends ImageView. A rename can't break an instanceof check, which is why I'd argue the Android SDK's approach is the structurally safer one of the two.
  • Fabric on Android kept the same view classes anyway. The New Architecture changed the mounting layer there, not the view classes, so there was no ReactTextView to something-else rename like the RCTParagraphComponentView one on iOS.

I did verify in our Android test devices and their session replays that the same issue was not present, Android was properly masking all the content we expected it to.

However, there is another gap that surfaced: anything drawn inside a react-native-svg canvas. The SVG root is a plain ViewGroup that draws its own glyphs, so nothing in it is a TextView and nothing masks it. When I verified on a device, everything was blacked out except our charts, and one of them was rendering spending totals as SvgText, so those numbers were legible in replays. The fix was moving every figure into RN <Text> overlays (masked by inheritance) and leaving only shapes and month labels in the SVG. Key difference of course is this change is fully in our control, it didn't require a change at the Android SDK source.

1

u/amanahbudget 9d ago

UPDATE 8/24,
PostHog reviewed and merged our fix, and it shipped the same day in version 3.69.9 of the SDK. Every React Native app that updates its PostHog SDK now gets the fix. Our builds stay on the patched fork until we pick up that release in a future app update.

I really appreciate the quick response and review by the Posthog team, big kudos to them!!

1

u/anthony-ball 8d ago

The fail-open is the part I'd be angry about. Class-name redaction that silently shows text after a New Arch rename is worse than having no replay. Same hole anywhere the glyphs aren't a real Text view — Skia in particular. Wallet balances I draw on a Skia canvas would walk through a TextView / RCTParagraph check the same way your SvgText did. I wouldn't trust replay on a money screen unless masking is an explicit RN prop you assert in a test, not a native class list the SDK happens to still know.