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..7442ced6e5 --- /dev/null +++ b/.github/workflows/spm-application.yml @@ -0,0 +1,196 @@ +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 + # 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 }} + 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 }} + # 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 + # `--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 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" + + # 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 'SentryOptions'; then + echo "sentry-cocoa is missing from the app binary." + 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:' + + # 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" ]; 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 + + 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 774b6f78d2..9f79542e99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ ### Features - Populate `trace_ids` in mobile replays so replays can be searched by trace ID ([#6786](https://github.com/getsentry/sentry-react-native/pull/6786)) +- 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. ### Dependencies 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). diff --git a/packages/core/.npmignore b/packages/core/.npmignore index 871ec44541..57238e445d 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..6e364ea953 --- /dev/null +++ b/packages/core/Package.swift @@ -0,0 +1,154 @@ +// 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") +] + +// 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.2" +let sentryCocoaChecksum = "8d54d420474afdf6bee4ca060b8edbbd8948798d635ce7a9b477d884094b16b0" + +let sentryCocoa: Target.Dependency = "Sentry" + +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") + ], + 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 + // `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"), + // 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++") + ] + ) + ], + cxxLanguageStandard: .cxx20 +) diff --git a/packages/core/RNSentry.podspec b/packages/core/RNSentry.podspec index 785c1c57ae..d43681f8c9 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 5d85c45cfe..19daad723f 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); @@ -970,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); } @@ -1058,7 +1064,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 @@ -1084,7 +1090,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) { @@ -1098,7 +1105,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; } @@ -1110,7 +1117,7 @@ + (BOOL)isPathUnderAllowedRootsForTesting:(NSString *)path } catch (...) { if (nativeProfileTraceId != nil) { # if SENTRY_TARGET_PROFILING_SUPPORTED - [RNSentryInternal discardProfilerForTrace:nativeProfileTraceId]; + [RNSentryInternalWrapper discardProfilerForTrace:nativeProfileTraceId]; # endif nativeProfileTraceId = nil; } @@ -1130,9 +1137,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 @@ -1187,7 +1194,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; } @@ -1199,7 +1206,7 @@ + (BOOL)isPathUnderAllowedRootsForTesting:(NSString *)path } catch (...) { if (nativeProfileTraceId != nil) { # if SENTRY_TARGET_PROFILING_SUPPORTED - [RNSentryInternal discardProfilerForTrace:nativeProfileTraceId]; + [RNSentryInternalWrapper discardProfilerForTrace:nativeProfileTraceId]; # endif nativeProfileTraceId = nil; } @@ -1250,7 +1257,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..a05121c3d8 --- /dev/null +++ b/packages/core/ios/RNSentryInternalWrapper.h @@ -0,0 +1,106 @@ +#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 + +// `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; + ++ (void)startReplay; + ++ (void)startReplayBuffering; + ++ (void)stopReplay; + ++ (void)pauseReplay; + ++ (void)resumeReplay; + ++ (void)flushReplay; + ++ (void)registerReplayTraceId:(NSString *)traceId; + +@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..0ffde0639d --- /dev/null +++ b/packages/core/ios/RNSentryInternalWrapper.m @@ -0,0 +1,168 @@ +#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 + +#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST + ++ (void)setCurrentScreen:(NSString *)screenName +{ + [RNSentryInternal setCurrentScreen:screenName]; +} + ++ (NSArray *)captureScreenshots +{ + return [RNSentryInternal captureScreenshots]; +} + ++ (NSData *)captureViewHierarchy +{ + return [RNSentryInternal captureViewHierarchy]; +} + +#endif + +// 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]; +} + ++ (void)registerReplayTraceId:(NSString *)traceId +{ + [RNSentryInternal registerReplayTraceId:traceId]; +} + ++ (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 99% rename from packages/core/ios/RNSentryInternal.swift rename to packages/core/ios/Swift/RNSentryInternal.swift index e00b5d2299..51a0fc4f4d 100644 --- a/packages/core/ios/RNSentryInternal.swift +++ b/packages/core/ios/Swift/RNSentryInternal.swift @@ -274,7 +274,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 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..eec2a5c238 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,14 @@ 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 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 echo "Failed to rewrite the checksum version key in $utils" exit 1 @@ -55,6 +63,14 @@ set-version) echo "Failed to rewrite the Sentry checksum in $utils" exit 1 fi + 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"