fix(ios): Detect dynamic linkage from the Podfile when USE_FRAMEWORKS is unset - #6621
fix(ios): Detect dynamic linkage from the Podfile when USE_FRAMEWORKS is unset#6621antonis wants to merge 7 commits into
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>
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>
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>
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>
… is unset The `-force_load` flag for the Sentry static archive must ride whichever target's link does the ObjC-category stripping: the app link under static linkage (`user_target_xcconfig`), or the RNSentry dylib link under dynamic frameworks (`pod_target_xcconfig`). Previously this was decided solely by `ENV['USE_FRAMEWORKS'] == 'dynamic'`. CocoaPods does not export that env var for a bare `use_frameworks!` / `use_frameworks! :linkage => :dynamic` in a consumer's Podfile, so such a setup wrongly took the static path — force-loading Sentry into the app while RNSentry is really a dylib (a second Sentry copy in the app, and the framework link still strips the replay category, #6609). Add `sentry_uses_dynamic_linkage`: honor `USE_FRAMEWORKS` as an explicit override when set, otherwise read the declared linkage off the Podfile CocoaPods has already loaded. Behavior is unchanged for this repo's CI and both samples (which set the env var, or don't use frameworks at all); it only newly covers the bare-`use_frameworks!` consumer that the env-only check missed. Falls back to the static default outside a real `pod install` (`pod ipc spec`, `pod lib lint`), where no Podfile is available. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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 (legacy) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 64630e5+dirty | 3842.70 ms | 1218.11 ms | -2624.60 ms |
| b0d3373+dirty | 3831.75 ms | 1227.29 ms | -2604.46 ms |
| b04af96+dirty | 3818.92 ms | 1219.76 ms | -2599.16 ms |
| 3d31fcf+dirty | 3838.09 ms | 1223.46 ms | -2614.63 ms |
| a0a3177+dirty | 3844.73 ms | 1225.23 ms | -2619.51 ms |
| af33f3b+dirty | 3849.98 ms | 1236.45 ms | -2613.53 ms |
| 09a902f+dirty | 3835.67 ms | 1217.11 ms | -2618.57 ms |
| 5a316ea+dirty | 3820.11 ms | 1211.28 ms | -2608.83 ms |
| 403b9f3+dirty | 3832.18 ms | 1213.32 ms | -2618.87 ms |
| acd838e+dirty | 3849.78 ms | 1230.00 ms | -2619.78 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 64630e5+dirty | 4.98 MiB | 6.46 MiB | 1.49 MiB |
| b0d3373+dirty | 5.15 MiB | 6.68 MiB | 1.53 MiB |
| b04af96+dirty | 4.98 MiB | 6.54 MiB | 1.56 MiB |
| 3d31fcf+dirty | 4.98 MiB | 6.56 MiB | 1.58 MiB |
| a0a3177+dirty | 4.98 MiB | 6.55 MiB | 1.58 MiB |
| af33f3b+dirty | 4.98 MiB | 6.51 MiB | 1.54 MiB |
| 09a902f+dirty | 4.98 MiB | 6.46 MiB | 1.49 MiB |
| 5a316ea+dirty | 4.98 MiB | 6.51 MiB | 1.53 MiB |
| 403b9f3+dirty | 5.08 MiB | 6.72 MiB | 1.64 MiB |
| acd838e+dirty | 5.15 MiB | 6.70 MiB | 1.55 MiB |
iOS (new) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 3ce5254+dirty | 1217.70 ms | 1224.69 ms | 6.99 ms |
| 0b1b5e3+dirty | 3820.72 ms | 1207.94 ms | -2612.78 ms |
| 44abcc2+dirty | 3841.42 ms | 1214.77 ms | -2626.65 ms |
| 1d3572b+dirty | 3830.71 ms | 1222.23 ms | -2608.49 ms |
| 6177334+dirty | 3851.52 ms | 1226.23 ms | -2625.29 ms |
| 5ee78d6+dirty | 3815.71 ms | 1217.04 ms | -2598.67 ms |
| 083e3ef+dirty | 3871.96 ms | 1234.95 ms | -2637.01 ms |
| ce7b368+dirty | 3851.41 ms | 1222.37 ms | -2629.04 ms |
| 7887847+dirty | 3844.89 ms | 1221.67 ms | -2623.22 ms |
| 1122a96+dirty | 3839.17 ms | 1219.23 ms | -2619.93 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 3ce5254+dirty | 3.38 MiB | 4.76 MiB | 1.38 MiB |
| 0b1b5e3+dirty | 5.15 MiB | 6.70 MiB | 1.54 MiB |
| 44abcc2+dirty | 4.98 MiB | 6.55 MiB | 1.57 MiB |
| 1d3572b+dirty | 4.98 MiB | 6.56 MiB | 1.58 MiB |
| 6177334+dirty | 5.15 MiB | 6.68 MiB | 1.53 MiB |
| 5ee78d6+dirty | 5.15 MiB | 6.69 MiB | 1.53 MiB |
| 083e3ef+dirty | 4.98 MiB | 6.55 MiB | 1.57 MiB |
| ce7b368+dirty | 4.98 MiB | 6.51 MiB | 1.53 MiB |
| 7887847+dirty | 4.98 MiB | 6.46 MiB | 1.48 MiB |
| 1122a96+dirty | 5.15 MiB | 6.68 MiB | 1.53 MiB |
Android (new) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 5a010b7+dirty | 475.15 ms | 519.02 ms | 43.88 ms |
| f9c1ed4+dirty | 520.57 ms | 594.84 ms | 74.27 ms |
| c2e182c+dirty | 468.50 ms | 545.44 ms | 76.94 ms |
| 2c735cc+dirty | 435.20 ms | 459.48 ms | 24.28 ms |
| af33f3b+dirty | 417.68 ms | 448.04 ms | 30.37 ms |
| 7d8c8bd+dirty | 406.06 ms | 460.88 ms | 54.81 ms |
| 890d145+dirty | 486.42 ms | 514.85 ms | 28.43 ms |
| ef27341+dirty | 519.02 ms | 553.42 ms | 34.40 ms |
| bf168a4+dirty | 430.60 ms | 459.31 ms | 28.71 ms |
| 41d6254+dirty | 406.20 ms | 445.52 ms | 39.32 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 5a010b7+dirty | 48.30 MiB | 53.58 MiB | 5.28 MiB |
| f9c1ed4+dirty | 49.74 MiB | 54.86 MiB | 5.12 MiB |
| c2e182c+dirty | 49.74 MiB | 54.85 MiB | 5.11 MiB |
| 2c735cc+dirty | 43.94 MiB | 48.94 MiB | 5.00 MiB |
| af33f3b+dirty | 49.74 MiB | 55.09 MiB | 5.35 MiB |
| 7d8c8bd+dirty | 48.30 MiB | 53.54 MiB | 5.23 MiB |
| 890d145+dirty | 43.94 MiB | 49.00 MiB | 5.06 MiB |
| ef27341+dirty | 48.30 MiB | 53.54 MiB | 5.24 MiB |
| bf168a4+dirty | 49.74 MiB | 55.09 MiB | 5.35 MiB |
| 41d6254+dirty | 48.30 MiB | 53.60 MiB | 5.30 MiB |
Android (legacy) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 8e18f68+dirty | 426.20 ms | 456.44 ms | 30.24 ms |
| 038a6d7+dirty | 524.82 ms | 531.92 ms | 7.10 ms |
| a858ac3+dirty | 426.50 ms | 484.82 ms | 58.32 ms |
| 822d35b+dirty | 429.31 ms | 498.04 ms | 68.73 ms |
| 5ca03f9+dirty | 423.30 ms | 467.00 ms | 43.70 ms |
| 083e3ef+dirty | 446.67 ms | 491.68 ms | 45.01 ms |
| 4953e94+dirty | 442.02 ms | 456.52 ms | 14.50 ms |
| 244f6e8+dirty | 424.36 ms | 488.29 ms | 63.92 ms |
| 2e64504+dirty | 450.52 ms | 482.70 ms | 32.18 ms |
| a9df5ce+dirty | 418.40 ms | 452.38 ms | 33.98 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 8e18f68+dirty | 49.74 MiB | 55.45 MiB | 5.71 MiB |
| 038a6d7+dirty | 48.30 MiB | 53.60 MiB | 5.30 MiB |
| a858ac3+dirty | 49.74 MiB | 55.08 MiB | 5.34 MiB |
| 822d35b+dirty | 49.74 MiB | 54.84 MiB | 5.10 MiB |
| 5ca03f9+dirty | 49.74 MiB | 55.26 MiB | 5.52 MiB |
| 083e3ef+dirty | 49.74 MiB | 55.36 MiB | 5.61 MiB |
| 4953e94+dirty | 43.75 MiB | 48.08 MiB | 4.33 MiB |
| 244f6e8+dirty | 49.74 MiB | 55.38 MiB | 5.63 MiB |
| 2e64504+dirty | 49.74 MiB | 55.45 MiB | 5.71 MiB |
| a9df5ce+dirty | 49.74 MiB | 55.36 MiB | 5.62 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 2a2fb0d. Configure here.
…tion-warden # Conflicts: # packages/core/RNSentry.podspec
| podfile.target_definition_list.any? do |td| | ||
| !td.root? && (td.build_type.dynamic_framework? rescue false) | ||
| end |
There was a problem hiding this comment.
Bug: The sentry_uses_dynamic_linkage check is too broad. It incorrectly detects dynamic linkage in mixed-target projects, causing build flags to be misapplied, which can lead to runtime crashes.
Severity: HIGH
Suggested Fix
The logic should be changed to check the linkage setting of the specific target that actually includes the RNSentry pod, rather than checking if any target in the Podfile uses dynamic linkage. This will ensure the -force_load flag is correctly applied based on RNSentry's context.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location: packages/core/scripts/sentry_utils.rb#L267-L269
Potential issue: The `sentry_uses_dynamic_linkage` function incorrectly determines the
linkage type in projects with multiple targets and mixed linkage settings. It checks if
`any?` target uses dynamic frameworks, which can return `true` even if the target
containing `RNSentry` uses static linkage. This misdetection leads to the `-force_load`
build flag being applied to the wrong configuration (`pod_target_xcconfig` instead of
the user target's xcconfig). As a result, necessary Objective-C category methods are
stripped during the link phase for the static target, causing the application to crash
at runtime.
📢 Type of change
📜 Description
Stacked on top of #6615.
The
-force_loadflag for the Sentry static archive has to ride whichever target's link does the ObjC-category stripping: the app link under static linkage (user_target_xcconfig), or the RNSentry dylib link under dynamic frameworks (pod_target_xcconfig) — never both, or a second copy of Sentry lands in the app.Previously that choice was made solely from
ENV['USE_FRAMEWORKS'] == 'dynamic'. This PR introducessentry_uses_dynamic_linkage(inscripts/sentry_utils.rb):USE_FRAMEWORKSis honored as an explicit override when set (dynamic→ dynamic path, anything else → static).target_definition_list … build_type.dynamic_framework?).pod install(pod ipc spec,pod lib lint) there is no Podfile, so it falls back to the static default — correct for the RN default.💡 Motivation and Context
CocoaPods does not export
USE_FRAMEWORKSfor a bareuse_frameworks!/use_frameworks! :linkage => :dynamicin a consumer's Podfile. Such a project therefore took the static branch and force-loaded Sentry into the app binary while RNSentry was really a dylib — a second Sentry copy in the app, and the framework's own link still dead-stripped the replay network-details category (the #6609 crash class). Reading the Podfile closes that gap.This is a follow-up to the warden feedback on #6615. It is a best-effort proxy: the linkage that ultimately governs is RNSentry's own pod
build_type, computed only afterpre_installhooks run and unavailable at podspec-eval time, so the declared target linkage is used instead (matching, and widening, what the env var proxied).💚 How did you test it?
ruby -con both touched files — Syntax OK.use_frameworks!dynamic → the gap, now closed; no env + static Podfile; no env +use_frameworks! :linkage => :static; no env + no Podfile for lint/ipc) — each resolves to the correct target.USE_FRAMEWORKS=dynamic(env override), and theno-frameworksmatrix leaves it unset while the sample Podfiles only calluse_frameworks!when the env var is set → introspection returnsfalse→ sameuser_targetpath as before.📝 Checklist
sendDefaultPIIis enabled.🔮 Next steps
Merge #6615 first (or retarget this PR to
mainonce #6615 lands). Known limitation: a project that globally enablesuse_frameworks!but forces RNSentry itself static without settingUSE_FRAMEWORKSwould read as dynamic; setUSE_FRAMEWORKS=staticto override.🤖 Generated with Claude Code