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