From e1c292cf8946214c961acf9f03ed9ee7bf6db777 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Tue, 30 Jun 2026 13:48:54 +0200 Subject: [PATCH 01/13] refactor(ios): Migrate from PrivateSentrySDKOnly to SentrySDK.internal Replace the deprecated PrivateSentrySDKOnly SPI and SentrySwizzle.h macro with the new SentrySDK.internal Swift API exposed by sentry-cocoa 9.19.0, via a thin in-pod ObjC<->Swift bridge (RNSentryInternal.swift). Imports Sentry with @_spi(Private) for SPI-gated sub-APIs (performance, replay, envelope). Drops the performSelector workaround around getReplayIntegration since SentrySDK.internal.replay.capture() returns Bool directly. Closes #6370 Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 6 + packages/core/RNSentry.podspec | 3 +- packages/core/ios/AGENTS.md | 12 +- packages/core/ios/RNSentry+fetchNativeStack.m | 1 - packages/core/ios/RNSentry.mm | 100 +++----- packages/core/ios/RNSentryInternal.swift | 218 ++++++++++++++++++ packages/core/ios/RNSentryRNSScreen.m | 22 +- packages/core/ios/RNSentryReplay.mm | 6 +- .../RNSentryReplayBreadcrumbConverterHelper.m | 3 +- packages/core/ios/RNSentrySDK.m | 4 +- packages/core/ios/RNSentryStart.m | 25 +- packages/core/ios/SentrySDKWrapper.m | 5 +- packages/core/ios/SentryScreenFramesWrapper.m | 9 +- 13 files changed, 299 insertions(+), 115 deletions(-) create mode 100644 packages/core/ios/RNSentryInternal.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 37243643ca..e062e8d08c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ ## Unreleased +### 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)) + +## 8.16.0 + ### Features - Expose top-level `Sentry.setAttribute` and `Sentry.setAttributes` APIs ([#6354](https://github.com/getsentry/sentry-react-native/pull/6354)). diff --git a/packages/core/RNSentry.podspec b/packages/core/RNSentry.podspec index ea38efc95b..2d813c18d1 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/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..8b780c5393 100644 --- a/packages/core/ios/RNSentry.mm +++ b/packages/core/ios/RNSentry.mm @@ -16,9 +16,9 @@ # define SENTRY_TARGET_PROFILING_SUPPORTED 0 #endif +#import "RNSentry-Swift.h" #import "RNSentryBreadcrumb.h" #import "RNSentryId.h" -#import #import #import #import @@ -403,10 +403,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 +456,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 +494,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 +507,7 @@ - (void)handleShakeDetected } }]; - NSDictionary *extraContext = [PrivateSentrySDKOnly getExtraContext]; + NSDictionary *extraContext = [RNSentryInternal extraContext]; NSMutableDictionary *> *contexts = [serializedScope[@"context"] mutableCopy]; @@ -547,8 +544,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 +570,7 @@ - (void)handleShakeDetected { #if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST - if (PrivateSentrySDKOnly.isFramesTrackingRunning) { + if (RNSentryInternal.isFramesTrackingRunning) { if (![SentryScreenFramesWrapper canTrackFrames]) { resolve(nil); return; @@ -626,20 +622,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 +645,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 +678,7 @@ - (void)handleShakeDetected fetchViewHierarchy : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject) { #if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST - NSData *rawViewHierarchy = [PrivateSentrySDKOnly captureViewHierarchy]; + NSData *rawViewHierarchy = [RNSentryInternal captureViewHierarchy]; NSMutableArray *viewHierarchy = [NSMutableArray arrayWithCapacity:rawViewHierarchy.length]; const char *bytes = (char *)[rawViewHierarchy bytes]; @@ -733,7 +729,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 +812,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 +836,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 +926,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 +952,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 +966,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 +978,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 +998,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 +1055,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 +1067,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..6c33218063 --- /dev/null +++ b/packages/core/ios/RNSentryInternal.swift @@ -0,0 +1,218 @@ +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 + + @_spi(Private) @objc public static func envelope(fromData data: Data) -> SentryEnvelope? { + 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 + + #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 + + #if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) + /// 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:") + let keyPtr = withUnsafePointer(to: &rnsScreenViewDidAppearKey) { + UnsafeRawPointer($0) + } + + _ = SentrySDK.internal.swizzle.instanceMethod( + selector, + in: cls, + mode: .oncePerClass, + key: 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..edd1a1f6db 100644 --- a/packages/core/ios/RNSentryRNSScreen.m +++ b/packages/core/ios/RNSentryRNSScreen.m @@ -2,31 +2,17 @@ #if SENTRY_HAS_UIKIT +# import "RNSentry-Swift.h" # import "RNSentryDependencyContainer.h" # import "RNSentryFramesTrackerListener.h" -# if __has_include() -# import -# else -# import "SentrySwizzle.h" -# endif -@import Sentry; @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..162de3b141 100644 --- a/packages/core/ios/RNSentryReplay.mm +++ b/packages/core/ios/RNSentryReplay.mm @@ -1,10 +1,10 @@ #import "RNSentryReplay.h" +#import "RNSentry-Swift.h" #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 +76,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..d22c872d7e 100644 --- a/packages/core/ios/RNSentryReplayBreadcrumbConverterHelper.m +++ b/packages/core/ios/RNSentryReplayBreadcrumbConverterHelper.m @@ -1,6 +1,7 @@ #import "RNSentryReplayBreadcrumbConverterHelper.h" #if SENTRY_TARGET_REPLAY_SUPPORTED +# import "RNSentry-Swift.h" # import "RNSentryReplayBreadcrumbConverter.h" @implementation RNSentryReplayBreadcrumbConverterHelper @@ -9,7 +10,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..56e5e5cc20 100644 --- a/packages/core/ios/RNSentrySDK.m +++ b/packages/core/ios/RNSentrySDK.m @@ -1,6 +1,6 @@ #import "RNSentrySDK.h" +#import "RNSentry-Swift.h" #import "RNSentryStart.h" -#import #import static NSString *SENTRY_OPTIONS_RESOURCE_NAME = @"sentry.options"; @@ -60,7 +60,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..d724dbf4e2 100644 --- a/packages/core/ios/RNSentryStart.m +++ b/packages/core/ios/RNSentryStart.m @@ -3,7 +3,7 @@ #import "RNSentryReplay.h" #import "RNSentryVersion.h" -#import +#import "RNSentry-Swift.h" #import @import Sentry; @@ -31,14 +31,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 +62,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 +237,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 +300,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..f46a436419 100644 --- a/packages/core/ios/SentrySDKWrapper.m +++ b/packages/core/ios/SentrySDKWrapper.m @@ -1,4 +1,5 @@ #import "SentrySDKWrapper.h" +#import "RNSentry-Swift.h" @import Sentry; @implementation SentrySDKWrapper @@ -35,12 +36,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..05a12998fe 100644 --- a/packages/core/ios/SentryScreenFramesWrapper.m +++ b/packages/core/ios/SentryScreenFramesWrapper.m @@ -1,4 +1,5 @@ #import "SentryScreenFramesWrapper.h" +#import "RNSentry-Swift.h" @import Sentry; #if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST @@ -7,7 +8,7 @@ @implementation SentryScreenFramesWrapper + (BOOL)canTrackFrames { - return PrivateSentrySDKOnly.currentScreenFrames != nil; + return RNSentryInternal.currentScreenFrames != nil; } + (NSNumber *)totalFrames @@ -15,7 +16,7 @@ + (NSNumber *)totalFrames if (![self canTrackFrames]) { return nil; } - return [NSNumber numberWithLong:PrivateSentrySDKOnly.currentScreenFrames.total]; + return [NSNumber numberWithLong:RNSentryInternal.currentScreenFrames.total]; } + (NSNumber *)frozenFrames @@ -23,7 +24,7 @@ + (NSNumber *)frozenFrames if (![self canTrackFrames]) { return nil; } - return [NSNumber numberWithLong:PrivateSentrySDKOnly.currentScreenFrames.frozen]; + return [NSNumber numberWithLong:RNSentryInternal.currentScreenFrames.frozen]; } + (NSNumber *)slowFrames @@ -31,7 +32,7 @@ + (NSNumber *)slowFrames if (![self canTrackFrames]) { return nil; } - return [NSNumber numberWithLong:PrivateSentrySDKOnly.currentScreenFrames.slow]; + return [NSNumber numberWithLong:RNSentryInternal.currentScreenFrames.slow]; } + (NSNumber *)framesDelayForStartTimestamp:(double)startTimestampSeconds From ee0d89e31990f5aa9f303373bdd9141d012101fa Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Tue, 30 Jun 2026 14:28:32 +0200 Subject: [PATCH 02/13] test(ios): Migrate Swift Cocoa tests off PrivateSentrySDKOnly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move RNSentryStartTests, RNSentryStartFromFileTests, and RNSentryReplayOptionsTests to SentrySDK.internal.{options, options(fromDictionary:), appStart.hybridSDKMode, performance.framesTrackingHybridSDKMode}. The ObjC RNSentryTests.m keeps PrivateSentrySDKOnly for now — the test target is a separate consumer of the RNSentry static-lib pod and cannot see RNSentry-Swift.h without modulemap surgery. Worth revisiting when sentry-cocoa removes PrivateSentrySDKOnly in the next major. Co-Authored-By: Claude Opus 4.7 --- .../RNSentryReplayOptionsTests.swift | 40 +++++++++---------- .../RNSentryStartFromFileTests.swift | 11 ++--- .../RNSentryStartTests.swift | 29 +++++++------- 3 files changed, 41 insertions(+), 39 deletions(-) diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayOptionsTests.swift b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayOptionsTests.swift index 7d2c99c7b3..f56ed8de32 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayOptionsTests.swift +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayOptionsTests.swift @@ -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..feb66ba7d3 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartFromFileTests.swift +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartFromFileTests.swift @@ -1,3 +1,4 @@ +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..e98f43eb4c 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartTests.swift +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartTests.swift @@ -1,3 +1,4 @@ +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 From 330236b509ce2f37f0d75fa5f137ef1ced375a78 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Thu, 2 Jul 2026 09:22:51 +0200 Subject: [PATCH 03/13] fix(ios): Include visionOS in the RNSScreen swizzle bridge `SentryInternalSwizzleApi` in sentry-cocoa has no platform gating, and `SENTRY_HAS_UIKIT` (which guards the `[RNSentryRNSScreen swizzleViewDidAppear]` call in RNSentry.mm) covers visionOS. The Swift bridge was gated to `iOS/tvOS/macCatalyst` only, silently no-oping the swizzle on visionOS where the deprecated `SentrySwizzle` macro used to work. Also documents why `setCurrentScreen` remains a visionOS no-op: sentry-cocoa's `SentryInternalScreenApi` is intentionally gated to iOS/tvOS, so the new API surface itself doesn't expose the setter on visionOS. Co-Authored-By: Claude Opus 4.7 --- packages/core/ios/RNSentryInternal.swift | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/core/ios/RNSentryInternal.swift b/packages/core/ios/RNSentryInternal.swift index 6c33218063..5fcb5a9aad 100644 --- a/packages/core/ios/RNSentryInternal.swift +++ b/packages/core/ios/RNSentryInternal.swift @@ -96,6 +96,13 @@ import Foundation // 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() @@ -153,7 +160,13 @@ import Foundation // MARK: - Swizzle - #if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) + // `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 From 29cd5091bf2f25e4a70f0f9bef1cf9d73abeb09b Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Mon, 6 Jul 2026 11:44:55 +0200 Subject: [PATCH 04/13] fix(ios): Scope RNSScreen swizzle key pointer inside withUnsafePointer `withUnsafePointer(to:)` only guarantees the pointer stays valid inside the closure body. Capturing it in `keyPtr` and using it outside the closure was undefined behaviour by contract (even though `static var` storage happens to be stable in practice). Wrap the entire `instanceMethod` call inside `withUnsafePointer` so the pointer is always used within its documented lifetime; the `static var` backing storage keeps the address itself stable across calls, so sentry-cocoa's `oncePerClass` dedup keeps working. Flagged by Cursor Bugbot. Co-Authored-By: Claude Opus 4.7 --- packages/core/ios/RNSentryInternal.swift | 38 +++++++++++++----------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/packages/core/ios/RNSentryInternal.swift b/packages/core/ios/RNSentryInternal.swift index 5fcb5a9aad..52799da58f 100644 --- a/packages/core/ios/RNSentryInternal.swift +++ b/packages/core/ios/RNSentryInternal.swift @@ -176,25 +176,29 @@ import Foundation @_spi(Private) @objc public static func swizzleRNSScreenViewDidAppear(hook: @escaping () -> Void) { guard let cls = NSClassFromString("RNSScreen") else { return } let selector = NSSelectorFromString("viewDidAppear:") - let keyPtr = withUnsafePointer(to: &rnsScreenViewDidAppearKey) { - UnsafeRawPointer($0) - } - _ = SentrySDK.internal.swizzle.instanceMethod( - selector, - in: cls, - mode: .oncePerClass, - key: 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) + // `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 } - return block as AnyObject - } - ) + ) + } } #else @_spi(Private) @objc public static func swizzleRNSScreenViewDidAppear(hook: @escaping () -> Void) {} From b0090c9746f86860a1fe2922ace301724c834e31 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Mon, 6 Jul 2026 11:47:07 +0200 Subject: [PATCH 05/13] fix(ios): Use conditional import for RNSentry-Swift.h Handle both static-lib and framework-style Pod integrations by falling back from `` (frameworks path) to the local `"RNSentry-Swift.h"` (static-lib path) via `__has_include`. Applied to every `.m`/`.mm` in packages/core/ios that consumes the Swift bridge. Suggested by @antonis. Co-Authored-By: Claude Opus 4.7 --- packages/core/ios/RNSentry.mm | 6 +++++- packages/core/ios/RNSentryRNSScreen.m | 6 +++++- packages/core/ios/RNSentryReplay.mm | 6 +++++- packages/core/ios/RNSentryReplayBreadcrumbConverterHelper.m | 6 +++++- packages/core/ios/RNSentrySDK.m | 6 +++++- packages/core/ios/RNSentryStart.m | 6 +++++- packages/core/ios/SentrySDKWrapper.m | 6 +++++- packages/core/ios/SentryScreenFramesWrapper.m | 6 +++++- 8 files changed, 40 insertions(+), 8 deletions(-) diff --git a/packages/core/ios/RNSentry.mm b/packages/core/ios/RNSentry.mm index 8b780c5393..986ce975bf 100644 --- a/packages/core/ios/RNSentry.mm +++ b/packages/core/ios/RNSentry.mm @@ -16,7 +16,11 @@ # define SENTRY_TARGET_PROFILING_SUPPORTED 0 #endif -#import "RNSentry-Swift.h" +#if __has_include() +# import +#else +# import "RNSentry-Swift.h" +#endif #import "RNSentryBreadcrumb.h" #import "RNSentryId.h" #import diff --git a/packages/core/ios/RNSentryRNSScreen.m b/packages/core/ios/RNSentryRNSScreen.m index edd1a1f6db..52c3658fba 100644 --- a/packages/core/ios/RNSentryRNSScreen.m +++ b/packages/core/ios/RNSentryRNSScreen.m @@ -2,7 +2,11 @@ #if SENTRY_HAS_UIKIT -# import "RNSentry-Swift.h" +# if __has_include() +# import +# else +# import "RNSentry-Swift.h" +# endif # import "RNSentryDependencyContainer.h" # import "RNSentryFramesTrackerListener.h" diff --git a/packages/core/ios/RNSentryReplay.mm b/packages/core/ios/RNSentryReplay.mm index 162de3b141..60c4cfe93f 100644 --- a/packages/core/ios/RNSentryReplay.mm +++ b/packages/core/ios/RNSentryReplay.mm @@ -1,5 +1,9 @@ #import "RNSentryReplay.h" -#import "RNSentry-Swift.h" +#if __has_include() +# import +#else +# import "RNSentry-Swift.h" +#endif #import "RNSentryReplayBreadcrumbConverterHelper.h" #import "RNSentryReplayQuality.h" #import "RNSentryVersion.h" diff --git a/packages/core/ios/RNSentryReplayBreadcrumbConverterHelper.m b/packages/core/ios/RNSentryReplayBreadcrumbConverterHelper.m index d22c872d7e..08ace7cc7a 100644 --- a/packages/core/ios/RNSentryReplayBreadcrumbConverterHelper.m +++ b/packages/core/ios/RNSentryReplayBreadcrumbConverterHelper.m @@ -1,7 +1,11 @@ #import "RNSentryReplayBreadcrumbConverterHelper.h" #if SENTRY_TARGET_REPLAY_SUPPORTED -# import "RNSentry-Swift.h" +# if __has_include() +# import +# else +# import "RNSentry-Swift.h" +# endif # import "RNSentryReplayBreadcrumbConverter.h" @implementation RNSentryReplayBreadcrumbConverterHelper diff --git a/packages/core/ios/RNSentrySDK.m b/packages/core/ios/RNSentrySDK.m index 56e5e5cc20..1f9de9fe52 100644 --- a/packages/core/ios/RNSentrySDK.m +++ b/packages/core/ios/RNSentrySDK.m @@ -1,5 +1,9 @@ #import "RNSentrySDK.h" -#import "RNSentry-Swift.h" +#if __has_include() +# import +#else +# import "RNSentry-Swift.h" +#endif #import "RNSentryStart.h" #import diff --git a/packages/core/ios/RNSentryStart.m b/packages/core/ios/RNSentryStart.m index d724dbf4e2..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 "RNSentry-Swift.h" +#if __has_include() +# import +#else +# import "RNSentry-Swift.h" +#endif #import @import Sentry; diff --git a/packages/core/ios/SentrySDKWrapper.m b/packages/core/ios/SentrySDKWrapper.m index f46a436419..881d3bc69b 100644 --- a/packages/core/ios/SentrySDKWrapper.m +++ b/packages/core/ios/SentrySDKWrapper.m @@ -1,5 +1,9 @@ #import "SentrySDKWrapper.h" -#import "RNSentry-Swift.h" +#if __has_include() +# import +#else +# import "RNSentry-Swift.h" +#endif @import Sentry; @implementation SentrySDKWrapper diff --git a/packages/core/ios/SentryScreenFramesWrapper.m b/packages/core/ios/SentryScreenFramesWrapper.m index 05a12998fe..e7db337459 100644 --- a/packages/core/ios/SentryScreenFramesWrapper.m +++ b/packages/core/ios/SentryScreenFramesWrapper.m @@ -1,5 +1,9 @@ #import "SentryScreenFramesWrapper.h" -#import "RNSentry-Swift.h" +#if __has_include() +# import +#else +# import "RNSentry-Swift.h" +#endif @import Sentry; #if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST From 5cfce5eb50b6553a5c952a77d68a90d54720038c Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Mon, 6 Jul 2026 12:12:07 +0200 Subject: [PATCH 06/13] fix(ios): Guard against nil NSData and pin tests to SPI import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes flagged by Warden: 1. `RNSentryInternal.envelope(fromData:)` accepted a non-optional Swift `Data`. If the ObjC caller passed a nil `NSData*` (e.g. from a failed base64 decode of the envelope payload), the ObjC→Swift bridge would force-unwrap it and crash before the method body ran. Change the parameter to `Data?` with an internal `guard let` — matches the nil-tolerant behaviour of the deprecated `PrivateSentrySDKOnly.envelopeWithData:`. 2. Swift Cocoa tests accessed `SentrySDK.internal.*` with plain `import Sentry`. The `.internal` accessor is public today, but the sub-APIs the tests touch (`options`, `appStart`, `performance`) sit next to `@_spi(Private)`-gated siblings on the same struct. Add `@_spi(Private) import Sentry` to `RNSentryStartTests.swift`, `RNSentryStartFromFileTests.swift`, and `RNSentryReplayOptionsTests.swift` so they keep compiling if the surface is ever tightened. Co-Authored-By: Claude Opus 4.7 --- .../RNSentryReplayOptionsTests.swift | 2 +- .../RNSentryStartFromFileTests.swift | 2 +- .../RNSentryCocoaTesterTests/RNSentryStartTests.swift | 2 +- packages/core/ios/RNSentryInternal.swift | 9 +++++++-- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayOptionsTests.swift b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayOptionsTests.swift index f56ed8de32..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`. diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartFromFileTests.swift b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartFromFileTests.swift index feb66ba7d3..38107d3f66 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartFromFileTests.swift +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartFromFileTests.swift @@ -1,4 +1,4 @@ -import Sentry +@_spi(Private) import Sentry import XCTest final class RNSentryStartFromFileTests: XCTestCase { diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartTests.swift b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartTests.swift index e98f43eb4c..682c651936 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartTests.swift +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartTests.swift @@ -1,4 +1,4 @@ -import Sentry +@_spi(Private) import Sentry import XCTest final class RNSentryStartTests: XCTestCase { diff --git a/packages/core/ios/RNSentryInternal.swift b/packages/core/ios/RNSentryInternal.swift index 52799da58f..241bc8ac1b 100644 --- a/packages/core/ios/RNSentryInternal.swift +++ b/packages/core/ios/RNSentryInternal.swift @@ -82,8 +82,13 @@ import Foundation // MARK: - Envelope - @_spi(Private) @objc public static func envelope(fromData data: Data) -> SentryEnvelope? { - SentrySDK.internal.envelope.deserialize(from: data) + // 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) { From c70a93ae72c16acf599defc2764182d0738c7dcd Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Mon, 6 Jul 2026 12:26:50 +0200 Subject: [PATCH 07/13] fix(ios): Resolve fetchViewHierarchy with nil on capture failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When `captureViewHierarchy` returned nil the ObjC code still constructed an empty `NSMutableArray` and resolved with it. In JS, `[]` is truthy — the caller would treat the failure as a successful (empty) attachment. Bail out with `resolve(nil)` up front so the JS side sees the actual failure signal. Pre-existing since 2023; flagged by Seer while reviewing this PR. Co-Authored-By: Claude Opus 4.7 --- packages/core/ios/RNSentry.mm | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/core/ios/RNSentry.mm b/packages/core/ios/RNSentry.mm index 986ce975bf..1207a09a93 100644 --- a/packages/core/ios/RNSentry.mm +++ b/packages/core/ios/RNSentry.mm @@ -683,6 +683,12 @@ - (void)handleShakeDetected { #if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST 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]; From 2f691f138c380c28db5bb05866adba26693a0872 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Mon, 6 Jul 2026 14:16:51 +0200 Subject: [PATCH 08/13] test(ios): Add smoke tests for RNSentryInternal Swift bridge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cover the bridge's own contract — nil-guard on `envelope(fromData:)`, metadata accessors, options factory, hybrid-SDK-mode flags, and the `swizzleRNSScreenViewDidAppear` early-return when RNSScreen is unavailable. sentry-cocoa owns the underlying `SentrySDK.internal.*` behaviour; we just assert the wrapper forwards correctly and honours the documented nil / platform guards. Registers the new file in the RNSentryCocoaTester project explicitly (the target does not use file-system-synchronized groups yet). Flagged by Warden. Co-Authored-By: Claude Opus 4.7 --- .../project.pbxproj | 4 + .../RNSentryInternalTests.swift | 103 ++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryInternalTests.swift 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") + } +} From 2cb3360a4ffde45a78321dd9269ec25bbabd2edf Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Mon, 6 Jul 2026 14:28:10 +0200 Subject: [PATCH 09/13] fix(ci): Enable modular headers in e2e Podfile patch Adding Swift to the RNSentry pod means CocoaPods refuses to integrate it against non-modular ObjC dependencies. On older React Native versions (e.g. 0.71 with Hermes) the React-hermes pod does not define modules, so `pod install` fails with: The Swift pod `RNSentry` depends upon `React-hermes`, which does not define modules. Inject `use_modular_headers!` into the Podfile via the existing e2e patch script, and document the same knob for downstream users in the CHANGELOG. Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 12 ++++++++---- .../e2e-tests/patch-scripts/rn.patch.podfile.js | 16 +++++++++++++++- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25ae072e88..58b0be6823 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ > make sure you follow our [migration guide](https://docs.sentry.io/platforms/react-native/migration/) first. +## Unreleased + +### 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,10 +35,6 @@ > [!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)**. -### 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)) - ## 8.16.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..1272f37717 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,22 @@ 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!')) { + content = content.replace( + /prepare_react_native_project!\s*\n/, + "prepare_react_native_project!\nuse_modular_headers!\n", + ); + modularPatched = true; + debug.log('Patching Podfile with 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 { From 1816a7ecee338e03d610e28c0bd305bd6f72eb90 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Mon, 6 Jul 2026 14:31:57 +0200 Subject: [PATCH 10/13] fix(ci): Bump codeql-action/init to v4.36.3 to match analyze/autobuild Cherry-picked from #6410. Dependabot bumped `codeql-action/analyze` and `codeql-action/autobuild` to v4.36.3 but left `init` on v4.36.2, so `init` writes a config stamped 4.36.2 that `analyze` (4.36.3) rejects: Loaded a configuration file for version '4.36.2', but running version '4.36.3' Pull this fix into the PR branch so CI stops failing while #6410 is still open on main. Co-Authored-By: Claude Opus 4.7 --- .github/workflows/codeql-analysis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. From 0b3be81df92d02959754c93f432f6e81ba683d2b Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Mon, 6 Jul 2026 14:37:10 +0200 Subject: [PATCH 11/13] fix(ci): Only mark Podfile patched when anchor actually matched Previous version unconditionally set `modularPatched = true` inside the `use_modular_headers!` branch, so a Podfile without a `prepare_react_native_project!` anchor would be reported as successfully patched even though `content.replace` returned it unchanged. Compare replace output against the input and log a warning instead when the anchor is missing. Flagged by Cursor Bugbot. Co-Authored-By: Claude Opus 4.7 --- .../e2e-tests/patch-scripts/rn.patch.podfile.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) 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 1272f37717..c488ef39fe 100755 --- a/dev-packages/e2e-tests/patch-scripts/rn.patch.podfile.js +++ b/dev-packages/e2e-tests/patch-scripts/rn.patch.podfile.js @@ -69,12 +69,17 @@ if (currentMatch) { // so ensure the Podfile requests modular headers globally. let modularPatched = false; if (!content.includes('use_modular_headers!')) { - content = content.replace( + const patched = content.replace( /prepare_react_native_project!\s*\n/, "prepare_react_native_project!\nuse_modular_headers!\n", ); - modularPatched = true; - debug.log('Patching Podfile with use_modular_headers!'); + 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 From 7513940ce5c34e83f86e45bbd1efd0febb06cbd4 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Mon, 6 Jul 2026 14:43:58 +0200 Subject: [PATCH 12/13] docs: Note visionOS setCurrentScreen no-op in CHANGELOG The migration to `SentrySDK.internal.screen.setCurrent` drops the current-screen breadcrumb enrichment on visionOS because sentry-cocoa's new `SentryInternalScreenApi` is gated to iOS/tvOS only. Already documented in a code comment; call it out for downstream users too. Flagged by Seer. Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58b0be6823..29f52c8fa4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ ## 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)) From b5e317f7172ed6d2086b97256ec1148fef262eb6 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Mon, 6 Jul 2026 14:58:46 +0200 Subject: [PATCH 13/13] fix(test): Import RNSentry in RNSentryInternalTests `RNSentryInternal` is a Swift type compiled into the `RNSentry` pod module, not the `Sentry` module. Without `@_spi(Private) import RNSentry` the test target cannot resolve `RNSentryInternal.*` and fails to build. Flagged by Warden. Co-Authored-By: Claude Opus 4.7 --- .../RNSentryCocoaTesterTests/RNSentryInternalTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryInternalTests.swift b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryInternalTests.swift index 07a24dc5d7..811ce65d75 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryInternalTests.swift +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryInternalTests.swift @@ -1,3 +1,4 @@ +@_spi(Private) import RNSentry @_spi(Private) import Sentry import XCTest