feat(ios): Support SPM autolinking - #6784
Conversation
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.
|
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. |
`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.
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.
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.
📲 Install BuildsAndroid
|
iOS (legacy) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 64630e5+dirty | 3842.70 ms | 1218.11 ms | -2624.60 ms |
| b0d3373+dirty | 3831.75 ms | 1227.29 ms | -2604.46 ms |
| b04af96+dirty | 3818.92 ms | 1219.76 ms | -2599.16 ms |
| 3d31fcf+dirty | 3838.09 ms | 1223.46 ms | -2614.63 ms |
| a0a3177+dirty | 3844.73 ms | 1225.23 ms | -2619.51 ms |
| af33f3b+dirty | 3849.98 ms | 1236.45 ms | -2613.53 ms |
| 09a902f+dirty | 3835.67 ms | 1217.11 ms | -2618.57 ms |
| 5a316ea+dirty | 3820.11 ms | 1211.28 ms | -2608.83 ms |
| acd838e+dirty | 3849.78 ms | 1230.00 ms | -2619.78 ms |
| c2e182c+dirty | 3848.40 ms | 1211.79 ms | -2636.61 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 64630e5+dirty | 4.98 MiB | 6.46 MiB | 1.49 MiB |
| b0d3373+dirty | 5.15 MiB | 6.68 MiB | 1.53 MiB |
| b04af96+dirty | 4.98 MiB | 6.54 MiB | 1.56 MiB |
| 3d31fcf+dirty | 4.98 MiB | 6.56 MiB | 1.58 MiB |
| a0a3177+dirty | 4.98 MiB | 6.55 MiB | 1.58 MiB |
| af33f3b+dirty | 4.98 MiB | 6.51 MiB | 1.54 MiB |
| 09a902f+dirty | 4.98 MiB | 6.46 MiB | 1.49 MiB |
| 5a316ea+dirty | 4.98 MiB | 6.51 MiB | 1.53 MiB |
| acd838e+dirty | 5.15 MiB | 6.70 MiB | 1.55 MiB |
| c2e182c+dirty | 4.98 MiB | 6.50 MiB | 1.52 MiB |
Previous results on branch: alwx/feature/full-spm-support
Startup times
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 2cca10e+dirty | 3857.27 ms | 1232.14 ms | -2625.13 ms |
| 7a7af85+dirty | 3865.35 ms | 1235.00 ms | -2630.35 ms |
| 98145a6+dirty | 3878.28 ms | 1232.33 ms | -2645.95 ms |
| 9eb54c2+dirty | 3860.19 ms | 1234.41 ms | -2625.77 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 2cca10e+dirty | 5.15 MiB | 6.93 MiB | 1.78 MiB |
| 7a7af85+dirty | 5.15 MiB | 6.92 MiB | 1.77 MiB |
| 98145a6+dirty | 5.15 MiB | 6.93 MiB | 1.78 MiB |
| 9eb54c2+dirty | 5.15 MiB | 6.90 MiB | 1.75 MiB |
`@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.
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 |
Previous results on branch: alwx/feature/full-spm-support
Startup times
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 7a7af85+dirty | 542.80 ms | 585.04 ms | 42.24 ms |
| 2cca10e+dirty | 434.78 ms | 457.98 ms | 23.20 ms |
| 9eb54c2+dirty | 421.29 ms | 435.85 ms | 14.56 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 7a7af85+dirty | 50.56 MiB | 56.51 MiB | 5.95 MiB |
| 2cca10e+dirty | 50.56 MiB | 56.51 MiB | 5.95 MiB |
| 9eb54c2+dirty | 50.56 MiB | 56.51 MiB | 5.95 MiB |
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 |
Previous results on branch: alwx/feature/full-spm-support
Startup times
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 7a7af85+dirty | 437.28 ms | 481.92 ms | 44.64 ms |
| 2cca10e+dirty | 466.67 ms | 512.09 ms | 45.42 ms |
| 9eb54c2+dirty | 423.14 ms | 456.60 ms | 33.46 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 7a7af85+dirty | 50.56 MiB | 56.51 MiB | 5.95 MiB |
| 2cca10e+dirty | 50.56 MiB | 56.51 MiB | 5.95 MiB |
| 9eb54c2+dirty | 50.56 MiB | 56.51 MiB | 5.95 MiB |
iOS (new) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 7d6fd3a+dirty | 1210.89 ms | 1217.63 ms | 6.74 ms |
| 774257e+dirty | 3821.35 ms | 1211.96 ms | -2609.39 ms |
| 4e0b819+dirty | 3828.96 ms | 1205.64 ms | -2623.32 ms |
| d038a14+dirty | 3831.11 ms | 1216.30 ms | -2614.81 ms |
| 882f8ae+dirty | 3842.51 ms | 1230.40 ms | -2612.11 ms |
| 5125c43+dirty | 3827.94 ms | 1208.79 ms | -2619.15 ms |
| 15d4514+dirty | 3843.73 ms | 1228.09 ms | -2615.64 ms |
| 3d31fcf+dirty | 3857.46 ms | 1237.17 ms | -2620.29 ms |
| 4b87b12+dirty | 1199.49 ms | 1199.78 ms | 0.29 ms |
| 5c1e987+dirty | 1208.43 ms | 1220.72 ms | 12.29 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 7d6fd3a+dirty | 3.38 MiB | 4.77 MiB | 1.39 MiB |
| 774257e+dirty | 5.15 MiB | 6.70 MiB | 1.54 MiB |
| 4e0b819+dirty | 4.98 MiB | 6.46 MiB | 1.49 MiB |
| d038a14+dirty | 5.15 MiB | 6.67 MiB | 1.51 MiB |
| 882f8ae+dirty | 5.15 MiB | 6.70 MiB | 1.54 MiB |
| 5125c43+dirty | 5.15 MiB | 6.68 MiB | 1.53 MiB |
| 15d4514+dirty | 5.15 MiB | 6.70 MiB | 1.55 MiB |
| 3d31fcf+dirty | 4.98 MiB | 6.56 MiB | 1.58 MiB |
| 4b87b12+dirty | 3.38 MiB | 4.77 MiB | 1.39 MiB |
| 5c1e987+dirty | 3.38 MiB | 4.73 MiB | 1.35 MiB |
Previous results on branch: alwx/feature/full-spm-support
Startup times
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 2cca10e+dirty | 3863.70 ms | 1235.02 ms | -2628.68 ms |
| 7a7af85+dirty | 3864.15 ms | 1222.57 ms | -2641.57 ms |
| 9eb54c2+dirty | 3811.25 ms | 3721.77 ms | -89.48 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 2cca10e+dirty | 5.15 MiB | 6.93 MiB | 1.78 MiB |
| 7a7af85+dirty | 5.15 MiB | 6.92 MiB | 1.77 MiB |
| 9eb54c2+dirty | 5.15 MiB | 6.90 MiB | 1.75 MiB |
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.
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.
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.
…-support # Conflicts: # CHANGELOG.md
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>
#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>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 69a15e6. Configure here.
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>
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>
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>
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>
antonis
left a comment
There was a problem hiding this comment.
Thank you for your work on this Alex 🙇 The SPM changes LGMT 🎉
Added a comment suggestion on the masking change https://github.com/getsentry/sentry-react-native/pull/6784/changes#r4123462658
|
Might be a good idea to tip the |
| // C++ ABI, or the Release link fails on anything that includes React headers. | ||
| let configurationDefines: [CXXSetting] = [ | ||
| .define("DEBUG", .when(configuration: .debug)), | ||
| .define("NDEBUG", .when(configuration: .release)) |
There was a problem hiding this comment.
Q: Isn't it possible to check when it's not debug? This way we could gover other types of name like staging
There was a problem hiding this comment.
SwiftPM has only two build configurations, so .release already means "not debug".
PackageDescription in the Xcode toolchain defines them like this:
public struct BuildConfiguration : Sendable {
public static let debug: BuildConfiguration
public static let release: BuildConfiguration
}There is no third value, and there is no "not debug" predicate. A configuration named Staging cannot be matched by name at all.
|
Overall looks good! left just some questions before merging it. |
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>

📢 Type of change
📜 Description
This is experimental support for the Swift Package Manager (SPM).
The SDK now has a
Package.swiftfile. React Native 0.87 and later versions canuse it. CocoaPods is still the default, and it does not change.
The PR also adds the iOS component provider for the two session replay mask
components. React Native then finds the classes directly. Before this change, it
used the legacy interop layer, which an app can disable.
💡 Motivation and Context
React Native 0.87 added SPM support. It is a preview.
The
npx react-native spmcommand stops if a library has noPackage.swiftfile. Thus you cannot build an app that uses this SDK with SPM.
Related to #5780.
💚 How did you test it?
npm install @sentry/react-nativecd ios && npx react-native spm add --deintegrateMore details can be found in README.md
A new CI job builds the app for each PR. It also makes sure that the app binary
contains the SDK and sentry-cocoa.
📝 Checklist
sendDefaultPIIis enabled.🔮 Next steps