Skip to content

Android: turbo_module.* crash tags permanently pinned to RNSentry.initNativeReactNavigationNewFrameTracking (promise never settles) #6821

Description

@andreycretsu

Summary

On Android, the crash-time TurboModule attribution added in #6227 is permanently pinned to a single startup call. Every native crash report from an Android app carries:

turbo_module.name:   RNSentry
turbo_module.method: initNativeReactNavigationNewFrameTracking

regardless of what was actually executing. The tags are not stale-by-a-moment; they are stuck for the entire process lifetime, so the feature reports a wrong answer rather than no answer. iOS is unaffected.

Cause

Three facts compose:

  1. wrapTurboModule pops a tracker frame for a promise-returning method only when that promise settles (src/js/turbomodule/wrapTurboModule.ts — the isThenable(result) branch pops in both then handlers).

  2. The Android implementation never settles the promise:

    // android/src/main/java/io/sentry/react/RNSentryModuleImpl.java
    public void initNativeReactNavigationNewFrameTracking(Promise promise) {
      this.initFragmentInitialFrameTracking();
    }

    promise is accepted and dropped — no resolve, no reject, no path that settles it later. iOS does the equivalent work and then calls resolve(nil) (ios/RNSentry.mm).

  3. RNSENTRY_SKIP in turboModuleContextIntegration does not include this method, so it is wrapped like any other.

The frame pushed at startup therefore never pops. popTurboModuleCall walks the stack and re-syncs the scope onto "the newest remaining frame on the same scope" after every subsequent pop — which, once the app is idle, is always the leaked startup frame. The native scope is left holding it, and sentry-java serialises it into every crash report.

Two smaller consequences of the same leak: the tracker stack never drains to empty (so clearScope's empty-string sentinel path is unreachable on Android), and the aggregate record for that call is never emitted.

Steps to reproduce

  1. Expo app on Android with enableTimeToInitialDisplay on (expoRouterIntegration or reactNavigationIntegration).
  2. Let the app finish starting, navigate anywhere, idle.
  3. Trigger any native crash — e.g. Sentry.nativeCrash() — or read getTurboModuleCallStack().
  4. The event's tags report RNSentry.initNativeReactNavigationNewFrameTracking as the in-flight call; the stack still holds that frame.

Suggested fix

Settle the promise in the Android implementation, mirroring iOS:

public void initNativeReactNavigationNewFrameTracking(Promise promise) {
  this.initFragmentInitialFrameTracking();
  promise.resolve(null);
}

Adding the method to RNSENTRY_SKIP would hide the symptom but leave the dangling promise, so the native-side fix seems the right one. It may be worth auditing the other Promise-taking methods in RNSentryModuleImpl for the same shape.

Versions

  • @sentry/react-native: reproduced on 8.27.0; the Android code is unchanged on 8.29.0 (current latest)
  • Platform: Android only (iOS resolves correctly)

Activity

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

Metadata

Metadata

Assignees

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions