Conversation
`codegenConfig` declared no `ios.componentProvider`, so `RCTThirdPartyComponentsProvider` carried no entry for `RNSentryReplayMask` and `RNSentryReplayUnmask`. React Native then resolved them through the legacy view manager interop layer, which 0.87 lets an app turn off with `RCT_REMOVE_LEGACY_COMPONENT_INTEROP` — and without it the components fall back to `UnimplementedView`, which leaves nothing for sentry-cocoa to redact, since it masks by view class. Map both component names to their view classes, the same way other community libraries do.
alwx
force-pushed
the
alwx/fix/replay-mask-codegen-provider
branch
from
September 30, 2026 08:00
4c6a3fa to
bbd056b
Compare
Contributor
Semver Impact of This PR⚪ None (no version bump detected) 📋 Changelog PreviewThis is how your changes will appear in the changelog.
🤖 This preview updates automatically when you update the PR. |
alwx
added a commit
that referenced
this pull request
Sep 30, 2026
The codegen componentProvider entry is independent of the SwiftPM work and affects CocoaPods builds on React Native 0.87 the same way. It also affects PII, so it gets its own test and revert trail. Moved to #6810. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
alwx
marked this pull request as ready for review
September 30, 2026 08:07
alwx
added a commit
that referenced
this pull request
Sep 30, 2026
* feat(ios): Support SwiftPM autolinking
React Native 0.87 added an opt-in Swift Package Manager integration, and
`npx react-native spm` refuses to set up an app whose autolinked library
ships no `Package.swift`. Add one, so the SDK can be consumed that way.
Three SwiftPM constraints shape the layout:
* No mixed-language targets, so the Swift sources move to `ios/Swift/`
and build as their own `RNSentrySwift` target. `.m` callers reach it
through `ios/RNSentrySwiftBridge.h`, which picks the pod's or the
package's generated header.
* `@import` is rejected in Objective-C++ and enabling C++ modules breaks
React Native's C++ headers, so `.mm` callers go through the new
`RNSentryInternalWrapper`, a plain Objective-C forwarder.
* No header maps, so the public headers are mirrored under
`ios/include/RNSentry/` (the target's `publicHeadersPath`) to keep
`#import <RNSentry/RNSentrySDK.h>` resolving. The podspec excludes the
mirrors.
The autolinked target name is pinned to `RNSentry` in both places React
Native reads it, since the name derived from `@sentry/react-native`
collides with React Native's reserved `ReactNative` and is also the
prefix consumers import our headers under.
CocoaPods is unaffected and stays the default.
* fix(ios): Register replay masks via codegen provider
`codegenConfig` declared no `ios.componentProvider`, so
`RCTThirdPartyComponentsProvider` carried no entry for
`RNSentryReplayMask` and `RNSentryReplayUnmask`. React Native then
resolved them through the legacy view manager interop layer, which 0.87
lets an app turn off with `RCT_REMOVE_LEGACY_COMPONENT_INTEROP` — and
without it the components fall back to `UnimplementedView`, which leaves
nothing for sentry-cocoa to redact, since it masks by view class.
Map both component names to their view classes, the same way other
community libraries do.
* ci: Build a SwiftPM app on iOS
Nothing covered the SwiftPM path, so an autolinking or manifest
regression would only surface in a user's project.
The app is generated per run from the React Native template instead of
committed: `react-native spm` stops on any autolinked dependency that
ships no `Package.swift`, and most community libraries still don't, so
the existing sample app cannot take this path. The job packs the SDK with
`yarn pack` (which resolves the `workspace:` ranges), installs the
tarball like a user would, and asserts that both RNSentry and
sentry-cocoa reach the app binary — a green build alone would not catch
a dependency that silently dropped out of the graph.
Also add `Package.swift` and `react-native.config.js` to the change
filters, since both drive the iOS build.
* ref(ios): Consume sentry-cocoa as a binary target
sentry-cocoa's manifest declares a binary target per distribution
variant, and SwiftPM downloads every artifact of a resolved package, not
only the ones the selected product needs. Depending on the package for
the one variant we use therefore pulled all seven archives: 2.9 GB in
DerivedData for the 339 MB we need, and seven chances for a failed
download to break the build — CI hit a GitHub 500 on
`SentryObjC-Dynamic.xcframework.zip`, which the build never uses.
Declare the `Sentry.xcframework` archive as our own binary target, the
same archive and checksum `pod install` already verifies. That leaves
one download. `update-cocoa.sh` keeps the version and the checksum in
step with the podspec.
`.linkedLibrary("c++")` replaces `SentryCppHelper`, an empty target that
sentry-cocoa pairs with the binary target to carry exactly that setting.
Reported upstream as getsentry/sentry-cocoa#9146.
* ci: Install both tarballs in the SwiftPM job
`@sentry/react-native` depends on `@sentry/expo-upload-sourcemaps` with a
`workspace:` range, which `yarn pack` rewrites to the version being
released. The job packed and installed the core tarball only, so npm
fetched that dependency from the registry — and on a `release/**` branch
the bumped version is not published yet, which fails with ETARGET before
the build even starts.
Pack both workspaces through `yarn build:tarball` and install both
tarballs, the way `buildandtest.yml` already does. `build:tarball` also
restores the executable bits that `yarn pack` drops.
Reported by Warden.
* fix(ios): Build RNSentryInternalWrapper on every platform
The wrapper mirrored `RNSentryInternal` without its platform gating, so
the macOS, tvOS and visionOS sample builds failed to compile it.
`setCurrentScreen:`, `captureScreenshots` and `captureViewHierarchy`
exist for iOS, tvOS and visionOS only — `RNSentryInternal` declares no
stubs for the other platforms — so the mirror now carries the guard the
call sites in `RNSentry.mm` already use.
`collectProfileBetween:and:forTrace:` was a second, older problem: the
watchOS/tvOS/visionOS stub was missing the explicit `@objc` selector
that its counterpart declares, so the selector differed by platform.
Nothing noticed because the only caller sits behind
`SENTRY_TARGET_PROFILING_SUPPORTED`. Declare it on the stub too.
* fix(ios): Pin sentry-cocoa 9.29.1 in Package.swift
main moved to 9.29.1 while this branch was open, so the SwiftPM path
stayed a patch behind the CocoaPods one. Take the version and the
checksum from `sentry_utils.rb`, which the CocoaPods path already
verifies — a clean resolve accepts them, which also confirms both
consumers hash the same archive.
* ci: Assert a class name, not the package name
The SwiftPM link check searched the app binary for `sentry-cocoa`. That
string is incidental to the prebuilt dependency, so an upstream change
could drop it and fail a valid build. Look for `SentryOptions` instead:
the linker keeps the name of every Objective-C class it links, so the
marker is present whenever the library is.
Reported by Warden.
* fix(ios): Sync Package.swift to sentry-cocoa 9.29.2
The 9.29.2 bump (#6785, #6787) landed on main while this branch was open
and ran update-cocoa.sh before it learned about Package.swift, so the
SwiftPM manifest still pinned 9.29.1. Checksum matches sentry-cocoa's own
Package.swift at tag 9.29.2.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(ios): Route registerReplayTraceId through RNSentryInternalWrapper
#6786 added registerReplayTraceId on main while this branch was moving the
.mm callers off the Swift module. git merged both cleanly, but the result
calls RNSentryInternal directly from RNSentry.mm, which no longer imports
the generated Swift header -- breaking every iOS build, CocoaPods included.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(changelog): Note SwiftPM support is iOS-only
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* ci(ios): Assert Sentry ObjC categories survive the SwiftPM link
The existing check greps for class names, which reach the binary even when
their categories do not. sentry-cocoa ships category-only object files
(SentryReplayNetworkDetails+Capture, Options+Dictionary,
SentryNSNotificationCenterWrapper) that nothing references, so the linker
drops them from the static archive unless it is force-loaded -- #6609.
CocoaPods uses -force_load for this; the SwiftPM path has no equivalent,
so measure whether it actually matters here.
The check confirms the canary selector still exists upstream before
treating its absence in the app binary as a failure, so a sentry-cocoa
rename warns instead of failing spuriously.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* ci(ios): Fail the category check when the Sentry archive is missing
A path or layout change made `find` return nothing, which took the same
soft-pass branch as a renamed upstream selector and skipped the assertion
entirely -- the one SwiftPM guard against the #6609 dead-strip crash.
Split the two: a missing archive fails, a stale canary still warns.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(ios): Anchor the Package.swift npmignore rule to the package root
Unanchored, `!Package.swift` re-included the file at any depth, so a
machine that had run the Cocoa tests also shipped the gitignored
`RNSentryCocoaTester/build/generated/ios/Package.swift` -- making the
published tarball depend on local build state. Anchor it like the other
root-level entries.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: Document the SwiftPM setup in the README
The steps only existed in spm-application.yml, so trying SwiftPM meant
reading a CI workflow. Covers the two things people hit first: every
autolinked dependency needs a Package.swift, and the path is iOS only.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(ios): Move the replay mask codegen fix to its own PR
The codegen componentProvider entry is independent of the SwiftPM work
and affects CocoaPods builds on React Native 0.87 the same way. It also
affects PII, so it gets its own test and revert trail. Moved to #6810.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
📲 Install BuildsAndroid
|
Contributor
iOS (legacy) Performance metrics 🚀
|
Contributor
Android (legacy) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 5789645+dirty | 426.82 ms | 495.42 ms | 68.60 ms |
| fa21fca+dirty | 453.80 ms | 468.46 ms | 14.66 ms |
| a216cb9+dirty | 458.66 ms | 531.47 ms | 72.81 ms |
| 0307fa4+dirty | 454.16 ms | 518.35 ms | 64.18 ms |
| 5569641+dirty | 406.43 ms | 428.51 ms | 22.08 ms |
| 6a3eb4c+dirty | 430.90 ms | 489.98 ms | 59.08 ms |
| 5fe1c6c+dirty | 401.62 ms | 445.28 ms | 43.66 ms |
| a636fa4+dirty | 486.70 ms | 508.53 ms | 21.83 ms |
| 15d4514+dirty | 406.77 ms | 428.06 ms | 21.29 ms |
| 1e5d96d+dirty | 519.43 ms | 543.62 ms | 24.19 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 5789645+dirty | 49.74 MiB | 54.85 MiB | 5.11 MiB |
| fa21fca+dirty | 49.74 MiB | 55.37 MiB | 5.63 MiB |
| a216cb9+dirty | 49.74 MiB | 55.08 MiB | 5.34 MiB |
| 0307fa4+dirty | 49.74 MiB | 54.81 MiB | 5.07 MiB |
| 5569641+dirty | 48.30 MiB | 53.48 MiB | 5.18 MiB |
| 6a3eb4c+dirty | 49.74 MiB | 55.44 MiB | 5.70 MiB |
| 5fe1c6c+dirty | 43.75 MiB | 48.14 MiB | 4.39 MiB |
| a636fa4+dirty | 49.74 MiB | 55.36 MiB | 5.62 MiB |
| 15d4514+dirty | 48.30 MiB | 53.60 MiB | 5.30 MiB |
| 1e5d96d+dirty | 49.74 MiB | 54.81 MiB | 5.07 MiB |
Contributor
iOS (new) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 9253345+dirty | 3862.73 ms | 1237.15 ms | -2625.59 ms |
| ef27341+dirty | 3835.20 ms | 1212.23 ms | -2622.97 ms |
| d387874+dirty | 3838.71 ms | 1211.11 ms | -2627.60 ms |
| 4acd4e9+dirty | 3806.73 ms | 1208.48 ms | -2598.25 ms |
| 0b1b5e3+dirty | 3820.72 ms | 1207.94 ms | -2612.78 ms |
| 267d3ed+dirty | 3860.14 ms | 1223.39 ms | -2636.76 ms |
| 853723c+dirty | 3849.33 ms | 1221.07 ms | -2628.26 ms |
| b726ced+dirty | 3840.50 ms | 1217.29 ms | -2623.21 ms |
| 9474ead+dirty | 3823.33 ms | 1208.31 ms | -2615.03 ms |
| 3d377b5+dirty | 1201.55 ms | 1201.80 ms | 0.25 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 9253345+dirty | 5.15 MiB | 6.89 MiB | 1.74 MiB |
| ef27341+dirty | 5.15 MiB | 6.68 MiB | 1.53 MiB |
| d387874+dirty | 5.08 MiB | 6.68 MiB | 1.60 MiB |
| 4acd4e9+dirty | 4.98 MiB | 6.53 MiB | 1.55 MiB |
| 0b1b5e3+dirty | 5.15 MiB | 6.70 MiB | 1.54 MiB |
| 267d3ed+dirty | 5.15 MiB | 6.69 MiB | 1.54 MiB |
| 853723c+dirty | 5.15 MiB | 6.69 MiB | 1.53 MiB |
| b726ced+dirty | 5.15 MiB | 6.89 MiB | 1.74 MiB |
| 9474ead+dirty | 5.15 MiB | 6.71 MiB | 1.55 MiB |
| 3d377b5+dirty | 3.38 MiB | 4.76 MiB | 1.38 MiB |
antonis
requested changes
Sep 30, 2026
antonis
left a comment
Contributor
There was a problem hiding this comment.
Thank you for splitting this fix @alwx 🙇
I've tested the runtime behavior of this with the following patch:
Podfile patch
diff --git a/samples/react-native/ios/Podfile b/samples/react-native/ios/Podfile
index e0c97aadb..678c52eee 100644
--- a/samples/react-native/ios/Podfile
+++ b/samples/react-native/ios/Podfile
@@ -64,5 +64,29 @@ target 'sentryreactnativesample' do
:mac_catalyst_enabled => ENV['ENABLE_MAC_CATALYST'] == '1',
# :ccache_enabled => true
)
+ # TEMP (verifying getsentry/sentry-react-native#6810): compile React Native's
+ # Fabric factory with the legacy view-manager interop layer removed, so
+ # RNSentryReplayMask/Unmask must resolve through the codegen component
+ # provider. Toggled by REMOVE_LEGACY_INTEROP=1. Not for commit.
+ if ENV['REMOVE_LEGACY_INTEROP'] == '1'
+ installer.pods_project.targets.each do |t|
+ next unless t.name == 'React-RCTFabric'
+ t.build_configurations.each do |cfg|
+ defs = cfg.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] || ['$(inherited)']
+ defs = [defs] unless defs.is_a?(Array)
+ defs << 'RCT_REMOVE_LEGACY_COMPONENT_INTEROP=1'
+ cfg.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] = defs
+ end
+ Pod::UI.puts "[#6810] RCT_REMOVE_LEGACY_COMPONENT_INTEROP set on React-RCTFabric"
+ end
+ end
+ # TEMP: Xcode 27's simulator SDK rejects deployment targets < 15.0; a few pod
+ # resource/privacy-info bundles default lower. Floor every pod target. Not for commit.
+ installer.pods_project.targets.each do |t|
+ t.build_configurations.each do |cfg|
+ cur = cfg.build_settings['IPHONEOS_DEPLOYMENT_TARGET'].to_f
+ cfg.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0' if cur < 15.0
+ end
+ end
end
end
The default masking in the sample app worked as expected (example) but I get a crash when I browse to the playground tab were custom masking/unmasking is applied.
Adding frame constructors like the following worked for me.
Frame constructors patch
diff --git a/packages/core/ios/Replay/RNSentryReplayMask.mm b/packages/core/ios/Replay/RNSentryReplayMask.mm
index 14453e26a..6d8651071 100644
--- a/packages/core/ios/Replay/RNSentryReplayMask.mm
+++ b/packages/core/ios/Replay/RNSentryReplayMask.mm
@@ -6,6 +6,7 @@
# ifdef RCT_NEW_ARCH_ENABLED
# import <react/renderer/components/RNSentrySpec/ComponentDescriptors.h>
+# import <react/renderer/components/RNSentrySpec/Props.h>
# import <react/renderer/components/RNSentrySpec/RCTComponentViewHelpers.h>
// RCTFabricComponentsPlugins needed for RNSentryReplayMaskCls
# import <React/RCTFabricComponentsPlugins.h>
@@ -30,6 +31,20 @@ RCT_EXPORT_MODULE(RNSentryReplayMask)
@implementation RNSentryReplayMask
# ifdef RCT_NEW_ARCH_ENABLED
+// Required for RCTViewComponentView subclasses: without a default-initialized
+// `_props`, React Native aborts in `-updateProps:oldProps:` on mount. This only
+// surfaces when the component resolves directly through the codegen component
+// provider instead of the legacy view-manager interop layer (e.g. an app that
+// sets `RCT_REMOVE_LEGACY_COMPONENT_INTEROP`).
+- (instancetype)initWithFrame:(CGRect)frame
+{
+ if (self = [super initWithFrame:frame]) {
+ static const auto defaultProps = std::make_shared<const facebook::react::RNSentryReplayMaskProps>();
+ _props = defaultProps;
+ }
+ return self;
+}
+
+ (facebook::react::ComponentDescriptorProvider)componentDescriptorProvider
{
return facebook::react::concreteComponentDescriptorProvider<
diff --git a/packages/core/ios/Replay/RNSentryReplayUnmask.mm b/packages/core/ios/Replay/RNSentryReplayUnmask.mm
index f0ec5139d..feaeb1f80 100644
--- a/packages/core/ios/Replay/RNSentryReplayUnmask.mm
+++ b/packages/core/ios/Replay/RNSentryReplayUnmask.mm
@@ -6,6 +6,7 @@
# ifdef RCT_NEW_ARCH_ENABLED
# import <react/renderer/components/RNSentrySpec/ComponentDescriptors.h>
+# import <react/renderer/components/RNSentrySpec/Props.h>
# import <react/renderer/components/RNSentrySpec/RCTComponentViewHelpers.h>
// RCTFabricComponentsPlugins needed for RNSentryReplayUnmaskCls
# import <React/RCTFabricComponentsPlugins.h>
@@ -30,6 +31,20 @@ RCT_EXPORT_MODULE(RNSentryReplayUnmask)
@implementation RNSentryReplayUnmask
# ifdef RCT_NEW_ARCH_ENABLED
+// Required for RCTViewComponentView subclasses: without a default-initialized
+// `_props`, React Native aborts in `-updateProps:oldProps:` on mount. This only
+// surfaces when the component resolves directly through the codegen component
+// provider instead of the legacy view-manager interop layer (e.g. an app that
+// sets `RCT_REMOVE_LEGACY_COMPONENT_INTEROP`).
+- (instancetype)initWithFrame:(CGRect)frame
+{
+ if (self = [super initWithFrame:frame]) {
+ static const auto defaultProps = std::make_shared<const facebook::react::RNSentryReplayUnmaskProps>();
+ _props = defaultProps;
+ }
+ return self;
+}
+
+ (facebook::react::ComponentDescriptorProvider)componentDescriptorProvider
{
return facebook::react::concreteComponentDescriptorProvider<
Contributor
Android (new) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| bf168a4+dirty | 430.60 ms | 459.31 ms | 28.71 ms |
| 0bd8916+dirty | 400.15 ms | 442.72 ms | 42.57 ms |
| a2585ce+dirty | 414.04 ms | 456.83 ms | 42.79 ms |
| 9c84b9a+dirty | 429.26 ms | 448.90 ms | 19.64 ms |
| bc0d8cf+dirty | 407.66 ms | 461.35 ms | 53.69 ms |
| a736b76+dirty | 405.78 ms | 458.74 ms | 52.96 ms |
| 1122a96+dirty | 510.16 ms | 542.00 ms | 31.84 ms |
| 267d3ed+dirty | 424.69 ms | 483.70 ms | 59.01 ms |
| 6177334+dirty | 404.80 ms | 456.74 ms | 51.94 ms |
| 7887847+dirty | 420.47 ms | 460.55 ms | 40.08 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| bf168a4+dirty | 49.74 MiB | 55.09 MiB | 5.35 MiB |
| 0bd8916+dirty | 48.30 MiB | 53.57 MiB | 5.26 MiB |
| a2585ce+dirty | 49.74 MiB | 55.36 MiB | 5.61 MiB |
| 9c84b9a+dirty | 49.74 MiB | 55.36 MiB | 5.62 MiB |
| bc0d8cf+dirty | 48.30 MiB | 53.48 MiB | 5.18 MiB |
| a736b76+dirty | 48.30 MiB | 53.48 MiB | 5.18 MiB |
| 1122a96+dirty | 48.30 MiB | 53.54 MiB | 5.24 MiB |
| 267d3ed+dirty | 48.30 MiB | 53.58 MiB | 5.28 MiB |
| 6177334+dirty | 48.30 MiB | 53.54 MiB | 5.23 MiB |
| 7887847+dirty | 49.74 MiB | 54.81 MiB | 5.07 MiB |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
📢 Type of change
📜 Description
codegenConfiginpackages/core/package.jsondeclared noios.componentProvider.RCTThirdPartyComponentsProvidertherefore carried no entry forRNSentryReplayMaskandRNSentryReplayUnmask.This change maps both component names to their view classes, the same way other community libraries do.
💡 Motivation and Context
Without the entry, React Native resolves the two components through the legacy interop layer for view managers. React Native 0.87 lets an app turn that layer off with
RCT_REMOVE_LEGACY_COMPONENT_INTEROP. If an app turns it off, the components fall back toUnimplementedView.sentry-cocoa redacts session replays by view class. An
UnimplementedViewcarries no mask class, so sentry-cocoa finds nothing to redact. Content that the app marked as masked can then appear in the replay.Split out of #6784 at review request. The change is independent of the SwiftPM work in that pull request, and it affects CocoaPods builds on React Native 0.87 in the same way. A separate pull request gives this change its own test and revert trail, because it affects PII.
💚 How did you test it?
Not tested at runtime yet. The change is declarative configuration consumed by React Native codegen, and it built cleanly in CI on the #6784 branch.
A runtime check is still worth doing: build an app with
RCT_REMOVE_LEGACY_COMPONENT_INTEROPturned on, mask a view, and confirm that the masked content stays redacted in the replay.📝 Checklist
sendDefaultPIIis enabled.🔮 Next steps
Run the runtime check described above before release.