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