mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
5eddf1d79a
Summary: This diff adds error handling to logbox so that if there is an error either when parsing logs or when rendering LogBox, we show a native redbox with the error that was thrown and a message explaining that it's an internal React Native error. Changelog: [Internal] Reviewed By: cpojer Differential Revision: D18394788 fbshipit-source-id: 5d74d58e4b28ef6d863079e83677fb23ef4ccb34
287 lines
7.3 KiB
JavaScript
287 lines
7.3 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.
|
|
*
|
|
* @flow strict-local
|
|
* @format
|
|
*/
|
|
|
|
('use strict');
|
|
|
|
import LogBoxLog from './LogBoxLog';
|
|
import {parseLogBoxException} from './parseLogBoxLog';
|
|
import type {LogLevel} from './LogBoxLog';
|
|
import type {Message, Category, ComponentStack} from './parseLogBoxLog';
|
|
import parseErrorStack from '../../Core/Devtools/parseErrorStack';
|
|
import type {ExceptionData} from '../../Core/NativeExceptionsManager';
|
|
import type {ExtendedError} from '../../Core/Devtools/parseErrorStack';
|
|
|
|
export type LogBoxLogs = Set<LogBoxLog>;
|
|
export type LogData = $ReadOnly<{|
|
|
level: LogLevel,
|
|
message: Message,
|
|
category: Category,
|
|
componentStack: ComponentStack,
|
|
|}>;
|
|
|
|
export type Observer = (
|
|
$ReadOnly<{|logs: LogBoxLogs, isDisabled: boolean|}>,
|
|
) => void;
|
|
|
|
export type IgnorePattern = string | RegExp;
|
|
|
|
export type Subscription = $ReadOnly<{|
|
|
unsubscribe: () => void,
|
|
|}>;
|
|
|
|
const observers: Set<{observer: Observer}> = new Set();
|
|
const ignorePatterns: Set<IgnorePattern> = new Set();
|
|
let logs: LogBoxLogs = new Set();
|
|
let updateTimeout = null;
|
|
let _isDisabled = false;
|
|
|
|
const LOGBOX_ERROR_MESSAGE =
|
|
'An error was thrown when attempting to render log messages via LogBox.';
|
|
|
|
export function reportLogBoxError(
|
|
error: ExtendedError,
|
|
componentStack?: string,
|
|
): void {
|
|
const ExceptionsManager = require('../../Core/ExceptionsManager');
|
|
|
|
error.forceRedbox = true;
|
|
error.message = `${LOGBOX_ERROR_MESSAGE}\n\n${error.message}`;
|
|
if (componentStack != null) {
|
|
error.componentStack = componentStack;
|
|
}
|
|
ExceptionsManager.handleException(error, /* isFatal */ true);
|
|
}
|
|
|
|
export function isLogBoxErrorMessage(message: string): boolean {
|
|
return typeof message === 'string' && message.includes(LOGBOX_ERROR_MESSAGE);
|
|
}
|
|
|
|
export function isMessageIgnored(message: string): boolean {
|
|
for (const pattern of ignorePatterns) {
|
|
if (
|
|
(pattern instanceof RegExp && pattern.test(message)) ||
|
|
(typeof pattern === 'string' && message.includes(pattern))
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function handleUpdate(): void {
|
|
if (updateTimeout == null) {
|
|
updateTimeout = setImmediate(() => {
|
|
updateTimeout = null;
|
|
observers.forEach(({observer}) =>
|
|
observer({logs, isDisabled: _isDisabled}),
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
export function addLog(log: LogData): void {
|
|
const errorForStackTrace = new Error();
|
|
|
|
// Parsing logs are expensive so we schedule this
|
|
// otherwise spammy logs would pause rendering.
|
|
setImmediate(() => {
|
|
try {
|
|
// TODO: Use Error.captureStackTrace on Hermes
|
|
const stack = parseErrorStack(errorForStackTrace);
|
|
|
|
// If the next log has the same category as the previous one
|
|
// then we want to roll it up into the last log in the list
|
|
// by incrementing the count (simar to how Chrome does it).
|
|
const lastLog = Array.from(logs).pop();
|
|
if (lastLog && lastLog.category === log.category) {
|
|
lastLog.incrementCount();
|
|
} else {
|
|
logs.add(
|
|
new LogBoxLog(
|
|
log.level,
|
|
log.message,
|
|
stack,
|
|
log.category,
|
|
log.componentStack,
|
|
),
|
|
);
|
|
}
|
|
|
|
handleUpdate();
|
|
} catch (error) {
|
|
reportLogBoxError(error);
|
|
}
|
|
});
|
|
}
|
|
|
|
export function symbolicateLogNow(log: LogBoxLog) {
|
|
log.symbolicate(() => {
|
|
handleUpdate();
|
|
});
|
|
}
|
|
export function retrySymbolicateLogNow(log: LogBoxLog) {
|
|
log.retrySymbolicate(() => {
|
|
handleUpdate();
|
|
});
|
|
}
|
|
|
|
export function symbolicateLogLazy(log: LogBoxLog) {
|
|
log.symbolicate();
|
|
}
|
|
|
|
export function addException(error: ExceptionData): void {
|
|
// Parsing logs are expensive so we schedule this
|
|
// otherwise spammy logs would pause rendering.
|
|
setImmediate(() => {
|
|
try {
|
|
const {
|
|
category,
|
|
message,
|
|
codeFrame,
|
|
componentStack,
|
|
stack,
|
|
level,
|
|
} = parseLogBoxException(error);
|
|
|
|
// We don't want to store these logs because they trigger a
|
|
// state update whenever we add them to the store, which is
|
|
// expensive to noisy logs. If we later want to display these
|
|
// we will store them in a different state object.
|
|
if (isMessageIgnored(message.content)) {
|
|
return;
|
|
}
|
|
|
|
const lastLog = Array.from(logs).pop();
|
|
if (lastLog && lastLog.category === category) {
|
|
lastLog.incrementCount();
|
|
} else {
|
|
const newLog = new LogBoxLog(
|
|
level,
|
|
message,
|
|
stack,
|
|
category,
|
|
componentStack != null ? componentStack : [],
|
|
codeFrame,
|
|
);
|
|
|
|
// Start symbolicating now so it's warm when it renders.
|
|
if (level === 'fatal') {
|
|
symbolicateLogLazy(newLog);
|
|
}
|
|
logs.add(newLog);
|
|
}
|
|
|
|
handleUpdate();
|
|
} catch (loggingError) {
|
|
reportLogBoxError(loggingError);
|
|
}
|
|
});
|
|
}
|
|
|
|
export function clear(): void {
|
|
if (logs.size > 0) {
|
|
logs.clear();
|
|
handleUpdate();
|
|
}
|
|
}
|
|
|
|
export function clearWarnings(): void {
|
|
const newLogs = Array.from(logs).filter(log => log.level !== 'warn');
|
|
if (newLogs.length !== logs.size) {
|
|
logs = new Set(newLogs);
|
|
handleUpdate();
|
|
}
|
|
}
|
|
|
|
export function clearErrors(): void {
|
|
const newLogs = Array.from(logs).filter(log => log.level !== 'error');
|
|
if (newLogs.length !== logs.size) {
|
|
logs = new Set(newLogs);
|
|
handleUpdate();
|
|
}
|
|
}
|
|
|
|
export function clearSyntaxErrors(): void {
|
|
const newLogs = Array.from(logs).filter(log => log.level !== 'syntax');
|
|
if (newLogs.length !== logs.size) {
|
|
logs = new Set(newLogs);
|
|
handleUpdate();
|
|
}
|
|
}
|
|
|
|
export function dismiss(log: LogBoxLog): void {
|
|
if (logs.has(log)) {
|
|
logs.delete(log);
|
|
handleUpdate();
|
|
}
|
|
}
|
|
|
|
export function addIgnorePatterns(
|
|
patterns: $ReadOnlyArray<IgnorePattern>,
|
|
): void {
|
|
// The same pattern may be added multiple times, but adding a new pattern
|
|
// can be expensive so let's find only the ones that are new.
|
|
const newPatterns = patterns.filter((pattern: IgnorePattern) => {
|
|
if (pattern instanceof RegExp) {
|
|
for (const existingPattern of ignorePatterns.entries()) {
|
|
if (
|
|
existingPattern instanceof RegExp &&
|
|
existingPattern.toString() === pattern.toString()
|
|
) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
return !ignorePatterns.has(pattern);
|
|
});
|
|
|
|
if (newPatterns.length === 0) {
|
|
return;
|
|
}
|
|
for (const pattern of newPatterns) {
|
|
ignorePatterns.add(pattern);
|
|
|
|
// We need to recheck all of the existing logs.
|
|
// This allows adding an ignore pattern anywhere in the codebase.
|
|
// Without this, if you ignore a pattern after the a log is created,
|
|
// then we would keep showing the log.
|
|
logs = new Set(
|
|
Array.from(logs).filter(log => !isMessageIgnored(log.message.content)),
|
|
);
|
|
}
|
|
handleUpdate();
|
|
}
|
|
|
|
export function setDisabled(value: boolean): void {
|
|
if (value === _isDisabled) {
|
|
return;
|
|
}
|
|
_isDisabled = value;
|
|
handleUpdate();
|
|
}
|
|
|
|
export function isDisabled(): boolean {
|
|
return _isDisabled;
|
|
}
|
|
|
|
export function observe(observer: Observer): Subscription {
|
|
const subscription = {observer};
|
|
observers.add(subscription);
|
|
|
|
observer({logs, isDisabled: _isDisabled});
|
|
|
|
return {
|
|
unsubscribe(): void {
|
|
observers.delete(subscription);
|
|
},
|
|
};
|
|
}
|