UNPKG

reactotron-react-native

Version:

A development tool to explore, inspect, and diagnose your React Native apps.

177 lines (155 loc) 6 kB
/** * Provides a global error handler to report errors.. */ import { type InferFeatures, type LoggerPlugin, type ReactotronCore, assertHasLoggerPlugin, type Plugin, } from "reactotron-core-client" import _LogBox, { type LogBoxStatic as LogBoxStaticPublic, // eslint-disable-next-line import/default, import/namespace } from "react-native/Libraries/LogBox/LogBox" // eslint-disable-next-line import/namespace import type { ExtendedExceptionData } from "react-native/Libraries/LogBox/Data/parseLogBoxLog" import type { SymbolicateStackTraceFn } from "../helpers/symbolicateStackTrace" import type { ParseErrorStackFn } from "../helpers/parseErrorStack" interface LogBoxStaticPrivate extends LogBoxStaticPublic { /** * @see https://github.com/facebook/react-native/blob/v0.72.1/packages/react-native/Libraries/LogBox/LogBox.js#L29 */ addException: (error: ExtendedExceptionData) => void } const LogBox = _LogBox as unknown as LogBoxStaticPrivate // a few functions to help source map errors -- these seem to be not available immediately // so we're lazy loading. let parseErrorStack: ParseErrorStackFn let symbolicateStackTrace: SymbolicateStackTraceFn export interface ErrorStackFrame { fileName: string functionName: string lineNumber: number columnNumber?: number | null } export interface TrackGlobalErrorsOptions { veto?: (frame: ErrorStackFrame) => boolean } // defaults const PLUGIN_DEFAULTS: TrackGlobalErrorsOptions = { veto: null, } const objectifyError = (error: Error) => { const objectifiedError = {} as Record<string, unknown> Object.getOwnPropertyNames(error).forEach((key) => { objectifiedError[key] = error[key] }) return objectifiedError } // const reactNativeFrameFinder = frame => contains('/node_modules/react-native/', frame.fileName) /** * Track global errors and send them to Reactotron logger. */ const trackGlobalErrors = (options?: TrackGlobalErrorsOptions) => (reactotron: ReactotronCore) => { // make sure we have the logger plugin assertHasLoggerPlugin(reactotron) const client = reactotron as ReactotronCore & InferFeatures<ReactotronCore, LoggerPlugin> // setup configuration const config = Object.assign({}, PLUGIN_DEFAULTS, options || {}) // manually fire an error function reportError(error: Parameters<typeof LogBox.addException>[0]) { try { if (!parseErrorStack) { const parseErrorStackModule = require("react-native/Libraries/Core/Devtools/parseErrorStack") // Handle both CommonJS (module.exports) and ESM (export default) formats parseErrorStack = typeof parseErrorStackModule === "function" ? parseErrorStackModule : parseErrorStackModule.default } if (!symbolicateStackTrace) { const symbolicateStackTraceModule = require("react-native/Libraries/Core/Devtools/symbolicateStackTrace") // Handle both CommonJS (module.exports) and ESM (export default) formats symbolicateStackTrace = typeof symbolicateStackTraceModule === "function" ? symbolicateStackTraceModule : symbolicateStackTraceModule.default } } catch (e) { client.error( 'Unable to load "react-native/Libraries/Core/Devtools/parseErrorStack" or "react-native/Libraries/Core/Devtools/symbolicateStackTrace"', [] ) client.debug(objectifyError(e)) return } if (!parseErrorStack || !symbolicateStackTrace) { client.error("parseErrorStack or symbolicateStackTrace is not available", []) client.debug({ parseErrorStackAvailable: !!parseErrorStack, symbolicateStackTraceAvailable: !!symbolicateStackTrace, }) return } if (typeof parseErrorStack !== "function") { client.error("parseErrorStack is not a function", []) client.debug({ parseErrorStackType: typeof parseErrorStack, parseErrorStack, }) return } if (typeof symbolicateStackTrace !== "function") { client.error("symbolicateStackTrace is not a function", []) client.debug({ symbolicateStackTraceType: typeof symbolicateStackTrace, symbolicateStackTrace, }) return } let parsedStacktrace: ReturnType<typeof parseErrorStack> try { // parseErrorStack arg type is wrong, it's expecting an array, a string, or a hermes error data, https://github.com/facebook/react-native/blob/v0.72.1/packages/react-native/Libraries/Core/Devtools/parseErrorStack.js#L41 parsedStacktrace = parseErrorStack(error.stack) } catch (e) { client.error("Unable to parse stack trace from error object", []) client.debug(objectifyError(e)) return } symbolicateStackTrace(parsedStacktrace) .then((symbolicatedStackTrace) => { let prettyStackFrames = symbolicatedStackTrace.stack.map((stackFrame) => ({ fileName: stackFrame.file, functionName: stackFrame.methodName, lineNumber: stackFrame.lineNumber, })) // does the dev want us to keep each frame? if (config.veto) { prettyStackFrames = prettyStackFrames.filter((frame) => config?.veto(frame)) } client.error(error.message, prettyStackFrames) // TODO: Fix this. }) .catch((e) => { client.error("Unable to symbolicate stack trace from error object", []) client.debug(objectifyError(e)) }) } // the reactotron plugin interface return { onConnect: () => { LogBox.addException = new Proxy(LogBox.addException, { apply: function (target, thisArg, argumentsList: Parameters<typeof LogBox.addException>) { const error = argumentsList[0] reportError(error) return target.apply(thisArg, argumentsList) }, }) }, // attach these functions to the Reactotron features: { reportError, }, } satisfies Plugin<ReactotronCore> } export default trackGlobalErrors