diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 5681e9c669..4461c0afd0 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -44,7 +44,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # pin@v4.36.2 + uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # pin@v4.36.3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. diff --git a/CHANGELOG.md b/CHANGELOG.md index 5743ebc7fe..29f52c8fa4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ > make sure you follow our [migration guide](https://docs.sentry.io/platforms/react-native/migration/) first. +## Unreleased + +### Changes + +- **visionOS:** breadcrumbs no longer include the current screen name. sentry-cocoa's new hybrid-SDK `SentryInternalScreenApi` (which replaces the deprecated `PrivateSentrySDKOnly.setCurrentScreen:`) is gated to iOS/tvOS only, so the bridge stubs the setter on visionOS. Other visionOS behaviour (frames tracking, RNSScreen swizzle) is unchanged. + +### Internal + +- Migrate iOS code from the deprecated `PrivateSentrySDKOnly` SPI (and `SentrySwizzle.h` macro) to the new `SentrySDK.internal` Swift API exposed by sentry-cocoa 9.19.0, via a thin in-pod ObjC↔Swift bridge ([#6370](https://github.com/getsentry/sentry-react-native/issues/6370)) + + Adding Swift to the `RNSentry` pod means CocoaPods needs modular headers for its ObjC dependencies. On React Native versions where `React-hermes` (or another RN pod) is not modularized by default (e.g. RN 0.71), add `use_modular_headers!` to your `ios/Podfile` above the `target` block. Newer React Native versions require no change. + ## 8.17.2 ### Fixes @@ -27,6 +39,8 @@ > [!WARNING] > ⚠️ **Known Issue (Android):** Apps built with the New Architecture on `sentry-react-native` **8.17.0** bundle a native library (`libsentry-tm-perf-logger.so`) that is not 16 KB page aligned, which breaks [16 KB page size](https://developer.android.com/guide/practices/page-sizes) compatibility on Android 15+ (and fails Google Play's 16 KB requirement). See [#6394](https://github.com/getsentry/sentry-react-native/issues/6394). **Please use [8.17.2](https://github.com/getsentry/sentry-react-native/releases/tag/8.17.2)**. +## 8.16.0 + ### Features - Add experimental `enableStandaloneAppStartTracing` to send app start as a standalone `app.start` transaction ([#6359](https://github.com/getsentry/sentry-react-native/pull/6359)) diff --git a/dev-packages/e2e-tests/patch-scripts/rn.patch.podfile.js b/dev-packages/e2e-tests/patch-scripts/rn.patch.podfile.js index 8ab2afa36b..c488ef39fe 100755 --- a/dev-packages/e2e-tests/patch-scripts/rn.patch.podfile.js +++ b/dev-packages/e2e-tests/patch-scripts/rn.patch.podfile.js @@ -63,8 +63,27 @@ if (currentMatch) { debug.log('Warning: Could not find platform :ios line to patch'); } +// RNSentry now contains Swift code (via the RNSentryInternal bridge over +// SentrySDK.internal). CocoaPods refuses to integrate a Swift pod against +// non-modular ObjC dependencies (e.g. React-hermes on older RN versions), +// so ensure the Podfile requests modular headers globally. +let modularPatched = false; +if (!content.includes('use_modular_headers!')) { + const patched = content.replace( + /prepare_react_native_project!\s*\n/, + "prepare_react_native_project!\nuse_modular_headers!\n", + ); + if (patched !== content) { + content = patched; + modularPatched = true; + debug.log('Patching Podfile with use_modular_headers!'); + } else { + debug.log('Warning: Could not find prepare_react_native_project! anchor to inject use_modular_headers!'); + } +} + // Write the file if any changes were made -if (shouldPatch || currentMatch) { +if (shouldPatch || currentMatch || modularPatched) { fs.writeFileSync(args['pod-file'], content); debug.log('Podfile patched successfully!'); } else { diff --git a/packages/core/RNSentry.podspec b/packages/core/RNSentry.podspec index 84189b7df6..91266468ff 100644 --- a/packages/core/RNSentry.podspec +++ b/packages/core/RNSentry.podspec @@ -45,7 +45,8 @@ Pod::Spec.new do |s| # is pulled in here; on Android it is compiled by the dedicated CMake target # in `android/CMakeLists.txt`. The files are guarded with # `RCT_NEW_ARCH_ENABLED` so they compile to empty TUs on Old Arch. - s.source_files = 'ios/**/*.{h,m,mm}', 'cpp/**/*.{h,cpp}' + s.source_files = 'ios/**/*.{h,m,mm,swift}', 'cpp/**/*.{h,cpp}' + 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' s.compiler_flags = other_cflags diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj b/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj index 8c47e431b6..31f11f872d 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ 33F58AD02977037D008F60EA /* RNSentryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33F58ACF2977037D008F60EA /* RNSentryTests.m */; }; 2639D71D3BD04F17B0BAC987 /* RNSentryTurboModulePerfControllerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = E795057A6D534A80A9D06356 /* RNSentryTurboModulePerfControllerTests.mm */; }; AEFB00422CC90C4B00EC8A9A /* RNSentryBreadcrumbTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3360843C2C340C76008CC412 /* RNSentryBreadcrumbTests.swift */; }; + AB000123456789ABCDEF0002 /* RNSentryInternalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB000123456789ABCDEF0001 /* RNSentryInternalTests.swift */; }; B4DEB41739F14AA38202D4D4 /* RNSentryUriValidationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3E3742693F7643C2ADE1BDF2 /* RNSentryUriValidationTests.m */; }; B5859A50A3E865EF5E61465A /* libPods-RNSentryCocoaTesterTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 650CB718ACFBD05609BF2126 /* libPods-RNSentryCocoaTesterTests.a */; }; /* End PBXBuildFile section */ @@ -27,6 +28,7 @@ 1482D5685A340AB93348A43D /* Pods-RNSentryCocoaTesterTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNSentryCocoaTesterTests.release.xcconfig"; path = "Target Support Files/Pods-RNSentryCocoaTesterTests/Pods-RNSentryCocoaTesterTests.release.xcconfig"; sourceTree = ""; }; 330F308D2C0F385A002A0D4E /* RNSentryBreadcrumb.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSentryBreadcrumb.h; path = ../ios/RNSentryBreadcrumb.h; sourceTree = ""; }; 332D33462CDBDBB600547D76 /* RNSentryReplayOptionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNSentryReplayOptionsTests.swift; sourceTree = ""; }; + AB000123456789ABCDEF0001 /* RNSentryInternalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNSentryInternalTests.swift; sourceTree = ""; }; 332D33482CDBDC7300547D76 /* RNSentry.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNSentry.h; path = ../ios/RNSentry.h; sourceTree = SOURCE_ROOT; }; 332D33492CDCC8E100547D76 /* RNSentryTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNSentryTests.h; sourceTree = ""; }; 332D334A2CDCC8EB00547D76 /* RNSentryCocoaTesterTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "RNSentryCocoaTesterTests-Bridging-Header.h"; sourceTree = ""; }; @@ -119,6 +121,7 @@ 3360843C2C340C76008CC412 /* RNSentryBreadcrumbTests.swift */, 332D33462CDBDBB600547D76 /* RNSentryReplayOptionsTests.swift */, 3380C6C32CE25ECA0018B9B6 /* RNSentryReplayPostInitTests.swift */, + AB000123456789ABCDEF0001 /* RNSentryInternalTests.swift */, ); path = RNSentryCocoaTesterTests; sourceTree = ""; @@ -273,6 +276,7 @@ 33DEDFF02D9185EB006066E4 /* RNSentryTimeToDisplayTests.swift in Sources */, 3380C6C42CE25ECA0018B9B6 /* RNSentryReplayPostInitTests.swift in Sources */, 33AFDFED2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m in Sources */, + AB000123456789ABCDEF0002 /* RNSentryInternalTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryInternalTests.swift b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryInternalTests.swift new file mode 100644 index 0000000000..07a24dc5d7 --- /dev/null +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryInternalTests.swift @@ -0,0 +1,103 @@ +@_spi(Private) import Sentry +import XCTest + +/// Smoke coverage for the `RNSentryInternal` ObjC↔Swift bridge. +/// +/// These are not exhaustive tests of the underlying `SentrySDK.internal.*` +/// surface — sentry-cocoa owns that. We only assert the wrapper does not +/// crash, forwards data correctly, and honours the nil / platform guards +/// documented in `RNSentryInternal.swift`. +final class RNSentryInternalTests: XCTestCase { + + override func setUp() { + super.setUp() + // Start the native SDK so `SentrySDK.internal.*` returns real state + // instead of the no-op hub. A minimal, offline-safe DSN is enough. + RNSentrySDK.start { options in + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + } + } + + override func tearDown() { + SentrySDK.close() + super.tearDown() + } + + // MARK: - envelope(fromData:) + + func testEnvelopeFromNilDataReturnsNil() { + // Regression guard: the old ObjC `PrivateSentrySDKOnly.envelopeWithData:` + // tolerated nil. `RNSentryInternal.envelope(fromData:)` must too, or + // the ObjC bridge boundary would crash on a nil `NSData*` before the + // Swift body runs. See RNSentry.mm captureEnvelope path. + XCTAssertNil(RNSentryInternal.envelope(fromData: nil)) + } + + func testEnvelopeFromInvalidDataReturnsNil() { + let junk = Data("this is not an envelope".utf8) + XCTAssertNil(RNSentryInternal.envelope(fromData: junk)) + } + + // MARK: - SDK metadata + + func testSdkMetadataAccessorsAreNonEmpty() { + XCTAssertFalse(RNSentryInternal.sdkName.isEmpty) + XCTAssertFalse(RNSentryInternal.sdkVersionString.isEmpty) + XCTAssertFalse(RNSentryInternal.installationID.isEmpty) + // `extraContext` may be empty on an unstarted SDK; here we only + // assert the accessor does not crash and returns a dictionary. + _ = RNSentryInternal.extraContext + } + + func testSetSdkNameAndAddPackageRoundTrip() { + RNSentryInternal.setSdkName("sentry.cocoa.react-native.test", version: "42.42.42") + XCTAssertEqual(RNSentryInternal.sdkName, "sentry.cocoa.react-native.test") + XCTAssertEqual(RNSentryInternal.sdkVersionString, "42.42.42") + + // Add-package is void; assert it does not throw. Idempotency across + // sentry-cocoa releases is not part of the bridge's contract. + RNSentryInternal.addSdkPackage("test-package", version: "1.0.0") + } + + // MARK: - Options + + func testOptionsAccessorReturnsLiveOptions() { + let options = RNSentryInternal.options + XCTAssertNotNil(options.dsn) + } + + func testOptionsFromDictionaryValidatesInput() throws { + let dict: [String: Any] = ["dsn": "https://abcd@efgh.ingest.sentry.io/123456"] + let options = try RNSentryInternal.options(fromDictionary: dict) + XCTAssertNotNil(options.dsn) + } + + // MARK: - App start / performance hybrid flags + + func testAppStartHybridSDKModeIsReadWrite() { + let previous = RNSentryInternal.appStartMeasurementHybridSDKMode + RNSentryInternal.appStartMeasurementHybridSDKMode = !previous + XCTAssertEqual(RNSentryInternal.appStartMeasurementHybridSDKMode, !previous) + RNSentryInternal.appStartMeasurementHybridSDKMode = previous + } + + #if os(iOS) || os(tvOS) || os(visionOS) + func testFramesTrackingHybridSDKModeIsReadWrite() { + let previous = RNSentryInternal.framesTrackingMeasurementHybridSDKMode + RNSentryInternal.framesTrackingMeasurementHybridSDKMode = !previous + XCTAssertEqual(RNSentryInternal.framesTrackingMeasurementHybridSDKMode, !previous) + RNSentryInternal.framesTrackingMeasurementHybridSDKMode = previous + } + #endif + + // MARK: - Swizzle bridge + + func testSwizzleRNSScreenViewDidAppearNoOpWhenClassMissing() { + // `RNSScreen` is not linked in this test target; the bridge should + // early-return without invoking the hook and without touching the + // ObjC runtime. + var hookCalled = false + RNSentryInternal.swizzleRNSScreenViewDidAppear { hookCalled = true } + XCTAssertFalse(hookCalled, "hook must not be called at registration time") + } +} diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayOptionsTests.swift b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayOptionsTests.swift index 7d2c99c7b3..da7f1ccad1 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayOptionsTests.swift +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayOptionsTests.swift @@ -1,4 +1,4 @@ -import Sentry +@_spi(Private) import Sentry import XCTest // File length grows as replay option coverage is added; lint runs with `--strict`. @@ -117,7 +117,7 @@ final class RNSentryReplayOptions: XCTestCase { ] as NSDictionary).mutableCopy() as! NSMutableDictionary RNSentryReplay.updateOptions(optionsDict) - let actualOptions = try! PrivateSentrySDKOnly.options(with: optionsDict as! [String: Any]) + let actualOptions = try! SentrySDK.internal.options(fromDictionary: optionsDict as! [String: Any]) XCTAssertEqual(actualOptions.sessionReplay.sessionSampleRate, 0.75) } @@ -128,7 +128,7 @@ final class RNSentryReplayOptions: XCTestCase { ] as NSDictionary).mutableCopy() as! NSMutableDictionary RNSentryReplay.updateOptions(optionsDict) - let actualOptions = try! PrivateSentrySDKOnly.options(with: optionsDict as! [String: Any]) + let actualOptions = try! SentrySDK.internal.options(fromDictionary: optionsDict as! [String: Any]) XCTAssertEqual(actualOptions.sessionReplay.onErrorSampleRate, 0.75) } @@ -158,7 +158,7 @@ final class RNSentryReplayOptions: XCTestCase { RNSentryReplay.updateOptions(optionsDict) - let actualOptions = try! PrivateSentrySDKOnly.options(with: optionsDict as! [String: Any]) + let actualOptions = try! SentrySDK.internal.options(fromDictionary: optionsDict as! [String: Any]) XCTAssertEqual(actualOptions.sessionReplay.maskAllImages, true) assertContainsClass(classArray: actualOptions.sessionReplay.maskedViewClasses, stringClass: "RCTImageView") @@ -173,7 +173,7 @@ final class RNSentryReplayOptions: XCTestCase { RNSentryReplay.updateOptions(optionsDict) - let actualOptions = try! PrivateSentrySDKOnly.options(with: optionsDict as! [String: Any]) + let actualOptions = try! SentrySDK.internal.options(fromDictionary: optionsDict as! [String: Any]) XCTAssertEqual(actualOptions.sessionReplay.maskAllImages, false) XCTAssertEqual(actualOptions.sessionReplay.maskedViewClasses.count, 0) @@ -188,7 +188,7 @@ final class RNSentryReplayOptions: XCTestCase { RNSentryReplay.updateOptions(optionsDict) - let actualOptions = try! PrivateSentrySDKOnly.options(with: optionsDict as! [String: Any]) + let actualOptions = try! SentrySDK.internal.options(fromDictionary: optionsDict as! [String: Any]) XCTAssertEqual(actualOptions.sessionReplay.maskAllText, true) assertContainsClass(classArray: actualOptions.sessionReplay.maskedViewClasses, stringClass: "RCTTextView") @@ -215,7 +215,7 @@ final class RNSentryReplayOptions: XCTestCase { RNSentryReplay.updateOptions(optionsDict) - let actualOptions = try! PrivateSentrySDKOnly.options(with: optionsDict as! [String: Any]) + let actualOptions = try! SentrySDK.internal.options(fromDictionary: optionsDict as! [String: Any]) XCTAssertEqual(actualOptions.sessionReplay.maskAllText, false) XCTAssertEqual(actualOptions.sessionReplay.maskedViewClasses.count, 0) @@ -229,7 +229,7 @@ final class RNSentryReplayOptions: XCTestCase { RNSentryReplay.updateOptions(optionsDict) - let actualOptions = try! PrivateSentrySDKOnly.options(with: optionsDict as! [String: Any]) + let actualOptions = try! SentrySDK.internal.options(fromDictionary: optionsDict as! [String: Any]) XCTAssertTrue(actualOptions.sessionReplay.enableViewRendererV2) } @@ -243,7 +243,7 @@ final class RNSentryReplayOptions: XCTestCase { RNSentryReplay.updateOptions(optionsDict) - let actualOptions = try! PrivateSentrySDKOnly.options(with: optionsDict as! [String: Any]) + let actualOptions = try! SentrySDK.internal.options(fromDictionary: optionsDict as! [String: Any]) XCTAssertTrue(actualOptions.sessionReplay.enableViewRendererV2) } @@ -257,7 +257,7 @@ final class RNSentryReplayOptions: XCTestCase { RNSentryReplay.updateOptions(optionsDict) - let actualOptions = try! PrivateSentrySDKOnly.options(with: optionsDict as! [String: Any]) + let actualOptions = try! SentrySDK.internal.options(fromDictionary: optionsDict as! [String: Any]) XCTAssertFalse(actualOptions.sessionReplay.enableViewRendererV2) } @@ -270,7 +270,7 @@ final class RNSentryReplayOptions: XCTestCase { RNSentryReplay.updateOptions(optionsDict) - let actualOptions = try! PrivateSentrySDKOnly.options(with: optionsDict as! [String: Any]) + let actualOptions = try! SentrySDK.internal.options(fromDictionary: optionsDict as! [String: Any]) XCTAssertFalse(actualOptions.sessionReplay.enableFastViewRendering) } @@ -284,7 +284,7 @@ final class RNSentryReplayOptions: XCTestCase { RNSentryReplay.updateOptions(optionsDict) - let actualOptions = try! PrivateSentrySDKOnly.options(with: optionsDict as! [String: Any]) + let actualOptions = try! SentrySDK.internal.options(fromDictionary: optionsDict as! [String: Any]) XCTAssertTrue(actualOptions.sessionReplay.enableFastViewRendering) } @@ -298,7 +298,7 @@ final class RNSentryReplayOptions: XCTestCase { RNSentryReplay.updateOptions(optionsDict) - let actualOptions = try! PrivateSentrySDKOnly.options(with: optionsDict as! [String: Any]) + let actualOptions = try! SentrySDK.internal.options(fromDictionary: optionsDict as! [String: Any]) XCTAssertFalse(actualOptions.sessionReplay.enableFastViewRendering) } @@ -311,7 +311,7 @@ final class RNSentryReplayOptions: XCTestCase { RNSentryReplay.updateOptions(optionsDict) - let actualOptions = try! PrivateSentrySDKOnly.options(with: optionsDict as! [String: Any]) + let actualOptions = try! SentrySDK.internal.options(fromDictionary: optionsDict as! [String: Any]) XCTAssertEqual(actualOptions.sessionReplay.quality, SentryReplayOptions.SentryReplayQuality.medium) } @@ -325,7 +325,7 @@ final class RNSentryReplayOptions: XCTestCase { RNSentryReplay.updateOptions(optionsDict) - let actualOptions = try! PrivateSentrySDKOnly.options(with: optionsDict as! [String: Any]) + let actualOptions = try! SentrySDK.internal.options(fromDictionary: optionsDict as! [String: Any]) XCTAssertEqual(actualOptions.sessionReplay.quality, SentryReplayOptions.SentryReplayQuality.low) } @@ -339,7 +339,7 @@ final class RNSentryReplayOptions: XCTestCase { RNSentryReplay.updateOptions(optionsDict) - let actualOptions = try! PrivateSentrySDKOnly.options(with: optionsDict as! [String: Any]) + let actualOptions = try! SentrySDK.internal.options(fromDictionary: optionsDict as! [String: Any]) XCTAssertEqual(actualOptions.sessionReplay.quality, SentryReplayOptions.SentryReplayQuality.medium) } @@ -353,7 +353,7 @@ final class RNSentryReplayOptions: XCTestCase { RNSentryReplay.updateOptions(optionsDict) - let actualOptions = try! PrivateSentrySDKOnly.options(with: optionsDict as! [String: Any]) + let actualOptions = try! SentrySDK.internal.options(fromDictionary: optionsDict as! [String: Any]) XCTAssertEqual(actualOptions.sessionReplay.quality, SentryReplayOptions.SentryReplayQuality.high) } @@ -367,7 +367,7 @@ final class RNSentryReplayOptions: XCTestCase { RNSentryReplay.updateOptions(optionsDict) - let actualOptions = try! PrivateSentrySDKOnly.options(with: optionsDict as! [String: Any]) + let actualOptions = try! SentrySDK.internal.options(fromDictionary: optionsDict as! [String: Any]) XCTAssertEqual(actualOptions.sessionReplay.quality, SentryReplayOptions.SentryReplayQuality.medium) } @@ -381,7 +381,7 @@ final class RNSentryReplayOptions: XCTestCase { RNSentryReplay.updateOptions(optionsDict) - let actualOptions = try! PrivateSentrySDKOnly.options(with: optionsDict as! [String: Any]) + let actualOptions = try! SentrySDK.internal.options(fromDictionary: optionsDict as! [String: Any]) let includedViewClasses = actualOptions.sessionReplay.includedViewClasses XCTAssertEqual(includedViewClasses.count, 3) @@ -399,7 +399,7 @@ final class RNSentryReplayOptions: XCTestCase { RNSentryReplay.updateOptions(optionsDict) - let actualOptions = try! PrivateSentrySDKOnly.options(with: optionsDict as! [String: Any]) + let actualOptions = try! SentrySDK.internal.options(fromDictionary: optionsDict as! [String: Any]) let excludedViewClasses = actualOptions.sessionReplay.excludedViewClasses XCTAssertEqual(excludedViewClasses.count, 3) @@ -420,7 +420,7 @@ final class RNSentryReplayOptions: XCTestCase { RNSentryReplay.updateOptions(optionsDict) - let actualOptions = try! PrivateSentrySDKOnly.options(with: optionsDict as! [String: Any]) + let actualOptions = try! SentrySDK.internal.options(fromDictionary: optionsDict as! [String: Any]) let includedViewClasses = actualOptions.sessionReplay.includedViewClasses XCTAssertEqual(includedViewClasses.count, 2) diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartFromFileTests.swift b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartFromFileTests.swift index e0269a5961..38107d3f66 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartFromFileTests.swift +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartFromFileTests.swift @@ -1,3 +1,4 @@ +@_spi(Private) import Sentry import XCTest final class RNSentryStartFromFileTests: XCTestCase { @@ -11,7 +12,7 @@ final class RNSentryStartFromFileTests: XCTestCase { XCTAssertTrue(wasConfigurationCalled) - let actualOptions = PrivateSentrySDKOnly.options + let actualOptions = SentrySDK.internal.options XCTAssertNil(actualOptions.dsn) XCTAssertNil(actualOptions.parsedDsn) } @@ -25,7 +26,7 @@ final class RNSentryStartFromFileTests: XCTestCase { XCTAssertTrue(wasConfigurationCalled) - let actualOptions = PrivateSentrySDKOnly.options + let actualOptions = SentrySDK.internal.options XCTAssertNil(actualOptions.dsn) XCTAssertNil(actualOptions.parsedDsn) } @@ -39,7 +40,7 @@ final class RNSentryStartFromFileTests: XCTestCase { XCTAssertTrue(wasConfigurationCalled) - let actualOptions = PrivateSentrySDKOnly.options + let actualOptions = SentrySDK.internal.options XCTAssertNil(actualOptions.dsn) XCTAssertNotNil(actualOptions.parsedDsn) XCTAssertEqual(actualOptions.environment, "environment-from-invalid-file") @@ -54,7 +55,7 @@ final class RNSentryStartFromFileTests: XCTestCase { XCTAssertTrue(wasConfigurationCalled) - let actualOptions = PrivateSentrySDKOnly.options + let actualOptions = SentrySDK.internal.options XCTAssertNil(actualOptions.dsn) XCTAssertNotNil(actualOptions.parsedDsn) XCTAssertEqual(actualOptions.environment, "environment-from-valid-file") @@ -76,7 +77,7 @@ final class RNSentryStartFromFileTests: XCTestCase { options.environment = "new-environment" } - let actualOptions = PrivateSentrySDKOnly.options + let actualOptions = SentrySDK.internal.options XCTAssertEqual(actualOptions.environment, "new-environment") } diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartTests.swift b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartTests.swift index c437b83049..682c651936 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartTests.swift +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartTests.swift @@ -1,3 +1,4 @@ +@_spi(Private) import Sentry import XCTest final class RNSentryStartTests: XCTestCase { @@ -29,7 +30,7 @@ final class RNSentryStartTests: XCTestCase { "dsn": "https://abcd@efgh.ingest.sentry.io/123456" ]) - let actualOptions = PrivateSentrySDKOnly.options + let actualOptions = SentrySDK.internal.options assertReactDefaults(actualOptions) } @@ -63,10 +64,10 @@ final class RNSentryStartTests: XCTestCase { for startMethod in testCases { try startMethod() - let actualOptions = PrivateSentrySDKOnly.options + let actualOptions = SentrySDK.internal.options - XCTAssertTrue(PrivateSentrySDKOnly.appStartMeasurementHybridSDKMode) - XCTAssertTrue(PrivateSentrySDKOnly.framesTrackingMeasurementHybridSDKMode) + XCTAssertTrue(SentrySDK.internal.appStart.hybridSDKMode) + XCTAssertTrue(SentrySDK.internal.performance.framesTrackingHybridSDKMode) } } @@ -89,10 +90,10 @@ final class RNSentryStartTests: XCTestCase { for startMethod in testCases { try startMethod() - let actualOptions = PrivateSentrySDKOnly.options + let actualOptions = SentrySDK.internal.options - XCTAssertFalse(PrivateSentrySDKOnly.appStartMeasurementHybridSDKMode) - XCTAssertFalse(PrivateSentrySDKOnly.framesTrackingMeasurementHybridSDKMode) + XCTAssertFalse(SentrySDK.internal.appStart.hybridSDKMode) + XCTAssertFalse(SentrySDK.internal.performance.framesTrackingHybridSDKMode) } } @@ -113,7 +114,7 @@ final class RNSentryStartTests: XCTestCase { for startMethod in testCases { try startMethod() - let actualOptions = PrivateSentrySDKOnly.options + let actualOptions = SentrySDK.internal.options let actualEvent = actualOptions.beforeSend!(createUnhandledJsExceptionEvent()) @@ -138,7 +139,7 @@ final class RNSentryStartTests: XCTestCase { for startMethod in testCases { try startMethod() - let actualOptions = PrivateSentrySDKOnly.options + let actualOptions = SentrySDK.internal.options let actualEvent = actualOptions.beforeSend!(createNativeEvent()) @@ -160,7 +161,7 @@ final class RNSentryStartTests: XCTestCase { } } - PrivateSentrySDKOnly.options.beforeSend!(genericEvent()) + SentrySDK.internal.options.beforeSend!(genericEvent()) XCTAssertTrue(executed) } @@ -207,7 +208,7 @@ final class RNSentryStartTests: XCTestCase { ] ]) - let actualOptions = PrivateSentrySDKOnly.options + let actualOptions = SentrySDK.internal.options XCTAssertTrue(actualOptions.attachScreenshot) XCTAssertFalse(actualOptions.screenshot.maskAllText) XCTAssertTrue(actualOptions.screenshot.maskAllImages) @@ -219,7 +220,7 @@ final class RNSentryStartTests: XCTestCase { "attachScreenshot": true ]) - let actualOptions = PrivateSentrySDKOnly.options + let actualOptions = SentrySDK.internal.options XCTAssertTrue(actualOptions.attachScreenshot) XCTAssertTrue(actualOptions.screenshot.maskAllText) XCTAssertTrue(actualOptions.screenshot.maskAllImages) @@ -258,8 +259,8 @@ final class RNSentryStartTests: XCTestCase { var actualEvent: Event? // This is the closest to the sent event we can get using the actual Sentry start method - let originalBeforeSend = PrivateSentrySDKOnly.options.beforeSend - PrivateSentrySDKOnly.options.beforeSend = { event in + let originalBeforeSend = SentrySDK.internal.options.beforeSend + SentrySDK.internal.options.beforeSend = { event in if let originalBeforeSend = originalBeforeSend { let processedEvent = originalBeforeSend(event) actualEvent = processedEvent diff --git a/packages/core/ios/AGENTS.md b/packages/core/ios/AGENTS.md index b13a4c617a..04edda1bad 100644 --- a/packages/core/ios/AGENTS.md +++ b/packages/core/ios/AGENTS.md @@ -56,5 +56,15 @@ RCT_EXPORT_METHOD(nativeOperation:(NSString *)param 2. Edit `RNSentry.podspec` to remove version constraint 3. Add local pod to sample's Podfile: ```ruby - pod 'Sentry/HybridSDK', :path => '../../../../sentry-cocoa' + pod 'Sentry', :path => '../../../../sentry-cocoa' ``` + +## Internal API access (`SentrySDK.internal`) + +RNSentry consumes sentry-cocoa's hybrid-SDK surface (`SentrySDK.internal.*`) +through a Swift bridge in `RNSentryInternal.swift`. The bridge imports Sentry +with `@_spi(Private)` because several sub-APIs (`performance.currentScreenFrames`, +`replay.configure`, `envelope.{store,capture,deserialize}`) are SPI-gated. +`.m`/`.mm` callers import the auto-generated `RNSentry-Swift.h` and route +through `[RNSentryInternal …]` instead of touching `PrivateSentrySDKOnly` +(deprecated since cocoa 9.19.0 and slated for removal in the next major). diff --git a/packages/core/ios/RNSentry+fetchNativeStack.m b/packages/core/ios/RNSentry+fetchNativeStack.m index 196e726ddf..1298cf10c3 100644 --- a/packages/core/ios/RNSentry+fetchNativeStack.m +++ b/packages/core/ios/RNSentry+fetchNativeStack.m @@ -2,7 +2,6 @@ #import "RNSentryBreadcrumb.h" #import "RNSentryHexFormatter.h" #import "RNSentryId.h" -#import @import Sentry; // This method was moved to a new category so we can use `@import Sentry` to use Sentry's Swift diff --git a/packages/core/ios/RNSentry.mm b/packages/core/ios/RNSentry.mm index 6cc9e946f4..1207a09a93 100644 --- a/packages/core/ios/RNSentry.mm +++ b/packages/core/ios/RNSentry.mm @@ -16,9 +16,13 @@ # define SENTRY_TARGET_PROFILING_SUPPORTED 0 #endif +#if __has_include() +# import +#else +# import "RNSentry-Swift.h" +#endif #import "RNSentryBreadcrumb.h" #import "RNSentryId.h" -#import #import #import #import @@ -403,10 +407,8 @@ - (void)handleShakeDetected RCT_EXPORT_METHOD( fetchNativeSdkInfo : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject) { - resolve(@ { - @"name" : PrivateSentrySDKOnly.getSdkName, - @"version" : PrivateSentrySDKOnly.getSdkVersionString - }); + resolve( + @ { @"name" : RNSentryInternal.sdkName, @"version" : RNSentryInternal.sdkVersionString }); } RCT_EXPORT_METHOD( @@ -458,7 +460,7 @@ - (void)handleShakeDetected contexts[@"release"] = releaseName; } // Merge extra context - NSDictionary *extraContext = [PrivateSentrySDKOnly getExtraContext]; + NSDictionary *extraContext = [RNSentryInternal extraContext]; if (extraContext) { NSDictionary *extraDevice = extraContext[@"device"]; @@ -496,8 +498,7 @@ - (void)handleShakeDetected NSDictionary *user = [serializedScope valueForKey:@"user"]; if (user == nil) { - [serializedScope setValue:@ { @"id" : PrivateSentrySDKOnly.installationID } - forKey:@"user"]; + [serializedScope setValue:@ { @"id" : RNSentryInternal.installationID } forKey:@"user"]; } if ([SentrySDKWrapper debug]) { @@ -510,7 +511,7 @@ - (void)handleShakeDetected } }]; - NSDictionary *extraContext = [PrivateSentrySDKOnly getExtraContext]; + NSDictionary *extraContext = [RNSentryInternal extraContext]; NSMutableDictionary *> *contexts = [serializedScope[@"context"] mutableCopy]; @@ -547,8 +548,7 @@ - (void)handleShakeDetected fetchNativeAppStart : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject) { #if SENTRY_HAS_UIKIT - NSDictionary *measurements = - [PrivateSentrySDKOnly appStartMeasurementWithSpans]; + NSDictionary *measurements = [RNSentryInternal appStartMeasurementWithSpans]; if (measurements == nil) { resolve(nil); return; @@ -574,7 +574,7 @@ - (void)handleShakeDetected { #if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST - if (PrivateSentrySDKOnly.isFramesTrackingRunning) { + if (RNSentryInternal.isFramesTrackingRunning) { if (![SentryScreenFramesWrapper canTrackFrames]) { resolve(nil); return; @@ -626,20 +626,20 @@ - (void)handleShakeDetected { NSData *data = [[NSData alloc] initWithBase64EncodedString:rawBytes options:0]; - SentryEnvelope *envelope = [PrivateSentrySDKOnly envelopeWithData:data]; + SentryEnvelope *envelope = [RNSentryInternal envelopeFromData:data]; if (envelope == nil) { reject(@"SentryReactNative", @"Failed to parse envelope from byte array.", nil); return; } #if DEBUG - [PrivateSentrySDKOnly captureEnvelope:envelope]; + [RNSentryInternal capture:envelope]; #else if ([[options objectForKey:@"hardCrashed"] boolValue]) { // Storing to disk happens asynchronously with captureEnvelope - [PrivateSentrySDKOnly storeEnvelope:envelope]; + [RNSentryInternal store:envelope]; } else { - [PrivateSentrySDKOnly captureEnvelope:envelope]; + [RNSentryInternal capture:envelope]; } #endif resolve(@YES); @@ -649,7 +649,7 @@ - (void)handleShakeDetected captureScreenshot : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject) { #if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST - NSArray *rawScreenshots = [PrivateSentrySDKOnly captureScreenshots]; + NSArray *rawScreenshots = [RNSentryInternal captureScreenshots]; NSMutableArray *screenshotsArray = [NSMutableArray arrayWithCapacity:[rawScreenshots count]]; int counter = 1; @@ -682,7 +682,13 @@ - (void)handleShakeDetected fetchViewHierarchy : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject) { #if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST - NSData *rawViewHierarchy = [PrivateSentrySDKOnly captureViewHierarchy]; + NSData *rawViewHierarchy = [RNSentryInternal captureViewHierarchy]; + if (rawViewHierarchy == nil) { + // Propagate the capture failure to JS instead of a truthy `[]`, which + // would be treated as a successful (empty) attachment. + resolve(nil); + return; + } NSMutableArray *viewHierarchy = [NSMutableArray arrayWithCapacity:rawViewHierarchy.length]; const char *bytes = (char *)[rawViewHierarchy bytes]; @@ -733,7 +739,7 @@ + (SentryUser *_Nullable)userFrom:(NSDictionary *)userKeys #if SENTRY_HAS_UIKIT NSString *_Nullable screen = [RNSentryBreadcrumb getCurrentScreenFrom:breadcrumb]; if (screen != nil) { - [PrivateSentrySDKOnly setCurrentScreen:screen]; + [RNSentryInternal setCurrentScreen:screen]; } #endif // SENTRY_HAS_UIKIT } @@ -816,52 +822,19 @@ + (SentryUser *_Nullable)userFrom:(NSDictionary *)userKeys RCT_EXPORT_METHOD(resumeAppHangTracking) { [SentrySDKWrapper resumeAppHangTracking]; } -/** - * Calls captureReplay on the native replay integration and returns - * the BOOL result indicating whether the capture succeeded. - * - * PrivateSentrySDKOnly.captureReplay is void and discards the result, - * so we call the integration directly to get the success status. - * This prevents returning a stale buffer-mode replay ID when the - * capture actually failed (e.g., replay not running). - * - * Falls back to the old void captureReplay if the integration - * cannot be accessed directly (e.g., future Cocoa SDK changes). - * - * See https://github.com/getsentry/sentry-react-native/issues/5074 - */ +// Calls `SentrySDK.internal.replay.capture()` via the Swift bridge and returns +// the BOOL result. The `@try`/`@catch` is retained as cheap insurance for one +// release cycle — see getsentry/sentry-react-native#5074 for the historical +// fault path that motivated defensive handling here. + (BOOL)captureReplayWithReturnValue { #if SENTRY_TARGET_REPLAY_SUPPORTED @try { - if ([PrivateSentrySDKOnly respondsToSelector:@selector(getReplayIntegration)]) { -# pragma clang diagnostic push -# pragma clang diagnostic ignored "-Warc-performSelector-leaks" - id replayIntegration = - [PrivateSentrySDKOnly performSelector:@selector(getReplayIntegration)]; -# pragma clang diagnostic pop - if (replayIntegration && - [replayIntegration respondsToSelector:@selector(captureReplay)]) { - typedef BOOL (*CaptureReplayIMP)(id, SEL); - CaptureReplayIMP captureFunc = (CaptureReplayIMP) - [replayIntegration methodForSelector:@selector(captureReplay)]; - return captureFunc(replayIntegration, @selector(captureReplay)); - } - } - } @catch (NSException *exception) { - NSLog(@"[RNSentry] Failed to call captureReplay on integration: %@", exception); - } - // Fallback: call the void method and assume success if a replay ID exists. - // This preserves the old behavior when the integration isn't directly accessible. - // clang-format off - @try { - [PrivateSentrySDKOnly captureReplay]; - return [PrivateSentrySDKOnly getReplayId] != nil; + return [RNSentryInternal captureReplay]; } @catch (NSException *exception) { - NSLog(@"[RNSentry] Failed to call captureReplay fallback: %@", exception); + NSLog(@"[RNSentry] Failed to call captureReplay: %@", exception); return NO; } - // clang-format on #else return NO; #endif @@ -873,7 +846,7 @@ + (BOOL)captureReplayWithReturnValue #if SENTRY_TARGET_REPLAY_SUPPORTED BOOL captured = [RNSentry captureReplayWithReturnValue]; if (captured) { - resolve([PrivateSentrySDKOnly getReplayId]); + resolve([RNSentryInternal replayId]); } else { resolve(nil); } @@ -963,7 +936,7 @@ + (BOOL)isPathUnderAllowedRootsForTesting:(NSString *)path RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSString *, getCurrentReplayId) { #if SENTRY_TARGET_REPLAY_SUPPORTED - return [PrivateSentrySDKOnly getReplayId]; + return [RNSentryInternal replayId]; #else return nil; #endif @@ -989,8 +962,7 @@ + (BOOL)isPathUnderAllowedRootsForTesting:(NSString *)path if (nativeProfileTraceId == nil && nativeProfileStartTime == 0 && platformProfilers) { # if SENTRY_TARGET_PROFILING_SUPPORTED nativeProfileTraceId = [RNSentryId newId]; - nativeProfileStartTime = - [PrivateSentrySDKOnly startProfilerForTrace:nativeProfileTraceId]; + nativeProfileStartTime = [RNSentryInternal startProfilerForTrace:nativeProfileTraceId]; # endif } else { if (!platformProfilers) { @@ -1004,7 +976,7 @@ + (BOOL)isPathUnderAllowedRootsForTesting:(NSString *)path } catch (const std::exception &ex) { if (nativeProfileTraceId != nil) { # if SENTRY_TARGET_PROFILING_SUPPORTED - [PrivateSentrySDKOnly discardProfilerForTrace:nativeProfileTraceId]; + [RNSentryInternal discardProfilerForTrace:nativeProfileTraceId]; # endif nativeProfileTraceId = nil; } @@ -1016,7 +988,7 @@ + (BOOL)isPathUnderAllowedRootsForTesting:(NSString *)path } catch (...) { if (nativeProfileTraceId != nil) { # if SENTRY_TARGET_PROFILING_SUPPORTED - [PrivateSentrySDKOnly discardProfilerForTrace:nativeProfileTraceId]; + [RNSentryInternal discardProfilerForTrace:nativeProfileTraceId]; # endif nativeProfileTraceId = nil; } @@ -1036,9 +1008,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 = [PrivateSentrySDKOnly collectProfileBetween:nativeProfileStartTime - and:nativeProfileStopTime - forTrace:nativeProfileTraceId]; + nativeProfile = [RNSentryInternal collectProfileBetween:nativeProfileStartTime + and:nativeProfileStopTime + forTrace:nativeProfileTraceId]; # endif } // Cleanup native profiles @@ -1093,7 +1065,7 @@ + (BOOL)isPathUnderAllowedRootsForTesting:(NSString *)path } catch (const std::exception &ex) { if (nativeProfileTraceId != nil) { # if SENTRY_TARGET_PROFILING_SUPPORTED - [PrivateSentrySDKOnly discardProfilerForTrace:nativeProfileTraceId]; + [RNSentryInternal discardProfilerForTrace:nativeProfileTraceId]; # endif nativeProfileTraceId = nil; } @@ -1105,7 +1077,7 @@ + (BOOL)isPathUnderAllowedRootsForTesting:(NSString *)path } catch (...) { if (nativeProfileTraceId != nil) { # if SENTRY_TARGET_PROFILING_SUPPORTED - [PrivateSentrySDKOnly discardProfilerForTrace:nativeProfileTraceId]; + [RNSentryInternal discardProfilerForTrace:nativeProfileTraceId]; # endif nativeProfileTraceId = nil; } diff --git a/packages/core/ios/RNSentryInternal.swift b/packages/core/ios/RNSentryInternal.swift new file mode 100644 index 0000000000..241bc8ac1b --- /dev/null +++ b/packages/core/ios/RNSentryInternal.swift @@ -0,0 +1,240 @@ +import Foundation +@_spi(Private) import Sentry + +/// Thin Objective-C-visible bridge over `SentrySDK.internal.*`. +/// +/// React Native's iOS code is mostly `.m`/`.mm`, but the new hybrid-SDK API +/// (`SentrySDK.internal`) is Swift-only and several methods are gated by +/// `@_spi(Private)`. This file imports with the SPI name and exposes a flat +/// `@objc` surface that mirrors the call sites we use today. +/// +/// Platform gating mirrors each `SentryInternal*Api` in sentry-cocoa +/// (`Sources/Swift/HybridSDK/`). Where a sub-API is excluded on a platform, +/// stubs return safe defaults so `.m`/`.mm` callers still resolve symbols. +@_spi(Private) @objc public final class RNSentryInternal: NSObject { + + // MARK: - SDK metadata + + @_spi(Private) @objc public static var sdkName: String { SentrySDK.internal.sdk.name } + + @_spi(Private) @objc public static var sdkVersionString: String { SentrySDK.internal.sdk.versionString } + + @_spi(Private) @objc public static func setSdkName(_ name: String, version: String) { + SentrySDK.internal.sdk.setName(name, version: version) + } + + @_spi(Private) @objc public static func addSdkPackage(_ name: String, version: String) { + SentrySDK.internal.sdk.addPackage(name: name, version: version) + } + + @_spi(Private) @objc public static var extraContext: [String: Any] { + SentrySDK.internal.sdk.extraContext + } + + @_spi(Private) @objc public static var installationID: String { + SentrySDK.internal.sdk.installationID + } + + // MARK: - Options + + @_spi(Private) @objc public static var options: Options { SentrySDK.internal.options } + + @_spi(Private) @objc public static func options(fromDictionary dict: [String: Any]) throws -> Options { + try SentrySDK.internal.options(fromDictionary: dict) + } + + // MARK: - App start + + @_spi(Private) @objc public static var appStartMeasurementHybridSDKMode: Bool { + get { SentrySDK.internal.appStart.hybridSDKMode } + set { SentrySDK.internal.appStart.hybridSDKMode = newValue } + } + + @_spi(Private) @objc public static var appStartMeasurementWithSpans: [String: Any]? { + SentrySDK.internal.appStart.measurementWithSpans + } + + // MARK: - Performance / frames + + #if os(iOS) || os(tvOS) || os(visionOS) + @_spi(Private) @objc public static var framesTrackingMeasurementHybridSDKMode: Bool { + get { SentrySDK.internal.performance.framesTrackingHybridSDKMode } + set { SentrySDK.internal.performance.framesTrackingHybridSDKMode = newValue } + } + + @_spi(Private) @objc public static var isFramesTrackingRunning: Bool { + SentrySDK.internal.performance.isFramesTrackingRunning + } + + @_spi(Private) @objc public static var currentScreenFrames: SentryScreenFrames? { + SentrySDK.internal.performance.currentScreenFrames + } + #else + @_spi(Private) @objc public static var framesTrackingMeasurementHybridSDKMode: Bool { + get { false } + set {} + } + + @_spi(Private) @objc public static var isFramesTrackingRunning: Bool { false } + + @_spi(Private) @objc public static var currentScreenFrames: SentryScreenFrames? { nil } + #endif + + // MARK: - Envelope + + // Accepts `Data?` (nil-safe) rather than `Data` so the ObjC bridge boundary + // doesn't force-unwrap a nil `NSData*` from a failed base64 decode — that + // would crash before we ever get a chance to check the result. Matches the + // nil-tolerant behaviour of the deprecated `PrivateSentrySDKOnly.envelopeWithData:`. + @_spi(Private) @objc public static func envelope(fromData data: Data?) -> SentryEnvelope? { + guard let data = data else { return nil } + return SentrySDK.internal.envelope.deserialize(from: data) + } + + @_spi(Private) @objc public static func capture(_ envelope: SentryEnvelope) { + SentrySDK.internal.envelope.capture(envelope) + } + + @_spi(Private) @objc public static func store(_ envelope: SentryEnvelope) { + SentrySDK.internal.envelope.store(envelope) + } + + // MARK: - Screenshot / view hierarchy / screen + + // sentry-cocoa's `SentryInternalScreen/Screenshot/ViewHierarchyApi` are all + // gated to `(os(iOS) || os(tvOS)) && !SENTRY_NO_UI_FRAMEWORK`. On visionOS + // the new API surface is intentionally absent — so `setCurrentScreen` is a + // no-op there, unlike the deprecated `PrivateSentrySDKOnly.setCurrentScreen` + // (which worked under `SENTRY_UIKIT_AVAILABLE`, i.e. also on visionOS). + // Callers on visionOS lose the current-screen breadcrumb enrichment as a + // result; this matches the direction of sentry-cocoa's new hybrid API. + #if os(iOS) || os(tvOS) + @_spi(Private) @objc public static var captureScreenshots: [Data]? { + SentrySDK.internal.screenshot.capture() + } + + @_spi(Private) @objc public static var captureViewHierarchy: Data? { + SentrySDK.internal.viewHierarchy.capture() + } + + @_spi(Private) @objc public static func setCurrentScreen(_ screenName: String?) { + SentrySDK.internal.screen.setCurrent(screenName) + } + #else + @_spi(Private) @objc public static var captureScreenshots: [Data]? { nil } + @_spi(Private) @objc public static var captureViewHierarchy: Data? { nil } + @_spi(Private) @objc public static func setCurrentScreen(_ screenName: String?) {} + #endif + + // MARK: - Replay + + #if os(iOS) || os(tvOS) + @_spi(Private) @objc public static func captureReplay() -> Bool { + SentrySDK.internal.replay.capture() + } + + @_spi(Private) @objc public static var replayId: String? { + SentrySDK.internal.replay.replayId + } + + @_spi(Private) @objc public static func setReplayRedactContainerClass(_ containerClass: AnyClass) { + SentrySDK.internal.replay.setRedactContainerClass(containerClass) + } + + @_spi(Private) @objc public static func setReplayIgnoreContainerClass(_ containerClass: AnyClass) { + SentrySDK.internal.replay.setIgnoreContainerClass(containerClass) + } + + @_spi(Private) @objc public static func configureReplay( + breadcrumbConverter: SentryReplayBreadcrumbConverter + ) { + SentrySDK.internal.replay.configure( + breadcrumbConverter: breadcrumbConverter, + screenshotProvider: nil + ) + } + #else + @_spi(Private) @objc public static func captureReplay() -> Bool { false } + @_spi(Private) @objc public static var replayId: String? { nil } + @_spi(Private) @objc public static func setReplayRedactContainerClass(_ containerClass: AnyClass) {} + @_spi(Private) @objc public static func setReplayIgnoreContainerClass(_ containerClass: AnyClass) {} + @_spi(Private) @objc public static func configureReplay( + breadcrumbConverter: SentryReplayBreadcrumbConverter + ) {} + #endif + + // MARK: - Swizzle + + // `SentryInternalSwizzleApi` in sentry-cocoa has no platform gating. + // We enable the wrapper on every UIKit-capable platform (iOS/tvOS/visionOS + // — matching `SENTRY_UIKIT_AVAILABLE`, which is what `SENTRY_HAS_UIKIT` + // resolves to in the RNSentry.mm caller). `os(iOS)` in Swift already + // covers Mac Catalyst, so no separate `targetEnvironment(macCatalyst)` is + // needed. + #if os(iOS) || os(tvOS) || os(visionOS) + /// Stable identity for the `RNSScreen.viewDidAppear:` swizzle so the underlying + /// `oncePerClass` bookkeeping in sentry-cocoa can dedupe re-entries. + private static var rnsScreenViewDidAppearKey: UInt8 = 0 + + /// Swizzles `-[RNSScreen viewDidAppear:]`, invoking `hook` before the original + /// implementation on every call. No-op when RNSScreen is unavailable. + @_spi(Private) @objc public static func swizzleRNSScreenViewDidAppear(hook: @escaping () -> Void) { + guard let cls = NSClassFromString("RNSScreen") else { return } + let selector = NSSelectorFromString("viewDidAppear:") + + // `withUnsafePointer(to:)` scopes the pointer's validity to the closure + // body. Perform the entire swizzle call inside so we never rely on the + // pointer surviving beyond the closure. The backing storage is a + // `static var`, so the address itself stays stable across calls — + // sentry-cocoa's `oncePerClass` bookkeeping continues to dedupe. + withUnsafePointer(to: &rnsScreenViewDidAppearKey) { keyPtr in + _ = SentrySDK.internal.swizzle.instanceMethod( + selector, + in: cls, + mode: .oncePerClass, + key: UnsafeRawPointer(keyPtr), + factory: { getOriginal in + let block: @convention(block) (AnyObject, ObjCBool) -> Void = { receiver, animated in + hook() + typealias OriginalIMP = @convention(c) (AnyObject, Selector, ObjCBool) -> Void + let original = unsafeBitCast(getOriginal(), to: OriginalIMP.self) + original(receiver, selector, animated) + } + return block as AnyObject + } + ) + } + } + #else + @_spi(Private) @objc public static func swizzleRNSScreenViewDidAppear(hook: @escaping () -> Void) {} + #endif + + // MARK: - Profiling + + #if !(os(watchOS) || os(tvOS) || os(visionOS)) + @_spi(Private) @objc public static func startProfiler(forTrace traceId: SentryId) -> UInt64 { + SentrySDK.internal.profiling.start(for: traceId) + } + + @_spi(Private) @objc(collectProfileBetween:and:forTrace:) + public static func collectProfile( + between startTime: UInt64, + and endTime: UInt64, + forTrace traceId: SentryId + ) -> [String: Any]? { + SentrySDK.internal.profiling.collect(between: startTime, and: endTime, for: traceId) + } + + @_spi(Private) @objc public static func discardProfiler(forTrace traceId: SentryId) { + SentrySDK.internal.profiling.discard(for: traceId) + } + #else + @_spi(Private) @objc public static func startProfiler(forTrace traceId: SentryId) -> UInt64 { 0 } + @_spi(Private) @objc public static func collectProfile( + between startTime: UInt64, + and endTime: UInt64, + forTrace traceId: SentryId + ) -> [String: Any]? { nil } + @_spi(Private) @objc public static func discardProfiler(forTrace traceId: SentryId) {} + #endif +} diff --git a/packages/core/ios/RNSentryRNSScreen.m b/packages/core/ios/RNSentryRNSScreen.m index 91bbb58c08..52c3658fba 100644 --- a/packages/core/ios/RNSentryRNSScreen.m +++ b/packages/core/ios/RNSentryRNSScreen.m @@ -2,31 +2,21 @@ #if SENTRY_HAS_UIKIT -# import "RNSentryDependencyContainer.h" -# import "RNSentryFramesTrackerListener.h" -# if __has_include() -# import +# if __has_include() +# import # else -# import "SentrySwizzle.h" +# import "RNSentry-Swift.h" # endif -@import Sentry; +# import "RNSentryDependencyContainer.h" +# import "RNSentryFramesTrackerListener.h" @implementation RNSentryRNSScreen + (void)swizzleViewDidAppear { - Class rnsscreenclass = NSClassFromString(@"RNSScreen"); - if (rnsscreenclass == nil) { - return; - } - - SEL selector = NSSelectorFromString(@"viewDidAppear:"); - SentrySwizzleInstanceMethod(rnsscreenclass, selector, SentrySWReturnType(void), - SentrySWArguments(BOOL animated), SentrySWReplacement({ - [[[RNSentryDependencyContainer sharedInstance] framesTrackerListener] startListening]; - SentrySWCallOriginal(animated); - }), - SentrySwizzleModeOncePerClass, (void *)selector); + [RNSentryInternal swizzleRNSScreenViewDidAppearWithHook:^{ + [[[RNSentryDependencyContainer sharedInstance] framesTrackerListener] startListening]; + }]; } @end diff --git a/packages/core/ios/RNSentryReplay.mm b/packages/core/ios/RNSentryReplay.mm index 2c3273687e..60c4cfe93f 100644 --- a/packages/core/ios/RNSentryReplay.mm +++ b/packages/core/ios/RNSentryReplay.mm @@ -1,10 +1,14 @@ #import "RNSentryReplay.h" +#if __has_include() +# import +#else +# import "RNSentry-Swift.h" +#endif #import "RNSentryReplayBreadcrumbConverterHelper.h" #import "RNSentryReplayQuality.h" #import "RNSentryVersion.h" #import "Replay/RNSentryReplayMask.h" #import "Replay/RNSentryReplayUnmask.h" -#import #if SENTRY_TARGET_REPLAY_SUPPORTED @@ -76,8 +80,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 - [PrivateSentrySDKOnly setRedactContainerClass:[RNSentryReplay getMaskClass]]; - [PrivateSentrySDKOnly setIgnoreContainerClass:[RNSentryReplay getUnmaskClass]]; + [RNSentryInternal setReplayRedactContainerClass:[RNSentryReplay getMaskClass]]; + [RNSentryInternal setReplayIgnoreContainerClass:[RNSentryReplay getUnmaskClass]]; [RNSentryReplayBreadcrumbConverterHelper configureSessionReplayWithConverter]; } diff --git a/packages/core/ios/RNSentryReplayBreadcrumbConverterHelper.m b/packages/core/ios/RNSentryReplayBreadcrumbConverterHelper.m index ef3da7ec38..08ace7cc7a 100644 --- a/packages/core/ios/RNSentryReplayBreadcrumbConverterHelper.m +++ b/packages/core/ios/RNSentryReplayBreadcrumbConverterHelper.m @@ -1,6 +1,11 @@ #import "RNSentryReplayBreadcrumbConverterHelper.h" #if SENTRY_TARGET_REPLAY_SUPPORTED +# if __has_include() +# import +# else +# import "RNSentry-Swift.h" +# endif # import "RNSentryReplayBreadcrumbConverter.h" @implementation RNSentryReplayBreadcrumbConverterHelper @@ -9,7 +14,7 @@ + (void)configureSessionReplayWithConverter { RNSentryReplayBreadcrumbConverter *breadcrumbConverter = [[RNSentryReplayBreadcrumbConverter alloc] init]; - [PrivateSentrySDKOnly configureSessionReplayWith:breadcrumbConverter screenshotProvider:nil]; + [RNSentryInternal configureReplayWithBreadcrumbConverter:breadcrumbConverter]; } @end diff --git a/packages/core/ios/RNSentrySDK.m b/packages/core/ios/RNSentrySDK.m index 0f38cf6c7b..1f9de9fe52 100644 --- a/packages/core/ios/RNSentrySDK.m +++ b/packages/core/ios/RNSentrySDK.m @@ -1,6 +1,10 @@ #import "RNSentrySDK.h" +#if __has_include() +# import +#else +# import "RNSentry-Swift.h" +#endif #import "RNSentryStart.h" -#import #import static NSString *SENTRY_OPTIONS_RESOURCE_NAME = @"sentry.options"; @@ -60,7 +64,7 @@ + (void)start:(NSString *)path configureOptions:(void (^)(SentryOptions *options if (options == nil) { // Fallback in case that options file could not be parsed. NSError *fallbackError = nil; - options = [PrivateSentrySDKOnly optionsWithDictionary:@{ } didFailWithError:&fallbackError]; + options = [RNSentryInternal optionsFromDictionary:@{ } error:&fallbackError]; if (fallbackError != nil) { NSLog(@"[RNSentry] Failed to create fallback options with error: %@", fallbackError.localizedDescription); diff --git a/packages/core/ios/RNSentryStart.m b/packages/core/ios/RNSentryStart.m index fa463f116e..ce365badea 100644 --- a/packages/core/ios/RNSentryStart.m +++ b/packages/core/ios/RNSentryStart.m @@ -3,7 +3,11 @@ #import "RNSentryReplay.h" #import "RNSentryVersion.h" -#import +#if __has_include() +# import +#else +# import "RNSentry-Swift.h" +#endif #import @import Sentry; @@ -31,14 +35,14 @@ + (void)startWithOptions:(SentryOptions *)options NS_SWIFT_NAME(start(options:)) + (void)startWithOptions:(SentryOptions *)options jsSdkVersion:(NSString *_Nullable)jsSdkVersion { - NSString *sdkVersion = [PrivateSentrySDKOnly getSdkVersionString]; - [PrivateSentrySDKOnly setSdkName:NATIVE_SDK_NAME andVersionString:sdkVersion]; - [PrivateSentrySDKOnly addSdkPackage:REACT_NATIVE_SDK_PACKAGE_NAME - version:REACT_NATIVE_SDK_PACKAGE_VERSION]; + NSString *sdkVersion = [RNSentryInternal sdkVersionString]; + [RNSentryInternal setSdkName:NATIVE_SDK_NAME version:sdkVersion]; + [RNSentryInternal addSdkPackage:REACT_NATIVE_SDK_PACKAGE_NAME + version:REACT_NATIVE_SDK_PACKAGE_VERSION]; if (jsSdkVersion != nil && ![jsSdkVersion isEqualToString:REACT_NATIVE_SDK_PACKAGE_VERSION]) { NSString *otaPackageName = [REACT_NATIVE_SDK_PACKAGE_NAME stringByAppendingString:@":ota"]; - [PrivateSentrySDKOnly addSdkPackage:otaPackageName version:jsSdkVersion]; + [RNSentryInternal addSdkPackage:otaPackageName version:jsSdkVersion]; } [SentrySDK startWithOptions:options]; @@ -62,8 +66,8 @@ + (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull) [RNSentryReplay updateOptions:mutableOptions]; #endif - SentryOptions *sentryOptions = [PrivateSentrySDKOnly optionsWithDictionary:mutableOptions - didFailWithError:errorPointer]; + SentryOptions *sentryOptions = [RNSentryInternal optionsFromDictionary:mutableOptions + error:errorPointer]; if (*errorPointer != nil) { return nil; } @@ -237,12 +241,11 @@ + (void)updateWithReactFinals:(SentryOptions *)options // App Start Hybrid mode doesn't wait for didFinishLaunchNotification and the // didBecomeVisibleNotification as they will be missed when auto initializing from JS // App Start measurements are created right after the tracking starts - PrivateSentrySDKOnly.appStartMeasurementHybridSDKMode = options.enableAutoPerformanceTracing; + RNSentryInternal.appStartMeasurementHybridSDKMode = options.enableAutoPerformanceTracing; #if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST // Frames Tracking Hybrid Mode ensures tracking // is enabled without tracing enabled in the native SDK - PrivateSentrySDKOnly.framesTrackingMeasurementHybridSDKMode - = options.enableAutoPerformanceTracing; + RNSentryInternal.framesTrackingMeasurementHybridSDKMode = options.enableAutoPerformanceTracing; #endif } @@ -301,8 +304,8 @@ + (void)postDidBecomeActiveNotification // If the app is active/in foreground, and we have not sent the SentryHybridSdkDidBecomeActive // notification, send it. if (appIsActive && !sentHybridSdkDidBecomeActive - && (PrivateSentrySDKOnly.options.enableAutoSessionTracking - || PrivateSentrySDKOnly.options.enableWatchdogTerminationTracking)) { + && (RNSentryInternal.options.enableAutoSessionTracking + || RNSentryInternal.options.enableWatchdogTerminationTracking)) { // Updates Native App State Manager // https://github.com/getsentry/sentry-cocoa/blob/888a145b144b8077e03151a886520f332e47e297/Sources/Sentry/SentryAppStateManager.m#L136 // Triggers Session Tracker diff --git a/packages/core/ios/SentrySDKWrapper.m b/packages/core/ios/SentrySDKWrapper.m index 9aa5a62493..881d3bc69b 100644 --- a/packages/core/ios/SentrySDKWrapper.m +++ b/packages/core/ios/SentrySDKWrapper.m @@ -1,4 +1,9 @@ #import "SentrySDKWrapper.h" +#if __has_include() +# import +#else +# import "RNSentry-Swift.h" +#endif @import Sentry; @implementation SentrySDKWrapper @@ -35,12 +40,12 @@ + (void)configureScope:(void (^)(SentryScope *scope))callback + (BOOL)debug { - return PrivateSentrySDKOnly.options.debug; + return RNSentryInternal.options.debug; } + (NSString *)releaseName { - return PrivateSentrySDKOnly.options.releaseName; + return RNSentryInternal.options.releaseName; } @end diff --git a/packages/core/ios/SentryScreenFramesWrapper.m b/packages/core/ios/SentryScreenFramesWrapper.m index dd79c5a214..e7db337459 100644 --- a/packages/core/ios/SentryScreenFramesWrapper.m +++ b/packages/core/ios/SentryScreenFramesWrapper.m @@ -1,4 +1,9 @@ #import "SentryScreenFramesWrapper.h" +#if __has_include() +# import +#else +# import "RNSentry-Swift.h" +#endif @import Sentry; #if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST @@ -7,7 +12,7 @@ @implementation SentryScreenFramesWrapper + (BOOL)canTrackFrames { - return PrivateSentrySDKOnly.currentScreenFrames != nil; + return RNSentryInternal.currentScreenFrames != nil; } + (NSNumber *)totalFrames @@ -15,7 +20,7 @@ + (NSNumber *)totalFrames if (![self canTrackFrames]) { return nil; } - return [NSNumber numberWithLong:PrivateSentrySDKOnly.currentScreenFrames.total]; + return [NSNumber numberWithLong:RNSentryInternal.currentScreenFrames.total]; } + (NSNumber *)frozenFrames @@ -23,7 +28,7 @@ + (NSNumber *)frozenFrames if (![self canTrackFrames]) { return nil; } - return [NSNumber numberWithLong:PrivateSentrySDKOnly.currentScreenFrames.frozen]; + return [NSNumber numberWithLong:RNSentryInternal.currentScreenFrames.frozen]; } + (NSNumber *)slowFrames @@ -31,7 +36,7 @@ + (NSNumber *)slowFrames if (![self canTrackFrames]) { return nil; } - return [NSNumber numberWithLong:PrivateSentrySDKOnly.currentScreenFrames.slow]; + return [NSNumber numberWithLong:RNSentryInternal.currentScreenFrames.slow]; } + (NSNumber *)framesDelayForStartTimestamp:(double)startTimestampSeconds