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