1/**
2 * Copyright (c) 650 Industries.
3 * Copyright (c) Meta Platforms, Inc. and affiliates.
4 *
5 * This source code is licensed under the MIT license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9import { Platform } from 'react-native';
10
11import { IgnorePattern, LogData } from './Data/LogBoxData';
12import { ExtendedExceptionData } from './Data/parseLogBoxLog';
13
14export { LogData, ExtendedExceptionData, IgnorePattern };
15
16let LogBox: ILogBox;
17
18interface ILogBox {
19  install(): void;
20  uninstall(): void;
21  isInstalled(): boolean;
22  ignoreLogs(patterns: readonly IgnorePattern[]): void;
23  ignoreAllLogs(ignore?: boolean): void;
24  clearAllLogs(): void;
25  addLog(log: LogData): void;
26  addException(error: ExtendedExceptionData): void;
27}
28
29/**
30 * LogBox displays logs in the app.
31 */
32if (__DEV__) {
33  const LogBoxData = require('./Data/LogBoxData');
34  const { parseLogBoxLog, parseInterpolation } =
35    require('./Data/parseLogBoxLog') as typeof import('./Data/parseLogBoxLog');
36
37  let originalConsoleError: typeof console.error | undefined;
38  let consoleErrorImpl: typeof console.error | undefined;
39
40  let isLogBoxInstalled: boolean = false;
41
42  LogBox = {
43    install(): void {
44      if (isLogBoxInstalled) {
45        return;
46      }
47
48      isLogBoxInstalled = true;
49
50      // Trigger lazy initialization of module.
51      // require("../NativeModules/specs/NativeLogBox");
52
53      // IMPORTANT: we only overwrite `console.error` and `console.warn` once.
54      // When we uninstall we keep the same reference and only change its
55      // internal implementation
56      const isFirstInstall = originalConsoleError == null;
57      if (isFirstInstall) {
58        originalConsoleError = console.error.bind(console);
59
60        console.error = (...args) => {
61          consoleErrorImpl?.(...args);
62        };
63      }
64
65      consoleErrorImpl = registerError;
66
67      if (Platform.isTesting) {
68        LogBoxData.setDisabled(true);
69      }
70    },
71
72    uninstall(): void {
73      if (!isLogBoxInstalled) {
74        return;
75      }
76
77      isLogBoxInstalled = false;
78
79      // IMPORTANT: we don't re-assign to `console` in case the method has been
80      // decorated again after installing LogBox. E.g.:
81      // Before uninstalling: original > LogBox > OtherErrorHandler
82      // After uninstalling:  original > LogBox (noop) > OtherErrorHandler
83      consoleErrorImpl = originalConsoleError;
84      delete (console as any).disableLogBox;
85    },
86
87    isInstalled(): boolean {
88      return isLogBoxInstalled;
89    },
90
91    ignoreLogs(patterns: readonly IgnorePattern[]): void {
92      LogBoxData.addIgnorePatterns(patterns);
93    },
94
95    ignoreAllLogs(value?: boolean): void {
96      LogBoxData.setDisabled(value == null ? true : value);
97    },
98
99    clearAllLogs(): void {
100      LogBoxData.clear();
101    },
102
103    addLog(log: LogData): void {
104      if (isLogBoxInstalled) {
105        LogBoxData.addLog(log);
106      }
107    },
108
109    addException(error: ExtendedExceptionData): void {
110      if (isLogBoxInstalled) {
111        LogBoxData.addException(error);
112      }
113    },
114  };
115
116  const isWarningModuleWarning = (...args: any) => {
117    return typeof args[0] === 'string' && args[0].startsWith('Warning: ');
118  };
119
120  const registerError = (...args: Parameters<typeof console.error>): void => {
121    // Let errors within LogBox itself fall through.
122    if (LogBoxData.isLogBoxErrorMessage(args[0])) {
123      originalConsoleError?.(...args);
124      return;
125    }
126
127    try {
128      if (!isWarningModuleWarning(...args)) {
129        // Only show LogBox for the 'warning' module, otherwise pass through.
130        // By passing through, this will get picked up by the React console override,
131        // potentially adding the component stack. React then passes it back to the
132        // React Native ExceptionsManager, which reports it to LogBox as an error.
133        //
134        // The 'warning' module needs to be handled here because React internally calls
135        // `console.error('Warning: ')` with the component stack already included.
136        originalConsoleError?.(...args);
137        return;
138      }
139
140      const { category, message, componentStack } = parseLogBoxLog(args);
141
142      if (!LogBoxData.isMessageIgnored(message.content)) {
143        // Interpolate the message so they are formatted for adb and other CLIs.
144        // This is different than the message.content above because it includes component stacks.
145        const interpolated = parseInterpolation(args);
146        originalConsoleError?.(interpolated.message.content);
147
148        LogBoxData.addLog({
149          // Always show the static rendering issues as full screen since they
150          // are too confusing otherwise.
151          level: /did not match\. Server:/.test(message.content) ? 'fatal' : 'error',
152          category,
153          message,
154          componentStack,
155        });
156      }
157    } catch (err) {
158      LogBoxData.reportUnexpectedLogBoxError(err);
159    }
160  };
161} else {
162  LogBox = {
163    install(): void {},
164    uninstall(): void {},
165    isInstalled(): boolean {
166      return false;
167    },
168    ignoreLogs(patterns: readonly IgnorePattern[]): void {},
169    ignoreAllLogs(value?: boolean): void {},
170    clearAllLogs(): void {},
171    addLog(log: LogData): void {},
172    addException(ex: ExtendedExceptionData): void {},
173  };
174}
175
176export default LogBox;
177