Skip to content

WIP: Turbo Modules crash time context#6227

Draft
alwx wants to merge 1 commit into
mainfrom
alwx/improvement/turbo-modules-chash-time-context
Draft

WIP: Turbo Modules crash time context#6227
alwx wants to merge 1 commit into
mainfrom
alwx/improvement/turbo-modules-chash-time-context

Conversation

@alwx
Copy link
Copy Markdown
Contributor

@alwx alwx commented May 28, 2026

📢 Type of change

  • Bugfix
  • New feature
  • Enhancement
  • Refactoring

📜 Description

💡 Motivation and Context

💚 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.
  • I updated the wizard if needed.
  • All tests passing
  • No breaking changes

🔮 Next steps

@alwx alwx self-assigned this May 28, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 28, 2026

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).


  • WIP: Turbo Modules crash time context by alwx in #6227
  • fix(ci): Remove @sentry/types from JS SDK updater script by antonis in #6219
  • feat(tracing): Add breadcrumbs for dispatched React Navigation events by antonis in #6218
  • feat(tracing): Add Sentry.NavigationContainer wrapper for React Navigation by antonis in #6199
  • chore(e2e): Update Expo sample to SDK 56 by antonis in #6216
  • feat(ios): opt-in consumption of sentry-cocoa via Swift Package Manager by alwx in #6182
  • chore(deps): bump getsentry/craft from 2.26.5 to 2.26.6 by dependabot in #6213
  • chore(deps): bump getsentry/craft/.github/workflows/changelog-preview.yml from 2.26.5 to 2.26.6 by dependabot in #6214
  • chore(deps): bump github/codeql-action from 4.35.5 to 4.36.0 by dependabot in #6215
  • fix(ios): Return NO from requiresMainQueueSetup by antonis in #6202
  • fix(tracing): Bound TTID/TTFD to prevent inflated transactions by antonis in #6210
  • feat(core): Add disableAutoUpload option to Expo plugin by antonis in #6195
  • chore(deps): Remove unused @sentry/types dependency by antonis in #6207
  • Correct route and dynamic param extraction for Expo Router (Correct route and dynamic param extraction for Expo Router #6157) by alwx in #6197
  • chore(deps): update CLI to v3.4.3 by github-actions in #6205
  • chore(deps): update Cocoa SDK to v9.14.0 by github-actions in #6204
  • chore(deps): update Maestro to v2.6.0 by github-actions in #6198
  • chore(deps): bump js-cookie from 3.0.5 to 3.0.7 by dependabot in #6203
  • chore(deps): bump @tootallnate/once from 2.0.0 to 2.0.1 by dependabot in #6206
  • chore(deps): update Sentry Android Gradle Plugin to v6.8.1 by github-actions in #6196
  • feat(core): Expose pauseAppHangTracking and resumeAppHangTracking APIs by antonis in #6192

🤖 This preview updates automatically when you update the PR.

@alwx alwx changed the title Turbo Modules crash time context WIP: Turbo Modules crash time context May 28, 2026
@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.

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

- Turbo Modules crash time context ([#6227](https://github.com/getsentry/sentry-react-native/pull/6227))

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 f43a4f5

Comment on lines +58 to +75
const callId = pushTurboModuleCall({ name, method: key, kind: 'sync' });
let result: unknown;
try {
result = originalFn.apply(this, args);
} catch (e) {
popTurboModuleCall(callId);
throw e;
}

if (isThenable(result)) {
// Re-record as async — clearer in the report. We just overwrite the
// existing tracker frame in place by popping + re-pushing with a fresh
// id would lose ordering, so instead we leave the stack frame alone
// and only relabel for the scope on completion (it's the *active*
// call's `kind` that ends up in `contexts.turbo_module`, and the
// outer perf-logger driven users can push with `kind: 'async'`
// directly when they know up front).
return (result as Promise<unknown>).then(
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.

Async TurboModule calls always recorded as kind: 'sync' in crash context

Every method call pushes with kind: 'sync' (line 58), and when the result is thenable there is no mutation or re-push to change the stack frame's kind to 'async' — so async calls appear as kind: 'sync' in contexts.turbo_module for the entire duration of the async operation.

Evidence
  • pushTurboModuleCall at line 58 is always called with kind: 'sync'.
  • syncToScope is invoked inside pushTurboModuleCall immediately, writing kind: 'sync' into contexts.turbo_module.
  • The isThenable branch (line 65) only wraps .then/.catch to call popTurboModuleCall; it never calls pushTurboModuleCall again with kind: 'async' or mutates the existing stack frame.
  • popTurboModuleCall in turboModuleTracker.ts only removes the frame and updates the scope to the new top — it does not relabel the kind.
  • The inline comment at lines 67-77 describes a relabeling that never materialises in the implementation.
Also found at 1 additional location
  • packages/core/etc/sentry-react-native.api.md:734

Identified by Warden code-review · EUK-DJP

Comment on lines +88 to +99
return result;
};
}

try {
Object.defineProperty(module, WRAPPED_FLAG, {
value: true,
enumerable: false,
configurable: false,
writable: false,
});
} catch (e) {
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.

Sealed TurboModule proxies can be double-wrapped, causing duplicate tracker pushes per call

When Object.defineProperty throws for a sealed proxy, WRAPPED_FLAG is never set, so the guard at line 39 (if (maybeWrapped[WRAPPED_FLAG])) always passes on subsequent calls to wrapTurboModule, re-wrapping already-wrapped methods and pushing the same call twice onto the tracker stack.

Evidence
  • The guard if (maybeWrapped[WRAPPED_FLAG]) at line 39 relies solely on Object.defineProperty having succeeded.
  • The catch block (lines 93-99) only logs a warning and does not apply any fallback to prevent future wrapping.
  • A second call to wrapTurboModule on the same sealed proxy will iterate all keys again and wrap the already-wrapped function references, so each real call pushes callId twice.
  • The inline comment in the catch block explicitly acknowledges "the per-call pushes would double up" without mitigating it.
  • No test covers the sealed-proxy re-wrap scenario.
Also found at 1 additional location
  • packages/core/etc/sentry-react-native.api.md:799

Identified by Warden code-review · AF3-FXS

Comment on lines +21 to +25
const fakeModule = {
addListener: jest.fn(),
removeListeners: jest.fn(),
crash: jest.fn(),
};
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.

Object.keys in wrapTurboModule silently skips prototype methods on JSI TurboModule proxies

wrapTurboModule uses Object.keys(target) which only enumerates own enumerable properties; if a real JSI-backed TurboModule proxy exposes methods via its prototype chain (as some RN HostObject implementations do), nothing gets wrapped and the integration silently provides no crash context.

Evidence
  • wrapTurboModule.ts:45 iterates Object.keys(target) – only own, enumerable string keys.
  • getRNSentryModule() in wrapper.ts:45 returns TurboModuleRegistry.get('RNSentry'), which is a JSI HostObject proxy in RN's TurboModule architecture; JSI HostObjects can surface methods through the prototype chain rather than as own properties.
  • The test at lines 21-25 creates a plain JS object literal ({ addListener, removeListeners, crash }) where all methods are own enumerable properties, so Object.keys finds them and the test passes regardless of how real proxies work.
  • wrapTurboModule.test.ts also exclusively uses plain object literals, leaving the prototype-method path untested.
  • If real module methods are not own properties, Object.keys returns nothing to wrap; no warning or error is emitted, so the failure is silent.
Also found at 1 additional location
  • packages/core/src/js/integrations/default.ts:45-45

Identified by Warden find-bugs · B3Z-ZZF

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.

1 participant