From ecce235477e2027c7c5c99773ac75737a597293c Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Thu, 24 Sep 2026 15:24:44 +0200 Subject: [PATCH 01/16] 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 ` 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. --- CHANGELOG.md | 6 + packages/core/.npmignore | 1 + packages/core/Package.swift | 132 +++++++++++++++ packages/core/RNSentry.podspec | 5 + packages/core/ios/AGENTS.md | 33 ++++ packages/core/ios/RNSentry.mm | 86 +++++----- packages/core/ios/RNSentryInternalWrapper.h | 97 +++++++++++ packages/core/ios/RNSentryInternalWrapper.m | 159 ++++++++++++++++++ packages/core/ios/RNSentryRNSScreen.m | 6 +- packages/core/ios/RNSentryReplay.h | 1 + packages/core/ios/RNSentryReplay.mm | 10 +- .../RNSentryReplayBreadcrumbConverterHelper.m | 6 +- packages/core/ios/RNSentrySDK.m | 6 +- packages/core/ios/RNSentryStart.m | 6 +- packages/core/ios/RNSentrySwiftBridge.h | 31 ++++ packages/core/ios/SentrySDKWrapper.m | 6 +- packages/core/ios/SentryScreenFramesWrapper.m | 6 +- .../ios/{ => Swift}/RNSentryInternal.swift | 0 .../{ => Swift}/RNSentrySwiftLinkStub.swift | 0 packages/core/ios/include/RNSentry/RNSentry.h | 5 + .../ios/include/RNSentry/RNSentryBreadcrumb.h | 4 + .../ios/include/RNSentry/RNSentryReplay.h | 5 + .../RNSentryReplayBreadcrumbConverter.h | 5 + .../ios/include/RNSentry/RNSentryReplayMask.h | 5 + .../include/RNSentry/RNSentryReplayUnmask.h | 5 + .../core/ios/include/RNSentry/RNSentrySDK.h | 5 + .../core/ios/include/RNSentry/RNSentryStart.h | 5 + .../include/RNSentry/RNSentryTimeToDisplay.h | 5 + .../ios/include/RNSentry/RNSentryVersion.h | 5 + packages/core/package.json | 3 + packages/core/react-native.config.js | 8 + scripts/update-cocoa.sh | 13 +- 32 files changed, 593 insertions(+), 77 deletions(-) create mode 100644 packages/core/Package.swift create mode 100644 packages/core/ios/RNSentryInternalWrapper.h create mode 100644 packages/core/ios/RNSentryInternalWrapper.m create mode 100644 packages/core/ios/RNSentrySwiftBridge.h rename packages/core/ios/{ => Swift}/RNSentryInternal.swift (100%) rename packages/core/ios/{ => Swift}/RNSentrySwiftLinkStub.swift (100%) create mode 100644 packages/core/ios/include/RNSentry/RNSentry.h create mode 100644 packages/core/ios/include/RNSentry/RNSentryBreadcrumb.h create mode 100644 packages/core/ios/include/RNSentry/RNSentryReplay.h create mode 100644 packages/core/ios/include/RNSentry/RNSentryReplayBreadcrumbConverter.h create mode 100644 packages/core/ios/include/RNSentry/RNSentryReplayMask.h create mode 100644 packages/core/ios/include/RNSentry/RNSentryReplayUnmask.h create mode 100644 packages/core/ios/include/RNSentry/RNSentrySDK.h create mode 100644 packages/core/ios/include/RNSentry/RNSentryStart.h create mode 100644 packages/core/ios/include/RNSentry/RNSentryTimeToDisplay.h create mode 100644 packages/core/ios/include/RNSentry/RNSentryVersion.h diff --git a/CHANGELOG.md b/CHANGELOG.md index 39b84a8875..06e6a0206b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ ## Unreleased +### Features + +- Support React Native's Swift Package Manager integration ([#6784](https://github.com/getsentry/sentry-react-native/pull/6784)) + - The SDK now ships a `Package.swift`, so `npx react-native spm` picks it up through SwiftPM autolinking instead of failing with `Package.swift is missing for library "@sentry/react-native"`. + - Requires React Native 0.87 or newer, where SwiftPM support is an opt-in preview. CocoaPods remains the default and is unaffected. + ### Dependencies - Bump Android SDK from v8.57.0 to v8.58.0 ([#6778](https://github.com/getsentry/sentry-react-native/pull/6778)) diff --git a/packages/core/.npmignore b/packages/core/.npmignore index 871ec44541..adc3daa56f 100644 --- a/packages/core/.npmignore +++ b/packages/core/.npmignore @@ -9,6 +9,7 @@ !/dist/**/* !ts3.8/**/* !RNSentry.podspec +!Package.swift !sentry.gradle !sentry.gradle.kts !react-native.config.js diff --git a/packages/core/Package.swift b/packages/core/Package.swift new file mode 100644 index 0000000000..fabcc34065 --- /dev/null +++ b/packages/core/Package.swift @@ -0,0 +1,132 @@ +// swift-tools-version: 6.0 +import PackageDescription + +// Swift Package Manager support for React Native's SwiftPM autolinking +// (react-native >= 0.87, where it ships as an opt-in preview). CocoaPods users +// are unaffected and keep building through `RNSentry.podspec`. +// +// React Native resolves this manifest through a symlink at +// `/ios/build/generated/autolinking/libs/RNSentry`, and SwiftPM evaluates +// relative package paths against that symlink rather than `node_modules` — so +// `../../../../xcframeworks` is `/ios/build/xcframeworks` (the prebuilt +// React Native artifacts) and `../../../ios` is +// `/ios/build/generated/ios` (this app's codegen output). The paths are +// fixed because the symlink location is. +// +// Every target points at a single source directory rather than the package +// root: SwiftPM scans a target's whole directory for resources, so a root-level +// target picks up whatever else happens to sit next to the sources (a local +// `node_modules`, Xcode build output, `Pods/`) and fails to resolve. +// +// Keep the build settings below in explicitly typed constants. Concatenating +// them inline inside `Package(...)` is enough to make the manifest fail to +// type-check ("unable to type-check this expression in reasonable time"). + +// `RCT_NEW_ARCH_ENABLED`: the SwiftPM path exists only on React Native 0.87+, +// where the New Architecture is the only architecture. +// `SENTRY_PROFILING_SUPPORTED` / `NEW_HERMES_RUNTIME`: both gates +// (`RNSentry.podspec` → `scripts/sentry_utils.rb`) are satisfied by every +// React Native version that can consume this manifest. +// `RN_SENTRY_SWIFTPM`: tells `ios/RNSentrySwiftBridge.h` to import the Swift +// sources as their own module instead of the pod's `RNSentry-Swift.h`. +let defines: [(name: String, value: String?)] = [ + ("RCT_NEW_ARCH_ENABLED", nil), + ("SENTRY_PROFILING_SUPPORTED", "1"), + ("NEW_HERMES_RUNTIME", nil), + ("RN_SENTRY_SWIFTPM", "1") +] + +let cDefines: [CSetting] = defines.map { .define($0.name, to: $0.value) } +let cxxDefines: [CXXSetting] = defines.map { .define($0.name, to: $0.value) } + +// DEBUG/NDEBUG must match the prebuilt React.framework's configuration-gated +// 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)) +] + +// Unlike CocoaPods, SwiftPM has no header maps, so the quoted cross-directory +// imports inside `ios/` need explicit search paths. Keep in sync when adding a +// directory that holds headers. +let iosCHeaderPaths: [CSetting] = [.headerSearchPath("."), .headerSearchPath("Replay")] +let iosCxxHeaderPaths: [CXXSetting] = [.headerSearchPath("."), .headerSearchPath("Replay")] + +let iosCSettings: [CSetting] = iosCHeaderPaths + cDefines +let iosCxxSettings: [CXXSetting] = iosCxxHeaderPaths + cxxDefines + configurationDefines +let cppCxxSettings: [CXXSetting] = cxxDefines + configurationDefines + +// React Native's own headers, its bundled third-party headers (folly, glog, +// hermes, ...), and this app's codegen output. All three are plain product +// dependencies — React Native's SwiftPM integration serves headers through the +// package graph instead of search paths. +let reactNativeHeaderProducts: [Target.Dependency] = [ + .product(name: "ReactHeaders", package: "ReactNative"), + .product(name: "ReactNativeHeaders", package: "ReactNative"), + .product(name: "ReactNativeDependenciesHeaders", package: "ReactNative"), + .product(name: "ReactAppHeaders", package: "React-GeneratedCode") +] + +let sentryCocoa: Target.Dependency = .product(name: "Sentry", package: "sentry-cocoa") + +let rnSentryDependencies: [Target.Dependency] = + ["RNSentrySwift", "RNSentryCpp", sentryCocoa] + reactNativeHeaderProducts + +let package = Package( + name: "RNSentry", + platforms: [.iOS(.v15)], + products: [ + .library(name: "RNSentry", targets: ["RNSentry"]) + ], + dependencies: [ + .package(name: "ReactNative", path: "../../../../xcframeworks"), + .package(name: "React-GeneratedCode", path: "../../../ios"), + .package(url: "https://github.com/getsentry/sentry-cocoa.git", exact: "9.29.0") + ], + targets: [ + // SwiftPM cannot mix Swift with Objective-C(++) in one target, so the + // Swift bridge over sentry-cocoa's `SentrySDK.internal.*` compiles on + // its own. `.m`/`.mm` callers reach it through + // `ios/RNSentrySwiftBridge.h`, which picks the generated header of + // whichever build system is in use. + .target( + name: "RNSentrySwift", + dependencies: [sentryCocoa], + path: "ios/Swift", + // Matches `s.swift_versions` in the podspec. A swift-tools-version + // of 6.0 would otherwise compile these sources in the Swift 6 + // language mode, which the pod never does. + swiftSettings: [.swiftLanguageMode(.v5)] + ), + // Platform-agnostic C++ shared with Android, which compiles it through + // `android/CMakeLists.txt` instead. `ios/RNSentry.mm` includes these + // headers by relative path, so this target only has to compile and link + // them. + .target( + name: "RNSentryCpp", + dependencies: reactNativeHeaderProducts, + path: "cpp", + publicHeadersPath: ".", + cxxSettings: cppCxxSettings + ), + .target( + name: "RNSentry", + dependencies: rnSentryDependencies, + path: "ios", + exclude: [ + "AGENTS.md", + "RNSentry.xcodeproj", + "Swift" + ], + publicHeadersPath: "include", + cSettings: iosCSettings, + cxxSettings: iosCxxSettings, + linkerSettings: [ + .linkedFramework("Foundation"), + .linkedFramework("UIKit"), + .linkedFramework("QuartzCore") + ] + ) + ], + cxxLanguageStandard: .cxx20 +) diff --git a/packages/core/RNSentry.podspec b/packages/core/RNSentry.podspec index bb8cdbbb41..346ffdf9e9 100644 --- a/packages/core/RNSentry.podspec +++ b/packages/core/RNSentry.podspec @@ -54,6 +54,11 @@ Pod::Spec.new do |s| # and friends aren't modularized by default) must add # `use_modular_headers!` to their Podfile — see CHANGELOG. s.source_files = 'ios/**/*.{h,m,mm,swift}', 'cpp/**/*.{h,cpp}' + # `ios/include/RNSentry/` mirrors the public headers for Swift Package + # Manager, which has no header maps (see `Package.swift`). CocoaPods builds + # the namespaced spelling itself, and picking the mirrors up here would give + # the pod two headers per public name. + s.exclude_files = 'ios/include/**/*' s.swift_versions = ['5.5'] s.public_header_files = 'ios/RNSentry.h', 'ios/RNSentrySDK.h', 'ios/RNSentryStart.h', 'ios/RNSentryVersion.h', 'ios/RNSentryBreadcrumb.h', 'ios/RNSentryReplay.h', 'ios/RNSentryReplayBreadcrumbConverter.h', 'ios/Replay/RNSentryReplayMask.h', 'ios/Replay/RNSentryReplayUnmask.h', 'ios/RNSentryTimeToDisplay.h' diff --git a/packages/core/ios/AGENTS.md b/packages/core/ios/AGENTS.md index 75851237fb..f31c701ad3 100644 --- a/packages/core/ios/AGENTS.md +++ b/packages/core/ios/AGENTS.md @@ -62,6 +62,39 @@ RCT_EXPORT_METHOD(nativeOperation:(NSString *)param pod 'Sentry', :path => '../../../../sentry-cocoa' ``` +## Swift Package Manager (`../Package.swift`) + +React Native 0.87+ can autolink through SwiftPM instead of CocoaPods, so the +package ships a `Package.swift` next to `RNSentry.podspec`. Both describe the +same sources; keep them in sync. + +Three SwiftPM constraints shape the layout — don't undo them by moving files: + +- **No mixed-language targets.** Swift lives in `ios/Swift/` and compiles as the + separate `RNSentrySwift` target. `.m` files import its generated interface + through `ios/RNSentrySwiftBridge.h`, never `` + directly — that spelling only exists under CocoaPods. +- **No Swift module in Objective-C++.** Under SwiftPM the Swift interface is + only reachable as a Clang module, and `@import` is rejected in `.mm` files + (enabling C++ modules breaks React Native's C++ headers). So `.mm` callers go + through `RNSentryInternalWrapper`, a plain Objective-C forwarder that mirrors + `RNSentryInternal` selector for selector. Add a member there when a `.mm` + file needs a new one; don't include the bridge header from `.mm`. +- **No header maps.** The public headers are mirrored as forwarding headers in + `ios/include/RNSentry/`, which is the target's `publicHeadersPath`, so + `#import ` keeps working. Add a mirror for every new + public header (and to `s.public_header_files`); the podspec excludes the + directory so CocoaPods doesn't see two headers per name. +- **A target's whole directory is scanned for resources.** Every target points + at one source directory (`ios`, `ios/Swift`, `cpp`) rather than the package + root, which would pick up a local `node_modules` or Xcode build output and + fail to resolve. + +The autolinked target name must stay `RNSentry`, because it is also the prefix +consumers import headers under. It is pinned in both places React Native looks: +`spm.name` in `react-native.config.js` (0.87) and `swiftpmConfig.name` in +`package.json` (0.88+). + ## Internal API access (`SentrySDK.internal`) RNSentry consumes sentry-cocoa's hybrid-SDK surface (`SentrySDK.internal.*`) diff --git a/packages/core/ios/RNSentry.mm b/packages/core/ios/RNSentry.mm index a809117e1a..7ce98e1ccb 100644 --- a/packages/core/ios/RNSentry.mm +++ b/packages/core/ios/RNSentry.mm @@ -16,13 +16,9 @@ # define SENTRY_TARGET_PROFILING_SUPPORTED 0 #endif -#if __has_include() -# import -#else -# import "RNSentry-Swift.h" -#endif #import "RNSentryBreadcrumb.h" #import "RNSentryId.h" +#import "RNSentryInternalWrapper.h" #import #import #import @@ -41,7 +37,13 @@ - (instancetype)initWithDictionary:(NSDictionary *)dictionary; // Thanks to this guard, we won't import this header when we build for the old architecture. #ifdef RCT_NEW_ARCH_ENABLED -# import "RNSentrySpec.h" +// CocoaPods flattens the generated spec headers into the pod's header dir; +// Swift Package Manager serves them namespaced from the app's codegen package. +# if __has_include() +# import +# else +# import "RNSentrySpec.h" +# endif #endif #import "RNSentryDependencyContainer.h" @@ -407,8 +409,10 @@ - (void)handleShakeDetected RCT_EXPORT_METHOD( fetchNativeSdkInfo : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject) { - resolve( - @ { @"name" : RNSentryInternal.sdkName, @"version" : RNSentryInternal.sdkVersionString }); + resolve(@ { + @"name" : RNSentryInternalWrapper.sdkName, + @"version" : RNSentryInternalWrapper.sdkVersionString + }); } RCT_EXPORT_METHOD( @@ -460,7 +464,7 @@ - (void)handleShakeDetected contexts[@"release"] = releaseName; } // Merge extra context - NSDictionary *extraContext = [RNSentryInternal extraContext]; + NSDictionary *extraContext = [RNSentryInternalWrapper extraContext]; if (extraContext) { NSDictionary *extraDevice = extraContext[@"device"]; @@ -498,7 +502,8 @@ - (void)handleShakeDetected NSDictionary *user = [serializedScope valueForKey:@"user"]; if (user == nil) { - [serializedScope setValue:@ { @"id" : RNSentryInternal.installationID } forKey:@"user"]; + [serializedScope setValue:@ { @"id" : RNSentryInternalWrapper.installationID } + forKey:@"user"]; } if ([SentrySDKWrapper debug]) { @@ -511,7 +516,7 @@ - (void)handleShakeDetected } }]; - NSDictionary *extraContext = [RNSentryInternal extraContext]; + NSDictionary *extraContext = [RNSentryInternalWrapper extraContext]; NSMutableDictionary *> *contexts = [serializedScope[@"context"] mutableCopy]; @@ -548,7 +553,8 @@ - (void)handleShakeDetected fetchNativeAppStart : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject) { #if SENTRY_HAS_UIKIT - NSDictionary *measurements = [RNSentryInternal appStartMeasurementWithSpans]; + NSDictionary *measurements = + [RNSentryInternalWrapper appStartMeasurementWithSpans]; if (measurements == nil) { resolve(nil); return; @@ -574,7 +580,7 @@ - (void)handleShakeDetected { #if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST - if (RNSentryInternal.isFramesTrackingRunning) { + if (RNSentryInternalWrapper.isFramesTrackingRunning) { if (![SentryScreenFramesWrapper canTrackFrames]) { resolve(nil); return; @@ -626,20 +632,20 @@ - (void)handleShakeDetected { NSData *data = [[NSData alloc] initWithBase64EncodedString:rawBytes options:0]; - SentryEnvelope *envelope = [RNSentryInternal envelopeFromData:data]; + SentryEnvelope *envelope = [RNSentryInternalWrapper envelopeFromData:data]; if (envelope == nil) { reject(@"SentryReactNative", @"Failed to parse envelope from byte array.", nil); return; } #if DEBUG - [RNSentryInternal capture:envelope]; + [RNSentryInternalWrapper capture:envelope]; #else if ([[options objectForKey:@"hardCrashed"] boolValue]) { // Storing to disk happens asynchronously with captureEnvelope - [RNSentryInternal store:envelope]; + [RNSentryInternalWrapper store:envelope]; } else { - [RNSentryInternal capture:envelope]; + [RNSentryInternalWrapper capture:envelope]; } #endif resolve(@YES); @@ -649,7 +655,7 @@ - (void)handleShakeDetected captureScreenshot : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject) { #if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST - NSArray *rawScreenshots = [RNSentryInternal captureScreenshots]; + NSArray *rawScreenshots = [RNSentryInternalWrapper captureScreenshots]; NSMutableArray *screenshotsArray = [NSMutableArray arrayWithCapacity:[rawScreenshots count]]; int counter = 1; @@ -682,7 +688,7 @@ - (void)handleShakeDetected fetchViewHierarchy : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject) { #if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST - NSData *rawViewHierarchy = [RNSentryInternal captureViewHierarchy]; + NSData *rawViewHierarchy = [RNSentryInternalWrapper captureViewHierarchy]; if (rawViewHierarchy == nil) { // Propagate the capture failure to JS instead of a truthy `[]`, which // would be treated as a successful (empty) attachment. @@ -739,7 +745,7 @@ + (SentryUser *_Nullable)userFrom:(NSDictionary *)userKeys #if SENTRY_HAS_UIKIT NSString *_Nullable screen = [RNSentryBreadcrumb getCurrentScreenFrom:breadcrumb]; if (screen != nil) { - [RNSentryInternal setCurrentScreen:screen]; + [RNSentryInternalWrapper setCurrentScreen:screen]; } #endif // SENTRY_HAS_UIKIT } @@ -788,7 +794,7 @@ + (SentryUser *_Nullable)userFrom:(NSDictionary *)userKeys RCT_EXPORT_METHOD(addFeatureFlag : (NSString *)name value : (BOOL)value) { - [RNSentryInternal addFeatureFlag:name value:value]; + [RNSentryInternalWrapper addFeatureFlag:name value:value]; } RCT_EXPORT_METHOD(setAttribute : (NSString *)key value : (NSString *)value) @@ -845,7 +851,7 @@ + (BOOL)captureReplayWithReturnValue { #if SENTRY_TARGET_REPLAY_SUPPORTED @try { - return [RNSentryInternal captureReplay]; + return [RNSentryInternalWrapper captureReplay]; } @catch (NSException *exception) { NSLog(@"[RNSentry] Failed to call captureReplay: %@", exception); return NO; @@ -861,7 +867,7 @@ + (BOOL)captureReplayWithReturnValue #if SENTRY_TARGET_REPLAY_SUPPORTED BOOL captured = [RNSentry captureReplayWithReturnValue]; if (captured) { - resolve([RNSentryInternal replayId]); + resolve([RNSentryInternalWrapper replayId]); } else { resolve(nil); } @@ -875,7 +881,7 @@ + (BOOL)captureReplayWithReturnValue { #if SENTRY_TARGET_REPLAY_SUPPORTED @try { - [RNSentryInternal startReplay]; + [RNSentryInternalWrapper startReplay]; resolve(nil); } @catch (NSException *exception) { NSLog(@"[RNSentry] Failed to call startReplay: %@", exception); @@ -891,7 +897,7 @@ + (BOOL)captureReplayWithReturnValue { #if SENTRY_TARGET_REPLAY_SUPPORTED @try { - [RNSentryInternal startReplayBuffering]; + [RNSentryInternalWrapper startReplayBuffering]; resolve(nil); } @catch (NSException *exception) { NSLog(@"[RNSentry] Failed to call startReplayBuffering: %@", exception); @@ -907,7 +913,7 @@ + (BOOL)captureReplayWithReturnValue { #if SENTRY_TARGET_REPLAY_SUPPORTED @try { - [RNSentryInternal stopReplay]; + [RNSentryInternalWrapper stopReplay]; resolve(nil); } @catch (NSException *exception) { NSLog(@"[RNSentry] Failed to call stopReplay: %@", exception); @@ -923,7 +929,7 @@ + (BOOL)captureReplayWithReturnValue { #if SENTRY_TARGET_REPLAY_SUPPORTED @try { - [RNSentryInternal pauseReplay]; + [RNSentryInternalWrapper pauseReplay]; resolve(nil); } @catch (NSException *exception) { NSLog(@"[RNSentry] Failed to call pauseReplay: %@", exception); @@ -939,7 +945,7 @@ + (BOOL)captureReplayWithReturnValue { #if SENTRY_TARGET_REPLAY_SUPPORTED @try { - [RNSentryInternal resumeReplay]; + [RNSentryInternalWrapper resumeReplay]; resolve(nil); } @catch (NSException *exception) { NSLog(@"[RNSentry] Failed to call resumeReplay: %@", exception); @@ -955,7 +961,7 @@ + (BOOL)captureReplayWithReturnValue { #if SENTRY_TARGET_REPLAY_SUPPORTED @try { - [RNSentryInternal flushReplay]; + [RNSentryInternalWrapper flushReplay]; resolve(nil); } @catch (NSException *exception) { NSLog(@"[RNSentry] Failed to call flushReplay: %@", exception); @@ -1047,7 +1053,7 @@ + (BOOL)isPathUnderAllowedRootsForTesting:(NSString *)path RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSString *, getCurrentReplayId) { #if SENTRY_TARGET_REPLAY_SUPPORTED - return [RNSentryInternal replayId]; + return [RNSentryInternalWrapper replayId]; #else return nil; #endif @@ -1073,7 +1079,8 @@ + (BOOL)isPathUnderAllowedRootsForTesting:(NSString *)path if (nativeProfileTraceId == nil && nativeProfileStartTime == 0 && platformProfilers) { # if SENTRY_TARGET_PROFILING_SUPPORTED nativeProfileTraceId = [RNSentryId newId]; - nativeProfileStartTime = [RNSentryInternal startProfilerForTrace:nativeProfileTraceId]; + nativeProfileStartTime = + [RNSentryInternalWrapper startProfilerForTrace:nativeProfileTraceId]; # endif } else { if (!platformProfilers) { @@ -1087,7 +1094,7 @@ + (BOOL)isPathUnderAllowedRootsForTesting:(NSString *)path } catch (const std::exception &ex) { if (nativeProfileTraceId != nil) { # if SENTRY_TARGET_PROFILING_SUPPORTED - [RNSentryInternal discardProfilerForTrace:nativeProfileTraceId]; + [RNSentryInternalWrapper discardProfilerForTrace:nativeProfileTraceId]; # endif nativeProfileTraceId = nil; } @@ -1099,7 +1106,7 @@ + (BOOL)isPathUnderAllowedRootsForTesting:(NSString *)path } catch (...) { if (nativeProfileTraceId != nil) { # if SENTRY_TARGET_PROFILING_SUPPORTED - [RNSentryInternal discardProfilerForTrace:nativeProfileTraceId]; + [RNSentryInternalWrapper discardProfilerForTrace:nativeProfileTraceId]; # endif nativeProfileTraceId = nil; } @@ -1119,9 +1126,9 @@ + (BOOL)isPathUnderAllowedRootsForTesting:(NSString *)path if (nativeProfileTraceId != nil && nativeProfileStartTime != 0) { # if SENTRY_TARGET_PROFILING_SUPPORTED uint64_t nativeProfileStopTime = clock_gettime_nsec_np(CLOCK_UPTIME_RAW); - nativeProfile = [RNSentryInternal collectProfileBetween:nativeProfileStartTime - and:nativeProfileStopTime - forTrace:nativeProfileTraceId]; + nativeProfile = [RNSentryInternalWrapper collectProfileBetween:nativeProfileStartTime + and:nativeProfileStopTime + forTrace:nativeProfileTraceId]; # endif } // Cleanup native profiles @@ -1176,7 +1183,7 @@ + (BOOL)isPathUnderAllowedRootsForTesting:(NSString *)path } catch (const std::exception &ex) { if (nativeProfileTraceId != nil) { # if SENTRY_TARGET_PROFILING_SUPPORTED - [RNSentryInternal discardProfilerForTrace:nativeProfileTraceId]; + [RNSentryInternalWrapper discardProfilerForTrace:nativeProfileTraceId]; # endif nativeProfileTraceId = nil; } @@ -1188,7 +1195,7 @@ + (BOOL)isPathUnderAllowedRootsForTesting:(NSString *)path } catch (...) { if (nativeProfileTraceId != nil) { # if SENTRY_TARGET_PROFILING_SUPPORTED - [RNSentryInternal discardProfilerForTrace:nativeProfileTraceId]; + [RNSentryInternalWrapper discardProfilerForTrace:nativeProfileTraceId]; # endif nativeProfileTraceId = nil; } @@ -1239,7 +1246,8 @@ + (BOOL)isPathUnderAllowedRootsForTesting:(NSString *)path NSString *traceId = ctx[@"traceId"]; NSString *spanId = ctx[@"spanId"]; if (traceId && spanId) { - [RNSentryInternal setCurrentScopePropagationContextWithTraceId:traceId spanId:spanId]; + [RNSentryInternalWrapper setCurrentScopePropagationContextWithTraceId:traceId + spanId:spanId]; } return @YES; } diff --git a/packages/core/ios/RNSentryInternalWrapper.h b/packages/core/ios/RNSentryInternalWrapper.h new file mode 100644 index 0000000000..89e67027e4 --- /dev/null +++ b/packages/core/ios/RNSentryInternalWrapper.h @@ -0,0 +1,97 @@ +#import + +@class SentryEnvelope; +@class SentryId; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Objective-C++-safe mirror of `RNSentryInternal` (see `ios/Swift/`). + * + * `.mm` files cannot reach the Swift sources directly under Swift Package + * Manager: the generated Objective-C interface is only available as a Clang + * module, and `@import` is rejected in Objective-C++ unless C++ modules are + * enabled — which React Native's C++ headers do not survive. So the `.mm` + * callers go through this wrapper, whose implementation is plain Objective-C. + * + * Every member mirrors the `RNSentryInternal` selector of the same name, so the + * two stay diffable. `.m` callers keep using `RNSentryInternal` directly + * through `RNSentrySwiftBridge.h`. + */ +@interface RNSentryInternalWrapper : NSObject + +// MARK: - SDK metadata + +@property (class, readonly) NSString *sdkName; + +@property (class, readonly) NSString *sdkVersionString; + +@property (class, readonly) NSDictionary *extraContext; + +@property (class, readonly) NSString *installationID; + +// MARK: - Feature flags + ++ (void)addFeatureFlag:(NSString *)name value:(BOOL)value; + +// MARK: - App start and frames + +@property (class, readonly, nullable) NSDictionary *appStartMeasurementWithSpans; + +@property (class, readonly) BOOL isFramesTrackingRunning; + +// MARK: - Envelopes + ++ (nullable SentryEnvelope *)envelopeFromData:(nullable NSData *)data; + ++ (void)capture:(SentryEnvelope *)envelope; + ++ (void)store:(SentryEnvelope *)envelope; + +// MARK: - Screenshots and view hierarchy + ++ (void)setCurrentScreen:(nullable NSString *)screenName; + +@property (class, readonly, nullable) NSArray *captureScreenshots; + +@property (class, readonly, nullable) NSData *captureViewHierarchy; + +// MARK: - Session replay + ++ (BOOL)captureReplay; + ++ (void)startReplay; + ++ (void)startReplayBuffering; + ++ (void)stopReplay; + ++ (void)pauseReplay; + ++ (void)resumeReplay; + ++ (void)flushReplay; + +@property (class, readonly, nullable) NSString *replayId; + ++ (void)setReplayRedactContainerClass:(Class)containerClass; + ++ (void)setReplayIgnoreContainerClass:(Class)containerClass; + +// MARK: - Profiling + ++ (uint64_t)startProfilerForTrace:(SentryId *)traceId; + ++ (nullable NSDictionary *)collectProfileBetween:(uint64_t)startTime + and:(uint64_t)endTime + forTrace:(SentryId *)traceId; + ++ (void)discardProfilerForTrace:(SentryId *)traceId; + +// MARK: - Scope propagation context + ++ (void)setCurrentScopePropagationContextWithTraceId:(NSString *)traceId spanId:(NSString *)spanId; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/core/ios/RNSentryInternalWrapper.m b/packages/core/ios/RNSentryInternalWrapper.m new file mode 100644 index 0000000000..9e9f924439 --- /dev/null +++ b/packages/core/ios/RNSentryInternalWrapper.m @@ -0,0 +1,159 @@ +#import "RNSentryInternalWrapper.h" +#import "RNSentrySwiftBridge.h" + +@implementation RNSentryInternalWrapper + +// MARK: - SDK metadata + ++ (NSString *)sdkName +{ + return RNSentryInternal.sdkName; +} + ++ (NSString *)sdkVersionString +{ + return RNSentryInternal.sdkVersionString; +} + ++ (NSDictionary *)extraContext +{ + return [RNSentryInternal extraContext]; +} + ++ (NSString *)installationID +{ + return RNSentryInternal.installationID; +} + +// MARK: - Feature flags + ++ (void)addFeatureFlag:(NSString *)name value:(BOOL)value +{ + [RNSentryInternal addFeatureFlag:name value:value]; +} + +// MARK: - App start and frames + ++ (NSDictionary *)appStartMeasurementWithSpans +{ + return [RNSentryInternal appStartMeasurementWithSpans]; +} + ++ (BOOL)isFramesTrackingRunning +{ + return RNSentryInternal.isFramesTrackingRunning; +} + +// MARK: - Envelopes + ++ (SentryEnvelope *)envelopeFromData:(NSData *)data +{ + return [RNSentryInternal envelopeFromData:data]; +} + ++ (void)capture:(SentryEnvelope *)envelope +{ + [RNSentryInternal capture:envelope]; +} + ++ (void)store:(SentryEnvelope *)envelope +{ + [RNSentryInternal store:envelope]; +} + +// MARK: - Screenshots and view hierarchy + ++ (void)setCurrentScreen:(NSString *)screenName +{ + [RNSentryInternal setCurrentScreen:screenName]; +} + ++ (NSArray *)captureScreenshots +{ + return [RNSentryInternal captureScreenshots]; +} + ++ (NSData *)captureViewHierarchy +{ + return [RNSentryInternal captureViewHierarchy]; +} + +// MARK: - Session replay + ++ (BOOL)captureReplay +{ + return [RNSentryInternal captureReplay]; +} + ++ (void)startReplay +{ + [RNSentryInternal startReplay]; +} + ++ (void)startReplayBuffering +{ + [RNSentryInternal startReplayBuffering]; +} + ++ (void)stopReplay +{ + [RNSentryInternal stopReplay]; +} + ++ (void)pauseReplay +{ + [RNSentryInternal pauseReplay]; +} + ++ (void)resumeReplay +{ + [RNSentryInternal resumeReplay]; +} + ++ (void)flushReplay +{ + [RNSentryInternal flushReplay]; +} + ++ (NSString *)replayId +{ + return [RNSentryInternal replayId]; +} + ++ (void)setReplayRedactContainerClass:(Class)containerClass +{ + [RNSentryInternal setReplayRedactContainerClass:containerClass]; +} + ++ (void)setReplayIgnoreContainerClass:(Class)containerClass +{ + [RNSentryInternal setReplayIgnoreContainerClass:containerClass]; +} + +// MARK: - Profiling + ++ (uint64_t)startProfilerForTrace:(SentryId *)traceId +{ + return [RNSentryInternal startProfilerForTrace:traceId]; +} + ++ (NSDictionary *)collectProfileBetween:(uint64_t)startTime + and:(uint64_t)endTime + forTrace:(SentryId *)traceId +{ + return [RNSentryInternal collectProfileBetween:startTime and:endTime forTrace:traceId]; +} + ++ (void)discardProfilerForTrace:(SentryId *)traceId +{ + [RNSentryInternal discardProfilerForTrace:traceId]; +} + +// MARK: - Scope propagation context + ++ (void)setCurrentScopePropagationContextWithTraceId:(NSString *)traceId spanId:(NSString *)spanId +{ + [RNSentryInternal setCurrentScopePropagationContextWithTraceId:traceId spanId:spanId]; +} + +@end diff --git a/packages/core/ios/RNSentryRNSScreen.m b/packages/core/ios/RNSentryRNSScreen.m index 52c3658fba..5e043cf333 100644 --- a/packages/core/ios/RNSentryRNSScreen.m +++ b/packages/core/ios/RNSentryRNSScreen.m @@ -2,13 +2,9 @@ #if SENTRY_HAS_UIKIT -# if __has_include() -# import -# else -# import "RNSentry-Swift.h" -# endif # import "RNSentryDependencyContainer.h" # import "RNSentryFramesTrackerListener.h" +# import "RNSentrySwiftBridge.h" @implementation RNSentryRNSScreen diff --git a/packages/core/ios/RNSentryReplay.h b/packages/core/ios/RNSentryReplay.h index 49d61d4b1a..94bb14cace 100644 --- a/packages/core/ios/RNSentryReplay.h +++ b/packages/core/ios/RNSentryReplay.h @@ -1,3 +1,4 @@ +#import @interface RNSentryReplay : NSObject diff --git a/packages/core/ios/RNSentryReplay.mm b/packages/core/ios/RNSentryReplay.mm index 63e12f8687..d9f30fbd9f 100644 --- a/packages/core/ios/RNSentryReplay.mm +++ b/packages/core/ios/RNSentryReplay.mm @@ -1,9 +1,5 @@ #import "RNSentryReplay.h" -#if __has_include() -# import -#else -# import "RNSentry-Swift.h" -#endif +#import "RNSentryInternalWrapper.h" #import "RNSentryReplayBreadcrumbConverterHelper.h" #import "RNSentryReplayQuality.h" #import "RNSentryVersion.h" @@ -98,8 +94,8 @@ + (void)postInit { // We can't import RNSentryReplayMask.h here because it's Objective-C++ // To avoid typos, we test the class existence in the tests - [RNSentryInternal setReplayRedactContainerClass:[RNSentryReplay getMaskClass]]; - [RNSentryInternal setReplayIgnoreContainerClass:[RNSentryReplay getUnmaskClass]]; + [RNSentryInternalWrapper setReplayRedactContainerClass:[RNSentryReplay getMaskClass]]; + [RNSentryInternalWrapper setReplayIgnoreContainerClass:[RNSentryReplay getUnmaskClass]]; [RNSentryReplayBreadcrumbConverterHelper configureSessionReplayWithConverter]; } diff --git a/packages/core/ios/RNSentryReplayBreadcrumbConverterHelper.m b/packages/core/ios/RNSentryReplayBreadcrumbConverterHelper.m index 08ace7cc7a..cfa7d19e00 100644 --- a/packages/core/ios/RNSentryReplayBreadcrumbConverterHelper.m +++ b/packages/core/ios/RNSentryReplayBreadcrumbConverterHelper.m @@ -1,12 +1,8 @@ #import "RNSentryReplayBreadcrumbConverterHelper.h" #if SENTRY_TARGET_REPLAY_SUPPORTED -# if __has_include() -# import -# else -# import "RNSentry-Swift.h" -# endif # import "RNSentryReplayBreadcrumbConverter.h" +# import "RNSentrySwiftBridge.h" @implementation RNSentryReplayBreadcrumbConverterHelper diff --git a/packages/core/ios/RNSentrySDK.m b/packages/core/ios/RNSentrySDK.m index 1f9de9fe52..f475040577 100644 --- a/packages/core/ios/RNSentrySDK.m +++ b/packages/core/ios/RNSentrySDK.m @@ -1,10 +1,6 @@ #import "RNSentrySDK.h" -#if __has_include() -# import -#else -# import "RNSentry-Swift.h" -#endif #import "RNSentryStart.h" +#import "RNSentrySwiftBridge.h" #import static NSString *SENTRY_OPTIONS_RESOURCE_NAME = @"sentry.options"; diff --git a/packages/core/ios/RNSentryStart.m b/packages/core/ios/RNSentryStart.m index 13c1230cab..029a4558b4 100644 --- a/packages/core/ios/RNSentryStart.m +++ b/packages/core/ios/RNSentryStart.m @@ -13,11 +13,7 @@ # import #endif -#if __has_include() -# import -#else -# import "RNSentry-Swift.h" -#endif +#import "RNSentrySwiftBridge.h" #import @import Sentry; diff --git a/packages/core/ios/RNSentrySwiftBridge.h b/packages/core/ios/RNSentrySwiftBridge.h new file mode 100644 index 0000000000..6269ef969d --- /dev/null +++ b/packages/core/ios/RNSentrySwiftBridge.h @@ -0,0 +1,31 @@ +#ifndef RNSentrySwiftBridge_h +#define RNSentrySwiftBridge_h + +// Imports the Objective-C interface of our Swift sources (`ios/Swift/`), where +// `RNSentryInternal` bridges sentry-cocoa's Swift-only `SentrySDK.internal.*` +// surface to `.m`/`.mm` callers. +// +// The module the generated header belongs to depends on how the SDK is built: +// +// * CocoaPods compiles Swift and Objective-C into the single `RNSentry` pod +// module, so the header is ``. +// * Swift Package Manager cannot mix languages in one target, so the Swift +// sources build as a separate `RNSentrySwift` module. Its generated header +// is reachable as a Clang module rather than a file on the header search +// path, so it is imported by module name, selected by the +// `RN_SENTRY_SWIFTPM` define that `Package.swift` sets. +// +// The unprefixed fallback covers targets that compile the sources directly +// without a module (for example the Xcode project in `ios/`). +// +// Only `.m` files may include this header: `@import` is rejected in +// Objective-C++. `.mm` callers use `RNSentryInternalWrapper` instead. +#if RN_SENTRY_SWIFTPM +@import RNSentrySwift; +#elif __has_include() +# import +#else +# import "RNSentry-Swift.h" +#endif + +#endif /* RNSentrySwiftBridge_h */ diff --git a/packages/core/ios/SentrySDKWrapper.m b/packages/core/ios/SentrySDKWrapper.m index 881d3bc69b..c8452c6c05 100644 --- a/packages/core/ios/SentrySDKWrapper.m +++ b/packages/core/ios/SentrySDKWrapper.m @@ -1,9 +1,5 @@ #import "SentrySDKWrapper.h" -#if __has_include() -# import -#else -# import "RNSentry-Swift.h" -#endif +#import "RNSentrySwiftBridge.h" @import Sentry; @implementation SentrySDKWrapper diff --git a/packages/core/ios/SentryScreenFramesWrapper.m b/packages/core/ios/SentryScreenFramesWrapper.m index e7db337459..5ffa8e2da4 100644 --- a/packages/core/ios/SentryScreenFramesWrapper.m +++ b/packages/core/ios/SentryScreenFramesWrapper.m @@ -1,9 +1,5 @@ #import "SentryScreenFramesWrapper.h" -#if __has_include() -# import -#else -# import "RNSentry-Swift.h" -#endif +#import "RNSentrySwiftBridge.h" @import Sentry; #if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST diff --git a/packages/core/ios/RNSentryInternal.swift b/packages/core/ios/Swift/RNSentryInternal.swift similarity index 100% rename from packages/core/ios/RNSentryInternal.swift rename to packages/core/ios/Swift/RNSentryInternal.swift diff --git a/packages/core/ios/RNSentrySwiftLinkStub.swift b/packages/core/ios/Swift/RNSentrySwiftLinkStub.swift similarity index 100% rename from packages/core/ios/RNSentrySwiftLinkStub.swift rename to packages/core/ios/Swift/RNSentrySwiftLinkStub.swift diff --git a/packages/core/ios/include/RNSentry/RNSentry.h b/packages/core/ios/include/RNSentry/RNSentry.h new file mode 100644 index 0000000000..e77d51ab10 --- /dev/null +++ b/packages/core/ios/include/RNSentry/RNSentry.h @@ -0,0 +1,5 @@ +// The namespaced spelling () of `ios/RNSentry.h`, matching how +// CocoaPods header maps expose it. Swift Package Manager has no header maps, so +// public headers are mirrored here — the directory is the target's +// `publicHeadersPath`. See `Package.swift`. +#import "../../RNSentry.h" diff --git a/packages/core/ios/include/RNSentry/RNSentryBreadcrumb.h b/packages/core/ios/include/RNSentry/RNSentryBreadcrumb.h new file mode 100644 index 0000000000..a724b6c757 --- /dev/null +++ b/packages/core/ios/include/RNSentry/RNSentryBreadcrumb.h @@ -0,0 +1,4 @@ +// The namespaced spelling () of `ios/RNSentryBreadcrumb.h`, matching +// how CocoaPods header maps expose it. Swift Package Manager has no header maps, so public headers +// are mirrored here — the directory is the target's `publicHeadersPath`. See `Package.swift`. +#import "../../RNSentryBreadcrumb.h" diff --git a/packages/core/ios/include/RNSentry/RNSentryReplay.h b/packages/core/ios/include/RNSentry/RNSentryReplay.h new file mode 100644 index 0000000000..52d0e10c3e --- /dev/null +++ b/packages/core/ios/include/RNSentry/RNSentryReplay.h @@ -0,0 +1,5 @@ +// The namespaced spelling () of `ios/RNSentryReplay.h`, matching how +// CocoaPods header maps expose it. Swift Package Manager has no header maps, so +// public headers are mirrored here — the directory is the target's +// `publicHeadersPath`. See `Package.swift`. +#import "../../RNSentryReplay.h" diff --git a/packages/core/ios/include/RNSentry/RNSentryReplayBreadcrumbConverter.h b/packages/core/ios/include/RNSentry/RNSentryReplayBreadcrumbConverter.h new file mode 100644 index 0000000000..4d39aed4e1 --- /dev/null +++ b/packages/core/ios/include/RNSentry/RNSentryReplayBreadcrumbConverter.h @@ -0,0 +1,5 @@ +// The namespaced spelling () of +// `ios/RNSentryReplayBreadcrumbConverter.h`, matching how CocoaPods header maps expose it. Swift +// Package Manager has no header maps, so public headers are mirrored here — the directory is the +// target's `publicHeadersPath`. See `Package.swift`. +#import "../../RNSentryReplayBreadcrumbConverter.h" diff --git a/packages/core/ios/include/RNSentry/RNSentryReplayMask.h b/packages/core/ios/include/RNSentry/RNSentryReplayMask.h new file mode 100644 index 0000000000..b2a308e1f7 --- /dev/null +++ b/packages/core/ios/include/RNSentry/RNSentryReplayMask.h @@ -0,0 +1,5 @@ +// The namespaced spelling () of `ios/Replay/RNSentryReplayMask.h`, +// matching how CocoaPods header maps expose it. Swift Package Manager has no +// header maps, so public headers are mirrored here — the directory is the +// target's `publicHeadersPath`. See `Package.swift`. +#import "../../Replay/RNSentryReplayMask.h" diff --git a/packages/core/ios/include/RNSentry/RNSentryReplayUnmask.h b/packages/core/ios/include/RNSentry/RNSentryReplayUnmask.h new file mode 100644 index 0000000000..625d557b70 --- /dev/null +++ b/packages/core/ios/include/RNSentry/RNSentryReplayUnmask.h @@ -0,0 +1,5 @@ +// The namespaced spelling () of +// `ios/Replay/RNSentryReplayUnmask.h`, matching how CocoaPods header maps expose it. Swift Package +// Manager has no header maps, so public headers are mirrored here — the directory is the target's +// `publicHeadersPath`. See `Package.swift`. +#import "../../Replay/RNSentryReplayUnmask.h" diff --git a/packages/core/ios/include/RNSentry/RNSentrySDK.h b/packages/core/ios/include/RNSentry/RNSentrySDK.h new file mode 100644 index 0000000000..20ddbaa16c --- /dev/null +++ b/packages/core/ios/include/RNSentry/RNSentrySDK.h @@ -0,0 +1,5 @@ +// The namespaced spelling () of `ios/RNSentrySDK.h`, matching how +// CocoaPods header maps expose it. Swift Package Manager has no header maps, so +// public headers are mirrored here — the directory is the target's +// `publicHeadersPath`. See `Package.swift`. +#import "../../RNSentrySDK.h" diff --git a/packages/core/ios/include/RNSentry/RNSentryStart.h b/packages/core/ios/include/RNSentry/RNSentryStart.h new file mode 100644 index 0000000000..8327c0f287 --- /dev/null +++ b/packages/core/ios/include/RNSentry/RNSentryStart.h @@ -0,0 +1,5 @@ +// The namespaced spelling () of `ios/RNSentryStart.h`, matching how +// CocoaPods header maps expose it. Swift Package Manager has no header maps, so +// public headers are mirrored here — the directory is the target's +// `publicHeadersPath`. See `Package.swift`. +#import "../../RNSentryStart.h" diff --git a/packages/core/ios/include/RNSentry/RNSentryTimeToDisplay.h b/packages/core/ios/include/RNSentry/RNSentryTimeToDisplay.h new file mode 100644 index 0000000000..6ed4aeb1ce --- /dev/null +++ b/packages/core/ios/include/RNSentry/RNSentryTimeToDisplay.h @@ -0,0 +1,5 @@ +// The namespaced spelling () of `ios/RNSentryTimeToDisplay.h`, +// matching how CocoaPods header maps expose it. Swift Package Manager has no header maps, so public +// headers are mirrored here — the directory is the target's `publicHeadersPath`. See +// `Package.swift`. +#import "../../RNSentryTimeToDisplay.h" diff --git a/packages/core/ios/include/RNSentry/RNSentryVersion.h b/packages/core/ios/include/RNSentry/RNSentryVersion.h new file mode 100644 index 0000000000..b493869ebb --- /dev/null +++ b/packages/core/ios/include/RNSentry/RNSentryVersion.h @@ -0,0 +1,5 @@ +// The namespaced spelling () of `ios/RNSentryVersion.h`, matching how +// CocoaPods header maps expose it. Swift Package Manager has no header maps, so +// public headers are mirrored here — the directory is the target's +// `publicHeadersPath`. See `Package.swift`. +#import "../../RNSentryVersion.h" diff --git a/packages/core/package.json b/packages/core/package.json index 30f7ba160f..887e381708 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -146,6 +146,9 @@ "javaPackageName": "io.sentry.react" } }, + "swiftpmConfig": { + "name": "RNSentry" + }, "peerDependenciesMeta": { "@babel/core": { "optional": true diff --git a/packages/core/react-native.config.js b/packages/core/react-native.config.js index 5281f24094..050eb257b2 100644 --- a/packages/core/react-native.config.js +++ b/packages/core/react-native.config.js @@ -1,4 +1,12 @@ module.exports = { + // React Native's SwiftPM autolinking names the package's target after the npm + // package, which for `@sentry/react-native` collides with React Native's own + // reserved `ReactNative`. The name is also the prefix our headers are imported + // under (``), so pin it to the pod name. React Native + // 0.88+ reads `swiftpmConfig.name` from package.json instead; both are set. + spm: { + name: 'RNSentry', + }, dependency: { platforms: { ios: {}, diff --git a/scripts/update-cocoa.sh b/scripts/update-cocoa.sh index 352f28c658..7cd2dde13f 100755 --- a/scripts/update-cocoa.sh +++ b/scripts/update-cocoa.sh @@ -4,6 +4,7 @@ set -euo pipefail script_dir="$(cd "$(dirname "$0")" && pwd)" podspec="$script_dir/../packages/core/RNSentry.podspec" utils="$script_dir/../packages/core/scripts/sentry_utils.rb" +package_swift="$script_dir/../packages/core/Package.swift" content=$(cat "$podspec") regex="(sentry_cocoa_version *= *)'([0-9\.]+)'" @@ -46,7 +47,13 @@ set-version) # Update the `Sentry` checksum (a 64-char lowercase hex string). perl -i -pe "s|'Sentry' => '[a-f0-9]{64}'|'Sentry' => '${new_sha}'|" "$utils" - # Sanity-check: both lines got rewritten with the new values. + # 3. Pin the same version in the Swift Package Manager manifest, which + # resolves sentry-cocoa from git instead of the xcframework archive. + # Leaving it behind would build the SwiftPM and CocoaPods paths against + # different sentry-cocoa versions. + perl -i -pe "s|(sentry-cocoa\\.git\", exact: )\"\\d+\\.\\d+\\.\\d+\"|\${1}\"${new_version}\"|" "$package_swift" + + # Sanity-check: every line got rewritten with the new values. if ! grep -q "'${new_version}' =>" "$utils"; then echo "Failed to rewrite the checksum version key in $utils" exit 1 @@ -55,6 +62,10 @@ set-version) echo "Failed to rewrite the Sentry checksum in $utils" exit 1 fi + if ! grep -q "exact: \"${new_version}\"" "$package_swift"; then + echo "Failed to rewrite the sentry-cocoa version in $package_swift" + exit 1 + fi ;; *) echo "Unknown argument $1" From 10b08a8f88713cd8c7d9841c2d575f620f5cdd62 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Thu, 24 Sep 2026 16:10:07 +0200 Subject: [PATCH 02/16] fix(ios): Register replay masks via codegen provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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. --- CHANGELOG.md | 5 +++++ packages/core/package.json | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06e6a0206b..8ab1833738 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,11 @@ - The SDK now ships a `Package.swift`, so `npx react-native spm` picks it up through SwiftPM autolinking instead of failing with `Package.swift is missing for library "@sentry/react-native"`. - Requires React Native 0.87 or newer, where SwiftPM support is an opt-in preview. CocoaPods remains the default and is unaffected. +### Fixes + +- Register the session replay mask components through the iOS codegen component provider ([#6784](https://github.com/getsentry/sentry-react-native/pull/6784)) + - `codegenConfig.ios.componentProvider` now maps `RNSentryReplayMask` and `RNSentryReplayUnmask` to their view classes, so React Native resolves them directly instead of falling back to the legacy view manager interop layer, which apps can turn off with `RCT_REMOVE_LEGACY_COMPONENT_INTEROP`. + ### Dependencies - Bump Android SDK from v8.57.0 to v8.58.0 ([#6778](https://github.com/getsentry/sentry-react-native/pull/6778)) diff --git a/packages/core/package.json b/packages/core/package.json index 887e381708..6788febd87 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -144,6 +144,12 @@ "jsSrcsDir": "src", "android": { "javaPackageName": "io.sentry.react" + }, + "ios": { + "componentProvider": { + "RNSentryReplayMask": "RNSentryReplayMask", + "RNSentryReplayUnmask": "RNSentryReplayUnmask" + } } }, "swiftpmConfig": { From bc62b270809eb4b219cda123e98c8bc8f6f2ae6c Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Thu, 24 Sep 2026 16:10:19 +0200 Subject: [PATCH 03/16] ci: Build a SwiftPM app on iOS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .github/file-filters.yml | 2 + .github/workflows/spm-application.yml | 142 ++++++++++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 .github/workflows/spm-application.yml diff --git a/.github/file-filters.yml b/.github/file-filters.yml index ab3cf66291..956069ea25 100644 --- a/.github/file-filters.yml +++ b/.github/file-filters.yml @@ -26,6 +26,7 @@ ios_native: - 'packages/core/ios/**' - 'packages/core/RNSentryCocoaTester/**' - 'packages/core/RNSentry.podspec' + - 'packages/core/Package.swift' # Changes to the SDK's Android native code only (packages/core). # Does NOT include sample apps — sample changes have their own filters below. @@ -40,6 +41,7 @@ js_source: - 'packages/core/plugin/**' - 'packages/core/scripts/**' - 'packages/core/package.json' + - 'packages/core/react-native.config.js' - 'packages/core/tsconfig.json' - 'yarn.lock' - 'package.json' diff --git a/.github/workflows/spm-application.yml b/.github/workflows/spm-application.yml new file mode 100644 index 0000000000..4497baf0d6 --- /dev/null +++ b/.github/workflows/spm-application.yml @@ -0,0 +1,142 @@ +name: SwiftPM Application + +# Builds a fresh React Native app that consumes the SDK through React Native's +# Swift Package Manager autolinking instead of CocoaPods. The app is generated +# per run rather than committed: the SwiftPM path only works on a plain +# template, because `react-native spm` stops on any autolinked dependency that +# ships no `Package.swift`, and most community libraries still don't. + +on: + push: + branches: + - main + - release/** + pull_request: + types: [opened, synchronize, reopened, labeled] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +env: + # The first React Native version with the SwiftPM integration. Bump it + # together with the `Package.swift` platform requirement. + RN_VERSION: "0.87.1" + APP_NAME: SpmTestApp + APP_PATH: spm-app + +jobs: + ready-to-merge-gate: + name: Ready-to-merge gate + uses: ./.github/workflows/ready-to-merge-workflow.yml + with: + is-pr: ${{ github.event_name == 'pull_request' }} + labels: ${{ toJson(github.event.pull_request.labels) }} + + diff_check: + needs: [ready-to-merge-gate] + uses: ./.github/workflows/skip-ci.yml + + detect-changes: + needs: [ready-to-merge-gate] + uses: ./.github/workflows/detect-changes.yml + with: + caller_event_name: ${{ github.event_name }} + caller_ref: ${{ github.ref }} + + build-ios: + name: swiftpm ios ${{ matrix.build-type }} + runs-on: macos-26-xlarge + needs: [diff_check, detect-changes] + if: ${{ needs.diff_check.outputs.skip_ci != 'true' && needs.detect-changes.outputs.needs_ios == 'true' }} + strategy: + fail-fast: false + matrix: + build-type: ["dev", "production"] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Enable Corepack + run: corepack enable + + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v6 + with: + package-manager-cache: false + node-version: 22 + cache: "yarn" + cache-dependency-path: yarn.lock + + - name: Install SDK Dependencies + run: yarn install + + - name: Build SDK + run: yarn build + + - name: Pack SDK + # `yarn pack` replaces the `workspace:` ranges with the versions the + # registry serves, so the tarball installs like the published package. + # `npm pack` leaves them in place and the install then fails. + run: yarn workspace @sentry/react-native pack --out ${{ runner.temp }}/sentry-react-native.tgz + + - name: Create React Native App + working-directory: ${{ runner.temp }} + run: | + npx --yes @react-native-community/cli@latest init "$APP_NAME" \ + --directory "$APP_PATH" \ + --version "$RN_VERSION" \ + --skip-git-init \ + --install-pods false + + - name: Install SDK in App + working-directory: ${{ runner.temp }}/${{ env.APP_PATH }} + run: npm install ${{ runner.temp }}/sentry-react-native.tgz + + - name: Set Up SwiftPM + working-directory: ${{ runner.temp }}/${{ env.APP_PATH }}/ios + # `--deintegrate` removes the template's CocoaPods integration first. + # A missing `Package.swift` in any autolinked dependency exits 2 here, + # which is the regression this job is meant to catch. + run: npx react-native spm add --deintegrate + + - name: Build iOS App + working-directory: ${{ runner.temp }}/${{ env.APP_PATH }}/ios + env: + # Fix for Xcode 26+ with MetalToolchain: use default Xcode toolchain to avoid + # missing Swift compatibility libraries (swiftCompatibility56, etc.) + # See: https://github.com/actions/runner-images/issues/13135 + TOOLCHAINS: com.apple.dt.toolchain.XcodeDefault + run: | + [[ "${{ matrix.build-type }}" == "production" ]] && export CONFIG='Release' || export CONFIG='Debug' + + xcodebuild \ + -project "$APP_NAME.xcodeproj" \ + -scheme "$APP_NAME" \ + -configuration "$CONFIG" \ + -sdk iphonesimulator \ + -destination 'generic/platform=iOS Simulator' \ + -derivedDataPath build \ + CODE_SIGNING_ALLOWED=NO \ + build + + - name: Verify SDK Is Linked + working-directory: ${{ runner.temp }}/${{ env.APP_PATH }}/ios + # A silent autolinking regression still produces a green build, so + # assert that the SDK's classes and sentry-cocoa both reached the app. + run: | + [[ "${{ matrix.build-type }}" == "production" ]] && export CONFIG='Release' || export CONFIG='Debug' + app="build/Build/Products/$CONFIG-iphonesimulator/$APP_NAME.app" + + # Debug builds put the code in a separate dylib, Release builds do not. + binary="$app/$APP_NAME" + [[ -f "$app/$APP_NAME.debug.dylib" ]] && binary="$app/$APP_NAME.debug.dylib" + echo "Inspecting $binary" + + if ! strings -a "$binary" | grep -q 'RNSentryReplayMask'; then + echo "RNSentry is missing from the app binary." + exit 1 + fi + if ! strings -a "$binary" | grep -q 'sentry-cocoa'; then + echo "sentry-cocoa is missing from the app binary." + exit 1 + fi + echo "The app binary contains both RNSentry and sentry-cocoa." From 0f378d2c89f3645617645e3e352df747e7e0416a Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Thu, 24 Sep 2026 16:25:34 +0200 Subject: [PATCH 04/16] ref(ios): Consume sentry-cocoa as a binary target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- packages/core/Package.swift | 30 ++++++++++++++++++++++++++---- scripts/update-cocoa.sh | 17 +++++++++++------ 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/packages/core/Package.swift b/packages/core/Package.swift index fabcc34065..94d204cbd2 100644 --- a/packages/core/Package.swift +++ b/packages/core/Package.swift @@ -67,7 +67,20 @@ let reactNativeHeaderProducts: [Target.Dependency] = [ .product(name: "ReactAppHeaders", package: "React-GeneratedCode") ] -let sentryCocoa: Target.Dependency = .product(name: "Sentry", package: "sentry-cocoa") +// sentry-cocoa is consumed as the single prebuilt `Sentry.xcframework` rather +// than as a package dependency. Its manifest declares a binary target per +// distribution variant (dynamic, ARM64e, without UIKit, ...) and SwiftPM +// downloads every artifact of a resolved package, not only the ones a product +// needs — 2.9 GB for the one 339 MB variant used here, and seven chances for a +// failed download to break the build. +// +// `scripts/update-cocoa.sh` keeps the version and the checksum in step with +// `sentry_cocoa_version` in RNSentry.podspec. The checksum is the SHA256 of the +// archive, which is what both `pod install` and SwiftPM verify. +let sentryCocoaVersion = "9.29.0" +let sentryCocoaChecksum = "63fe5a7258097fded9ef485bbb1d8e80e1e91d419ee6d8a6ad405454b5b50fef" + +let sentryCocoa: Target.Dependency = "Sentry" let rnSentryDependencies: [Target.Dependency] = ["RNSentrySwift", "RNSentryCpp", sentryCocoa] + reactNativeHeaderProducts @@ -80,10 +93,15 @@ let package = Package( ], dependencies: [ .package(name: "ReactNative", path: "../../../../xcframeworks"), - .package(name: "React-GeneratedCode", path: "../../../ios"), - .package(url: "https://github.com/getsentry/sentry-cocoa.git", exact: "9.29.0") + .package(name: "React-GeneratedCode", path: "../../../ios") ], targets: [ + .binaryTarget( + name: "Sentry", + url: "https://github.com/getsentry/sentry-cocoa/releases/download/" + + "\(sentryCocoaVersion)/Sentry.xcframework.zip", + checksum: sentryCocoaChecksum + ), // SwiftPM cannot mix Swift with Objective-C(++) in one target, so the // Swift bridge over sentry-cocoa's `SentrySDK.internal.*` compiles on // its own. `.m`/`.mm` callers reach it through @@ -124,7 +142,11 @@ let package = Package( linkerSettings: [ .linkedFramework("Foundation"), .linkedFramework("UIKit"), - .linkedFramework("QuartzCore") + .linkedFramework("QuartzCore"), + // The `Sentry` product in sentry-cocoa's own manifest pairs the + // binary target with an empty target that carries this setting; + // the static library needs the C++ runtime. + .linkedLibrary("c++") ] ) ], diff --git a/scripts/update-cocoa.sh b/scripts/update-cocoa.sh index 7cd2dde13f..eec2a5c238 100755 --- a/scripts/update-cocoa.sh +++ b/scripts/update-cocoa.sh @@ -47,11 +47,12 @@ set-version) # Update the `Sentry` checksum (a 64-char lowercase hex string). perl -i -pe "s|'Sentry' => '[a-f0-9]{64}'|'Sentry' => '${new_sha}'|" "$utils" - # 3. Pin the same version in the Swift Package Manager manifest, which - # resolves sentry-cocoa from git instead of the xcframework archive. - # Leaving it behind would build the SwiftPM and CocoaPods paths against - # different sentry-cocoa versions. - perl -i -pe "s|(sentry-cocoa\\.git\", exact: )\"\\d+\\.\\d+\\.\\d+\"|\${1}\"${new_version}\"|" "$package_swift" + # 3. Pin the same version and checksum in the Swift Package Manager + # manifest, which downloads the same archive as a binary target. + # Leaving them behind would build the SwiftPM and CocoaPods paths + # against different sentry-cocoa versions. + perl -i -pe "s|(let sentryCocoaVersion = )\"\\d+\\.\\d+\\.\\d+\"|\${1}\"${new_version}\"|" "$package_swift" + perl -i -pe "s|(let sentryCocoaChecksum = )\"[a-f0-9]{64}\"|\${1}\"${new_sha}\"|" "$package_swift" # Sanity-check: every line got rewritten with the new values. if ! grep -q "'${new_version}' =>" "$utils"; then @@ -62,10 +63,14 @@ set-version) echo "Failed to rewrite the Sentry checksum in $utils" exit 1 fi - if ! grep -q "exact: \"${new_version}\"" "$package_swift"; then + if ! grep -q "let sentryCocoaVersion = \"${new_version}\"" "$package_swift"; then echo "Failed to rewrite the sentry-cocoa version in $package_swift" exit 1 fi + if ! grep -q "let sentryCocoaChecksum = \"${new_sha}\"" "$package_swift"; then + echo "Failed to rewrite the sentry-cocoa checksum in $package_swift" + exit 1 + fi ;; *) echo "Unknown argument $1" From a9b9480a2c94b1b51e75f0b914b808c23227440c Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Thu, 24 Sep 2026 16:51:54 +0200 Subject: [PATCH 05/16] ci: Install both tarballs in the SwiftPM job MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `@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. --- .github/workflows/spm-application.yml | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/.github/workflows/spm-application.yml b/.github/workflows/spm-application.yml index 4497baf0d6..63682f69b5 100644 --- a/.github/workflows/spm-application.yml +++ b/.github/workflows/spm-application.yml @@ -73,10 +73,10 @@ jobs: run: yarn build - name: Pack SDK - # `yarn pack` replaces the `workspace:` ranges with the versions the - # registry serves, so the tarball installs like the published package. - # `npm pack` leaves them in place and the install then fails. - run: yarn workspace @sentry/react-native pack --out ${{ runner.temp }}/sentry-react-native.tgz + # Packs both workspaces with `yarn pack`, which replaces the + # `workspace:` ranges with concrete versions. `npm pack` leaves them in + # place and the install then fails. + run: yarn build:tarball - name: Create React Native App working-directory: ${{ runner.temp }} @@ -89,7 +89,17 @@ jobs: - name: Install SDK in App working-directory: ${{ runner.temp }}/${{ env.APP_PATH }} - run: npm install ${{ runner.temp }}/sentry-react-native.tgz + # Install both tarballs together so the `@sentry/expo-upload-sourcemaps` + # cross-dependency resolves locally. On a release branch the bumped + # version is not on the registry yet, and a fetch would fail with + # ETARGET — the same reason `buildandtest.yml` installs both. + run: | + set -e + CORE_TARBALL=$(find "$GITHUB_WORKSPACE/packages/core" -name 'sentry-react-native-*.tgz' -print -quit) + EXPO_TARBALL=$(find "$GITHUB_WORKSPACE/packages/expo-upload-sourcemaps" -name 'sentry-expo-upload-sourcemaps-*.tgz' -print -quit) + [ -n "$CORE_TARBALL" ] || { echo "::error::core tarball not found"; exit 1; } + [ -n "$EXPO_TARBALL" ] || { echo "::error::expo-upload-sourcemaps tarball not found"; exit 1; } + npm install "$EXPO_TARBALL" "$CORE_TARBALL" - name: Set Up SwiftPM working-directory: ${{ runner.temp }}/${{ env.APP_PATH }}/ios From 3a7cd49c2f462c22b40c3f51784dfb2bd7a60145 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Fri, 25 Sep 2026 10:40:57 +0200 Subject: [PATCH 06/16] fix(ios): Build RNSentryInternalWrapper on every platform MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- packages/core/ios/RNSentryInternalWrapper.h | 7 +++++++ packages/core/ios/RNSentryInternalWrapper.m | 4 ++++ packages/core/ios/Swift/RNSentryInternal.swift | 3 ++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/core/ios/RNSentryInternalWrapper.h b/packages/core/ios/RNSentryInternalWrapper.h index 89e67027e4..364b9ceff4 100644 --- a/packages/core/ios/RNSentryInternalWrapper.h +++ b/packages/core/ios/RNSentryInternalWrapper.h @@ -50,12 +50,19 @@ NS_ASSUME_NONNULL_BEGIN // MARK: - Screenshots and view hierarchy +// `RNSentryInternal` declares these three for iOS, tvOS and visionOS only, so +// the mirror follows. The guard matches the one the call sites in +// `RNSentry.mm` already use. +#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST + + (void)setCurrentScreen:(nullable NSString *)screenName; @property (class, readonly, nullable) NSArray *captureScreenshots; @property (class, readonly, nullable) NSData *captureViewHierarchy; +#endif + // MARK: - Session replay + (BOOL)captureReplay; diff --git a/packages/core/ios/RNSentryInternalWrapper.m b/packages/core/ios/RNSentryInternalWrapper.m index 9e9f924439..22ebf3e929 100644 --- a/packages/core/ios/RNSentryInternalWrapper.m +++ b/packages/core/ios/RNSentryInternalWrapper.m @@ -63,6 +63,8 @@ + (void)store:(SentryEnvelope *)envelope // MARK: - Screenshots and view hierarchy +#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST + + (void)setCurrentScreen:(NSString *)screenName { [RNSentryInternal setCurrentScreen:screenName]; @@ -78,6 +80,8 @@ + (NSData *)captureViewHierarchy return [RNSentryInternal captureViewHierarchy]; } +#endif + // MARK: - Session replay + (BOOL)captureReplay diff --git a/packages/core/ios/Swift/RNSentryInternal.swift b/packages/core/ios/Swift/RNSentryInternal.swift index 607c085bf9..152bb87f5a 100644 --- a/packages/core/ios/Swift/RNSentryInternal.swift +++ b/packages/core/ios/Swift/RNSentryInternal.swift @@ -269,7 +269,8 @@ import Foundation } #else @_spi(Private) @objc public static func startProfiler(forTrace traceId: SentryId) -> UInt64 { 0 } - @_spi(Private) @objc public static func collectProfile( + @_spi(Private) @objc(collectProfileBetween:and:forTrace:) + public static func collectProfile( between startTime: UInt64, and endTime: UInt64, forTrace traceId: SentryId From 99fa2cae5ee6e4cfbd8c32691947cb32e81d9d02 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Fri, 25 Sep 2026 11:18:33 +0200 Subject: [PATCH 07/16] fix(ios): Pin sentry-cocoa 9.29.1 in Package.swift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- packages/core/Package.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/Package.swift b/packages/core/Package.swift index 94d204cbd2..124f6112cc 100644 --- a/packages/core/Package.swift +++ b/packages/core/Package.swift @@ -77,8 +77,8 @@ let reactNativeHeaderProducts: [Target.Dependency] = [ // `scripts/update-cocoa.sh` keeps the version and the checksum in step with // `sentry_cocoa_version` in RNSentry.podspec. The checksum is the SHA256 of the // archive, which is what both `pod install` and SwiftPM verify. -let sentryCocoaVersion = "9.29.0" -let sentryCocoaChecksum = "63fe5a7258097fded9ef485bbb1d8e80e1e91d419ee6d8a6ad405454b5b50fef" +let sentryCocoaVersion = "9.29.1" +let sentryCocoaChecksum = "0e6ac8c8e0661e54ed9d30a84a45a730bf08e9499800afc63cb8be2a61e7adcf" let sentryCocoa: Target.Dependency = "Sentry" From 4542352e394351337c2f0910dcee51b1ac19fd78 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Fri, 25 Sep 2026 11:20:32 +0200 Subject: [PATCH 08/16] 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. --- .github/workflows/spm-application.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/spm-application.yml b/.github/workflows/spm-application.yml index 63682f69b5..234c6d5384 100644 --- a/.github/workflows/spm-application.yml +++ b/.github/workflows/spm-application.yml @@ -131,7 +131,10 @@ jobs: - name: Verify SDK Is Linked working-directory: ${{ runner.temp }}/${{ env.APP_PATH }}/ios # A silent autolinking regression still produces a green build, so - # assert that the SDK's classes and sentry-cocoa both reached the app. + # assert that a class from each layer reached the app. Both markers are + # Objective-C class names, which the linker keeps in the binary for + # every class it links — unlike, say, the sentry-cocoa package name, + # which appears only incidentally. run: | [[ "${{ matrix.build-type }}" == "production" ]] && export CONFIG='Release' || export CONFIG='Debug' app="build/Build/Products/$CONFIG-iphonesimulator/$APP_NAME.app" @@ -145,7 +148,7 @@ jobs: echo "RNSentry is missing from the app binary." exit 1 fi - if ! strings -a "$binary" | grep -q 'sentry-cocoa'; then + if ! strings -a "$binary" | grep -q 'SentryOptions'; then echo "sentry-cocoa is missing from the app binary." exit 1 fi From b720c97ee06bd1f55e396406ddc287e5722ccec5 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Mon, 28 Sep 2026 11:01:29 +0200 Subject: [PATCH 09/16] 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) --- packages/core/Package.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/Package.swift b/packages/core/Package.swift index 124f6112cc..6e364ea953 100644 --- a/packages/core/Package.swift +++ b/packages/core/Package.swift @@ -77,8 +77,8 @@ let reactNativeHeaderProducts: [Target.Dependency] = [ // `scripts/update-cocoa.sh` keeps the version and the checksum in step with // `sentry_cocoa_version` in RNSentry.podspec. The checksum is the SHA256 of the // archive, which is what both `pod install` and SwiftPM verify. -let sentryCocoaVersion = "9.29.1" -let sentryCocoaChecksum = "0e6ac8c8e0661e54ed9d30a84a45a730bf08e9499800afc63cb8be2a61e7adcf" +let sentryCocoaVersion = "9.29.2" +let sentryCocoaChecksum = "8d54d420474afdf6bee4ca060b8edbbd8948798d635ce7a9b477d884094b16b0" let sentryCocoa: Target.Dependency = "Sentry" From 9f0d075f45cbf91754d5015588d73c729d863284 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Mon, 28 Sep 2026 11:35:30 +0200 Subject: [PATCH 10/16] 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) --- packages/core/ios/RNSentry.mm | 2 +- packages/core/ios/RNSentryInternalWrapper.h | 2 ++ packages/core/ios/RNSentryInternalWrapper.m | 5 +++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/core/ios/RNSentry.mm b/packages/core/ios/RNSentry.mm index e89292895f..19daad723f 100644 --- a/packages/core/ios/RNSentry.mm +++ b/packages/core/ios/RNSentry.mm @@ -976,7 +976,7 @@ + (BOOL)captureReplayWithReturnValue { #if SENTRY_TARGET_REPLAY_SUPPORTED @try { - [RNSentryInternal registerReplayTraceId:traceId]; + [RNSentryInternalWrapper registerReplayTraceId:traceId]; } @catch (NSException *exception) { NSLog(@"[RNSentry] Failed to call registerReplayTraceId: %@", exception); } diff --git a/packages/core/ios/RNSentryInternalWrapper.h b/packages/core/ios/RNSentryInternalWrapper.h index 364b9ceff4..a05121c3d8 100644 --- a/packages/core/ios/RNSentryInternalWrapper.h +++ b/packages/core/ios/RNSentryInternalWrapper.h @@ -79,6 +79,8 @@ NS_ASSUME_NONNULL_BEGIN + (void)flushReplay; ++ (void)registerReplayTraceId:(NSString *)traceId; + @property (class, readonly, nullable) NSString *replayId; + (void)setReplayRedactContainerClass:(Class)containerClass; diff --git a/packages/core/ios/RNSentryInternalWrapper.m b/packages/core/ios/RNSentryInternalWrapper.m index 22ebf3e929..0ffde0639d 100644 --- a/packages/core/ios/RNSentryInternalWrapper.m +++ b/packages/core/ios/RNSentryInternalWrapper.m @@ -119,6 +119,11 @@ + (void)flushReplay [RNSentryInternal flushReplay]; } ++ (void)registerReplayTraceId:(NSString *)traceId +{ + [RNSentryInternal registerReplayTraceId:traceId]; +} + + (NSString *)replayId { return [RNSentryInternal replayId]; From 69a15e6f53ee236d0958665b7ecbdf398ebf6e0d Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Mon, 28 Sep 2026 11:35:31 +0200 Subject: [PATCH 11/16] docs(changelog): Note SwiftPM support is iOS-only Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0897315868..8c0ccdbb89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - Support React Native's Swift Package Manager integration ([#6784](https://github.com/getsentry/sentry-react-native/pull/6784)) - The SDK now ships a `Package.swift`, so `npx react-native spm` picks it up through SwiftPM autolinking instead of failing with `Package.swift is missing for library "@sentry/react-native"`. - Requires React Native 0.87 or newer, where SwiftPM support is an opt-in preview. CocoaPods remains the default and is unaffected. + - iOS only. macOS, tvOS and visionOS keep building through CocoaPods. ### Fixes From bfb0941f02cc4e02ec79cbc811214d10a96adcf3 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Mon, 28 Sep 2026 12:09:20 +0200 Subject: [PATCH 12/16] 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) --- .github/workflows/spm-application.yml | 35 +++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/.github/workflows/spm-application.yml b/.github/workflows/spm-application.yml index 234c6d5384..c2d2f4b091 100644 --- a/.github/workflows/spm-application.yml +++ b/.github/workflows/spm-application.yml @@ -153,3 +153,38 @@ jobs: exit 1 fi echo "The app binary contains both RNSentry and sentry-cocoa." + + - name: Verify Sentry Categories Survived The Link + working-directory: ${{ runner.temp }}/${{ env.APP_PATH }}/ios + # A class symbol reaching the binary does not prove its categories did. + # sentry-cocoa ships object files that hold nothing but a category + # (`SentryReplayNetworkDetails+Capture`, `Options+Dictionary`, + # `SentryNSNotificationCenterWrapper`), so nothing references them and + # the linker drops them from the static archive unless it is + # force-loaded — the selectors then fail at runtime with "unrecognized + # selector" (#6609, Apple QA1490). `RNSentry.podspec` solves this with + # `-force_load`; SwiftPM has no equivalent, so assert the outcome here. + run: | + [[ "${{ matrix.build-type }}" == "production" ]] && export CONFIG='Release' || export CONFIG='Debug' + app="build/Build/Products/$CONFIG-iphonesimulator/$APP_NAME.app" + binary="$app/$APP_NAME" + [[ -f "$app/$APP_NAME.debug.dylib" ]] && binary="$app/$APP_NAME.debug.dylib" + + # Selector defined only inside a category-only object file. + canary='setRequestWithSize:bodyData:contentType:allHeaders:configuredHeaders:' + + # Confirm the canary still exists upstream before trusting its absence + # below, so a sentry-cocoa rename surfaces as a warning to update this + # check rather than as a false "categories were stripped" failure. + archive=$(find build -type f -path '*Sentry.framework/Sentry' | head -1) + if [ -z "$archive" ] || ! strings -a "$archive" | grep -qF "$canary"; then + echo "::warning::Canary selector not found in sentry-cocoa; update it for this version." + exit 0 + fi + + if strings -a "$binary" | grep -qF "$canary"; then + echo "Category-only objects survived the link." + else + echo "::error::Sentry ObjC categories were dead-stripped from the app binary." + exit 1 + fi From 89d7b877913a1aa3330d758720c21f99f68fd571 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Mon, 28 Sep 2026 13:40:22 +0200 Subject: [PATCH 13/16] 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) --- .github/workflows/spm-application.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/spm-application.yml b/.github/workflows/spm-application.yml index c2d2f4b091..7442ced6e5 100644 --- a/.github/workflows/spm-application.yml +++ b/.github/workflows/spm-application.yml @@ -173,11 +173,17 @@ jobs: # Selector defined only inside a category-only object file. canary='setRequestWithSize:bodyData:contentType:allHeaders:configuredHeaders:' - # Confirm the canary still exists upstream before trusting its absence - # below, so a sentry-cocoa rename surfaces as a warning to update this - # check rather than as a false "categories were stripped" failure. + # Not finding the archive means this check can no longer see what it + # is asserting about, so fail rather than pass silently. archive=$(find build -type f -path '*Sentry.framework/Sentry' | head -1) - if [ -z "$archive" ] || ! strings -a "$archive" | grep -qF "$canary"; then + if [ -z "$archive" ]; then + echo "::error::Sentry.framework not found under build/; this check can no longer verify anything." + exit 1 + fi + + # A renamed selector is different: the canary is stale, not the link. + # Warn to update it rather than report a false dead-strip. + if ! strings -a "$archive" | grep -qF "$canary"; then echo "::warning::Canary selector not found in sentry-cocoa; update it for this version." exit 0 fi From 2092d0a85413e630e4f9e514815ce598b162ddb1 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Mon, 28 Sep 2026 14:50:02 +0200 Subject: [PATCH 14/16] 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) --- packages/core/.npmignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/.npmignore b/packages/core/.npmignore index adc3daa56f..57238e445d 100644 --- a/packages/core/.npmignore +++ b/packages/core/.npmignore @@ -9,7 +9,7 @@ !/dist/**/* !ts3.8/**/* !RNSentry.podspec -!Package.swift +!/Package.swift !sentry.gradle !sentry.gradle.kts !react-native.config.js From 726e6c1ed782900c88a46de2273e5ed057d5f2a8 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Mon, 28 Sep 2026 16:11:44 +0200 Subject: [PATCH 15/16] 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) --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index a9a446062c..ad58279572 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,24 @@ Sentry.addBreadcrumb({ message: "test" }); Sentry.captureMessage("Hello Sentry!"); ``` +### Swift Package Manager (iOS) + +React Native 0.87 and newer can autolink iOS dependencies through SwiftPM instead of CocoaPods. The SDK ships a `Package.swift`, so no extra configuration is needed: + +```sh +npm install @sentry/react-native +cd ios && npx react-native spm add --deintegrate +``` + +`--deintegrate` removes an existing CocoaPods setup; drop it on a project that never had one. Then build as usual — `npx react-native run-ios`, or open the `.xcodeproj` (SwiftPM projects have no `.xcworkspace`). + +Two limitations to know about: + +- **Every autolinked dependency must ship a `Package.swift`.** `react-native spm add` stops if one does not, and many community libraries still do not support SwiftPM. This is a React Native constraint, not a Sentry one. +- **iOS only.** macOS, tvOS and visionOS keep building through CocoaPods. + +CocoaPods remains the default and is unaffected. + ## Upgrade If you are coming from `react-native-sentry` which was our SDK `< 1.0` you should follow the [upgrade guide](https://docs.sentry.io/platforms/react-native/#upgrading-from-react-native-sentry) and then follow the [install steps](https://docs.sentry.io/platforms/react-native/#integrating-the-sdk). From 0b89c35d2718f147b0500b272d8bdf4e7bc23e30 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Wed, 30 Sep 2026 10:01:30 +0200 Subject: [PATCH 16/16] 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) --- CHANGELOG.md | 5 ----- packages/core/package.json | 6 ------ 2 files changed, 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c0ccdbb89..9f79542e99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,11 +16,6 @@ - Requires React Native 0.87 or newer, where SwiftPM support is an opt-in preview. CocoaPods remains the default and is unaffected. - iOS only. macOS, tvOS and visionOS keep building through CocoaPods. -### Fixes - -- Register the session replay mask components through the iOS codegen component provider ([#6784](https://github.com/getsentry/sentry-react-native/pull/6784)) - - `codegenConfig.ios.componentProvider` now maps `RNSentryReplayMask` and `RNSentryReplayUnmask` to their view classes, so React Native resolves them directly instead of falling back to the legacy view manager interop layer, which apps can turn off with `RCT_REMOVE_LEGACY_COMPONENT_INTEROP`. - ### Dependencies - Bump Android SDK from v8.57.0 to v8.58.0 ([#6778](https://github.com/getsentry/sentry-react-native/pull/6778)) diff --git a/packages/core/package.json b/packages/core/package.json index 6788febd87..887e381708 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -144,12 +144,6 @@ "jsSrcsDir": "src", "android": { "javaPackageName": "io.sentry.react" - }, - "ios": { - "componentProvider": { - "RNSentryReplayMask": "RNSentryReplayMask", - "RNSentryReplayUnmask": "RNSentryReplayUnmask" - } } }, "swiftpmConfig": {