296 lines
9.8 KiB
JavaScript
296 lines
9.8 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow strict
|
|
* @format
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
import type {ExtendedError} from './ExtendedError';
|
|
import type {ExceptionData} from './NativeExceptionsManager';
|
|
|
|
export class SyntheticError extends Error {
|
|
name: string = '';
|
|
}
|
|
|
|
type ExceptionDecorator = ExceptionData => ExceptionData;
|
|
|
|
let userExceptionDecorator: ?ExceptionDecorator;
|
|
let inUserExceptionDecorator = false;
|
|
|
|
// This string is used to decorate an ExtendedError with extra data in select usecases.
|
|
// Note that data passed using this method should be strictly contained,
|
|
// as data that's not serializable/too large may cause issues with passing the error to the native code.
|
|
// TODO(T204185517): We should use a Symbol for this, but jsi through jsc doesn't support it yet.
|
|
const decoratedExtraDataKey = 'RN$ErrorExtraDataKey';
|
|
|
|
/**
|
|
* Allows the app to add information to the exception report before it is sent
|
|
* to native. This API is not final.
|
|
*/
|
|
|
|
function unstable_setExceptionDecorator(
|
|
exceptionDecorator: ?ExceptionDecorator,
|
|
) {
|
|
userExceptionDecorator = exceptionDecorator;
|
|
}
|
|
|
|
function preprocessException(data: ExceptionData): ExceptionData {
|
|
if (userExceptionDecorator && !inUserExceptionDecorator) {
|
|
inUserExceptionDecorator = true;
|
|
try {
|
|
return userExceptionDecorator(data);
|
|
} catch {
|
|
// Fall through
|
|
} finally {
|
|
inUserExceptionDecorator = false;
|
|
}
|
|
}
|
|
return data;
|
|
}
|
|
|
|
/**
|
|
* Handles the developer-visible aspect of errors and exceptions
|
|
*/
|
|
let exceptionID = 0;
|
|
function reportException(
|
|
e: ExtendedError,
|
|
isFatal: boolean,
|
|
reportToConsole: boolean, // only true when coming from handleException; the error has not yet been logged
|
|
) {
|
|
const parseErrorStack = require('./Devtools/parseErrorStack').default;
|
|
const stack = parseErrorStack(e?.stack);
|
|
const currentExceptionID = ++exceptionID;
|
|
const originalMessage = e.message || '';
|
|
let message = originalMessage;
|
|
if (e.componentStack != null) {
|
|
message += `\n\nThis error is located at:${e.componentStack}`;
|
|
}
|
|
const namePrefix = e.name == null || e.name === '' ? '' : `${e.name}: `;
|
|
|
|
if (!message.startsWith(namePrefix)) {
|
|
message = namePrefix + message;
|
|
}
|
|
|
|
// $FlowFixMe[unclear-type]
|
|
const extraData: Object = {
|
|
// $FlowFixMe[incompatible-use] we can't define a type with a Symbol-keyed field in flow
|
|
...e[decoratedExtraDataKey],
|
|
jsEngine: e.jsEngine,
|
|
rawStack: e.stack,
|
|
};
|
|
if (e.cause != null && typeof e.cause === 'object') {
|
|
extraData.stackSymbols = e.cause.stackSymbols;
|
|
extraData.stackReturnAddresses = e.cause.stackReturnAddresses;
|
|
extraData.stackElements = e.cause.stackElements;
|
|
}
|
|
|
|
const data = preprocessException({
|
|
message,
|
|
originalMessage: message === originalMessage ? null : originalMessage,
|
|
name: e.name == null || e.name === '' ? null : e.name,
|
|
componentStack:
|
|
typeof e.componentStack === 'string' ? e.componentStack : null,
|
|
stack,
|
|
id: currentExceptionID,
|
|
isFatal,
|
|
extraData,
|
|
});
|
|
|
|
if (reportToConsole) {
|
|
// we feed back into console.error, to make sure any methods that are
|
|
// monkey patched on top of console.error are called when coming from
|
|
// handleException
|
|
console.error(e);
|
|
}
|
|
|
|
if (__DEV__) {
|
|
// reportToConsole is only true when coming from handleException,
|
|
// and the error has not yet been logged. If it's false, then it was
|
|
// reported to LogBox in reactConsoleErrorHandler, and we don't need to.
|
|
if (reportToConsole) {
|
|
const LogBox = require('../LogBox/LogBox').default;
|
|
LogBox.addException({
|
|
...data,
|
|
isComponentError: !!e.isComponentError,
|
|
});
|
|
}
|
|
} else if (isFatal || e.type !== 'warn') {
|
|
const NativeExceptionsManager =
|
|
require('./NativeExceptionsManager').default;
|
|
if (NativeExceptionsManager) {
|
|
if (isFatal) {
|
|
if (global.RN$hasHandledFatalException?.()) {
|
|
return;
|
|
}
|
|
global.RN$notifyOfFatalException?.();
|
|
}
|
|
NativeExceptionsManager.reportException(data);
|
|
}
|
|
}
|
|
}
|
|
|
|
declare var console: {
|
|
error: (...data: $ReadOnlyArray<mixed>) => void,
|
|
_errorOriginal: (...data: $ReadOnlyArray<mixed>) => void,
|
|
reportErrorsAsExceptions: boolean,
|
|
...
|
|
};
|
|
|
|
// If we trigger console.error _from_ handleException,
|
|
// we do want to make sure that console.error doesn't trigger error reporting again
|
|
let inExceptionHandler = false;
|
|
|
|
/**
|
|
* Logs exceptions to the (native) console and displays them
|
|
*/
|
|
function handleException(e: mixed, isFatal: boolean) {
|
|
// TODO(T196834299): We should really use a c++ turbomodule for this
|
|
const reportToConsole = true;
|
|
if (
|
|
!global.RN$handleException ||
|
|
!global.RN$handleException(e, isFatal, reportToConsole)
|
|
) {
|
|
let error: Error;
|
|
if (e instanceof Error) {
|
|
error = e;
|
|
} else {
|
|
// Workaround for reporting errors caused by `throw 'some string'`
|
|
// Unfortunately there is no way to figure out the stacktrace in this
|
|
// case, so if you ended up here trying to trace an error, look for
|
|
// `throw '<error message>'` somewhere in your codebase.
|
|
error = new SyntheticError(e);
|
|
}
|
|
try {
|
|
inExceptionHandler = true;
|
|
/* $FlowFixMe[class-object-subtyping] added when improving typing for this
|
|
* parameters */
|
|
// $FlowFixMe[incompatible-call]
|
|
reportException(error, isFatal, reportToConsole);
|
|
} finally {
|
|
inExceptionHandler = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
|
|
* LTI update could not be added via codemod */
|
|
function reactConsoleErrorHandler(...args) {
|
|
// bubble up to any original handlers
|
|
console._errorOriginal(...args);
|
|
if (!console.reportErrorsAsExceptions) {
|
|
return;
|
|
}
|
|
if (inExceptionHandler || global.RN$inExceptionHandler?.()) {
|
|
// The fundamental trick here is that are multiple entry point to logging errors:
|
|
// (see D19743075 for more background)
|
|
//
|
|
// 1. An uncaught exception being caught by the global handler
|
|
// 2. An error being logged throw console.error
|
|
//
|
|
// However, console.error is monkey patched multiple times: by this module, and by the
|
|
// DevTools setup that sends messages to Metro.
|
|
// The patching order cannot be relied upon.
|
|
//
|
|
// So, some scenarios that are handled by this flag:
|
|
//
|
|
// Logging an error:
|
|
// 1. console.error called from user code
|
|
// 2. (possibly) arrives _first_ at DevTool handler, send to Metro
|
|
// 3. Bubbles to here
|
|
// 4. goes into report Exception.
|
|
// 5. should not trigger console.error again, to avoid looping / logging twice
|
|
// 6. should still bubble up to original console
|
|
// (which might either be console.log, or the DevTools handler in case it patched _earlier_ and (2) didn't happen)
|
|
//
|
|
// Throwing an uncaught exception:
|
|
// 1. exception thrown
|
|
// 2. picked up by handleException
|
|
// 3. should be sent to console.error (not console._errorOriginal, as DevTools might have patched _later_ and it needs to send it to Metro)
|
|
// 4. that _might_ bubble again to the `reactConsoleErrorHandle` defined here
|
|
// -> should not handle exception _again_, to avoid looping / showing twice (this code branch)
|
|
// 5. should still bubble up to original console (which might either be console.log, or the DevTools handler in case that one patched _earlier_)
|
|
return;
|
|
}
|
|
|
|
let error;
|
|
|
|
const firstArg = args[0];
|
|
if (firstArg?.stack) {
|
|
// reportException will console.error this with high enough fidelity.
|
|
error = firstArg;
|
|
} else {
|
|
const stringifySafe = require('../Utilities/stringifySafe').default;
|
|
const message = args
|
|
.map(arg => (typeof arg === 'string' ? arg : stringifySafe(arg)))
|
|
.join(' ');
|
|
|
|
error = new SyntheticError(message);
|
|
error.name = 'console.error';
|
|
}
|
|
|
|
const isFatal = false;
|
|
const reportToConsole = false;
|
|
if (
|
|
!global.RN$handleException ||
|
|
!global.RN$handleException(error, isFatal, reportToConsole)
|
|
) {
|
|
if (__DEV__) {
|
|
// If we're not reporting to the console in reportException,
|
|
// we need to report it as a console.error here.
|
|
if (!reportToConsole) {
|
|
require('../LogBox/LogBox').default.addConsoleLog('error', ...args);
|
|
}
|
|
}
|
|
if (error.message.startsWith('Warning: ')) {
|
|
// React warnings use console.error so that a stack trace is shown, but
|
|
// we don't (currently) want these to report to native.
|
|
// Note: We can probably remove this, but would be a breaking change
|
|
// if the warning module is still used somewhere.
|
|
// Logic duplicated in polyfills/console.js
|
|
return;
|
|
}
|
|
reportException(
|
|
/* $FlowFixMe[class-object-subtyping] added when improving typing for this
|
|
* parameters */
|
|
// $FlowFixMe[incompatible-call]
|
|
error,
|
|
isFatal,
|
|
reportToConsole,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Shows a redbox with stacktrace for all console.error messages. Disable by
|
|
* setting `console.reportErrorsAsExceptions = false;` in your app.
|
|
*/
|
|
function installConsoleErrorReporter() {
|
|
// Enable reportErrorsAsExceptions
|
|
if (console._errorOriginal) {
|
|
return; // already installed
|
|
}
|
|
// Flow doesn't like it when you set arbitrary values on a global object
|
|
console._errorOriginal = console.error.bind(console);
|
|
console.error = reactConsoleErrorHandler;
|
|
if (console.reportErrorsAsExceptions === undefined) {
|
|
// Individual apps can disable this
|
|
// Flow doesn't like it when you set arbitrary values on a global object
|
|
console.reportErrorsAsExceptions = true;
|
|
}
|
|
}
|
|
|
|
const ExceptionsManager = {
|
|
decoratedExtraDataKey,
|
|
handleException,
|
|
installConsoleErrorReporter,
|
|
SyntheticError, // <- for backwards compatibility
|
|
unstable_setExceptionDecorator,
|
|
};
|
|
|
|
export default ExceptionsManager;
|