reactotron-react-native
Version:
A development tool to explore, inspect, and diagnose your React Native apps.
177 lines (155 loc) • 6 kB
text/typescript
/**
* 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