fix(ios): Force-load the Sentry static archive to keep ObjC category methods - #6615
Conversation
…methods 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 <noreply@anthropic.com>
de088e6 to
1d2ca67
Compare
Semver Impact of This PR⚪ None (no version bump detected) 📋 Changelog PreviewThis is how your changes will appear in the changelog.
🤖 This preview updates automatically when you update the PR. |
📲 Install BuildsAndroid
|
iOS (new) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 61cc206+dirty | 3822.60 ms | 1206.17 ms | -2616.43 ms |
| b9bebee+dirty | 3858.02 ms | 1231.92 ms | -2626.11 ms |
| 2363c0f+dirty | 3834.61 ms | 1224.46 ms | -2610.15 ms |
| 038a6d7+dirty | 3838.96 ms | 1218.86 ms | -2620.10 ms |
| 7ff4d0f+dirty | 3859.87 ms | 1224.75 ms | -2635.12 ms |
| d7d54c6+dirty | 3842.44 ms | 1204.87 ms | -2637.57 ms |
| 1122a96+dirty | 3839.17 ms | 1219.23 ms | -2619.93 ms |
| 23598c3+dirty | 1223.59 ms | 1229.13 ms | 5.53 ms |
| af33f3b+dirty | 3851.28 ms | 1233.06 ms | -2618.21 ms |
| 5a316ea+dirty | 3831.56 ms | 1214.31 ms | -2617.26 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 61cc206+dirty | 4.98 MiB | 6.55 MiB | 1.57 MiB |
| b9bebee+dirty | 5.15 MiB | 6.68 MiB | 1.53 MiB |
| 2363c0f+dirty | 4.98 MiB | 6.46 MiB | 1.49 MiB |
| 038a6d7+dirty | 5.15 MiB | 6.70 MiB | 1.55 MiB |
| 7ff4d0f+dirty | 5.15 MiB | 6.70 MiB | 1.55 MiB |
| d7d54c6+dirty | 4.98 MiB | 6.51 MiB | 1.53 MiB |
| 1122a96+dirty | 5.15 MiB | 6.68 MiB | 1.53 MiB |
| 23598c3+dirty | 3.38 MiB | 4.80 MiB | 1.42 MiB |
| af33f3b+dirty | 4.98 MiB | 6.51 MiB | 1.54 MiB |
| 5a316ea+dirty | 4.98 MiB | 6.51 MiB | 1.53 MiB |
Previous results on branch: fix/ios-force-load-sentry-xcframework
Startup times
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| cb0f8ba+dirty | 3854.25 ms | 1217.10 ms | -2637.15 ms |
| 407f7ca+dirty | 3821.19 ms | 1220.00 ms | -2601.19 ms |
| 3388a65+dirty | 3845.02 ms | 1217.58 ms | -2627.44 ms |
| 130342e+dirty | 3848.04 ms | 1222.47 ms | -2625.57 ms |
| aa762be+dirty | 3843.60 ms | 1221.47 ms | -2622.13 ms |
| 3cfd253+dirty | 3835.00 ms | 1222.95 ms | -2612.05 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| cb0f8ba+dirty | 5.08 MiB | 6.78 MiB | 1.71 MiB |
| 407f7ca+dirty | 5.08 MiB | 6.75 MiB | 1.67 MiB |
| 3388a65+dirty | 5.08 MiB | 6.78 MiB | 1.70 MiB |
| 130342e+dirty | 5.08 MiB | 6.75 MiB | 1.67 MiB |
| aa762be+dirty | 5.08 MiB | 6.78 MiB | 1.70 MiB |
| 3cfd253+dirty | 5.08 MiB | 6.78 MiB | 1.70 MiB |
iOS (legacy) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| ecf47a2+dirty | 3835.20 ms | 1222.43 ms | -2612.77 ms |
| 3817909+dirty | 1183.90 ms | 1187.50 ms | 3.60 ms |
| 5ee78d6+dirty | 3847.52 ms | 1224.27 ms | -2623.25 ms |
| 890d145+dirty | 1223.59 ms | 1231.37 ms | 7.78 ms |
| e763471+dirty | 3841.16 ms | 1218.06 ms | -2623.09 ms |
| 3d31fcf+dirty | 3838.09 ms | 1223.46 ms | -2614.63 ms |
| 9210ae6+dirty | 3815.93 ms | 1214.14 ms | -2601.79 ms |
| 26843eb+dirty | 3861.60 ms | 1238.77 ms | -2622.82 ms |
| 7a89652+dirty | 3861.46 ms | 1229.61 ms | -2631.85 ms |
| 9ad4522+dirty | 3850.40 ms | 1222.40 ms | -2628.00 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| ecf47a2+dirty | 4.98 MiB | 6.46 MiB | 1.49 MiB |
| 3817909+dirty | 3.38 MiB | 4.73 MiB | 1.35 MiB |
| 5ee78d6+dirty | 5.15 MiB | 6.69 MiB | 1.53 MiB |
| 890d145+dirty | 3.38 MiB | 4.77 MiB | 1.38 MiB |
| e763471+dirty | 4.98 MiB | 6.51 MiB | 1.53 MiB |
| 3d31fcf+dirty | 4.98 MiB | 6.56 MiB | 1.58 MiB |
| 9210ae6+dirty | 5.15 MiB | 6.68 MiB | 1.53 MiB |
| 26843eb+dirty | 4.98 MiB | 6.53 MiB | 1.55 MiB |
| 7a89652+dirty | 5.15 MiB | 6.70 MiB | 1.55 MiB |
| 9ad4522+dirty | 4.98 MiB | 6.56 MiB | 1.58 MiB |
Previous results on branch: fix/ios-force-load-sentry-xcframework
Startup times
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| cb0f8ba+dirty | 3854.67 ms | 1230.67 ms | -2624.00 ms |
| 407f7ca+dirty | 3851.39 ms | 1217.18 ms | -2634.20 ms |
| 130342e+dirty | 3850.30 ms | 1214.04 ms | -2636.25 ms |
| aa762be+dirty | 3864.52 ms | 1218.87 ms | -2645.65 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| cb0f8ba+dirty | 5.08 MiB | 6.78 MiB | 1.71 MiB |
| 407f7ca+dirty | 5.08 MiB | 6.75 MiB | 1.67 MiB |
| 130342e+dirty | 5.08 MiB | 6.75 MiB | 1.67 MiB |
| aa762be+dirty | 5.08 MiB | 6.78 MiB | 1.70 MiB |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 1ec02c1. Configure here.
139ed15 to
288b362
Compare
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 <noreply@anthropic.com>
288b362 to
8c1a078
Compare
| # `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')}") |
There was a problem hiding this comment.
m: why force_load over ObjC? I believe force load will disable dead code stripping for all members in the archive, where ObjC would only do that for Objective-C members. In reality it probably doesn't matter all that much, but force_load could increase binary size more than needed.
There was a problem hiding this comment.
Good point! force_load would probably future proof against wider swift refactorings in the cocoa sdk but ObjC should be enough for this bug because SentryReplayNetworkDetails+Capture compiles to an ObjC category.
Trying this out with dc780c5
There was a problem hiding this comment.
Thanks! My thinking was that using force_load will include all non-ObjC members too (i.e. C/C++) that might otherwise be stripped if not used - and thus inflate the binary unnecessarily. Since we're only calling it on Sentry frameworks it's probably not the end of the world - but when we have KSCrash installed we might see things being pulled in that we don't need.
There was a problem hiding this comment.
Trying this out with dc780c5
The CI is failing with the same issue that started this investigation :(
I'll revert back to the force_load solution and iterate on the warden warning
There was a problem hiding this comment.
Huh... weird. Okay thanks for trying it! LGTM
Switch the link-time fix for the stripped `SentryReplayNetworkDetails` ObjC category from `-force_load <Sentry binary>` 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 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f12d6e2. Configure here.
The -ObjC swap (dc780c5) 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 (8c1a078) 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 <noreply@anthropic.com>
Revert the sentry-cocoa bump from #6409. sentry-cocoa 9.25/9.26 moved the replay network-detail capture methods into a Swift `@objc extension`, which compiles to an ObjC category that the prebuilt static `Sentry.xcframework` dead-strips under the default static link, crashing at runtime with "unrecognized selector" (#6609). 9.24.0 predates that change, so the crashing category does not exist and no consumer-side linker workaround is needed. Roll back to it for this release; the 9.26 upgrade plus the `-force_load` link fix can land together as a proper follow-up (#6615). 9.26.0 was never released (Unreleased section only) and 8.23.0 already ships 9.24.0, so no user-facing changelog entry is needed — the Unreleased "Bump Cocoa SDK" line is dropped rather than replaced with a downgrade note. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

📢 Type of change
📜 Description
RN consumes sentry-cocoa as a prebuilt static
Sentry.xcframework, linked implicitly via@import Sentryautolink with no whole-archive linker flag. Objective-C category methods — which is what Swift@objc extensions compile to — are dead-stripped from a static library at link time unless the consumer opts in. When cocoa 9.25/9.26 movedSentryReplayNetworkDetails' capture methods into such an extension (SentryReplayNetworkDetails+Capture), the selector went missing at runtime and crashed with-[Sentry.SentryReplayNetworkDetails setRequestWithSize:bodyData:contentType:allHeaders:configuredHeaders:]: unrecognized selector.This adds a per-SDK
-force_loadof the Sentry binary, placed on whichever target's link actually performs the strip — which depends on linkage:user_target_xcconfig.use_frameworks! :linkage => :dynamic):@import Sentrypulls the static archive into the RNSentry dylib, so the strip happens at the RNSentry link → flag onpod_target_xcconfig.It's placed on exactly one target, never both, which avoids forcing a second copy of Sentry into the app binary (duplicate ObjC classes / two
SentrySDKsingletons). Linkage is detected viaENV['USE_FRAMEWORKS'], the convention RN's Podfile setup and our samples already use.-force_loadis chosen over-ObjC/-all_loadbecause it is scoped to the Sentry archive (no whole-app size bloat or duplicate-symbol risk) and, unlike-ObjC, also retains Swift-only metadata — so it hardens against future valid-but-strippable upstream changes, not only today's category case.Note
Skipping changelog since the bug exposed with the Cocoa bump has not been shipped
💡 Motivation and Context
Fixes the iOS Session Replay network-capture crash reported in #6609. Apple QA1490 documents category stripping from static libraries as the consumer's responsibility to resolve at link time. This is a durable consumer-side fix independent of any producer-side patch in sentry-cocoa (getsentry/sentry-cocoa#8872).
💚 How did you test it?
-force_loadthe selector is absent (crash reproduced); with the exact flag the podspec now emits, the selector is present. Verified manually via a localpod install+ sample-app build.pod installon the RN sample in both linkage modes and confirmed-force_loadlands on exactly the right target: static → app aggregate (present) / RNSentry pod (absent); dynamic → app aggregate (absent) / RNSentry pod (present).📝 Checklist
sendDefaultPIIis enabled.🔮 Next steps