Skip to content

feat(session-replay-react-native): Fabric-native dual-architecture AmpMaskView#1886

Merged
aliaksandr-kazarez merged 10 commits into
mainfrom
feat/fabric-ampmaskview
Jul 16, 2026
Merged

feat(session-replay-react-native): Fabric-native dual-architecture AmpMaskView#1886
aliaksandr-kazarez merged 10 commits into
mainfrom
feat/fabric-ampmaskview

Conversation

@aliaksandr-kazarez

@aliaksandr-kazarez aliaksandr-kazarez commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Stacked on #1885. Makes <AmpMaskView> a real Fabric component on the New Architecture while keeping the old-architecture path byte-identical — same public JS API, same native component name (AMPMaskComponentView).

Why

On the New Architecture the legacy Paper-only AmpMaskView is driven through RCTLegacyViewManagerInteropComponentView, whose out-of-band re-parenting mis-renders nested mask ⊃ unmask subtrees (June root-cause, react/react-native#44817-class; re-confirmed by a customer report 2026-07-15). A plain-layout-node Fabric component with this exact shape was verified immune in the customer's own environment (RN 0.83.6, Expo 55, New Arch) — including Fabric view recycling, which it survives via an explicit prepareForRecycle masking reset.

How

  • JS: codegen spec src/specs/AmpMaskViewNativeComponent.ts (mask: WithDefault<string,'amp-mask'>); src/amp-mask-view.tsx re-exports it typed with the required union prop. On old arch codegenNativeComponent falls back to requireNativeComponent('AMPMaskComponentView') → the unchanged legacy managers.
  • iOS: ios/fabric/AMPMaskComponentView.{h,mm}RCTViewComponentView subclass, codegen descriptor (plain Yoga, no custom ShadowNode), whole files guarded by RCT_NEW_ARCH_ENABLED (podspec globs ios/** unconditionally). Masking goes through a plain-ObjC AmpMaskApplier seam (amp_isBlocked) to keep the Swift-pod import out of ObjC++. prepareForRecycle resets masking state.
  • Android: newarch-only fabric/AmpMaskViewManager.kt (ReactViewManager + codegen AMPMaskComponentViewManagerDelegate) registered by the newarch package in place of the legacy manager; oldarch source set untouched. No build.gradle changes.
  • codegenConfig: type: "modules""all" + iOS componentProvider entry. Triple registration on iOS (componentProvider / AMPMaskComponentViewCls / +load) for RN-version coverage.
  • Tests: jest moduleNameMapper mock for the codegen deep path; component + spec tests. jest 55/55, typecheck, lint, build all green.

Semantics notes

  • Unknown mask values fail safe to masking on the Fabric path (legacy Paper no-ops on unknown — unchanged there).
  • Full device matrix (both archs × both platforms, nested-mask layout, navigation-recycle stress, RN version sweep, customer-reported Expo 55 / RN 0.83.6 lane) runs next; results will be attached before undrafting.

🤖 Generated with Claude Code


Note

Medium Risk
Changes session-replay privacy masking on New Architecture (nested mask/unmask and Fabric recycling); incorrect behavior could briefly expose or mis-layout sensitive UI, though old-arch paths are untouched and recycle handling is fail-closed.

Overview
<AmpMaskView> becomes a real Fabric component on React Native’s New Architecture while the public API and native name (AMPMaskComponentView) stay the same; legacy Paper managers are unchanged on old architecture.

JS now wires the view through a codegen spec (codegenNativeComponent) instead of requireNativeComponent alone, with codegenConfig expanded to type: "all" plus an iOS componentProvider. iOS adds a Fabric RCTViewComponentView subclass that applies masking via AmpMaskApplier, resets to masked in prepareForRecycle, and registers for multiple RN versions. Android newarch registers AmpMaskViewManager with the codegen delegate instead of the legacy view manager in the newarch package only.

The Fabric path fails closed on unknown mask values (masking) and on recycle reset. Docs note layout parity with <View>; the example app adds Masking and Recycle screens for manual verification. Jest gains a codegenNativeComponent mock and component/spec tests; lint scripts include tsx.

Reviewed by Cursor Bugbot for commit 09196ff. Bugbot is set up for automated code reviews on this repo. Configure here.

aliaksandr-kazarez and others added 4 commits July 15, 2026 12:46
…mpiles across RN 0.73-0.88 (SDKRN-41) (#1874)"

This reverts commit 01ec004.
…ual-architecture component

On the New Architecture the legacy Paper-only AmpMaskView was driven through
RCTLegacyViewManagerInteropComponentView, whose out-of-band re-parenting
mis-renders nested mask/unmask subtrees (react/react-native#44817-class,
customer-verified). AmpMaskView is now a real Fabric component under the same
native name (AMPMaskComponentView) and same public JS API:

- codegen spec src/specs/AmpMaskViewNativeComponent.ts; on the old
  architecture codegenNativeComponent falls back to requireNativeComponent,
  resolving to the unchanged legacy Paper managers (zero old-arch change)
- iOS: ios/fabric/AMPMaskComponentView (RCTViewComponentView, plain codegen
  descriptor), fully guarded by RCT_NEW_ARCH_ENABLED; masking applied through
  a plain-ObjC AmpMaskApplier seam (amp_isBlocked); prepareForRecycle resets
  masking state so recycled instances cannot leak markers
- Android: newarch-only AmpMaskViewManager (ReactViewManager + codegen
  delegate) registered in place of the legacy manager; oldarch untouched
- jest: codegenNativeComponent moduleNameMapper mock + component/spec tests

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown

size-limit report 📦

Path Size
packages/analytics-browser/lib/scripts/amplitude-min.js.gz 61.01 KB (0%)
packages/session-replay-browser/lib/scripts/session-replay-browser-min.js.gz 134.34 KB (0%)
packages/unified/lib/scripts/amplitude-min.umd.js.gz 214.9 KB (0%)
@amplitude/element-selector (gzipped esm) 2.67 KB (0%)

aliaksandr-kazarez and others added 2 commits July 15, 2026 13:04
…eens in example app

MaskingScreen: layout parity vs plain View (onLayout-asserted), nested
mask/unmask stacking (the legacy-interop Effect B trigger), and touch
pass-through — each with an on-screen PASS/FAIL verdict.
RecycleScreen: navigation-replace stress for Fabric view recycling; mask
fills must stay inside their dashed outline slots across visits with
alternating origins (the customer-reported stale-frame trigger).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t only

The two parity wrappers are stacked siblings, so y necessarily differs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…intro, drop beta.2 removal note

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@aliaksandr-kazarez

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit dc07885. Configure here.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR upgrades @amplitude/session-replay-react-native’s <AmpMaskView> to be a real codegen/Fabric component on React Native’s New Architecture while preserving the same JS API and native component name (AMPMaskComponentView) for legacy builds.

Changes:

  • Adds a codegen native component spec for AMPMaskComponentView and wires AmpMaskView to it (union-typed public prop, codegen-friendly native prop).
  • Implements New Architecture native components/managers for iOS (Fabric RCTViewComponentView) and Android (newarch ReactViewManager + codegen delegate).
  • Adds Jest mocks/tests and example-app screens to validate layout parity, nested mask/unmask behavior, and recycling behavior.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/session-replay-react-native/test/specs/AmpMaskViewNativeComponent.test.ts Verifies the codegen spec registers the expected native component name under Jest.
packages/session-replay-react-native/test/amp-mask-view.test.tsx Tests JS wrapper renders the codegen component and forwards props/children.
packages/session-replay-react-native/test/mocks/codegenNativeComponent.ts Provides a Jest-safe mock for the deep codegenNativeComponent import path.
packages/session-replay-react-native/src/specs/AmpMaskViewNativeComponent.ts Introduces the codegen component spec and native prop typing.
packages/session-replay-react-native/src/index.tsx Exports AmpMaskViewProps alongside AmpMaskView.
packages/session-replay-react-native/src/amp-mask-view.tsx Switches implementation to the codegen component while keeping the public union-typed required mask prop.
packages/session-replay-react-native/README.md Documents that <AmpMaskView> behaves like <View> and works on both architectures.
packages/session-replay-react-native/package.json Updates codegenConfig to generate components (type: "all") and adds iOS componentProvider entry.
packages/session-replay-react-native/jest.config.js Adds moduleNameMapper for the deep codegen import path used by the spec.
packages/session-replay-react-native/ios/fabric/AMPMaskComponentView.mm Adds Fabric RCTViewComponentView implementation for New Architecture masking + recycle reset.
packages/session-replay-react-native/ios/fabric/AMPMaskComponentView.h Declares the Fabric component view class, guarded by RCT_NEW_ARCH_ENABLED.
packages/session-replay-react-native/ios/AmpMaskApplier.m Adds an ObjC seam to apply/reset amp_isBlocked without importing Swift in ObjC++.
packages/session-replay-react-native/ios/AmpMaskApplier.h Declares the masking/reset seam used by the Fabric component view.
packages/session-replay-react-native/example/App.tsx Adds Masking/Recycle demo screens to validate layout nesting and Fabric recycling behavior.
packages/session-replay-react-native/android/src/newarch/java/com/amplitude/sessionreplayreactnative/SessionReplayReactNativePackage.kt Registers the Fabric-native mask view manager on New Architecture builds.
packages/session-replay-react-native/android/src/newarch/java/com/amplitude/sessionreplayreactnative/fabric/AmpMaskViewManager.kt Implements the New Architecture AMPMaskComponentView manager with codegen delegate routing.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/session-replay-react-native/README.md Outdated
Base automatically changed from revert/srmaskview-ampmask-stack to main July 15, 2026 22:24
# Conflicts:
#	packages/session-replay-react-native/android/src/newarch/java/com/amplitude/sessionreplayreactnative/SessionReplayReactNativePackage.kt
#	packages/session-replay-react-native/example/App.tsx
#	packages/session-replay-react-native/package.json
#	packages/session-replay-react-native/src/index.tsx
Comment thread packages/session-replay-react-native/ios/AmpMaskApplier.m Outdated

@chungdaniel chungdaniel left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM other than one comment

…iewCls

RN 0.76's generated RCTThirdPartyFabricComponentsProvider declares the Cls
lookup with C linkage; the mangled C++ name left the app link failing with
'Undefined symbols: _AMPMaskComponentViewCls' on RN 0.76 + New Architecture.
No effect on RN >= 0.77 (class resolved by name).
@aliaksandr-kazarez

aliaksandr-kazarez commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Verification results (2026-07-15) — full matrix green

Build matrix (12/12 PASS): RN 0.73 (old arch) / 0.76 (old arch) / 0.79 / 0.83.6-Expo55 / 0.86 / 0.88-nightly × {Android, iOS}. Codegen clean across the range. RN 0.76 + New Arch iOS additionally links after 52733a0 (extern "C" on AMPMaskComponentViewCls; RN ≤0.76's generated Fabric provider expects C linkage — this config was hard-gated in the pre-revert stack, so it's strictly better than 0.1.0-beta.2).

Functional (example app, both platforms × both archs): layout parity vs plain <View> PASS; nested mask ⊃ unmask renders stacked (the New-Arch legacy-interop overlap bug is gone); touch pass-through PASS; navigation-recycle stress (the customer-reported stale-frame trigger) PASS across repeated remounts with alternating origins.

Replay-content (captured rrweb payloads via dev-SDK DebugServer, both platforms New Arch + RN 0.79 + Expo 55/RN 0.83.6 lane):

  • Masked markers redacted in the payload: Android renders asterisks at capture; iOS emits no image data at all for blocked subtrees (structural redaction, #D6D7DC placeholder). Whole-payload raw-text leak scans: 0 hits.
  • Top-level unmask readable on both; nested unmask readable on Android, redacted on iOS by SDK design (max(parent, own) BlockStatus, SDKRN-38).
  • Post-recycle geometry in the payload matches intended layouts exactly (Android px and iOS pt to the decimal) — no stale origins.
  • 4-min 50 ms-setState soak on RN 0.79 Android: zero crashes, no ensureUnsealed/SIGABRT.

Cloud replays for eyeball: ios-newarch/1784151351367, android-newarch/1784151579699, sweep-expo55-ios/1784159095429 (project 697899). Full evidence log: projects/session-replay-react-native/2026-07-07-ampmask-test-matrix.md, 2026-07-15 sections.

Only skipped cell: RN 0.86 Android runtime privacy (dev-SDK harness Gradle-lockfile friction, not package code; covered by-proxy on 0.79 + 0.83.6).

🤖 Generated with Claude Code

@aliaksandr-kazarez

Copy link
Copy Markdown
Contributor Author

iOS runtime matrix completed (2026-07-16)

Two iOS runtime payload cells that were skipped on the first pass (port contention) are now closed with dev-SDK DebugServer payload-level proof, both PASS:

  • RN 0.79.5, New Arch — session sweep-079-ios/1784224886088: 13 masked elements rendered as gray placeholders with zero image data in any masked subtree, raw-text leak scan 0/5 markers, only the two visible markers exist as decodable tiles; EFFECT-B layout-neutral.
  • RN 0.86.0, New Arch — session sweep-stable-ios/1784225796332: identical structural + per-marker verdicts.

(Note: the 0.86 iOS cell turned out to have never actually run earlier — its lane Podfile was stock-SDK, no DebugServer — so this is a genuine first verification, not just a re-run.)

Net: iOS New-Arch masking is now directly payload-verified on 0.79, 0.83.6, and 0.86, in addition to all compile/link cells 0.73→nightly. Nested-unmask redacted per documented iOS SDK design; no raw marker text in any payload.

🤖 Generated with Claude Code

- resetView (iOS recycle) now fails closed to masked instead of unmasked:
  amp_isBlocked is a bool with no inherit/none state, so a recycled view
  can only reset to masked or unmasked; masked errs toward hiding content
  if a reused instance is captured before updateProps re-applies. Matches
  applyMask's unknown-value policy and the masked default. Corrects the
  header/comment that claimed it 'clears' state.
- lint/fix globs now cover *.tsx (were *.ts only), so src/index.tsx,
  src/amp-mask-view.tsx and test/amp-mask-view.test.tsx are linted.
- README: fix 'maks' -> 'mask' typo.
@aliaksandr-kazarez
aliaksandr-kazarez marked this pull request as ready for review July 16, 2026 19:55
@aliaksandr-kazarez
aliaksandr-kazarez requested a review from a team as a code owner July 16, 2026 19:55

@Mercy811 Mercy811 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's weird that it still need dx team approval after @chungdaniel's approval. I confirm he is in the team. Approve here to unblock but did't review this PR.

@aliaksandr-kazarez
aliaksandr-kazarez merged commit 92a9b16 into main Jul 16, 2026
25 of 26 checks passed
@aliaksandr-kazarez
aliaksandr-kazarez deleted the feat/fabric-ampmaskview branch July 16, 2026 20:51
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.

4 participants