11import { logger } from '@sentry/react' ;
22
3- import { popTurboModuleCall , pushTurboModuleCall } from './turboModuleTracker' ;
4-
5- const WRAPPED_FLAG = '__sentryTurboModuleWrapped__' ;
3+ import { popTurboModuleCall , pushTurboModuleCall , relabelTurboModuleCallKind } from './turboModuleTracker' ;
64
75/**
8- * Marker added to wrapped modules so we never double-wrap (which would push the
9- * same call twice onto the tracker stack) .
6+ * Modules we've already wrapped. Tracked off-module so that even sealed proxies
7+ * (which can't accept a marker property) are protected from double-wrapping .
108 */
11- interface MaybeWrapped {
12- [ WRAPPED_FLAG ] ?: boolean ;
9+ let wrappedModules = new WeakSet < object > ( ) ;
10+
11+ /** Tests only. */
12+ export function _resetWrappedModules ( ) : void {
13+ wrappedModules = new WeakSet < object > ( ) ;
1314}
1415
1516/**
@@ -18,8 +19,8 @@ interface MaybeWrapped {
1819 * `module` reference for chaining convenience.
1920 *
2021 * - Sync methods are tracked as `kind: 'sync'` and popped right after the call.
21- * - Async methods (those returning a thenable) are tracked as `kind: 'async'`
22- * and popped when the returned promise settles.
22+ * - Async methods (those returning a thenable) are relabelled to `kind: 'async'`
23+ * right after the call dispatches and popped when the returned promise settles.
2324 *
2425 * `skip` can be used to opt specific method names out of tracking (e.g. very
2526 * hot, no-op methods like RN's `addListener`/`removeListeners` event-emitter
@@ -34,15 +35,24 @@ export function wrapTurboModule<T extends object>(
3435 return module ;
3536 }
3637
37- const maybeWrapped = module as T & MaybeWrapped ;
38- if ( maybeWrapped [ WRAPPED_FLAG ] ) {
38+ if ( wrappedModules . has ( module ) ) {
3939 return module ;
4040 }
41+ wrappedModules . add ( module ) ;
4142
4243 const skip = new Set ( options . skip ?? [ ] ) ;
44+ const methodNames = collectMethodNames ( module ) ;
45+
46+ if ( methodNames . length === 0 ) {
47+ logger . warn (
48+ `[TurboModuleTracker] No methods discovered on '${ name } ' — TurboModule context will not be attached for this module. ` +
49+ `This usually means the module is a JSI HostObject that only materialises methods on first access.` ,
50+ ) ;
51+ return module ;
52+ }
4353
4454 const target = module as unknown as Record < string , unknown > ;
45- for ( const key of Object . keys ( target ) ) {
55+ for ( const key of methodNames ) {
4656 if ( skip . has ( key ) ) {
4757 continue ;
4858 }
@@ -52,9 +62,9 @@ export function wrapTurboModule<T extends object>(
5262 }
5363 const originalFn = original as ( ...a : unknown [ ] ) => unknown ;
5464
55- target [ key ] = function sentryTurboModuleWrapper ( this : unknown , ...args : unknown [ ] ) : unknown {
65+ const wrapper = function sentryTurboModuleWrapper ( this : unknown , ...args : unknown [ ] ) : unknown {
5666 // We don't know yet whether `original` is sync or async — start optimistic
57- // as sync, upgrade the scope context if the result is thenable.
67+ // as sync, relabel to 'async' if the result turns out to be thenable.
5868 const callId = pushTurboModuleCall ( { name, method : key , kind : 'sync' } ) ;
5969 let result : unknown ;
6070 try {
@@ -65,13 +75,7 @@ export function wrapTurboModule<T extends object>(
6575 }
6676
6777 if ( isThenable ( result ) ) {
68- // Re-record as async — clearer in the report. We just overwrite the
69- // existing tracker frame in place by popping + re-pushing with a fresh
70- // id would lose ordering, so instead we leave the stack frame alone
71- // and only relabel for the scope on completion (it's the *active*
72- // call's `kind` that ends up in `contexts.turbo_module`, and the
73- // outer perf-logger driven users can push with `kind: 'async'`
74- // directly when they know up front).
78+ relabelTurboModuleCallKind ( callId , 'async' ) ;
7579 return ( result as Promise < unknown > ) . then (
7680 value => {
7781 popTurboModuleCall ( callId ) ;
@@ -87,29 +91,41 @@ export function wrapTurboModule<T extends object>(
8791 popTurboModuleCall ( callId ) ;
8892 return result ;
8993 } ;
90- }
9194
92- try {
93- Object . defineProperty ( module , WRAPPED_FLAG , {
94- value : true ,
95- enumerable : false ,
96- configurable : false ,
97- writable : false ,
98- } ) ;
99- } catch ( e ) {
100- // Some TurboModule proxies are sealed — that's fine, we still patched the
101- // methods, but a second wrap call would be a no-op anyway because the
102- // properties now point at our wrappers (re-wrapping would still push
103- // through to `original` which is itself a wrapper, but the per-call
104- // pushes would double up). Log so this is visible during development.
105- logger . warn (
106- `[TurboModuleTracker] Could not mark ${ name } as wrapped — repeated wrapping would double-track invocations.` ,
107- ) ;
95+ try {
96+ target [ key ] = wrapper ;
97+ } catch {
98+ // Sealed / non-writable property — can't intercept this method, but we
99+ // can still wrap the rest. Skip silently; the module-level method-count
100+ // check above is the cliff that catches the "wrapped nothing" case.
101+ }
108102 }
109103
110104 return module ;
111105}
112106
107+ /**
108+ * Returns the union of own + prototype-chain method names on `module`,
109+ * deduplicated and skipping `Object.prototype`. Walking the prototype chain is
110+ * necessary for JSI HostObject-backed TurboModule proxies under RN's New
111+ * Architecture, which can expose methods via the proto chain rather than as
112+ * own enumerable properties.
113+ */
114+ function collectMethodNames ( module : object ) : string [ ] {
115+ const seen = new Set < string > ( ) ;
116+ let current : object | null = module ;
117+ while ( current && current !== Object . prototype ) {
118+ for ( const key of Object . getOwnPropertyNames ( current ) ) {
119+ if ( key === 'constructor' ) {
120+ continue ;
121+ }
122+ seen . add ( key ) ;
123+ }
124+ current = Object . getPrototypeOf ( current ) as object | null ;
125+ }
126+ return Array . from ( seen ) ;
127+ }
128+
113129function isThenable ( value : unknown ) : value is PromiseLike < unknown > {
114130 if ( ! value || ( typeof value !== 'object' && typeof value !== 'function' ) ) {
115131 return false ;
0 commit comments