Skip to content

feat(core): Wire TurboModulePerfLogger on iOS and Android#6307

Merged
alwx merged 24 commits into
mainfrom
alwx/feature/turbo-module-perf-logger
Jun 29, 2026
Merged

feat(core): Wire TurboModulePerfLogger on iOS and Android#6307
alwx merged 24 commits into
mainfrom
alwx/feature/turbo-module-perf-logger

Conversation

@alwx

@alwx alwx commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

📢 Type of change

  • Bugfix
  • New feature
  • Enhancement
  • Refactoring

📜 Description

Install a Sentry-owned facebook::react::NativeModulePerfLogger on both platforms so the SDK observes every TurboModule lifecycle event:

  • moduleDataCreate{Start,End}, moduleCreate{Start,CacheHit,Construct*,SetUp*,End,Fail}
  • moduleJSRequireBeginning*, moduleJSRequireEnding*
  • syncMethodCall{Start,ArgConversion*,Execution*,ReturnConversion*,End,Fail}
  • asyncMethodCall{Start,ArgConversion*,Dispatch,End,Fail}
  • asyncMethodCallBatchPreprocess{Start,End}
  • asyncMethodCallExecution{Start,ArgConversion*,End,Fail}

This is the foundation that the next three issues in the Turbo Modules instrumentation project build on: JS↔Native crash attribution, per-Turbo-Module spans, and aggregated per-module stats. Each will ship its own ISentryTurboModulePerfSink implementation and plug into the hook this PR exposes.

New enableTurboModuleTracking option on Sentry.init, default false for this first release so the foundation lands without behavioural change. The native logger is always installed (we never want to miss early lifecycle events); the flag only decides whether forwarded callbacks reach the sink. The option is plumbed through initNativeSdk on both platforms.

Sentry.init({
  dsn: "___PUBLIC_DSN___",
  enableTurboModuleTracking: true, // off by default — will activate the follow-up sinks once they ship
});

💡 Motivation and Context

Closes #6162.

💚 How did you test it?

📝 Checklist

  • I added tests to verify changes
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled
  • I updated the docs if needed.
    • Docs deferred until a sink ships and the option becomes user-visible.
  • I updated the wizard if needed.
  • All tests passing
  • No breaking changes

🔮 Next steps

This is the foundation for the Turbo Modules project. Follow-up issues plug in ISentryTurboModulePerfSink implementations:

  1. JS↔Native crash attribution — annotate native crashes with turbo_module.name / turbo_module.method of the call that was in flight.
  2. Per-Turbo-Module spans — open a span around each native call with duration, status, module.method data.
  3. Aggregated per-module stats — counters / duration histograms per module/method, attached to transactions.

@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Semver Impact of This PR

None (no version bump detected)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


  • feat(core): Wire TurboModulePerfLogger on iOS and Android by alwx in #6307
  • chore(deps): update CLI to v3.6.0 by github-actions in #6362
  • chore(deps): bump faraday from 1.10.5 to 1.10.6 in /samples/react-native by dependabot in #6363
  • chore(deps): update JavaScript SDK to v10.62.0 by github-actions in #6361
  • Expo Router ErrorBoundary auto wrapped by alwx in #6347
  • chore(ci): Move sample app iOS build jobs to GitHub Actions runners by itaybre in #6356
  • docs: Add missing 8.14.1 to changelog and SDK versions table by antonis in #6360
  • chore(deps): update Android SDK to v8.46.0 by github-actions in #6357
  • chore(ci): Move testflight and size-analysis iOS jobs to GitHub Actions macos-26 by itaybre in #6355
  • feat(core): Use native btoa for envelope base64 encoding by alwx in #6351

🤖 This preview updates automatically when you update the PR.

@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor
Warnings
⚠️

⚠️ Android SDK Version Mismatch

Component Version
sentry-android in build.gradle 8.46.0
sentry-android bundled by gradle plugin 6.13.0 8.45.0

If a user enables AGP autoInstallation (default: true), this mismatch causes:

IllegalStateException: Sentry SDK has detected a mix of versions

Our docs instruct React Native users to set autoInstallation.enabled = false, so users following the guide are unaffected. Consider either updating packages/core/android/build.gradle to 8.45.0 or waiting for a gradle plugin release that bundles 8.46.0.

Messages
📖 Do not forget to update Sentry-docs with your feature once the pull request gets approved.

Generated by 🚫 dangerJS against 56958a5

Comment thread packages/core/cpp/SentryTurboModulePerfLogger.h Outdated
@alwx

alwx commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 3b618c5. Configure here.

alwx added a commit that referenced this pull request Jun 18, 2026
Address Warden's medium-severity finding on PR #6307: the new
`SentryTurboModulePerfController` and `RNSentryTurboModulePerfTracker`
shipped without unit coverage. Add focused tests that exercise the
state machines independently of React Native's runtime.

- **iOS** (`RNSentryCocoaTester/.../RNSentryTurboModulePerfControllerTests.mm`):
  default `isEnabled() == false`, `setEnabled` toggle, the C-linkage
  `Sentry_SetTurboModuleTrackingEnabled` entry point matches the typed
  setter, `setSink`/`sink` round-trips including `nullptr` detach,
  and `Sentry_InstallTurboModulePerfLogger` idempotency under repeated
  calls. End-to-end forwarding through `facebook::react::TurboModulePerfLogger`
  is intentionally not covered here \u2014 it requires `+load` ordering and
  process-wide singletons that the follow-up sink PRs will integration-test.

- **Android** (`RNSentryAndroidTester/.../RNSentryTurboModulePerfTrackerTest.kt`):
  the JVM-side latch around the JNI symbol. In the test JVM the
  underlying `.so` is not loaded, so the first `setEnabled` call must
  catch `UnsatisfiedLinkError` and flip `nativeUnavailable`; subsequent
  calls must short-circuit. Uses Robolectric so the `android.util.Log.i`
  call inside the catch branch resolves instead of throwing the
  not-mocked stub. A small `@TestOnly` window on the tracker exposes
  the latch state to assertions.

Also fix the changelog entry to reference the PR (#6307) rather than
the issue (#6162) so danger stops nagging.
@alwx alwx marked this pull request as ready for review June 18, 2026 08:49
Comment thread packages/core/cpp/SentryTurboModulePerfLogger.cpp Outdated
@alwx alwx enabled auto-merge (squash) June 18, 2026 09:40
@alwx alwx disabled auto-merge June 18, 2026 09:40
Comment thread packages/core/ios/RNSentry.mm Outdated
Comment thread packages/core/android/build.gradle Outdated

@antonis antonis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for you work on this @alwx 🙇
Did a 1st pass and didn't notice anything off other what has been caught by the agents and the lint check. Let's also add the ready-to-merge since there are many changes on the native side that need to be validated.

alwx added a commit that referenced this pull request Jun 22, 2026
…g flips on

Address two related medium findings on #6307:

- Warden: `enableLogging` runs from `+load` / `JNI_OnLoad` regardless
  of the runtime flag, unconditionally evicting any pre-existing
  `NativeModulePerfLogger` (Metro, other SDKs, host-app instrumentation).
- Cursor: when `enableTurboModuleTracking: true`, callbacks between
  load time and `initNativeSdk` are dropped by the `enabled_=false`
  fast-path anyway, so the eager install was not actually delivering
  on its 'never miss early events' promise \u2014 just on its side effects.

The fix is a single one-way ratchet: `setEnabled(true)` lazily calls
`install()` on the first transition, and the typed setter doubles as
the public lifecycle hook. The `+load` installer class on iOS and the
`JNI_OnLoad` install on Android are gone; the C `Sentry_InstallTurboModulePerfLogger`
entry stays for hosts that want to claim the perf-logger slot eagerly
via their own native code, but it is no longer wired into our load
hooks. Header / JSDoc updated to describe the new contract.

Also fix two adjacent issues flagged on the same PR:

- Sentry HIGH (build.gradle): two sibling `buildFeatures { ... }`
  blocks under the same Android scope replace rather than merge, so
  `prefab = true` was clobbering `buildConfig = true` on AGP 8+.
  Merge into a single conditional block.
- Lint: ran `yarn java:format fix`, `yarn fix:clang`, and switched
  `RNSentryTurboModulePerfTracker.nativeUnavailable` from `volatile`
  to `AtomicBoolean` to satisfy the project-wide PMD `AvoidUsingVolatile`
  rule. Removed a Kotlin `no-consecutive-comments` violation from the
  Robolectric note above the tracker test.

Test updates:
- iOS: add `testSetEnabledFalseDoesNotInstall` and
  `testSetEnabledTrueIsLazyInstallAndSticky` to lock down the lazy
  install ratchet. Existing `testInstallIsIdempotent` still covers
  explicit-install callers.
- Android: tracker tests unchanged in behaviour; only the test-only
  `isNativeUnavailableForTests` / `resetNativeUnavailableForTests`
  helpers were updated to go through the new `AtomicBoolean`.
@alwx alwx requested a review from antonis June 22, 2026 08:55
@alwx alwx added the ready-to-merge Triggers the full CI test suite label Jun 22, 2026
Comment thread packages/core/cpp/SentryTurboModulePerfLogger.cpp Outdated
alwx added a commit that referenced this pull request Jun 22, 2026
Address Cursor's low-severity finding on #6307: `setEnabled(true)`
was storing `enabled_` *after* calling `install()`, so any callback
React Native fired synchronously from inside `enableLogging()` would
hit the `isEnabled() == false` fast-path and be dropped \u2014 a tiny
window of lost events for the very first opted-in invocation.

Swap the order: publish `enabled_ = true` (release ordering) before
the install, so by the time `enableLogging()` could re-enter us via a
synchronous callback, the flag is already visible to other threads.
On disable the order does not matter since we never uninstall.
@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Android (legacy) Performance metrics 🚀

  Plain With Sentry Diff
Startup time 436.69 ms 470.69 ms 34.01 ms
Size 49.74 MiB 55.06 MiB 5.32 MiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
eb93136+dirty 416.18 ms 467.32 ms 51.14 ms
5257d80+dirty 423.37 ms 467.54 ms 44.17 ms
ca9d079+dirty 411.29 ms 455.12 ms 43.83 ms
7887847+dirty 416.61 ms 462.04 ms 45.43 ms
1122a96+dirty 422.22 ms 464.33 ms 42.10 ms
88735e9+dirty 429.04 ms 484.17 ms 55.13 ms
5fe1c6c+dirty 401.62 ms 445.28 ms 43.66 ms
4953e94+dirty 442.02 ms 456.52 ms 14.50 ms
ecf47a2+dirty 420.40 ms 458.02 ms 37.62 ms
5a010b7+dirty 425.62 ms 469.38 ms 43.76 ms

App size

Revision Plain With Sentry Diff
eb93136+dirty 48.30 MiB 53.58 MiB 5.28 MiB
5257d80+dirty 48.30 MiB 53.58 MiB 5.28 MiB
ca9d079+dirty 48.30 MiB 53.58 MiB 5.28 MiB
7887847+dirty 49.74 MiB 54.81 MiB 5.07 MiB
1122a96+dirty 48.30 MiB 53.54 MiB 5.24 MiB
88735e9+dirty 49.74 MiB 54.82 MiB 5.07 MiB
5fe1c6c+dirty 43.75 MiB 48.14 MiB 4.39 MiB
4953e94+dirty 43.75 MiB 48.08 MiB 4.33 MiB
ecf47a2+dirty 49.74 MiB 54.82 MiB 5.07 MiB
5a010b7+dirty 48.30 MiB 53.58 MiB 5.28 MiB

@sentry

sentry Bot commented Jun 22, 2026

Copy link
Copy Markdown

📲 Install Builds

Android

🔗 App Name App ID Version Configuration
Sentry RN io.sentry.reactnative.sample 8.16.0 (94) Release

⚙️ sentry-react-native Build Distribution Settings

Comment thread packages/core/android/CMakeLists.txt Outdated
@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

iOS (legacy) Performance metrics 🚀

  Plain With Sentry Diff
Startup time 3842.18 ms 1220.76 ms -2621.42 ms
Size 4.98 MiB 6.51 MiB 1.53 MiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
9474ead+dirty 3864.29 ms 1223.55 ms -2640.74 ms
5fe1c6c+dirty 1220.79 ms 1217.63 ms -3.16 ms
ae37560+dirty 3832.10 ms 1219.09 ms -2613.02 ms
f170ec3+dirty 3822.26 ms 1218.33 ms -2603.93 ms
df5d108+dirty 1225.90 ms 1220.14 ms -5.76 ms
882f8ae+dirty 3840.30 ms 1224.41 ms -2615.88 ms
23598c3+dirty 1207.00 ms 1209.90 ms 2.90 ms
1122a96+dirty 3823.10 ms 1218.64 ms -2604.46 ms
580fb5c+dirty 3836.13 ms 1218.72 ms -2617.41 ms
5a23c47+dirty 3855.46 ms 1221.95 ms -2633.50 ms

App size

Revision Plain With Sentry Diff
9474ead+dirty 5.15 MiB 6.71 MiB 1.55 MiB
5fe1c6c+dirty 3.38 MiB 4.77 MiB 1.39 MiB
ae37560+dirty 5.15 MiB 6.70 MiB 1.54 MiB
f170ec3+dirty 5.15 MiB 6.69 MiB 1.53 MiB
df5d108+dirty 3.38 MiB 4.73 MiB 1.35 MiB
882f8ae+dirty 5.15 MiB 6.70 MiB 1.54 MiB
23598c3+dirty 3.38 MiB 4.80 MiB 1.42 MiB
1122a96+dirty 5.15 MiB 6.68 MiB 1.53 MiB
580fb5c+dirty 4.98 MiB 6.46 MiB 1.48 MiB
5a23c47+dirty 4.98 MiB 6.46 MiB 1.49 MiB

alwx added a commit that referenced this pull request Jun 22, 2026
…debug info

Address Warden's medium-severity finding on #6307: passing
`-Wl,--strip-all` at CMake link time strips DWARF (and `.symtab`)
from `libsentry-tm-perf-logger.so` *before* AGP's `StripDebugSymbolsTask`
gets a chance to copy the unstripped artefact for symbolication
upload. Any crash inside the library in production would be
unsymbolicated even with the Sentry Gradle plugin installed.

Drop the manual link option entirely. AGP already strips the .so for
the packaged APK while preserving the unstripped copy under
`intermediates/merged_native_libs/.../obj`, which is the one Sentry
Gradle plugin uploads. Verified locally with `llvm-readelf -S` on the
release intermediate: `.debug_*` and `.symtab` sections are now
present.
Comment thread packages/core/ios/RNSentry.mm Outdated
Comment thread packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java Outdated
@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

iOS (new) Performance metrics 🚀

  Plain With Sentry Diff
Startup time 3822.64 ms 1217.50 ms -2605.14 ms
Size 4.98 MiB 6.51 MiB 1.53 MiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
038a6d7+dirty 3838.96 ms 1218.86 ms -2620.10 ms
5748023+dirty 3844.74 ms 1225.49 ms -2619.26 ms
b0d3373+dirty 3842.49 ms 1218.49 ms -2624.00 ms
7887847+dirty 3844.89 ms 1221.67 ms -2623.22 ms
6177334+dirty 3851.52 ms 1226.23 ms -2625.29 ms
21a1e70+dirty 3826.22 ms 1217.74 ms -2608.47 ms
774257e+dirty 3821.35 ms 1211.96 ms -2609.39 ms
5789645+dirty 3841.36 ms 1214.81 ms -2626.55 ms
15d4514+dirty 3843.73 ms 1228.09 ms -2615.64 ms
94af3bd+dirty 3847.84 ms 1230.33 ms -2617.51 ms

App size

Revision Plain With Sentry Diff
038a6d7+dirty 5.15 MiB 6.70 MiB 1.55 MiB
5748023+dirty 5.15 MiB 6.68 MiB 1.53 MiB
b0d3373+dirty 5.15 MiB 6.68 MiB 1.53 MiB
7887847+dirty 4.98 MiB 6.46 MiB 1.48 MiB
6177334+dirty 5.15 MiB 6.68 MiB 1.53 MiB
21a1e70+dirty 4.98 MiB 6.46 MiB 1.49 MiB
774257e+dirty 5.15 MiB 6.70 MiB 1.54 MiB
5789645+dirty 4.98 MiB 6.50 MiB 1.52 MiB
15d4514+dirty 5.15 MiB 6.70 MiB 1.55 MiB
94af3bd+dirty 5.15 MiB 6.69 MiB 1.53 MiB

@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Android (new) Performance metrics 🚀

  Plain With Sentry Diff
Startup time 431.64 ms 476.94 ms 45.30 ms
Size 49.74 MiB 55.04 MiB 5.29 MiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
b9bebee+dirty 500.50 ms 536.42 ms 35.92 ms
ecf47a2+dirty 457.21 ms 498.10 ms 40.89 ms
5fe1c6c+dirty 365.84 ms 408.62 ms 42.78 ms
4953e94+dirty 398.80 ms 431.81 ms 33.01 ms
1a2e7e0+dirty 451.98 ms 501.50 ms 49.52 ms
5125c43+dirty 409.52 ms 451.00 ms 41.48 ms
0b1b5e3+dirty 425.58 ms 476.02 ms 50.44 ms
1122a96+dirty 510.16 ms 542.00 ms 31.84 ms
7ff4d0f+dirty 403.38 ms 427.06 ms 23.68 ms
5a21b51+dirty 505.16 ms 539.20 ms 34.04 ms

App size

Revision Plain With Sentry Diff
b9bebee+dirty 48.30 MiB 53.58 MiB 5.28 MiB
ecf47a2+dirty 49.74 MiB 54.82 MiB 5.07 MiB
5fe1c6c+dirty 43.94 MiB 49.00 MiB 5.06 MiB
4953e94+dirty 43.94 MiB 48.94 MiB 5.00 MiB
1a2e7e0+dirty 49.74 MiB 54.82 MiB 5.07 MiB
5125c43+dirty 48.30 MiB 53.54 MiB 5.24 MiB
0b1b5e3+dirty 48.30 MiB 53.60 MiB 5.29 MiB
1122a96+dirty 48.30 MiB 53.54 MiB 5.24 MiB
7ff4d0f+dirty 48.30 MiB 53.60 MiB 5.30 MiB
5a21b51+dirty 48.30 MiB 53.49 MiB 5.19 MiB

alwx and others added 13 commits June 29, 2026 10:34
Mirrors the iOS behavior: SentryAndroid.init can throw at runtime (e.g.\non invalid DSN or option configuration), but the Android initNativeSdk\npreviously caught nothing and unconditionally resolved the promise as\ntrue. The JS caller would then proceed to make native API calls against\nan uninitialized SDK and observe confusing downstream failures.\n\nWrap RNSentryStart.startWithOptions in a try/catch and reject the\npromise with the underlying error, matching the iOS reject(@"SentryReactNative",\n...) contract. The TurboModule perf-logger toggle is also skipped on\nfailure so we don't claim React Native's perf-logger slot when no\nSentry SDK is around to consume the data.\n\nAlso move the 8.15.0 changelog entry for enableTurboModuleTracking into\nthe Unreleased section, since this PR is now targeting the next\nrelease, and shorten it per review feedback.
Two related fixes from review:

1. Re-init bug: the native TurboModule perf controller is process-wide
   and not reset by closeNativeSdk. The previous code only forwarded the
   JS option when it was present and a boolean, so a sequence of
   init({enableTurboModuleTracking: true}) -> init({}) would leave
   tracking latched on indefinitely after a prior opt-in. Both iOS and
   Android now resolve the option to a concrete boolean (defaulting to
   false when the key is absent or non-boolean) and unconditionally
   call setEnabled, mirroring the JS-side default of false on every
   init. setEnabled(false) is cheap and never triggers the lazy
   install, so hosts that never opt in still pay zero native cost.

2. Stale JSDoc: the enableTurboModuleTracking docstring still claimed
   the native perf logger is installed at SDK load time. That changed
   with the lazy-load refactor — install() is now reached only on the
   first setEnabled(true). Updated the JSDoc to describe the actual
   behavior so consumers know the RN perf-logger slot stays untouched
   while the option is off.
Turns on the experimental TurboModule perf logger in the React Native\nsample app so reviewers can capture sample events end-to-end. The\nexisting "Native Crash" button on the Errors tab already routes through\nthe wrapped RNSentry.nativeCrash TurboModule call, so pressing it now\nproduces a crash report tagged with contexts.turbo_module +\nturbo_module.name=RNSentry / turbo_module.method=nativeCrash.\n\nNo-op on the Old Architecture and on hosts that strip Sentry's native\nlibraries; setEnabled(true) lazily loads libsentry-tm-perf-logger.so on\nfirst opt-in.
The TurboModule call tracker pushed context+tags onto getCurrentScope(),\nbut enableSyncToNative is wired only on the global and isolation scopes.\nUnder @sentry/core's async-context strategy getCurrentScope() can return\na forked scope that is neither \u2014 in which case the scope.setContext(\n'turbo_module', ...) call updates JS state only and never propagates to\nsentry-cocoa / sentry-java. As a result, a native crash captured during\na TurboModule invocation lost the contexts.turbo_module attribution and\nthe turbo_module.name / turbo_module.method tags it was supposed to\ncarry, defeating the whole point of turboModuleContextIntegration.\n\nDefault pushTurboModuleCall to the isolation scope instead. Isolation\nis always synced to native via enableSyncToNative, so the write reaches\nsentry-cocoa / sentry-java in time for the crash report; JS captures\nstill see the data because @sentry/core merges global + isolation +\ncurrent at event time. Tests can still pass an explicit scope.\n\nVerified end-to-end in the RN sample: triggering 'Native Crash' on the\nErrors tab now produces an issue with contexts.turbo_module = { name:\n'RNSentry', method: 'crash', kind: 'sync', \u2026 } and matching tags.\n\nAlso correct the App.tsx comment to reference the underlying\nRNSentry.crash TurboModule method (not RNSentry.nativeCrash, which is\nthe JS-side wrapper name).
The setEnabled() short-circuit `!enabled && !libraryLoadAttempted.get()`\nran *outside* the monitor that ensureNativeLibraryLoaded() acquires for\nthe lazy System.loadLibrary call. A second thread issuing setEnabled(\nfalse) while a first thread was still inside loadLibrary observed\nlibraryLoadAttempted == false (the first thread sets it only after the\nload completes), returned early, and left tracking latched on after the\nloader finished \u2014 the opposite of what the disabling caller asked for.\n\nMake setEnabled itself synchronized so the short-circuit and the lazy\nload share a single monitor. Concurrent enable/disable now resolves as\nlast-call-wins instead of dropping the disable.\n\nThe lock is contended only on the first few calls before the library\nfinishes loading; setEnabled is invoked once per initNativeSdk so the\noverhead is negligible.
CI was failing 2 tests in `wrapTurboModule.test.ts` ("tracker push\nthrows" and "tracker pop throws"): the spy on `scope.setContext` was\nnever fired, so `pushTurboModuleCall` never threw, so the diagnostic\n`warn` call the tests asserted on never happened.\n\nRoot cause: commit `fix(turbomodule): Default TM tracker to isolation\nscope for native sync` (`8a16f7de`) switched `pushTurboModuleCall`'s\ndefault scope from `getCurrentScope()` to `getIsolationScope()` and\nupdated `turboModuleTracker.test.ts` to mock the new entry point, but\nmissed the sibling `wrapTurboModule.test.ts` which still mocked only\n`getCurrentScope`. The wrapper therefore wrote context/tags onto the\nreal isolation scope, the test's mock\u2019d `scope.setContext` never\nfired, and the assertions about the diagnostic warn went un-met.\n\nMock both `getIsolationScope` and `getCurrentScope` so the test\nremains deterministic regardless of which scope the tracker walks.\nAll 1618 JS tests pass after the change.
…td::terminate

Cursor HIGH on #6307: `SentryTurboModulePerfController::install` is\ndeclared `noexcept`, but it calls `std::make_unique<ForwardingLogger>()`\n(can throw `std::bad_alloc`) and forwards to\n`facebook::react::TurboModulePerfLogger::enableLogging` (no exception\nguarantees). If either throws \u2014 most plausibly under memory pressure\nwhen a user opts in to TurboModule tracking \u2014 the runtime calls\n`std::terminate` and the host app dies, instead of gracefully degrading\nto tracking-off.\n\nWrap the allocation+install in a `try { } catch (...) { }` and roll\nback the `installed_` latch on failure so a later `setEnabled(true)`\ncan retry once memory pressure clears. The SDK keeps running; the\nworst-case observable effect is that tracking remained off when the\nuser opted in. That is strictly better than terminating the process.\n\n`setEnabled` is unchanged \u2014 it only writes atomics and calls `install`,\nwhich is now truly `noexcept` after this fix.\n\nThe other comment in this review pass (Sentry bot MEDIUM about a\nconcurrent enable/disable race in `RNSentryTurboModulePerfTracker.setEnabled`)\nwas already addressed in commit 482ac45, which made `setEnabled`\n`synchronized` so the short-circuit and the lazy `System.loadLibrary`\nshare a single monitor.
Two small follow-ups on #6307:

- Warden flagged the 23-character placeholder UUIDs I hand-rolled
  (`A1B2C3D4E5F600000000001` / `A1B2C3D4E5F600000000002`) in the
  RNSentryCocoaTester pbxproj when adding the new
  `RNSentryTurboModulePerfControllerTests.mm` source. Xcode expects
  exactly 24 uppercase hex characters; antonis confirmed. Replaced
  with real 24-character UUIDs (`2639D71D3BD04F17B0BAC987` /
  `E795057A6D534A80A9D06356`) across all four references
  (`PBXBuildFile`, `PBXFileReference`, `PBXSourcesBuildPhase`, and
  the group children).

- clang-format violation on the wrapped `enableLogging(...)` call in
  the noexcept-install fix \u2014 `yarn fix:clang` realigns it. CI lint
  job (run 28083145588) now matches local output.

Verified with `pod install` + `xcodebuild test` on the cocoa-tester
target: all 7 `RNSentryTurboModulePerfControllerTests` pass.
…ll race

Four fixes from PR #6307 review:

1. **antonis blocker** (RN 0.71.19 legacy Android matrix): the build
   failed with `CMake Error at CMakeLists.txt:21 (add_library): Target
   'sentry-tm-perf-logger' links to target 'ReactAndroid::reactnative'
   but the target was not found.` even though my gradle conditional
   only declared `externalNativeBuild` under NewArch. The cause is
   AGP/CMake auto-detection of `CMakeLists.txt` at the module root.
   Move the file to `src/main/jni/CMakeLists.txt` (next to OnLoad.cpp)
   so it lives outside any auto-detected path; gradle now references it
   explicitly via `cmake { path "src/main/jni/CMakeLists.txt" }` and
   the Old Arch build no longer touches it. Verified locally by running
   `./gradlew :sentry_react-native:assembleRelease` from
   `performance-tests/TestAppSentry/android` (newArchEnabled=false):
   no `configureCMake` task runs, no .so is produced, build succeeds.

2. **Warden** (`RNSentryModuleImpl.java:229`): if
   `RNSentryTurboModulePerfTracker.setEnabled` threw anything other
   than the already-caught `UnsatisfiedLinkError` (e.g.
   `SecurityException` from `System.loadLibrary`, any
   `RuntimeException` from the JNI symbol), execution skipped past
   `promise.resolve(true)` and the JS-side `initNativeSdk` promise
   hung forever. Wrap the `setEnabled` call in its own `try/catch`
   that logs and continues \u2014 the SDK has already started by this
   point, so a tracking-toggle failure is non-fatal to init.

3. **Cursor** (cross-platform parsing inconsistency): iOS accepted any
   `NSNumber` as the option value (so JS numeric `1` enabled
   tracking), Android required `ReadableType.Boolean` (so JS `1` was
   ignored). Tighten iOS to match: only honour an NSNumber whose
   `objCType` is `@encode(BOOL)`, which is what RN's bridge produces
   for a real JS boolean. JS numbers and strings now consistently fail
   the check on both platforms.

4. **Sentry bot** (race in `SentryTurboModulePerfController::install`):
   the previous fix rolled back `installed_` on `enableLogging`
   failure so a later caller could retry. That introduced a race: a
   concurrent thread observing the brief `installed_ == true` window
   would skip its own install attempt, then the originating thread's
   rollback would put us in a state where every caller thought someone
   else installed but nobody actually did. Switch to sticky
   "install attempted" semantics \u2014 the latch never rolls back. A
   failed install during the user opt-in path leaves tracking off for
   the rest of the process, which is strictly better than a silent
   half-installed state.

All builds + tests pass locally (`yarn lint`, samples on both arches,
`RNSentryAndroidTester`, `RNSentryCocoaTester/RNSentryTurboModulePerfControllerTests`).
… for option parsing

Warden caught a real bug in my previous `enableTurboModuleTracking`\nparsing fix: on 64-bit iOS `BOOL` is `typedef bool BOOL`, so\n`@encode(BOOL)` expands to `"B"` \u2014 but `[NSNumber numberWithBool:YES]`\n(including every JS boolean crossing the RN bridge) always reports\n`objCType == "c"` for historical compatibility. The\n`strcmp([(NSNumber *)value objCType], @encode(BOOL))` check therefore\nnever matched on any modern iOS device, and\n`enableTurboModuleTracking: true` was a silent no-op on iOS.\n\nReplace with the canonical, toll-free-bridged check\n`CFGetTypeID == CFBooleanGetTypeID()`. Verified with a small repro\nthat `@YES` returns `true` and `@1` returns `false` from\n`CFBooleanGetTypeID()`.\n\nAlso extract the parsing into a testable class method\n(`+ [RNSentry turboModuleTrackingEnabledFromOptions:]`) and add 7 unit\ntests in `RNSentryTurboModulePerfControllerTests.mm` covering:\n\n  - JS `true` \u2192 enabled (the bug)\n  - JS `false` \u2192 disabled\n  - JS `1` \u2192 disabled (cross-platform parity with Android's\n    `ReadableType.Boolean`)\n  - JS `0` \u2192 disabled\n  - String \u2192 disabled\n  - Missing key \u2192 disabled\n  - `NSNull` \u2192 disabled\n\nDictionary literals are hoisted into locals inside each test because\n`XCTAssertTrue`/`Fals`'s macro expansion uses `catch(T)` in ObjC++ and\nthe parser otherwise chokes on the comma inside `@{ k : v, }`.
…ode fallback error

Two follow-ups from latest review pass:

- **Cursor MEDIUM** (`SentryTurboModulePerfLogger.cpp`): the previous
  sticky-install fix accidentally let `isEnabled()` lie. The flow was
  `setEnabled(true)` \u2192 `enabled_ = true` \u2192 `install()` \u2192
  `enableLogging` throws \u2192 `installed_` stays sticky-true (no retry)
  \u2192 `isEnabled()` returns `true` even though RN never received the
  perf logger. Worse, a later `setEnabled(true)` would short-circuit
  on the sticky latch and the same lie persisted for the rest of the
  process.

  Split the latch into `installAttempted_` (sticky after the first
  try) and `installed_` (only `true` if `enableLogging` succeeded),
  and have `isEnabled()` AND user intent with actual install state.
  Now: a successful install \u2192 `isEnabled()` mirrors `enabled_`. A
  failed install \u2192 `isEnabled()` is permanently `false` for the
  process, even if the user keeps calling `setEnabled(true)`. This is
  strictly honest about what tracking can deliver and means tests and
  consumers stop seeing false-positive 'tracking is on' readings.

- **Sentry bot MEDIUM** (`build.gradle`): `resolveReactNativeDir()`
  calls `providers.exec("node", ...).get()` at gradle configure time,
  which throws hard if `node` is not on PATH. Wrap with a clearer
  `GradleException` pointing at the `REACT_NATIVE_NODE_MODULES_DIR`
  ext property the host project can use to skip the node lookup.

- **Warden** (RNSentry.mm BOOL parsing): the new comment was a snapshot
  of the bug that 416006d already fixed via `CFBooleanGetTypeID`.
  No code change needed beyond a reply pointing at that commit.
…lability

Address the RN 0.71.19 legacy CI failure. Root cause:

  `dev-packages/e2e-tests/cli.mjs` has a long-standing bug: the check
  `if (env.RCT_NEW_ARCH_ENABLED)` is truthy for the string "0", so
  the legacy matrix entries (which set the env var to "0") actually
  flip `newArchEnabled=true` in the host app's `gradle.properties`.
  That has been silently masking new-arch coverage as legacy on every
  PR for ages; my CMake config is just the first thing that requires
  the `ReactAndroid::reactnative` prefab, which only exists in RN
  >= 0.75. So configure fails on RN 0.71.19 with:

      CMake Error at CMakeLists.txt:27 (add_library):
        Target "sentry-tm-perf-logger" links to target
        "ReactAndroid::reactnative" but the target was not found.

Rather than touch the CI script (out of this PR's scope), gate our
CMake config on the host's actual React Native version. The new
`isReactNativePrefabAvailable()` helper:

  1. requires `newArchEnabled=true` (same as before)
  2. resolves the host's `react-native/package.json` (via the existing\n     `resolveReactNativeDir()` helper, which honours\n     `REACT_NATIVE_NODE_MODULES_DIR`)
  3. returns true only when the major.minor is >= 0.75

Both `buildFeatures { prefab true }` and `externalNativeBuild { cmake\n... }` blocks are now keyed off this gate. On RN < 0.75 we skip the
native build entirely; `RNSentryTurboModulePerfTracker.setEnabled`
catches the missing `.so` via its existing `UnsatisfiedLinkError`
latch, exactly as it already does on Old Architecture.

Verified locally:

  - RN 0.86.0 + NewArch  \u2192 prefab-available = true,  4 .so files in AAR
  - RN 0.71.11           \u2192 prefab-available = false, no .so files
  - Android unit tests   \u2192 still pass
Co-authored-by: Antonis Lilis <antonis.lilis@sentry.io>
@alwx alwx force-pushed the alwx/feature/turbo-module-perf-logger branch from 5da4254 to 955852e Compare June 29, 2026 08:36

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 955852e. Configure here.

Comment thread CHANGELOG.md Outdated
@alwx alwx requested a review from antonis June 29, 2026 08:46
Comment thread packages/core/cpp/SentryTurboModulePerfLogger.cpp

@antonis antonis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 🚀
Thank you for all your work on this @alwx 🙇
Let's wait for the CI to get 🟢 since this is a big PR :)

@alwx alwx merged commit 2607f3a into main Jun 29, 2026
98 of 103 checks passed
@alwx alwx deleted the alwx/feature/turbo-module-perf-logger branch June 29, 2026 09:30
antonis added a commit that referenced this pull request Jul 3, 2026
…so (#6396)

* fix(android): Force 16 KB ELF alignment for libsentry-tm-perf-logger.so

The native library added in 8.17.0 (#6307) for Turbo Module performance
tracking is compiled from source with the app's NDK. On NDK r27 and
earlier the linker defaults to 4 KB segment alignment, so this library
was the lone misaligned `.so` in the APK (all prebuilt libs are already
16 KB aligned). This tripped Android 15+'s 16 KB page size compatibility
check and Google Play's 16 KB requirement for New Architecture apps —
even when `enableTurboModuleTracking` was left disabled, since the
library is packaged whenever the New Architecture is enabled.

Pass `-Wl,-z,max-page-size=16384` to the CMake target. Verified with
NDK 27 against the RN 0.86 source tree: LOAD segment alignment goes from
0x1000 (4 KB) to 0x4000 (16 KB), with no file-size change. Backward
compatible with 4 KB-page devices (16 KB is a multiple of 4 KB) and a
no-op on NDK r28+ where 16 KB is already the default.

Fixes #6394

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Update changelog

* test(android): Add CI check for 16 KB native library alignment

Guards against regressing #6394. Adds scripts/check-android-16kb-alignment.sh
which unzips a built APK/AAB and asserts every bundled .so has ELF LOAD
segments aligned to at least 16 KB (p_align >= 0x4000), failing otherwise.

Wired into the sample-application workflow right after the New Architecture
Android app build, where a real APK is produced. The script is portable
across the macOS (bash 3.2) and Ubuntu CI (mawk) shells and resolves
readelf from $READELF, then llvm-readelf, then readelf.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: Fix grammar in changelog entry

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(android): Scope 16 KB alignment check to Sentry-owned libraries

The initial CI run failed because the check scanned every .so in the APK,
including React Native and third-party libraries (libreactnative,
libhermesvm, libreanimated, librnscreens, ...) which are 4 KB aligned in
the x86 build CI produces. RN aligns arm64 to 16 KB but not x86, and those
libraries are outside this repo's control.

Add an optional name-filter argument to the check script and pass
'libsentry' in CI so the guard verifies only the libraries this repo ships
(libsentry-tm-perf-logger.so et al.). A regression that drops the
max-page-size flag from our CMake target is still caught — verified
locally: with the filter, our lib at 4 KB still fails the check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
antonis added a commit that referenced this pull request Jul 6, 2026
…logger.so + CI ABI/link coverage (#6406)

* fix(android): Prevent New Arch build break linking libsentry-tm-perf-logger.so

The perf-logger native library added in 8.17.0 (#6307) references
facebook::react::TurboModulePerfLogger::enableLogging, which lives in
React Native's `reactnative` prefab. Some New Architecture build setups
(e.g. Expo builds on armeabi-v7a) do not satisfy that reference at link
time, and because the NDK/AGP toolchain links with -Wl,-z,defs
(--no-undefined) the unresolved symbol becomes a fatal "undefined symbol"
error that breaks the whole Android build — even for hosts that never opt
in to enableTurboModuleTracking, since the library is compiled whenever
the New Architecture is enabled.

Append -Wl,-z,undefs to the target's link options so undefined symbols
are non-fatal, leaving them to resolve at load time against the loaded
libreactnative.so. This is safe: the library is loaded lazily and only
when tracking is opted in, and an unresolvable symbol surfaces as an
UnsatisfiedLinkError from System.loadLibrary that the Java side already
swallows (RNSentryTurboModulePerfTracker), degrading the experimental
feature to a no-op rather than crashing.

Verified with NDK 27 against the RN 0.86 prefab:
- armeabi-v7a with --no-undefined and the prefab unresolved: FAILED
  before, links after the fix.
- normal builds (prefab linked) keep libreactnative.so in DT_NEEDED and
  resolve the symbol at load time -- the flag is a no-op there.
- x86 / arm64 still build and stay 16 KB aligned.

Fixes #6398

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: Reference PR instead of issue in changelog entry

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(android): Add ABI coverage and link regression guard for perf-logger

Two CI additions to catch the class of failures that shipped with the
app-compiled libsentry-tm-perf-logger.so (#6394, #6398), both of which
escaped because CI only built x86:

- Build all four ABIs (armeabi-v7a, arm64-v8a, x86, x86_64) in the New
  Architecture sample Android build instead of x86 only, so per-ABI
  native breakage is exercised and the 16 KB alignment check runs across
  every ABI.

- Add scripts/check-tm-perf-logger-link.sh, a lightweight guard that
  reproduces the #6398 link condition (armeabi-v7a, --no-undefined,
  reactnative prefab reference unresolved) using the real sources and the
  real link flags declared in CMakeLists.txt. It fails if -Wl,-z,undefs
  is ever removed. Wired into buildandtest.yml so it runs on every PR
  using the runner's preinstalled NDK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(android): Build all ABIs in the Expo sample too

#6398 (armeabi-v7a link failure) was reported on an Expo build, but the
Expo sample only built x86 in CI. Build all four ABIs to keep coverage
consistent with the bare React Native sample.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(android): Honor reactNativeArchitectures for the perf-logger native build

Root-cause fix for #6398. The module hardcoded `abiFilters` to all four
ABIs, ignoring the host app's `reactNativeArchitectures`. When the app
builds a subset (e.g. `-PreactNativeArchitectures=arm64-v8a`, common in
Expo/dev/CI builds), React Native only provides its `reactnative` prefab
for that subset, so building the extra ABIs cannot resolve
`TurboModulePerfLogger::enableLogging` at link time and breaks the build.

Honor `reactNativeArchitectures` (falling back to all four when unset),
matching react-native-screens, reanimated and the RN app template. Now
the module builds only the ABIs the app requested.

Verified locally against a subset build: with the old hardcoded filters,
`:sentry_react-native:externalNativeBuildDebug -PreactNativeArchitectures=arm64-v8a`
built arm64-v8a + armeabi-v7a + x86 + x86_64; with the fix it builds only
arm64-v8a.

The `-Wl,-z,undefs` link flag stays as defense-in-depth for any remaining
setup where a built ABI's prefab reference is unresolved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-merge Triggers the full CI test suite

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wire TurboModulePerfLogger on iOS and Android

3 participants