UNPKG

@sentry/react-native

Version:
200 lines 7.69 kB
import { logger } from '@sentry/core'; import { recordTurboModuleCall } from './turboModuleAggregator'; import { popTurboModuleCall, pushTurboModuleCall, relabelTurboModuleCallKind } from './turboModuleTracker'; /** * Modules we've already wrapped. Tracked off-module so that even sealed proxies * (which can't accept a marker property) are protected from double-wrapping. */ let wrappedModules = new WeakSet(); /** Tests only. */ export function _resetWrappedModules() { wrappedModules = new WeakSet(); } /** * Wraps every function-valued property on the given TurboModule so that each * invocation is recorded on the Sentry TurboModule tracker. Returns the same * `module` reference for chaining convenience. * * - Sync methods are tracked as `kind: 'sync'` and popped right after the call. * - Async methods (those returning a thenable) are relabelled to `kind: 'async'` * right after the call dispatches and popped when the returned promise settles. * * `skip` can be used to opt specific method names out of tracking (e.g. very * hot, no-op methods like RN's `addListener`/`removeListeners` event-emitter * stubs which would otherwise pollute the scope). */ export function wrapTurboModule(name, module, options = {}) { var _a; if (!module) { return module; } if (wrappedModules.has(module)) { return module; } const skip = new Set((_a = options.skip) !== null && _a !== void 0 ? _a : []); const methodNames = collectMethodNames(module); if (methodNames.length === 0) { // Do NOT add to wrappedModules — a later call (e.g. once a JSI HostObject // has materialised its methods) should still get a chance to wrap. logger.warn(`[TurboModuleTracker] No methods discovered on '${name}' — TurboModule context will not be attached for this module. ` + `This usually means the module is a JSI HostObject that only materialises methods on first access.`); return module; } let wrappedAny = false; const target = module; for (const key of methodNames) { if (skip.has(key)) { continue; } // `target[key]` may be a getter (some JSI HostObject proxies expose methods // as accessors under the New Architecture). Guard the read so a throwing // getter is treated like a non-wrappable property instead of aborting the // entire wrap loop. let original; try { original = target[key]; } catch (_b) { continue; } if (typeof original !== 'function') { continue; } const originalFn = original; const wrapper = function sentryTurboModuleWrapper(...args) { // The instrumentation must never break the user's call — every tracker // interaction is isolated so a failure inside Sentry only drops the // attribution data, never the real TurboModule invocation. // // We don't know yet whether `original` is sync or async — start optimistic // as sync, relabel to 'async' if the result turns out to be thenable. let callId; const startedAtMs = Date.now(); try { callId = pushTurboModuleCall({ name, method: key, kind: 'sync' }); } catch (e) { logger.warn(`[TurboModuleTracker] push failed for ${name}.${key}: ${String(e)}`); } let result; try { result = originalFn.apply(this, args); } catch (e) { safePop(callId, name, key); safeRecord(name, key, 'sync', startedAtMs, true); throw e; } if (isThenable(result)) { safeRelabel(callId, 'async', name, key); return result.then(value => { safePop(callId, name, key); safeRecord(name, key, 'async', startedAtMs, false); return value; }, err => { safePop(callId, name, key); safeRecord(name, key, 'async', startedAtMs, true); throw err; }); } safePop(callId, name, key); safeRecord(name, key, 'sync', startedAtMs, false); return result; }; try { target[key] = wrapper; wrappedAny = true; } catch (_c) { // Sealed / non-writable property — can't intercept this method, but we // can still wrap the rest. Skip silently. } } // Only mark as wrapped if we actually installed at least one wrapper. // Otherwise a future call (e.g. after the proxy has materialised methods) // should be allowed to retry. if (wrappedAny) { wrappedModules.add(module); } else { // We discovered methods but couldn't intercept any of them — e.g. the // module is frozen, or every method is a read-only accessor. Surface this // so the silent no-op is debuggable (the issue would otherwise look like // "no crash context attached" with no obvious cause). logger.warn(`[TurboModuleTracker] '${name}' has methods but none could be wrapped — TurboModule context will not be attached. ` + `This usually means the module is frozen or its methods are non-writable accessors.`); } return module; } /** * Returns the union of own + prototype-chain method names on `module`, * deduplicated and skipping `Object.prototype`. Walking the prototype chain is * necessary for JSI HostObject-backed TurboModule proxies under RN's New * Architecture, which can expose methods via the proto chain rather than as * own enumerable properties. */ function collectMethodNames(module) { const seen = new Set(); let current = module; while (current && current !== Object.prototype) { for (const key of Object.getOwnPropertyNames(current)) { if (key === 'constructor') { continue; } seen.add(key); } current = Object.getPrototypeOf(current); } return Array.from(seen); } function safePop(callId, name, method) { if (callId === undefined) { return; } try { popTurboModuleCall(callId); } catch (e) { logger.warn(`[TurboModuleTracker] pop failed for ${name}.${method}: ${String(e)}`); } } function safeRelabel(callId, kind, name, method) { if (callId === undefined) { return; } try { relabelTurboModuleCallKind(callId, kind); } catch (e) { logger.warn(`[TurboModuleTracker] relabel failed for ${name}.${method}: ${String(e)}`); } } function safeRecord(name, method, kind, startedAtMs, errored) { try { recordTurboModuleCall({ name, method, kind, durationMs: Date.now() - startedAtMs, errored, }); } catch (e) { logger.warn(`[TurboModuleTracker] record failed for ${name}.${method}: ${String(e)}`); } } function isThenable(value) { if (!value || (typeof value !== 'object' && typeof value !== 'function')) { return false; } // A user-defined `then` getter could throw — don't let that escape into the // wrapper (which would leak the in-flight tracker frame on top of the bug). try { const then = value.then; return typeof then === 'function'; } catch (_a) { return false; } } //# sourceMappingURL=wrapTurboModule.js.map