LogBoxData.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. /**
  2. * Copyright (c) Facebook, Inc. and its affiliates.
  3. *
  4. * This source code is licensed under the MIT license found in the
  5. * LICENSE file in the root directory of this source tree.
  6. *
  7. * @flow strict-local
  8. * @format
  9. */
  10. ('use strict');
  11. import * as React from 'react';
  12. import LogBoxLog from './LogBoxLog';
  13. import {parseLogBoxException} from './parseLogBoxLog';
  14. import type {LogLevel} from './LogBoxLog';
  15. import type {
  16. Message,
  17. Category,
  18. ComponentStack,
  19. ExtendedExceptionData,
  20. } from './parseLogBoxLog';
  21. import parseErrorStack from '../../Core/Devtools/parseErrorStack';
  22. import type {ExtendedError} from '../../Core/Devtools/parseErrorStack';
  23. import NativeLogBox from '../../NativeModules/specs/NativeLogBox';
  24. export type LogBoxLogs = Set<LogBoxLog>;
  25. export type LogData = $ReadOnly<{|
  26. level: LogLevel,
  27. message: Message,
  28. category: Category,
  29. componentStack: ComponentStack,
  30. |}>;
  31. export type Observer = (
  32. $ReadOnly<{|
  33. logs: LogBoxLogs,
  34. isDisabled: boolean,
  35. selectedLogIndex: number,
  36. |}>,
  37. ) => void;
  38. export type IgnorePattern = string | RegExp;
  39. export type Subscription = $ReadOnly<{|
  40. unsubscribe: () => void,
  41. |}>;
  42. export type WarningInfo = {|
  43. finalFormat: string,
  44. forceDialogImmediately: boolean,
  45. suppressDialog_LEGACY: boolean,
  46. suppressCompletely: boolean,
  47. monitorEvent: string | null,
  48. monitorListVersion: number,
  49. monitorSampleRate: number,
  50. |};
  51. export type WarningFilter = (format: string) => WarningInfo;
  52. type AppInfo = $ReadOnly<{|
  53. appVersion: string,
  54. engine: string,
  55. onPress?: ?() => void,
  56. |}>;
  57. const observers: Set<{observer: Observer, ...}> = new Set();
  58. const ignorePatterns: Set<IgnorePattern> = new Set();
  59. let appInfo: ?() => AppInfo = null;
  60. let logs: LogBoxLogs = new Set();
  61. let updateTimeout = null;
  62. let _isDisabled = false;
  63. let _selectedIndex = -1;
  64. let warningFilter: WarningFilter = function(format) {
  65. return {
  66. finalFormat: format,
  67. forceDialogImmediately: false,
  68. suppressDialog_LEGACY: true,
  69. suppressCompletely: false,
  70. monitorEvent: 'unknown',
  71. monitorListVersion: 0,
  72. monitorSampleRate: 1,
  73. };
  74. };
  75. const LOGBOX_ERROR_MESSAGE =
  76. 'An error was thrown when attempting to render log messages via LogBox.';
  77. function getNextState() {
  78. return {
  79. logs,
  80. isDisabled: _isDisabled,
  81. selectedLogIndex: _selectedIndex,
  82. };
  83. }
  84. export function reportLogBoxError(
  85. error: ExtendedError,
  86. componentStack?: string,
  87. ): void {
  88. const ExceptionsManager = require('../../Core/ExceptionsManager');
  89. error.forceRedbox = true;
  90. error.message = `${LOGBOX_ERROR_MESSAGE}\n\n${error.message}`;
  91. if (componentStack != null) {
  92. error.componentStack = componentStack;
  93. }
  94. ExceptionsManager.handleException(error, /* isFatal */ true);
  95. }
  96. export function isLogBoxErrorMessage(message: string): boolean {
  97. return typeof message === 'string' && message.includes(LOGBOX_ERROR_MESSAGE);
  98. }
  99. export function isMessageIgnored(message: string): boolean {
  100. for (const pattern of ignorePatterns) {
  101. if (
  102. (pattern instanceof RegExp && pattern.test(message)) ||
  103. (typeof pattern === 'string' && message.includes(pattern))
  104. ) {
  105. return true;
  106. }
  107. }
  108. return false;
  109. }
  110. function handleUpdate(): void {
  111. if (updateTimeout == null) {
  112. updateTimeout = setImmediate(() => {
  113. updateTimeout = null;
  114. const nextState = getNextState();
  115. observers.forEach(({observer}) => observer(nextState));
  116. });
  117. }
  118. }
  119. function appendNewLog(newLog) {
  120. // We don't want to store these logs because they trigger a
  121. // state update whenever we add them to the store, which is
  122. // expensive to noisy logs. If we later want to display these
  123. // we will store them in a different state object.
  124. if (isMessageIgnored(newLog.message.content)) {
  125. return;
  126. }
  127. // If the next log has the same category as the previous one
  128. // then we want to roll it up into the last log in the list
  129. // by incrementing the count (simar to how Chrome does it).
  130. const lastLog = Array.from(logs).pop();
  131. if (lastLog && lastLog.category === newLog.category) {
  132. lastLog.incrementCount();
  133. handleUpdate();
  134. return;
  135. }
  136. if (newLog.level === 'fatal') {
  137. // If possible, to avoid jank, we don't want to open the error before
  138. // it's symbolicated. To do that, we optimistically wait for
  139. // sybolication for up to a second before adding the log.
  140. const OPTIMISTIC_WAIT_TIME = 1000;
  141. let addPendingLog = () => {
  142. logs.add(newLog);
  143. if (_selectedIndex <= 0) {
  144. setSelectedLog(logs.size - 1);
  145. } else {
  146. handleUpdate();
  147. }
  148. addPendingLog = null;
  149. };
  150. const optimisticTimeout = setTimeout(() => {
  151. if (addPendingLog) {
  152. addPendingLog();
  153. }
  154. }, OPTIMISTIC_WAIT_TIME);
  155. newLog.symbolicate(status => {
  156. if (addPendingLog && status !== 'PENDING') {
  157. addPendingLog();
  158. clearTimeout(optimisticTimeout);
  159. } else if (status !== 'PENDING') {
  160. // The log has already been added but we need to trigger a render.
  161. handleUpdate();
  162. }
  163. });
  164. } else if (newLog.level === 'syntax') {
  165. logs.add(newLog);
  166. setSelectedLog(logs.size - 1);
  167. } else {
  168. logs.add(newLog);
  169. handleUpdate();
  170. }
  171. }
  172. export function addLog(log: LogData): void {
  173. const errorForStackTrace = new Error();
  174. // Parsing logs are expensive so we schedule this
  175. // otherwise spammy logs would pause rendering.
  176. setImmediate(() => {
  177. try {
  178. // TODO: Use Error.captureStackTrace on Hermes
  179. const stack = parseErrorStack(errorForStackTrace);
  180. appendNewLog(
  181. new LogBoxLog({
  182. level: log.level,
  183. message: log.message,
  184. isComponentError: false,
  185. stack,
  186. category: log.category,
  187. componentStack: log.componentStack,
  188. }),
  189. );
  190. } catch (error) {
  191. reportLogBoxError(error);
  192. }
  193. });
  194. }
  195. export function addException(error: ExtendedExceptionData): void {
  196. // Parsing logs are expensive so we schedule this
  197. // otherwise spammy logs would pause rendering.
  198. setImmediate(() => {
  199. try {
  200. appendNewLog(new LogBoxLog(parseLogBoxException(error)));
  201. } catch (loggingError) {
  202. reportLogBoxError(loggingError);
  203. }
  204. });
  205. }
  206. export function symbolicateLogNow(log: LogBoxLog) {
  207. log.symbolicate(() => {
  208. handleUpdate();
  209. });
  210. }
  211. export function retrySymbolicateLogNow(log: LogBoxLog) {
  212. log.retrySymbolicate(() => {
  213. handleUpdate();
  214. });
  215. }
  216. export function symbolicateLogLazy(log: LogBoxLog) {
  217. log.symbolicate();
  218. }
  219. export function clear(): void {
  220. if (logs.size > 0) {
  221. logs = new Set();
  222. setSelectedLog(-1);
  223. }
  224. }
  225. export function setSelectedLog(proposedNewIndex: number): void {
  226. const oldIndex = _selectedIndex;
  227. let newIndex = proposedNewIndex;
  228. const logArray = Array.from(logs);
  229. let index = logArray.length - 1;
  230. while (index >= 0) {
  231. // The latest syntax error is selected and displayed before all other logs.
  232. if (logArray[index].level === 'syntax') {
  233. newIndex = index;
  234. break;
  235. }
  236. index -= 1;
  237. }
  238. _selectedIndex = newIndex;
  239. handleUpdate();
  240. if (NativeLogBox) {
  241. setTimeout(() => {
  242. if (oldIndex < 0 && newIndex >= 0) {
  243. NativeLogBox.show();
  244. } else if (oldIndex >= 0 && newIndex < 0) {
  245. NativeLogBox.hide();
  246. }
  247. }, 0);
  248. }
  249. }
  250. export function clearWarnings(): void {
  251. const newLogs = Array.from(logs).filter(log => log.level !== 'warn');
  252. if (newLogs.length !== logs.size) {
  253. logs = new Set(newLogs);
  254. setSelectedLog(-1);
  255. handleUpdate();
  256. }
  257. }
  258. export function clearErrors(): void {
  259. const newLogs = Array.from(logs).filter(
  260. log => log.level !== 'error' && log.level !== 'fatal',
  261. );
  262. if (newLogs.length !== logs.size) {
  263. logs = new Set(newLogs);
  264. setSelectedLog(-1);
  265. }
  266. }
  267. export function dismiss(log: LogBoxLog): void {
  268. if (logs.has(log)) {
  269. logs.delete(log);
  270. handleUpdate();
  271. }
  272. }
  273. export function setWarningFilter(filter: WarningFilter): void {
  274. warningFilter = filter;
  275. }
  276. export function setAppInfo(info: () => AppInfo): void {
  277. appInfo = info;
  278. }
  279. export function getAppInfo(): ?AppInfo {
  280. return appInfo != null ? appInfo() : null;
  281. }
  282. export function checkWarningFilter(format: string): WarningInfo {
  283. return warningFilter(format);
  284. }
  285. export function addIgnorePatterns(
  286. patterns: $ReadOnlyArray<IgnorePattern>,
  287. ): void {
  288. // The same pattern may be added multiple times, but adding a new pattern
  289. // can be expensive so let's find only the ones that are new.
  290. const newPatterns = patterns.filter((pattern: IgnorePattern) => {
  291. if (pattern instanceof RegExp) {
  292. for (const existingPattern of ignorePatterns.entries()) {
  293. if (
  294. existingPattern instanceof RegExp &&
  295. existingPattern.toString() === pattern.toString()
  296. ) {
  297. return false;
  298. }
  299. }
  300. return true;
  301. }
  302. return !ignorePatterns.has(pattern);
  303. });
  304. if (newPatterns.length === 0) {
  305. return;
  306. }
  307. for (const pattern of newPatterns) {
  308. ignorePatterns.add(pattern);
  309. // We need to recheck all of the existing logs.
  310. // This allows adding an ignore pattern anywhere in the codebase.
  311. // Without this, if you ignore a pattern after the a log is created,
  312. // then we would keep showing the log.
  313. logs = new Set(
  314. Array.from(logs).filter(log => !isMessageIgnored(log.message.content)),
  315. );
  316. }
  317. handleUpdate();
  318. }
  319. export function setDisabled(value: boolean): void {
  320. if (value === _isDisabled) {
  321. return;
  322. }
  323. _isDisabled = value;
  324. handleUpdate();
  325. }
  326. export function isDisabled(): boolean {
  327. return _isDisabled;
  328. }
  329. export function observe(observer: Observer): Subscription {
  330. const subscription = {observer};
  331. observers.add(subscription);
  332. observer(getNextState());
  333. return {
  334. unsubscribe(): void {
  335. observers.delete(subscription);
  336. },
  337. };
  338. }
  339. type Props = $ReadOnly<{||}>;
  340. type State = $ReadOnly<{|
  341. logs: LogBoxLogs,
  342. isDisabled: boolean,
  343. hasError: boolean,
  344. selectedLogIndex: number,
  345. |}>;
  346. type SubscribedComponent = React.AbstractComponent<
  347. $ReadOnly<{|
  348. logs: $ReadOnlyArray<LogBoxLog>,
  349. isDisabled: boolean,
  350. selectedLogIndex: number,
  351. |}>,
  352. >;
  353. export function withSubscription(
  354. WrappedComponent: SubscribedComponent,
  355. ): React.AbstractComponent<{||}> {
  356. class LogBoxStateSubscription extends React.Component<Props, State> {
  357. static getDerivedStateFromError() {
  358. return {hasError: true};
  359. }
  360. componentDidCatch(err: Error, errorInfo: {componentStack: string, ...}) {
  361. reportLogBoxError(err, errorInfo.componentStack);
  362. }
  363. _subscription: ?Subscription;
  364. state = {
  365. logs: new Set(),
  366. isDisabled: false,
  367. hasError: false,
  368. selectedLogIndex: -1,
  369. };
  370. render(): React.Node {
  371. if (this.state.hasError) {
  372. // This happens when the component failed to render, in which case we delegate to the native redbox.
  373. // We can't show anyback fallback UI here, because the error may be with <View> or <Text>.
  374. return null;
  375. }
  376. return (
  377. <WrappedComponent
  378. logs={Array.from(this.state.logs)}
  379. isDisabled={this.state.isDisabled}
  380. selectedLogIndex={this.state.selectedLogIndex}
  381. />
  382. );
  383. }
  384. componentDidMount(): void {
  385. this._subscription = observe(data => {
  386. this.setState(data);
  387. });
  388. }
  389. componentWillUnmount(): void {
  390. if (this._subscription != null) {
  391. this._subscription.unsubscribe();
  392. }
  393. }
  394. _handleDismiss = (): void => {
  395. // Here we handle the cases when the log is dismissed and it
  396. // was either the last log, or when the current index
  397. // is now outside the bounds of the log array.
  398. const {selectedLogIndex, logs: stateLogs} = this.state;
  399. const logsArray = Array.from(stateLogs);
  400. if (selectedLogIndex != null) {
  401. if (logsArray.length - 1 <= 0) {
  402. setSelectedLog(-1);
  403. } else if (selectedLogIndex >= logsArray.length - 1) {
  404. setSelectedLog(selectedLogIndex - 1);
  405. }
  406. dismiss(logsArray[selectedLogIndex]);
  407. }
  408. };
  409. _handleMinimize = (): void => {
  410. setSelectedLog(-1);
  411. };
  412. _handleSetSelectedLog = (index: number): void => {
  413. setSelectedLog(index);
  414. };
  415. }
  416. return LogBoxStateSubscription;
  417. }