From bf0599b82f73ecc2918f78d7eedc9b4797f9528d Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Thu, 30 Jul 2026 13:27:22 +0200 Subject: [PATCH 1/2] fix(ios): Re-land SentrySDK.internal migration, bump Cocoa SDK to 9.24.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reapplies #6380 (reverted in 8.20.0 by #6491) now that the underlying sentry-cocoa bug is fixed. #6380 migrated iOS code from the deprecated PrivateSentrySDKOnly SPI to the SentrySDK.internal Swift API. It shipped in 8.19.0 and broke every iOS screenshot (Feedback Widget screenshot, attachScreenshot, Sentry.captureScreenshot) — #6497. Root cause was in sentry-cocoa: constructing SentryInternalApi eagerly reads SentryDependencyContainer.screenshotSource, a lazy var whose builder returns nil while startOptions is unset. Being lazy, that nil was cached for the process lifetime, and RN reads SentrySDK.internal before SentrySDK.start (from RNSentryStart, and from JS integrations calling fetchNativeAppStart / fetchNativeSdkInfo during init). getsentry/sentry-cocoa#8578 makes screenshotSource a computed property so the builder re-runs until startOptions is available; it shipped in 9.24.0, which this bumps to. Adds RNSentryScreenshotSourceTests as a regression guard: it reads SentrySDK.internal before starting the SDK and asserts screenshot capture still works. Verified to fail against 9.19.1 and pass against 9.24.0. Re-landing also restores the two improvements the revert removed: the fetchViewHierarchy nil-guard (returns null instead of a zero-byte attachment on capture failure) and the RNSentryTimeToDisplay extern linkage. Fixes #6507. --- CHANGELOG.md | 21 ++ .../patch-scripts/rn.patch.podfile.js | 21 +- packages/core/RNSentry.podspec | 28 +- .../project.pbxproj | 4 + .../RNSentryReplayOptionsTests.swift | 42 +-- .../RNSentryScreenshotSourceTests.swift | 47 ++++ .../RNSentryStartFromFileTests.swift | 11 +- .../RNSentryStartTests.swift | 29 +- packages/core/ios/AGENTS.md | 12 +- packages/core/ios/RNSentry+fetchNativeStack.m | 1 - packages/core/ios/RNSentry.mm | 110 +++----- packages/core/ios/RNSentryInternal.swift | 259 ++++++++++++++++++ packages/core/ios/RNSentryRNSScreen.m | 26 +- packages/core/ios/RNSentryReplay.mm | 10 +- .../RNSentryReplayBreadcrumbConverterHelper.m | 7 +- packages/core/ios/RNSentrySDK.m | 8 +- packages/core/ios/RNSentryStart.m | 29 +- packages/core/ios/RNSentryTimeToDisplay.h | 8 +- packages/core/ios/RNSentryTimeToDisplay.m | 2 + packages/core/ios/SentrySDKWrapper.m | 9 +- packages/core/ios/SentryScreenFramesWrapper.m | 13 +- packages/core/scripts/sentry_utils.rb | 4 +- 22 files changed, 525 insertions(+), 176 deletions(-) create mode 100644 packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryScreenshotSourceTests.swift create mode 100644 packages/core/ios/RNSentryInternal.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 4390d1fb4e..b41147fc01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,27 @@ > make sure you follow our [migration guide](https://docs.sentry.io/platforms/react-native/migration/) first. +## Unreleased + +### Changes + +- Migrate iOS code from the deprecated `PrivateSentrySDKOnly` SPI (and `SentrySwizzle.h` macro) to the new `SentrySDK.internal` Swift API, via a thin in-pod ObjC↔Swift bridge ([#6540](https://github.com/getsentry/sentry-react-native/pull/6540)) + + This re-lands [#6380](https://github.com/getsentry/sentry-react-native/pull/6380), which shipped in 8.19.0 and was reverted in 8.20.0 ([#6491](https://github.com/getsentry/sentry-react-native/pull/6491)) because it broke all iOS screenshot capture ([#6497](https://github.com/getsentry/sentry-react-native/issues/6497)). The root cause was in sentry-cocoa — `SentryDependencyContainer.screenshotSource` was a `lazy var` that permanently cached `nil` when first read before `SentrySDK.start` — and is fixed in sentry-cocoa 9.24.0 ([getsentry/sentry-cocoa#8578](https://github.com/getsentry/sentry-cocoa/pull/8578)), which this release bumps to. + + Re-landing also restores the two improvements the revert removed: `fetchViewHierarchy` now returns `null` instead of a zero-byte attachment when capture fails, and `RNSentryTimeToDisplay`'s shared state uses `extern` linkage. + + +> [!IMPORTANT] +> **iOS:** 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. This is required because the `RNSentry` pod now contains Swift code (the `RNSentryInternal` bridge over `SentrySDK.internal`) and CocoaPods refuses to integrate a Swift pod against non-modular ObjC dependencies. Newer React Native versions require no change. + + +### Dependencies + +- Bump Cocoa SDK from v9.19.1 to v9.24.0 ([#6540](https://github.com/getsentry/sentry-react-native/pull/6540)) + - [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#9240) + - [diff](https://github.com/getsentry/sentry-cocoa/compare/9.19.1...9.24.0) + ## 8.21.0 ### Features 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 4162b140dd..70f5a136d3 100644 --- a/packages/core/RNSentry.podspec +++ b/packages/core/RNSentry.podspec @@ -46,23 +46,15 @@ Pod::Spec.new do |s| # in `android/CMakeLists.txt`. The files are guarded with # `RCT_NEW_ARCH_ENABLED` so they compile to empty TUs on Old Arch. # - # We include `.swift` (for `RNSentrySwiftLinkStub.swift`) only on RN >= - # 0.75. Adding a Swift file makes CocoaPods treat RNSentry as a Swift - # pod, which then requires modular headers from its ObjC dependencies - # (React-Core, React-hermes) — RN < 0.75 doesn't emit those, so - # `pod install` fails with: - # "The Swift pod `RNSentry` depends upon `React-hermes`, which does - # not define modules." - # The stub is only needed when linking Sentry.xcframework's Swift - # symbols into a dynamic framework anyway (RN 0.86+ `use_frameworks! - # :dynamic`), so gating on RN 0.75 is safe. - supports_swift_stub = rn_version[:major] >= 1 || (rn_version[:major] == 0 && rn_version[:minor] >= 75) - if supports_swift_stub - s.source_files = 'ios/**/*.{h,m,mm,swift}', 'cpp/**/*.{h,cpp}' - s.swift_versions = ['5.5'] - else - s.source_files = 'ios/**/*.{h,m,mm}', 'cpp/**/*.{h,cpp}' - end + # Swift is compiled unconditionally because `RNSentryInternal.swift` is + # the sole ObjC↔Swift bridge over `SentrySDK.internal.*` — every `.m`/ + # `.mm` file in this pod calls into it. That makes RNSentry a Swift pod + # in CocoaPods' eyes, which in turn requires modular headers from its + # ObjC dependencies. Users on React Native < 0.75 (where `React-hermes` + # 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}' + 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 @@ -71,7 +63,7 @@ Pod::Spec.new do |s| 'DEFINES_MODULE' => 'YES' } - sentry_cocoa_version = '9.19.1' + sentry_cocoa_version = '9.24.0' # Consume sentry-cocoa as a prebuilt `Sentry.xcframework` by default. # diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj b/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj index 8c47e431b6..508193ce80 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj @@ -17,6 +17,7 @@ 33DEDFED2D8DC825006066E4 /* RNSentryOnDrawReporter+Test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 33DEDFEC2D8DC820006066E4 /* RNSentryOnDrawReporter+Test.mm */; }; 33DEDFF02D9185EB006066E4 /* RNSentryTimeToDisplayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33DEDFEF2D9185E3006066E4 /* RNSentryTimeToDisplayTests.swift */; }; 33F58AD02977037D008F60EA /* RNSentryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33F58ACF2977037D008F60EA /* RNSentryTests.m */; }; + 33F58AD12977037D008F60EB /* RNSentryScreenshotSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33F58ACE2977037D008F60EB /* RNSentryScreenshotSourceTests.swift */; }; 2639D71D3BD04F17B0BAC987 /* RNSentryTurboModulePerfControllerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = E795057A6D534A80A9D06356 /* RNSentryTurboModulePerfControllerTests.mm */; }; AEFB00422CC90C4B00EC8A9A /* RNSentryBreadcrumbTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3360843C2C340C76008CC412 /* RNSentryBreadcrumbTests.swift */; }; B4DEB41739F14AA38202D4D4 /* RNSentryUriValidationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3E3742693F7643C2ADE1BDF2 /* RNSentryUriValidationTests.m */; }; @@ -50,6 +51,7 @@ 33DEDFEE2D8DD431006066E4 /* RNSentryTimeToDisplay.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNSentryTimeToDisplay.h; path = ../ios/RNSentryTimeToDisplay.h; sourceTree = SOURCE_ROOT; }; 33DEDFEF2D9185E3006066E4 /* RNSentryTimeToDisplayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNSentryTimeToDisplayTests.swift; sourceTree = ""; }; 33F58ACF2977037D008F60EA /* RNSentryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNSentryTests.m; sourceTree = ""; }; + 33F58ACE2977037D008F60EB /* RNSentryScreenshotSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNSentryScreenshotSourceTests.swift; sourceTree = ""; }; 3E3742693F7643C2ADE1BDF2 /* RNSentryUriValidationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSentryUriValidationTests.m; sourceTree = ""; }; 650CB718ACFBD05609BF2126 /* libPods-RNSentryCocoaTesterTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RNSentryCocoaTesterTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; E795057A6D534A80A9D06356 /* RNSentryTurboModulePerfControllerTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = RNSentryTurboModulePerfControllerTests.mm; sourceTree = ""; }; @@ -111,6 +113,7 @@ 332D33492CDCC8E100547D76 /* RNSentryTests.h */, 336084382C32E382008CC412 /* RNSentryReplayBreadcrumbConverterTests.swift */, 33F58ACF2977037D008F60EA /* RNSentryTests.m */, + 33F58ACE2977037D008F60EB /* RNSentryScreenshotSourceTests.swift */, 3339C4802D6625570088EB3A /* RNSentryUserTests.m */, 3E3742693F7643C2ADE1BDF2 /* RNSentryUriValidationTests.m */, E795057A6D534A80A9D06356 /* RNSentryTurboModulePerfControllerTests.mm */, @@ -267,6 +270,7 @@ 336084392C32E382008CC412 /* RNSentryReplayBreadcrumbConverterTests.swift in Sources */, 33DEDFED2D8DC825006066E4 /* RNSentryOnDrawReporter+Test.mm in Sources */, 33F58AD02977037D008F60EA /* RNSentryTests.m in Sources */, + 33F58AD12977037D008F60EB /* RNSentryScreenshotSourceTests.swift in Sources */, 3339C4812D6625570088EB3A /* RNSentryUserTests.m in Sources */, B4DEB41739F14AA38202D4D4 /* RNSentryUriValidationTests.m in Sources */, 2639D71D3BD04F17B0BAC987 /* RNSentryTurboModulePerfControllerTests.mm in Sources */, 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/RNSentryScreenshotSourceTests.swift b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryScreenshotSourceTests.swift new file mode 100644 index 0000000000..503685a3b7 --- /dev/null +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryScreenshotSourceTests.swift @@ -0,0 +1,47 @@ +@_spi(Private) import Sentry +import XCTest + +/// Guards the iOS screenshot regression from 8.19.0 (#6497), which forced the revert +/// of the `SentrySDK.internal` migration (#6380) in 8.20.0. +/// +/// RN reads `SentrySDK.internal` before `SentrySDK.start`: `RNSentryStart` does it +/// itself, and JS integrations call native methods (`fetchNativeAppStart`, +/// `fetchNativeSdkInfo`) while the SDK is still initializing. Constructing +/// `SentryInternalApi` eagerly reads `SentryDependencyContainer.screenshotSource`, +/// which used to be a `lazy var` that permanently cached the `nil` its builder returns +/// while `startOptions` is unset. That killed every iOS screenshot for the rest of the +/// process — Feedback Widget screenshot, `attachScreenshot` and +/// `Sentry.captureScreenshot()` all silently returned nothing. +/// +/// Fixed in sentry-cocoa 9.24.0 (getsentry/sentry-cocoa#8578) by turning +/// `screenshotSource` into a computed property that rebuilds until `startOptions` +/// is available. This test fails against any earlier version. +final class RNSentryScreenshotSourceTests: XCTestCase { + + func testScreenshotCaptureSurvivesInternalApiAccessBeforeStart() throws { + // Model a process that has not started the SDK yet. + SentrySDK.close() + SentryDependencyContainer.reset() + // `reset()` deliberately carries `startOptions` over to the new container, so + // clear it explicitly. + SentrySDK.setStart(with: nil) + + // The pre-start access that used to poison the screenshot source. + _ = SentrySDK.internal + + var error: NSError? + RNSentryStart.start( + options: [ + "dsn": "https://abcd@efgh.ingest.sentry.io/123456", + "attachScreenshot": true + ], + error: &error) + if let error = error { + throw error + } + + XCTAssertNotNil( + SentrySDK.internal.screenshot.capture(), + "screenshot capture must still work after SentrySDK.internal was read before SentrySDK.start") + } +} 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..94859b44d6 --- /dev/null +++ b/packages/core/ios/RNSentryInternal.swift @@ -0,0 +1,259 @@ +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 } + + // `currentScreenFrames` is intentionally NOT declared on non-UIKit + // platforms: sentry-cocoa does not compile `SentryScreenFrames` there, + // so referencing it as a return type would fail to resolve. Every ObjC + // caller (`SentryScreenFramesWrapper.m`) is already gated to + // `TARGET_OS_IPHONE || TARGET_OS_MACCATALYST`, so no consumer needs the + // stub on macOS/watchOS. + #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 hybrid-SDK surface is intentionally absent, but the same + // functionality still lives on `PrivateSentrySDKOnly` (gated by + // `SENTRY_HAS_UIKIT`, which covers visionOS). Route the visionOS bridge + // through the deprecated SPI so we preserve pre-migration behaviour and + // keep this PR non-breaking. Remove the fallback once sentry-cocoa + // exposes these APIs on visionOS in the hybrid surface — or once cocoa + // drops `PrivateSentrySDKOnly` in a future major and forces our hand. + #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) + } + #elseif os(visionOS) + @_spi(Private) @objc public static var captureScreenshots: [Data]? { + PrivateSentrySDKOnly.captureScreenshots() + } + + @_spi(Private) @objc public static var captureViewHierarchy: Data? { + PrivateSentrySDKOnly.captureViewHierarchy() + } + + @_spi(Private) @objc public static func setCurrentScreen(_ screenName: String?) { + PrivateSentrySDKOnly.setCurrentScreen(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/RNSentryTimeToDisplay.h b/packages/core/ios/RNSentryTimeToDisplay.h index a29a8e1cd3..06e5132df8 100644 --- a/packages/core/ios/RNSentryTimeToDisplay.h +++ b/packages/core/ios/RNSentryTimeToDisplay.h @@ -1,6 +1,12 @@ #import -static const int TIME_TO_DISPLAY_ENTRIES_MAX_SIZE = 50; +// Declared `extern` (not `static`) so it gets external linkage and can be +// referenced from Swift after RNSentry became a Swift-containing pod +// (see `RNSentryInternal.swift`). With `static const` the constant has +// internal linkage per translation unit and Swift's module import fails +// to resolve it, breaking `RNSentryTimeToDisplayTests.swift` with +// `Undefined symbol: _TIME_TO_DISPLAY_ENTRIES_MAX_SIZE` at link time. +extern const int TIME_TO_DISPLAY_ENTRIES_MAX_SIZE; @interface RNSentryTimeToDisplay : NSObject diff --git a/packages/core/ios/RNSentryTimeToDisplay.m b/packages/core/ios/RNSentryTimeToDisplay.m index 9d31f41a25..d46dc23bdc 100644 --- a/packages/core/ios/RNSentryTimeToDisplay.m +++ b/packages/core/ios/RNSentryTimeToDisplay.m @@ -2,6 +2,8 @@ #import #import +const int TIME_TO_DISPLAY_ENTRIES_MAX_SIZE = 50; + // All static state below is accessed from the main thread (CADisplayLink, UI) and from the // React Native bridge / JS thread (setActiveSpanId, pop). Synchronize every access. @implementation RNSentryTimeToDisplay { 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 diff --git a/packages/core/scripts/sentry_utils.rb b/packages/core/scripts/sentry_utils.rb index 43a778c019..ee30f0ff7d 100644 --- a/packages/core/scripts/sentry_utils.rb +++ b/packages/core/scripts/sentry_utils.rb @@ -60,8 +60,8 @@ def is_new_hermes_runtime(rn_version) # `Sentry` module. `Sentry-Dynamic.xcframework` would ship the same # `Sentry.framework` inside but under a mismatched enclosing name, so # CocoaPods generates `-framework Sentry-Dynamic` and fails at link. - '9.19.1' => { - 'Sentry' => 'd6d545af17e49851cda2747b0f45cde78ce08ea37709dde5a956c6b4671224e8', + '9.24.0' => { + 'Sentry' => 'c530edd27b20f7c151e73d84a34ee03474e3d5ddab65ffe9d30366f80149668a', }, }.freeze From 91a34985b6343dce6888e95f0072712d4e2cf067 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Thu, 30 Jul 2026 13:31:23 +0200 Subject: [PATCH 2/2] docs(changelog): Point entries at the actual PR number --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b41147fc01..33f2a40b8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ ### Changes -- Migrate iOS code from the deprecated `PrivateSentrySDKOnly` SPI (and `SentrySwizzle.h` macro) to the new `SentrySDK.internal` Swift API, via a thin in-pod ObjC↔Swift bridge ([#6540](https://github.com/getsentry/sentry-react-native/pull/6540)) +- Migrate iOS code from the deprecated `PrivateSentrySDKOnly` SPI (and `SentrySwizzle.h` macro) to the new `SentrySDK.internal` Swift API, via a thin in-pod ObjC↔Swift bridge ([#6541](https://github.com/getsentry/sentry-react-native/pull/6541)) This re-lands [#6380](https://github.com/getsentry/sentry-react-native/pull/6380), which shipped in 8.19.0 and was reverted in 8.20.0 ([#6491](https://github.com/getsentry/sentry-react-native/pull/6491)) because it broke all iOS screenshot capture ([#6497](https://github.com/getsentry/sentry-react-native/issues/6497)). The root cause was in sentry-cocoa — `SentryDependencyContainer.screenshotSource` was a `lazy var` that permanently cached `nil` when first read before `SentrySDK.start` — and is fixed in sentry-cocoa 9.24.0 ([getsentry/sentry-cocoa#8578](https://github.com/getsentry/sentry-cocoa/pull/8578)), which this release bumps to. @@ -23,7 +23,7 @@ ### Dependencies -- Bump Cocoa SDK from v9.19.1 to v9.24.0 ([#6540](https://github.com/getsentry/sentry-react-native/pull/6540)) +- Bump Cocoa SDK from v9.19.1 to v9.24.0 ([#6541](https://github.com/getsentry/sentry-react-native/pull/6541)) - [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#9240) - [diff](https://github.com/getsentry/sentry-cocoa/compare/9.19.1...9.24.0)