From 1d2ca67104bb0a07029f249f84d0147a2a0c4ce2 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 26 Aug 2026 14:08:38 +0200 Subject: [PATCH 1/4] fix(ios): Force-load the Sentry static archive to keep ObjC category methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RN links sentry-cocoa as a prebuilt static xcframework with no whole-archive linker flag. Objective-C category methods (Swift `@objc extension`s, e.g. `SentryReplayNetworkDetails+Capture` in cocoa 9.25/9.26) are therefore dead-stripped at the app's final link and crash at runtime with `unrecognized selector`. Add per-SDK `-force_load` of the Sentry binary to `user_target_xcconfig` (the app target, where the stripping link happens). Scoped to the Sentry archive — unlike whole-link `-ObjC`/`-all_load` — and, unlike `-ObjC`, also retains Swift-only metadata, so it hardens against future valid-but-strippable upstream changes rather than only the current category case. Apple QA1490 documents category stripping as the consumer's responsibility. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 1 + packages/core/RNSentry.podspec | 28 +++++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2637f4883b..ef1d9af4ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ ### Fixes +- Fix iOS `unrecognized selector` crash when Session Replay captures network request/response details, caused by Objective-C category methods in the Sentry static framework being stripped at link time ([#6615](https://github.com/getsentry/sentry-react-native/pull/6615)) - Fix time to display spans causing transactions to be dropped by Relay ([#6608](https://github.com/getsentry/sentry-react-native/pull/6608)) - Fix Expo iOS build failing when the project path contains spaces ([#6604](https://github.com/getsentry/sentry-react-native/pull/6604)) - Fix Android fragment memory leak in `RNSentryReactFragmentLifecycleTracer` ([#6599](https://github.com/getsentry/sentry-react-native/pull/6599)) diff --git a/packages/core/RNSentry.podspec b/packages/core/RNSentry.podspec index bc48e15f9c..7979f613d1 100644 --- a/packages/core/RNSentry.podspec +++ b/packages/core/RNSentry.podspec @@ -150,8 +150,34 @@ Pod::Spec.new do |s| acc["FRAMEWORK_SEARCH_PATHS[sdk=#{sdk}*]"] = (['$(inherited)'] + paths).join(' ') end + # Force-load the whole Sentry static archive on the *app* link. We link + # Sentry statically (see the `vendored_frameworks` note above) with no + # whole-archive flag, so any symbol reachable only through an Objective-C + # category — a Swift `@objc extension`, e.g. + # `SentryReplayNetworkDetails+Capture` in sentry-cocoa 9.25/9.26 + # (getsentry/sentry-react-native#6609) — or through Swift-only metadata is + # dead-stripped at the final link and crashes at runtime with + # "unrecognized selector". Apple documents this as the *consumer's* + # responsibility (QA1490: https://developer.apple.com/library/archive/qa/qa1490/_index.html). + # + # `-force_load` on the Sentry binary is preferred over `-ObjC`/`-all_load`: + # it is scoped to the Sentry archive (those flags force-load every static + # lib in the app — size bloat + duplicate-symbol risk), and, unlike + # `-ObjC`, it also retains Swift-only metadata. So this hardens us against + # future valid-but-strippable upstream changes, not only the current + # category case — independent of any producer-side fix in sentry-cocoa. + # + # Lives on `user_target_xcconfig` (the app target) because that is where + # the link that strips happens; `pod_target_xcconfig` would be too early. + force_load_flags = SENTRY_XCFRAMEWORK_SLICES_BY_SDK.each_with_object({}) do |(sdk, slice_ids), acc| + loads = slice_ids.map do |slice| + %(-force_load "#{File.join(sentry_xcframework_ref, slice, 'Sentry.framework', 'Sentry')}") + end + acc["OTHER_LDFLAGS[sdk=#{sdk}*]"] = (['$(inherited)'] + loads).join(' ') + end + pod_target_xcconfig.merge!(xcframework_search_paths) - s.user_target_xcconfig = xcframework_search_paths + s.user_target_xcconfig = xcframework_search_paths.merge(force_load_flags) else raise <<~MSG [Sentry] SENTRY_USE_XCFRAMEWORK=0 is no longer supported. From 8c1a078f37c318ce06ab897c42f3855f68ba6822 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 26 Aug 2026 14:51:54 +0200 Subject: [PATCH 2/4] fix(ios): Place Sentry -force_load on the target whose link strips MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Under `use_frameworks! :linkage => :dynamic`, `@import Sentry` pulls the static Sentry archive into the RNSentry dylib, so RNSentry's own link is what dead-strips the `SentryReplayNetworkDetails+Capture` ObjC category — not the app link. Placing `-force_load` only on `user_target_xcconfig` missed that link, and additionally risked forcing a second copy of Sentry into the app binary (duplicate ObjC classes, two SentrySDK singletons). Detect linkage via `ENV['USE_FRAMEWORKS']` (the signal RN's Podfile setup and our samples already use) and put `-force_load` on exactly one target: `pod_target_xcconfig` (RNSentry dylib link) for dynamic, otherwise `user_target_xcconfig` (app link) for the static default. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 1 - packages/core/RNSentry.podspec | 43 +++++++++++++++++++--------------- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef1d9af4ee..2637f4883b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,6 @@ ### Fixes -- Fix iOS `unrecognized selector` crash when Session Replay captures network request/response details, caused by Objective-C category methods in the Sentry static framework being stripped at link time ([#6615](https://github.com/getsentry/sentry-react-native/pull/6615)) - Fix time to display spans causing transactions to be dropped by Relay ([#6608](https://github.com/getsentry/sentry-react-native/pull/6608)) - Fix Expo iOS build failing when the project path contains spaces ([#6604](https://github.com/getsentry/sentry-react-native/pull/6604)) - Fix Android fragment memory leak in `RNSentryReactFragmentLifecycleTracer` ([#6599](https://github.com/getsentry/sentry-react-native/pull/6599)) diff --git a/packages/core/RNSentry.podspec b/packages/core/RNSentry.podspec index 7979f613d1..89fa1b1aec 100644 --- a/packages/core/RNSentry.podspec +++ b/packages/core/RNSentry.podspec @@ -150,25 +150,24 @@ Pod::Spec.new do |s| acc["FRAMEWORK_SEARCH_PATHS[sdk=#{sdk}*]"] = (['$(inherited)'] + paths).join(' ') end - # Force-load the whole Sentry static archive on the *app* link. We link - # Sentry statically (see the `vendored_frameworks` note above) with no - # whole-archive flag, so any symbol reachable only through an Objective-C - # category — a Swift `@objc extension`, e.g. - # `SentryReplayNetworkDetails+Capture` in sentry-cocoa 9.25/9.26 - # (getsentry/sentry-react-native#6609) — or through Swift-only metadata is - # dead-stripped at the final link and crashes at runtime with - # "unrecognized selector". Apple documents this as the *consumer's* - # responsibility (QA1490: https://developer.apple.com/library/archive/qa/qa1490/_index.html). + # Force-load the Sentry static archive so its ObjC category methods survive + # linking. We link Sentry statically with no whole-archive flag, so symbols + # reachable only through an ObjC category (what a Swift `@objc extension` + # compiles to, e.g. `SentryReplayNetworkDetails+Capture` in sentry-cocoa + # 9.25/9.26, #6609) or through Swift-only metadata get dead-stripped and + # crash at runtime with "unrecognized selector" — the consumer's job to fix + # at link time, per Apple QA1490 + # (https://developer.apple.com/library/archive/qa/qa1490/_index.html). + # `-force_load` beats `-ObjC`/`-all_load`: it is scoped to Sentry and also + # keeps Swift-only metadata, guarding against future strippable upstream + # changes, not just today's category. # - # `-force_load` on the Sentry binary is preferred over `-ObjC`/`-all_load`: - # it is scoped to the Sentry archive (those flags force-load every static - # lib in the app — size bloat + duplicate-symbol risk), and, unlike - # `-ObjC`, it also retains Swift-only metadata. So this hardens us against - # future valid-but-strippable upstream changes, not only the current - # category case — independent of any producer-side fix in sentry-cocoa. - # - # Lives on `user_target_xcconfig` (the app target) because that is where - # the link that strips happens; `pod_target_xcconfig` would be too early. + # The flag must ride whichever target's link does the stripping, which + # depends on linkage: static libs (the default) absorb Sentry into the app + # binary → app link → `user_target_xcconfig`; `:linkage => :dynamic` absorbs + # it into the RNSentry dylib → that link → `pod_target_xcconfig`. Never + # both, or a second copy of Sentry lands in the app. Detected via + # `ENV['USE_FRAMEWORKS']`. force_load_flags = SENTRY_XCFRAMEWORK_SLICES_BY_SDK.each_with_object({}) do |(sdk, slice_ids), acc| loads = slice_ids.map do |slice| %(-force_load "#{File.join(sentry_xcframework_ref, slice, 'Sentry.framework', 'Sentry')}") @@ -177,7 +176,13 @@ Pod::Spec.new do |s| end pod_target_xcconfig.merge!(xcframework_search_paths) - s.user_target_xcconfig = xcframework_search_paths.merge(force_load_flags) + user_target_xcconfig = xcframework_search_paths.dup + if ENV['USE_FRAMEWORKS'] == 'dynamic' + pod_target_xcconfig.merge!(force_load_flags) + else + user_target_xcconfig.merge!(force_load_flags) + end + s.user_target_xcconfig = user_target_xcconfig else raise <<~MSG [Sentry] SENTRY_USE_XCFRAMEWORK=0 is no longer supported. From dc780c5a6fb03a99e34d3804ea919c61fb880133 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 27 Aug 2026 11:24:25 +0200 Subject: [PATCH 3/4] fix(ios): Use -ObjC instead of -force_load for Sentry categories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch the link-time fix for the stripped `SentryReplayNetworkDetails` ObjC category from `-force_load ` to `-ObjC`: - `-ObjC` names no archive, so it only affects static libs actually in the current link — the same flag is correct on both the app target (static linkage) and the RNSentry pod target (dynamic linkage) with no risk of pulling a second copy of Sentry into the app. This removes the need to detect linkage via `ENV['USE_FRAMEWORKS']`, closing the gap where a bare `use_frameworks!` (env var unset) mis-placed the flag. - It is Apple's documented fix for this exact problem (QA1490). - It only retains ObjC class/category objects (not the whole archive) and needs no per-slice binary paths, simplifying the podspec. Co-Authored-By: Claude Opus 4.8 --- packages/core/RNSentry.podspec | 46 ++++++++++------------------------ 1 file changed, 13 insertions(+), 33 deletions(-) diff --git a/packages/core/RNSentry.podspec b/packages/core/RNSentry.podspec index 89fa1b1aec..56049c48e8 100644 --- a/packages/core/RNSentry.podspec +++ b/packages/core/RNSentry.podspec @@ -150,39 +150,19 @@ Pod::Spec.new do |s| acc["FRAMEWORK_SEARCH_PATHS[sdk=#{sdk}*]"] = (['$(inherited)'] + paths).join(' ') end - # Force-load the Sentry static archive so its ObjC category methods survive - # linking. We link Sentry statically with no whole-archive flag, so symbols - # reachable only through an ObjC category (what a Swift `@objc extension` - # compiles to, e.g. `SentryReplayNetworkDetails+Capture` in sentry-cocoa - # 9.25/9.26, #6609) or through Swift-only metadata get dead-stripped and - # crash at runtime with "unrecognized selector" — the consumer's job to fix - # at link time, per Apple QA1490 - # (https://developer.apple.com/library/archive/qa/qa1490/_index.html). - # `-force_load` beats `-ObjC`/`-all_load`: it is scoped to Sentry and also - # keeps Swift-only metadata, guarding against future strippable upstream - # changes, not just today's category. - # - # The flag must ride whichever target's link does the stripping, which - # depends on linkage: static libs (the default) absorb Sentry into the app - # binary → app link → `user_target_xcconfig`; `:linkage => :dynamic` absorbs - # it into the RNSentry dylib → that link → `pod_target_xcconfig`. Never - # both, or a second copy of Sentry lands in the app. Detected via - # `ENV['USE_FRAMEWORKS']`. - force_load_flags = SENTRY_XCFRAMEWORK_SLICES_BY_SDK.each_with_object({}) do |(sdk, slice_ids), acc| - loads = slice_ids.map do |slice| - %(-force_load "#{File.join(sentry_xcframework_ref, slice, 'Sentry.framework', 'Sentry')}") - end - acc["OTHER_LDFLAGS[sdk=#{sdk}*]"] = (['$(inherited)'] + loads).join(' ') - end - - pod_target_xcconfig.merge!(xcframework_search_paths) - user_target_xcconfig = xcframework_search_paths.dup - if ENV['USE_FRAMEWORKS'] == 'dynamic' - pod_target_xcconfig.merge!(force_load_flags) - else - user_target_xcconfig.merge!(force_load_flags) - end - s.user_target_xcconfig = user_target_xcconfig + # Keep Sentry's ObjC category methods from being dead-stripped at link. + # We link Sentry statically with no whole-archive flag, so a method living + # only in an ObjC category (what a Swift `@objc extension` compiles to, e.g. + # `SentryReplayNetworkDetails+Capture` in sentry-cocoa 9.25/9.26, #6609) is + # dropped and crashes at runtime with "unrecognized selector". `-ObjC` is + # Apple's documented fix (QA1490). Unlike `-force_load` it names no archive + # — it only affects static libs in the current link — so the same flag is + # safe on both targets whatever the linkage (app link under static, RNSentry + # dylib link under dynamic), pulling no second Sentry copy and needing no + # linkage detection. + objc_ldflags = { 'OTHER_LDFLAGS' => '$(inherited) -ObjC' } + pod_target_xcconfig.merge!(xcframework_search_paths).merge!(objc_ldflags) + s.user_target_xcconfig = xcframework_search_paths.merge(objc_ldflags) else raise <<~MSG [Sentry] SENTRY_USE_XCFRAMEWORK=0 is no longer supported. From d082a3af0ea6dbf5d0c8cfa3ed2b57856c0e7301 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 27 Aug 2026 12:41:11 +0200 Subject: [PATCH 4/4] fix(ios): Revert to -force_load for Sentry categories (-ObjC regressed) The -ObjC swap (dc780c5a) regressed the "Test ios production REV2" job: captureSpaceflightNewsScreenTransaction fails with the #6609 crash ("No visible element found: Load More Articles") because the app crashes on the replay network-detail path. -force_load (8c1a078f) passes the same test (19/19). Verified directly from CI logs of both runs. Root cause confirmed against the checksum-verified released 9.26.0 slice: -setRequestWithSize:bodyData:contentType:allHeaders:configuredHeaders: lives only in the category-only object SentryReplayNetworkDetails+Capture.o (no class of its own), so a plain static link dead-strips it. -force_load loads the whole archive unconditionally; -ObjC does not rescue it in the real production build. Co-Authored-By: Claude Opus 4.8 --- packages/core/RNSentry.podspec | 46 ++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/packages/core/RNSentry.podspec b/packages/core/RNSentry.podspec index 56049c48e8..89fa1b1aec 100644 --- a/packages/core/RNSentry.podspec +++ b/packages/core/RNSentry.podspec @@ -150,19 +150,39 @@ Pod::Spec.new do |s| acc["FRAMEWORK_SEARCH_PATHS[sdk=#{sdk}*]"] = (['$(inherited)'] + paths).join(' ') end - # Keep Sentry's ObjC category methods from being dead-stripped at link. - # We link Sentry statically with no whole-archive flag, so a method living - # only in an ObjC category (what a Swift `@objc extension` compiles to, e.g. - # `SentryReplayNetworkDetails+Capture` in sentry-cocoa 9.25/9.26, #6609) is - # dropped and crashes at runtime with "unrecognized selector". `-ObjC` is - # Apple's documented fix (QA1490). Unlike `-force_load` it names no archive - # — it only affects static libs in the current link — so the same flag is - # safe on both targets whatever the linkage (app link under static, RNSentry - # dylib link under dynamic), pulling no second Sentry copy and needing no - # linkage detection. - objc_ldflags = { 'OTHER_LDFLAGS' => '$(inherited) -ObjC' } - pod_target_xcconfig.merge!(xcframework_search_paths).merge!(objc_ldflags) - s.user_target_xcconfig = xcframework_search_paths.merge(objc_ldflags) + # Force-load the Sentry static archive so its ObjC category methods survive + # linking. We link Sentry statically with no whole-archive flag, so symbols + # reachable only through an ObjC category (what a Swift `@objc extension` + # compiles to, e.g. `SentryReplayNetworkDetails+Capture` in sentry-cocoa + # 9.25/9.26, #6609) or through Swift-only metadata get dead-stripped and + # crash at runtime with "unrecognized selector" — the consumer's job to fix + # at link time, per Apple QA1490 + # (https://developer.apple.com/library/archive/qa/qa1490/_index.html). + # `-force_load` beats `-ObjC`/`-all_load`: it is scoped to Sentry and also + # keeps Swift-only metadata, guarding against future strippable upstream + # changes, not just today's category. + # + # The flag must ride whichever target's link does the stripping, which + # depends on linkage: static libs (the default) absorb Sentry into the app + # binary → app link → `user_target_xcconfig`; `:linkage => :dynamic` absorbs + # it into the RNSentry dylib → that link → `pod_target_xcconfig`. Never + # both, or a second copy of Sentry lands in the app. Detected via + # `ENV['USE_FRAMEWORKS']`. + force_load_flags = SENTRY_XCFRAMEWORK_SLICES_BY_SDK.each_with_object({}) do |(sdk, slice_ids), acc| + loads = slice_ids.map do |slice| + %(-force_load "#{File.join(sentry_xcframework_ref, slice, 'Sentry.framework', 'Sentry')}") + end + acc["OTHER_LDFLAGS[sdk=#{sdk}*]"] = (['$(inherited)'] + loads).join(' ') + end + + pod_target_xcconfig.merge!(xcframework_search_paths) + user_target_xcconfig = xcframework_search_paths.dup + if ENV['USE_FRAMEWORKS'] == 'dynamic' + pod_target_xcconfig.merge!(force_load_flags) + else + user_target_xcconfig.merge!(force_load_flags) + end + s.user_target_xcconfig = user_target_xcconfig else raise <<~MSG [Sentry] SENTRY_USE_XCFRAMEWORK=0 is no longer supported.