Skip to content

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

Draft
alwx wants to merge 1 commit into
mainfrom
alwx/feature/turbo-module-perf-logger
Draft

feat(core): Wire TurboModulePerfLogger on iOS and Android#6307
alwx wants to merge 1 commit 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.

How it's split

  • Shared C++ (packages/core/cpp/):

    • SentryTurboModulePerfSink.h — pluggable sink interface for follow-up features.
    • SentryTurboModulePerfLogger.h/.cpp — a SentryTurboModulePerfController singleton owns the installed logger and an atomic enabled flag. When disabled, every callback hits one atomic load and returns. When enabled, callbacks are forwarded to whichever sink is currently installed. The forwarding NativeModulePerfLogger subclass lives inside an anonymous namespace and is generated via macros to keep the ~30 callbacks readable.
  • iOS:

    • A dedicated RNSentryTurboModulePerfLoggerInstaller's +load calls Sentry_InstallTurboModulePerfLogger() so the perf logger is in place before RCTBridge / RCTHost instantiate any module. (RNSentry's own +load is reserved by RCT_EXPORT_MODULE().)
    • cpp/**/*.{h,cpp} added to the podspec sources; files are guarded with RCT_NEW_ARCH_ENABLED so Old Arch builds compile to empty TUs.
  • Android (New Architecture only):

    • New libsentry-tm-perf-logger.so shared library built via CMake. Exposes JNI_OnLoad (which installs the perf logger) and Java_io_sentry_react_RNSentryTurboModulePerfTracker_nativeSetEnabled (runtime gate).
    • Links against React Native's reactnative prefab. The missing <reactperflogger/NativeModulePerfLogger.h> header (which the prefab transitively pulls in but doesn't ship) is plugged by pointing CMake at the source tree — REACT_NATIVE_DIR is resolved at gradle config time via node --print require.resolve('react-native/package.json'), mirroring how react-native-reanimated does it. Honours an explicit REACT_NATIVE_NODE_MODULES_DIR override.
    • RNSentryPackage's static initializer System.loadLibrarys the perf-logger lib — host apps do NOT need to touch their own OnLoad.cpp. A guarded try { … } catch (UnsatisfiedLinkError) keeps Old Architecture (and any host that strips the lib) working as before.

Runtime gate

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.

Today the SDK auto-wraps only the RNSentry TurboModule via turboModuleContextIntegration({ modules: […] }). Every other TurboModule (RN's built-ins, third-party) requires the user to manually register it, and JSI HostObjects don't always expose methods to JS — which means many native calls slip past the JS-side wrapper entirely.

A native perf logger gives universal coverage of every TurboModule in the app, including ones the user has never heard of, for free.

Project: Turbo Modules instrumentation improvements · Issue: RN-638.

💚 How did you test it?

  • iOS sample (samples/react-native, New Architecture, Hermes, RN 0.86): xcodebuild build succeeds; +load fires; perf logger is installed before any module instantiates.
  • Android sample (samples/react-native, New Architecture, RN 0.86): both :sentry_react-native:assembleRelease and :app:assembleDebug build cleanly. libsentry-tm-perf-logger.so is built for all four ABIs (arm64-v8a, armeabi-v7a, x86, x86_64) and exports the expected symbols:
    T JNI_OnLoad
    T Java_io_sentry_react_RNSentryTurboModulePerfTracker_nativeSetEnabled
    T Sentry_InstallTurboModulePerfLogger
    T Sentry_SetTurboModuleTrackingEnabled
    
  • Standalone C++ syntax check with the New-Architecture-enabled and Old-Architecture include paths from the sample app's Pods — both compile clean.
  • No sink installed: the runtime flag is false by default and the foundation has no behaviour change beyond the install of the (otherwise inert) logger. End-to-end behavioural validation will come with the follow-up sink PRs.

📝 Checklist

  • I added tests to verify changes
    • No tests in this PR; the perf logger has no observable behaviour without a sink. Follow-up issues (sinks) will be covered by integration tests against real TurboModule call patterns.
  • 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.

Install a Sentry-owned `facebook::react::NativeModulePerfLogger` on
both platforms so the SDK observes every TurboModule lifecycle event \u2014
`moduleDataCreate*`, `moduleCreate*`, sync/async method call
`start`/`end`/`fail`, async dispatch and execution `start`/`end`/`fail`
\u2014 for follow-up features (crash attribution, per-module spans,
aggregated stats) to plug into.

The implementation is split into:

- **Shared C++** (`packages/core/cpp/`): a single
  `SentryTurboModulePerfController` singleton owns the installed logger
  and an atomic `enabled` flag. When disabled, every callback hits one
  atomic load and returns. When enabled, callbacks are forwarded to a
  swappable `ISentryTurboModulePerfSink` \u2014 follow-up issues ship the
  sinks; this PR just exposes the hook.

- **iOS**: the perf logger is installed from a dedicated installer
  class's `+load` so it fires before `RCTBridge` / `RCTHost` create
  their first TurboModule. (`RNSentry`'s own `+load` is reserved by
  `RCT_EXPORT_MODULE()`.) The cpp/ directory is added to the podspec
  sources; files are guarded with `RCT_NEW_ARCH_ENABLED` so Old Arch
  builds compile to empty TUs.

- **Android**: a new `libsentry-tm-perf-logger.so` shared library is
  built via CMake under New Architecture only and exposes `JNI_OnLoad`
  + a tiny `nativeSetEnabled` JNI hook. It links against React
  Native's `reactnative` prefab; the missing
  `<reactperflogger/NativeModulePerfLogger.h>` header is plugged by
  pointing the include path at the source tree (mirroring how
  react-native-reanimated resolves react-native via the standard
  `REACT_NATIVE_NODE_MODULES_DIR` / `require.resolve` fallback).
  `RNSentryPackage`'s static initializer `System.loadLibrary`s the
  perf-logger lib \u2014 host apps do NOT need to touch their own
  `OnLoad.cpp`. A guarded `try { \u2026 } catch (UnsatisfiedLinkError)`
  keeps Old Architecture (and any host that strips the lib) working
  as before.

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

Foundation only \u2014 no sink is installed in this PR. Follow-up issues
ship the actual instrumentation.

Closes #6162
@github-actions

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.5.1 by github-actions in #6305
  • chore(deps): update JavaScript SDK to v10.58.0 by github-actions in #6296
  • chore: Fix lint issues by antonis in #6300
  • chore: Bump sample and perf test apps to React Native 0.86.0 by antonis in #6287
  • fix(deps): bump form-data from 4.0.5 to 4.0.6 by antonis in #6297
  • fix(ci): Handle @sentry-internal/* package renames in JS updater by antonis in #6295
  • Record network request/response bodies in Session Replay by alwx in #6288
  • chore(deps): bump tar from 7.5.11 to 7.5.16 by dependabot in #6293
  • fix(ci): Update renamed @sentry-internal/* packages in JS updater script by antonis in #6294
  • chore(deps): bump launch-editor from 2.11.1 to 2.14.1 by dependabot in #6291
  • chore(deps-dev): bump @babel/core from 7.26.7 to 7.29.6 by dependabot in #6292
  • fix(deps): Resolve shell-quote to >=1.8.4 (Dependabot RNSentryModule.captureEvent is ignoring environment #547) by antonis in #6286
  • fix(ci): Support version catalog in android SDK version check by antonis in #6280
  • test(e2e): Bump E2E tests to React Native 0.86.0 by antonis in #6268
  • feat(android): Add nativeStackAndroid support to NativeLinkedErrors by lucas-zimerman in #6278
  • chore(deps): bump ruby/setup-ruby from 1.310.0 to 1.313.0 by dependabot in #6282
  • chore(deps): update Maestro to v2.6.1 by github-actions in #6277
  • chore(deps): bump gradle/actions from 6.1.0 to 6.2.0 by dependabot in #6284
  • chore(deps): bump getsentry/craft from 2.26.8 to 2.26.10 by dependabot in #6283
  • chore(deps): bump getsentry/craft/.github/workflows/changelog-preview.yml from 2.26.8 to 2.26.10 by dependabot in #6281
  • chore(deps): update Sentry Android Gradle Plugin to v6.11.0 by github-actions in #6275
  • chore(deps): update Android SDK to v8.43.2 by github-actions in #6273
  • chore(deps): bump joi from 17.13.3 to 17.13.4 by dependabot in #6279

Plus 3 more


🤖 This preview updates automatically when you update the PR.

@github-actions

Copy link
Copy Markdown
Contributor
Fails
🚫 Pull request is not ready for merge, please add the "ready-to-merge" label to the pull request
🚫 Please consider adding a changelog entry for the next release.
Messages
📖 Do not forget to update Sentry-docs with your feature once the pull request gets approved.

Instructions and example for changelog

Please add an entry to CHANGELOG.md to the "Unreleased" section. Make sure the entry includes this PR's number.

Example:

## Unreleased

### Features

- Wire TurboModulePerfLogger on iOS and Android ([#6307](https://github.com/getsentry/sentry-react-native/pull/6307))

If none of the above apply, you can opt out of this check by adding #skip-changelog to the PR description or adding a skip-changelog label.

Generated by 🚫 dangerJS against 3b618c5

/// as a C-linkage symbol so the JNI side can call it from `JNI_OnLoad`
/// without dragging the C++ ABI through the JNI boundary).
class SentryTurboModulePerfController {
public:

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.

No tests for SentryTurboModulePerfController or RNSentryTurboModulePerfTracker

The new SentryTurboModulePerfController (idempotent install, enable/disable flag, swappable sink) and RNSentryTurboModulePerfTracker (lazy-link error handling, nativeUnavailable guard) have no accompanying tests, leaving the core wiring logic uncovered.

Evidence
  • packages/core/android/src/test/java/io/sentry/react/ contains only RNSentryFramesDelayTest.java and RNSentryUriValidationTest.java — no test for RNSentryTurboModulePerfTracker.
  • No C++ test file matching *TurboModule*, *PerfLogger*, or *PerfController* was found anywhere in the repo.
  • RNSentryTurboModulePerfTracker.setEnabled() has a non-trivial nativeUnavailable latch and swallows UnsatisfiedLinkError; this branch logic has no unit coverage.
  • SentryTurboModulePerfController::install() idempotency via compare_exchange_strong and the setSink/sink concurrent access pattern are also untested.
Also found at 3 additional locations
  • packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java:198-201
  • packages/core/cpp/SentryTurboModulePerfLogger.cpp:191
  • packages/core/ios/RNSentry.mm:83

Identified by Warden code-review · 9WC-W6L

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wire TurboModulePerfLogger on iOS and Android

1 participant