123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480 |
- /**
- * 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 * as React from 'react';
- import LogBoxLog from './LogBoxLog';
- import {parseLogBoxException} from './parseLogBoxLog';
- import type {LogLevel} from './LogBoxLog';
- import type {
- Message,
- Category,
- ComponentStack,
- ExtendedExceptionData,
- } from './parseLogBoxLog';
- import parseErrorStack from '../../Core/Devtools/parseErrorStack';
- import type {ExtendedError} from '../../Core/Devtools/parseErrorStack';
- import NativeLogBox from '../../NativeModules/specs/NativeLogBox';
- 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,
- selectedLogIndex: number,
- |}>,
- ) => void;
- export type IgnorePattern = string | RegExp;
- export type Subscription = $ReadOnly<{|
- unsubscribe: () => void,
- |}>;
- export type WarningInfo = {|
- finalFormat: string,
- forceDialogImmediately: boolean,
- suppressDialog_LEGACY: boolean,
- suppressCompletely: boolean,
- monitorEvent: string | null,
- monitorListVersion: number,
- monitorSampleRate: number,
- |};
- export type WarningFilter = (format: string) => WarningInfo;
- type AppInfo = $ReadOnly<{|
- appVersion: string,
- engine: string,
- onPress?: ?() => void,
- |}>;
- const observers: Set<{observer: Observer, ...}> = new Set();
- const ignorePatterns: Set<IgnorePattern> = new Set();
- let appInfo: ?() => AppInfo = null;
- let logs: LogBoxLogs = new Set();
- let updateTimeout = null;
- let _isDisabled = false;
- let _selectedIndex = -1;
- let warningFilter: WarningFilter = function(format) {
- return {
- finalFormat: format,
- forceDialogImmediately: false,
- suppressDialog_LEGACY: true,
- suppressCompletely: false,
- monitorEvent: 'unknown',
- monitorListVersion: 0,
- monitorSampleRate: 1,
- };
- };
- const LOGBOX_ERROR_MESSAGE =
- 'An error was thrown when attempting to render log messages via LogBox.';
- function getNextState() {
- return {
- logs,
- isDisabled: _isDisabled,
- selectedLogIndex: _selectedIndex,
- };
- }
- 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;
- const nextState = getNextState();
- observers.forEach(({observer}) => observer(nextState));
- });
- }
- }
- function appendNewLog(newLog) {
- // 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(newLog.message.content)) {
- return;
- }
- // 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 === newLog.category) {
- lastLog.incrementCount();
- handleUpdate();
- return;
- }
- if (newLog.level === 'fatal') {
- // If possible, to avoid jank, we don't want to open the error before
- // it's symbolicated. To do that, we optimistically wait for
- // sybolication for up to a second before adding the log.
- const OPTIMISTIC_WAIT_TIME = 1000;
- let addPendingLog = () => {
- logs.add(newLog);
- if (_selectedIndex <= 0) {
- setSelectedLog(logs.size - 1);
- } else {
- handleUpdate();
- }
- addPendingLog = null;
- };
- const optimisticTimeout = setTimeout(() => {
- if (addPendingLog) {
- addPendingLog();
- }
- }, OPTIMISTIC_WAIT_TIME);
- newLog.symbolicate(status => {
- if (addPendingLog && status !== 'PENDING') {
- addPendingLog();
- clearTimeout(optimisticTimeout);
- } else if (status !== 'PENDING') {
- // The log has already been added but we need to trigger a render.
- handleUpdate();
- }
- });
- } else if (newLog.level === 'syntax') {
- logs.add(newLog);
- setSelectedLog(logs.size - 1);
- } else {
- logs.add(newLog);
- handleUpdate();
- }
- }
- 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);
- appendNewLog(
- new LogBoxLog({
- level: log.level,
- message: log.message,
- isComponentError: false,
- stack,
- category: log.category,
- componentStack: log.componentStack,
- }),
- );
- } catch (error) {
- reportLogBoxError(error);
- }
- });
- }
- export function addException(error: ExtendedExceptionData): void {
- // Parsing logs are expensive so we schedule this
- // otherwise spammy logs would pause rendering.
- setImmediate(() => {
- try {
- appendNewLog(new LogBoxLog(parseLogBoxException(error)));
- } catch (loggingError) {
- reportLogBoxError(loggingError);
- }
- });
- }
- 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 clear(): void {
- if (logs.size > 0) {
- logs = new Set();
- setSelectedLog(-1);
- }
- }
- export function setSelectedLog(proposedNewIndex: number): void {
- const oldIndex = _selectedIndex;
- let newIndex = proposedNewIndex;
- const logArray = Array.from(logs);
- let index = logArray.length - 1;
- while (index >= 0) {
- // The latest syntax error is selected and displayed before all other logs.
- if (logArray[index].level === 'syntax') {
- newIndex = index;
- break;
- }
- index -= 1;
- }
- _selectedIndex = newIndex;
- handleUpdate();
- if (NativeLogBox) {
- setTimeout(() => {
- if (oldIndex < 0 && newIndex >= 0) {
- NativeLogBox.show();
- } else if (oldIndex >= 0 && newIndex < 0) {
- NativeLogBox.hide();
- }
- }, 0);
- }
- }
- export function clearWarnings(): void {
- const newLogs = Array.from(logs).filter(log => log.level !== 'warn');
- if (newLogs.length !== logs.size) {
- logs = new Set(newLogs);
- setSelectedLog(-1);
- handleUpdate();
- }
- }
- export function clearErrors(): void {
- const newLogs = Array.from(logs).filter(
- log => log.level !== 'error' && log.level !== 'fatal',
- );
- if (newLogs.length !== logs.size) {
- logs = new Set(newLogs);
- setSelectedLog(-1);
- }
- }
- export function dismiss(log: LogBoxLog): void {
- if (logs.has(log)) {
- logs.delete(log);
- handleUpdate();
- }
- }
- export function setWarningFilter(filter: WarningFilter): void {
- warningFilter = filter;
- }
- export function setAppInfo(info: () => AppInfo): void {
- appInfo = info;
- }
- export function getAppInfo(): ?AppInfo {
- return appInfo != null ? appInfo() : null;
- }
- export function checkWarningFilter(format: string): WarningInfo {
- return warningFilter(format);
- }
- 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(getNextState());
- return {
- unsubscribe(): void {
- observers.delete(subscription);
- },
- };
- }
- type Props = $ReadOnly<{||}>;
- type State = $ReadOnly<{|
- logs: LogBoxLogs,
- isDisabled: boolean,
- hasError: boolean,
- selectedLogIndex: number,
- |}>;
- type SubscribedComponent = React.AbstractComponent<
- $ReadOnly<{|
- logs: $ReadOnlyArray<LogBoxLog>,
- isDisabled: boolean,
- selectedLogIndex: number,
- |}>,
- >;
- export function withSubscription(
- WrappedComponent: SubscribedComponent,
- ): React.AbstractComponent<{||}> {
- class LogBoxStateSubscription extends React.Component<Props, State> {
- static getDerivedStateFromError() {
- return {hasError: true};
- }
- componentDidCatch(err: Error, errorInfo: {componentStack: string, ...}) {
- reportLogBoxError(err, errorInfo.componentStack);
- }
- _subscription: ?Subscription;
- state = {
- logs: new Set(),
- isDisabled: false,
- hasError: false,
- selectedLogIndex: -1,
- };
- render(): React.Node {
- if (this.state.hasError) {
- // This happens when the component failed to render, in which case we delegate to the native redbox.
- // We can't show anyback fallback UI here, because the error may be with <View> or <Text>.
- return null;
- }
- return (
- <WrappedComponent
- logs={Array.from(this.state.logs)}
- isDisabled={this.state.isDisabled}
- selectedLogIndex={this.state.selectedLogIndex}
- />
- );
- }
- componentDidMount(): void {
- this._subscription = observe(data => {
- this.setState(data);
- });
- }
- componentWillUnmount(): void {
- if (this._subscription != null) {
- this._subscription.unsubscribe();
- }
- }
- _handleDismiss = (): void => {
- // Here we handle the cases when the log is dismissed and it
- // was either the last log, or when the current index
- // is now outside the bounds of the log array.
- const {selectedLogIndex, logs: stateLogs} = this.state;
- const logsArray = Array.from(stateLogs);
- if (selectedLogIndex != null) {
- if (logsArray.length - 1 <= 0) {
- setSelectedLog(-1);
- } else if (selectedLogIndex >= logsArray.length - 1) {
- setSelectedLog(selectedLogIndex - 1);
- }
- dismiss(logsArray[selectedLogIndex]);
- }
- };
- _handleMinimize = (): void => {
- setSelectedLog(-1);
- };
- _handleSetSelectedLog = (index: number): void => {
- setSelectedLog(index);
- };
- }
- return LogBoxStateSubscription;
- }
|