Files
react-native/Libraries/LogBox/Data/LogBoxData.js
T
Rick Hanlon 5eddf1d79a LogBox - Show native redbox when failing to handle logs in LogBox
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
2019-11-08 07:41:09 -08:00

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);
},
};
}