mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
0825c2b2e7
Summary: This diff adds handling for syntax errors. ## Strategy To do this we introduce a new log level type syntax, giving us these levels with semantics: - `warn` - console warns, show collapsed, dismissible - `error` - console errors, show collapsed, dismissible - `fatal` - thrown exceptions, show expanded, not dismissible - `syntax` - thrown exceptions for invalid syntax, show expanded, not dismissible Syntax errors shows expanded, covers all other errors, and are only dismissible when the syntax error is fixed and updated with Fast Refresh. Once the syntax error is fixed, it reveals any previously covered fatals, errors, or warnings behind it In many ways, this makes syntax errors the highest level error. ## Visuals Syntax errors also have their own display formatting. Stack traces for syntax errors don't make sense, so we don't show them. Instead, we show the syntax error message and a code frame for the error. The code frame is also updated so that is doesn't wrap and is horizontally scrollable, making it easier to read. ## Detecting syntax errors To detect syntax errors we've updated `LogBoxData.addException` to call the parse function `parseLogBoxException`. This method will perform a regex on the error message to detect: - file name - location - error message - codeframe If this regex fails for any reason to find all four parts, we'll fall back to a fatal. Over time we'll update this regex to be more robust and handle more cases we've missed. Changelog: [Internal] Reviewed By: motiz88 Differential Revision: D18278862 fbshipit-source-id: 59069aba38a27c44787e5248b2973c3a345c4a0a
213 lines
6.2 KiB
JavaScript
213 lines
6.2 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @format
|
|
* @flow strict-local
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
import type {ExtendedError} from './Devtools/parseErrorStack';
|
|
import * as LogBoxData from '../LogBox/Data/LogBoxData';
|
|
import type {ExceptionData} from './NativeExceptionsManager';
|
|
|
|
class SyntheticError extends Error {
|
|
name: string = '';
|
|
}
|
|
|
|
type ExceptionDecorator = ExceptionData => ExceptionData;
|
|
|
|
let userExceptionDecorator: ?ExceptionDecorator;
|
|
let inUserExceptionDecorator = false;
|
|
|
|
/**
|
|
* 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) {
|
|
const NativeExceptionsManager = require('./NativeExceptionsManager').default;
|
|
if (NativeExceptionsManager) {
|
|
const parseErrorStack = require('./Devtools/parseErrorStack');
|
|
const stack = parseErrorStack(e);
|
|
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}: `;
|
|
const isFromConsoleError = e.name === 'console.error';
|
|
|
|
if (!message.startsWith(namePrefix)) {
|
|
message = namePrefix + message;
|
|
}
|
|
|
|
// Errors created by `console.error` have already been printed.
|
|
if (!isFromConsoleError) {
|
|
if (console._errorOriginal) {
|
|
console._errorOriginal(message);
|
|
} else {
|
|
console.error(message);
|
|
}
|
|
}
|
|
|
|
message =
|
|
e.jsEngine == null ? message : `${message}, js engine: ${e.jsEngine}`;
|
|
|
|
const isHandledByLogBox = global.__reactExperimentalLogBox;
|
|
|
|
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: {
|
|
jsEngine: e.jsEngine,
|
|
rawStack: e.stack,
|
|
|
|
// Hack to hide native redboxes when in the LogBox experiment.
|
|
// This is intentionally untyped and stuffed here, because it is temporary.
|
|
suppressRedBox: isHandledByLogBox,
|
|
},
|
|
});
|
|
|
|
if (isHandledByLogBox) {
|
|
LogBoxData.addException(data);
|
|
}
|
|
|
|
NativeExceptionsManager.reportException(data);
|
|
|
|
if (__DEV__) {
|
|
if (e.preventSymbolication === true) {
|
|
return;
|
|
}
|
|
const symbolicateStackTrace = require('./Devtools/symbolicateStackTrace');
|
|
symbolicateStackTrace(stack)
|
|
.then(prettyStack => {
|
|
if (prettyStack) {
|
|
NativeExceptionsManager.updateExceptionMessage(
|
|
data.message,
|
|
prettyStack,
|
|
currentExceptionID,
|
|
);
|
|
} else {
|
|
throw new Error('The stack is null');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.log('Unable to symbolicate stack trace: ' + error.message);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
declare var console: typeof console & {
|
|
_errorOriginal: typeof console.error,
|
|
reportErrorsAsExceptions: boolean,
|
|
};
|
|
|
|
/**
|
|
* Logs exceptions to the (native) console and displays them
|
|
*/
|
|
function handleException(e: mixed, isFatal: boolean) {
|
|
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);
|
|
}
|
|
reportException(error, isFatal);
|
|
}
|
|
|
|
function reactConsoleErrorHandler() {
|
|
if (!console.reportErrorsAsExceptions) {
|
|
console._errorOriginal.apply(console, arguments);
|
|
return;
|
|
}
|
|
|
|
if (arguments[0] && arguments[0].stack) {
|
|
// reportException will console.error this with high enough fidelity.
|
|
reportException(arguments[0], /* isFatal */ false);
|
|
} else {
|
|
console._errorOriginal.apply(console, arguments);
|
|
const stringifySafe = require('../Utilities/stringifySafe');
|
|
const str = Array.prototype.map
|
|
.call(arguments, value =>
|
|
typeof value === 'string' ? value : stringifySafe(value),
|
|
)
|
|
.join(' ');
|
|
|
|
if (str.slice(0, 9) === 'Warning: ') {
|
|
// React warnings use console.error so that a stack trace is shown, but
|
|
// we don't (currently) want these to show a redbox
|
|
// (Note: Logic duplicated in polyfills/console.js.)
|
|
return;
|
|
}
|
|
const error: ExtendedError = new SyntheticError(str);
|
|
error.name = 'console.error';
|
|
reportException(error, /* isFatal */ false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
handleException,
|
|
installConsoleErrorReporter,
|
|
SyntheticError,
|
|
unstable_setExceptionDecorator,
|
|
};
|