@sentry/react-native
Version:
Official Sentry SDK for react-native
187 lines • 7.52 kB
JavaScript
import { getIsolationScope } from '@sentry/core';
const CONTEXT_KEY = 'turbo_module';
const TAG_NAME = 'turbo_module.name';
const TAG_METHOD = 'turbo_module.method';
let nextCallId = 0;
/**
* Stack of active TurboModule invocations.
*
* React Native's TurboModule perf logger fires `syncMethodCallStart/End` and
* `asyncMethodCallExecutionStart/End` from the thread executing the C++ method.
* In JS-land we don't have per-OS-thread storage, but the JS thread is single
* threaded — so a single shared stack faithfully models the active call chain
* for everything dispatched from JS.
*
* NOTE: This is an in-memory mirror only. For true async-signal-safety on the
* native crash path we'd want to also write a fixed-size ring buffer of
* `{module_id, method_id}` indexes into shared storage that sentry-cocoa /
* sentry-java can read from a signal handler. The current implementation relies
* on the native SDKs' existing scope mirroring (which serialises `contexts` and
* `tags` for crash reports) — this covers crashes that happen *after* the
* scope update is flushed but is not strictly async-signal-safe.
*/
const stack = [];
/**
* Returns the active TurboModule call (top of stack), or `undefined` if no
* TurboModule call is currently being tracked.
*/
export function getActiveTurboModuleCall() {
return stack[stack.length - 1];
}
/**
* Returns a copy of the current TurboModule call stack, top-most call last.
* Exposed for tests and diagnostics.
*/
export function getTurboModuleCallStack() {
return stack.slice();
}
/**
* Resets the tracker. Tests only.
*/
export function _resetTurboModuleTracker() {
stack.length = 0;
nextCallId = 0;
}
/**
* Records the start of a TurboModule method invocation and mirrors it onto the
* current Sentry scope so that any crash report captured during the call
* carries `contexts.turbo_module` + `turbo_module.*` tags.
*
* Returns the assigned `callId`, to be passed back into {@link popTurboModuleCall}.
*/
export function pushTurboModuleCall(args) {
var _a;
const call = {
name: args.name,
method: args.method,
kind: args.kind,
startedAtMs: Date.now(),
callId: nextCallId++,
// Default to the isolation scope: it's the one wired up to
// `enableSyncToNative`, so writes here propagate to the native SDKs and
// get serialised into crash reports. `getCurrentScope()` would be wrong
// here — it can return a forked scope (per async-context strategy) that
// sentry-cocoa / sentry-java never sees.
scope: (_a = args.scope) !== null && _a !== void 0 ? _a : getIsolationScope(),
};
// Atomic push: if `syncToScope` throws (e.g. a scope-sync hook calls into a
// native bridge that rejects with `_NativeClientError`), roll back the stack
// push so we don't leak a frame.
stack.push(call);
try {
syncToScope(call);
}
catch (e) {
stack.pop();
throw e;
}
return call.callId;
}
/**
* Updates the `kind` of a previously-pushed call (in place) and re-syncs the
* scope if the call is currently the active one. Used by
* {@link wrapTurboModule} once it discovers that a method's return value is
* thenable.
*
* Returns `true` if the call was found and relabelled.
*/
export function relabelTurboModuleCallKind(callId, kind) {
const call = stack.find(c => c.callId === callId);
if (!call || call.kind === kind) {
return !!call;
}
call.kind = kind;
if (stack[stack.length - 1] === call) {
syncToScope(call);
}
return true;
}
/**
* Records the end of a TurboModule method invocation previously started with
* {@link pushTurboModuleCall}. Pops the matching frame off the stack and
* updates the Sentry scope to point at the new top (or clears the context if
* the stack is now empty).
*
* `callId` is the value returned by `pushTurboModuleCall`. If the call cannot
* be found (e.g. due to a misuse / race), the pop is a no-op.
*/
export function popTurboModuleCall(callId) {
// The common case is a perfectly nested LIFO — pop from the end.
let popped;
const top = stack[stack.length - 1];
if ((top === null || top === void 0 ? void 0 : top.callId) === callId) {
popped = stack.pop();
}
else {
// Out-of-order completion (async). Find and splice.
const index = stack.findIndex(c => c.callId === callId);
if (index < 0) {
return;
}
[popped] = stack.splice(index, 1);
}
if (!popped) {
return;
}
// 1. Reflect the new state of `popped.scope` (the scope this call was pinned
// to). When scopes interleave on the stack (e.g. [A@s1, B@s2, C@s1] and
// we pop C), the immediate stack top is *not* the right thing to look at:
// there may still be a deeper frame holding `popped.scope` whose context
// we'd wipe by calling `clearScope`. Walk the stack from the top down and
// re-sync onto the newest remaining frame on `popped.scope`; only clear
// if none is left.
let remainingOnSameScope;
for (let i = stack.length - 1; i >= 0; i--) {
const frame = stack[i];
if (frame && frame.scope === popped.scope) {
remainingOnSameScope = frame;
break;
}
}
if (remainingOnSameScope) {
syncToScope(remainingOnSameScope);
}
else {
clearScope(popped.scope);
}
// 2. The native SDKs (sentry-cocoa / sentry-java) share a *single* native
// scope, but JS has many Scope objects (global, isolation, withScope, …).
// Every `Scope#setContext` / `Scope#setTag` we just made in step 1 fired
// the `scopeSync.ts` hook and overwrote the native scope's `turbo_module`
// context — even if the global top of the stack still lives on a
// *different* JS scope. Without this second sync, a crash that follows a
// cross-scope pop would land in native without the active TurboModule
// attribution, even though the global stack still has an in-flight call.
//
// Re-sync the global stack top via *its* scope so native ends up holding
// the correct active-call context. The intermediate native write in step 1
// is wasted but unavoidable without bypassing the public Scope API.
const globalTop = stack[stack.length - 1];
if (globalTop && globalTop !== remainingOnSameScope) {
syncToScope(globalTop);
}
}
function syncToScope(call) {
call.scope.setContext(CONTEXT_KEY, {
name: call.name,
method: call.method,
kind: call.kind,
started_at_ms: call.startedAtMs,
call_id: call.callId,
});
call.scope.setTag(TAG_NAME, call.name);
call.scope.setTag(TAG_METHOD, call.method);
}
// Empty-string sentinel for the "no active call" state. We don't pass
// `undefined` because the native `setTag(key, value)` TurboModule spec
// requires a string — the bridge would otherwise see `undefined` and either
// throw or silently drop the call. `setContext(CONTEXT_KEY, null)` is the
// canonical "no active call" signal; the empty tags exist only so the tag set
// doesn't carry stale `name`/`method` from the previous call.
const NO_ACTIVE_CALL = '';
function clearScope(scope) {
scope.setContext(CONTEXT_KEY, null);
scope.setTag(TAG_NAME, NO_ACTIVE_CALL);
scope.setTag(TAG_METHOD, NO_ACTIVE_CALL);
}
//# sourceMappingURL=turboModuleTracker.js.map