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 LogBoxSymbolication from './LogBoxSymbolication'; 10import type { Stack } from './LogBoxSymbolication'; 11import type { Category, Message, ComponentStack, CodeFrame } from './parseLogBoxLog'; 12 13type SymbolicationStatus = 'NONE' | 'PENDING' | 'COMPLETE' | 'FAILED'; 14 15export type LogLevel = 'warn' | 'error' | 'fatal' | 'syntax' | 'static'; 16 17export type LogBoxLogData = { 18 level: LogLevel; 19 type?: string; 20 message: Message; 21 stack: Stack; 22 category: string; 23 componentStack: ComponentStack; 24 codeFrame?: CodeFrame; 25 isComponentError: boolean; 26}; 27 28export type StackType = 'stack' | 'component'; 29 30function componentStackToStack(componentStack: ComponentStack): Stack { 31 return componentStack.map((stack) => ({ 32 file: stack.fileName, 33 methodName: stack.content, 34 lineNumber: stack.location?.row ?? 0, 35 column: stack.location?.column ?? 0, 36 arguments: [], 37 })); 38} 39 40type SymbolicationCallback = (status: SymbolicationStatus) => void; 41 42type SymbolicationResult = 43 | { error: null; stack: null; status: 'NONE' } 44 | { error: null; stack: null; status: 'PENDING' } 45 | { error: null; stack: Stack; status: 'COMPLETE' } 46 | { error: Error; stack: null; status: 'FAILED' }; 47 48export class LogBoxLog { 49 message: Message; 50 type: string; 51 category: Category; 52 componentStack: ComponentStack; 53 stack: Stack; 54 count: number; 55 level: LogLevel; 56 codeFrame?: CodeFrame; 57 isComponentError: boolean; 58 symbolicated: Record<StackType, SymbolicationResult> = { 59 stack: { 60 error: null, 61 stack: null, 62 status: 'NONE', 63 }, 64 component: { 65 error: null, 66 stack: null, 67 status: 'NONE', 68 }, 69 }; 70 71 private callbacks: Map<StackType, Set<SymbolicationCallback>> = new Map(); 72 73 constructor( 74 data: LogBoxLogData & { 75 symbolicated?: Record<StackType, SymbolicationResult>; 76 } 77 ) { 78 this.level = data.level; 79 this.type = data.type ?? 'error'; 80 this.message = data.message; 81 this.stack = data.stack; 82 this.category = data.category; 83 this.componentStack = data.componentStack; 84 this.codeFrame = data.codeFrame; 85 this.isComponentError = data.isComponentError; 86 this.count = 1; 87 this.symbolicated = data.symbolicated ?? this.symbolicated; 88 } 89 90 incrementCount(): void { 91 this.count += 1; 92 } 93 94 getAvailableStack(type: StackType): Stack | null { 95 if (this.symbolicated[type].status === 'COMPLETE') { 96 return this.symbolicated[type].stack; 97 } 98 return this.getStack(type); 99 } 100 101 private flushCallbacks(type: StackType): void { 102 const callbacks = this.callbacks.get(type); 103 const status = this.symbolicated[type].status; 104 if (callbacks) { 105 for (const callback of callbacks) { 106 callback(status); 107 } 108 callbacks.clear(); 109 } 110 } 111 112 private pushCallback(type: StackType, callback: SymbolicationCallback): void { 113 let callbacks = this.callbacks.get(type); 114 if (!callbacks) { 115 callbacks = new Set(); 116 this.callbacks.set(type, callbacks); 117 } 118 callbacks.add(callback); 119 } 120 121 retrySymbolicate(type: StackType, callback?: (status: SymbolicationStatus) => void): void { 122 this._symbolicate(type, true, callback); 123 } 124 125 symbolicate(type: StackType, callback?: (status: SymbolicationStatus) => void): void { 126 this._symbolicate(type, false, callback); 127 } 128 129 private _symbolicate( 130 type: StackType, 131 retry: boolean, 132 callback?: (status: SymbolicationStatus) => void 133 ): void { 134 if (callback) { 135 this.pushCallback(type, callback); 136 } 137 const status = this.symbolicated[type].status; 138 139 if (status === 'COMPLETE') { 140 return this.flushCallbacks(type); 141 } 142 143 if (retry) { 144 LogBoxSymbolication.deleteStack(this.getStack(type)); 145 this.handleSymbolicate(type); 146 } else { 147 if (status === 'NONE') { 148 this.handleSymbolicate(type); 149 } 150 } 151 } 152 153 private componentStackCache: Stack | null = null; 154 155 private getStack(type: StackType): Stack { 156 if (type === 'component') { 157 if (this.componentStackCache == null) { 158 this.componentStackCache = componentStackToStack(this.componentStack); 159 } 160 return this.componentStackCache; 161 } 162 return this.stack; 163 } 164 165 private handleSymbolicate(type: StackType): void { 166 if (type === 'component' && !this.componentStack?.length) { 167 return; 168 } 169 170 if (this.symbolicated[type].status !== 'PENDING') { 171 this.updateStatus(type, null, null, null); 172 LogBoxSymbolication.symbolicate(this.getStack(type)).then( 173 (data) => { 174 this.updateStatus(type, null, data?.stack, data?.codeFrame); 175 }, 176 (error) => { 177 this.updateStatus(type, error, null, null); 178 } 179 ); 180 } 181 } 182 183 private updateStatus( 184 type: StackType, 185 error?: Error | null, 186 stack?: Stack | null, 187 codeFrame?: CodeFrame | null 188 ): void { 189 const lastStatus = this.symbolicated[type].status; 190 if (error != null) { 191 this.symbolicated[type] = { 192 error, 193 stack: null, 194 status: 'FAILED', 195 }; 196 } else if (stack != null) { 197 if (codeFrame) { 198 this.codeFrame = codeFrame; 199 } 200 201 this.symbolicated[type] = { 202 error: null, 203 stack, 204 status: 'COMPLETE', 205 }; 206 } else { 207 this.symbolicated[type] = { 208 error: null, 209 stack: null, 210 status: 'PENDING', 211 }; 212 } 213 214 const status = this.symbolicated[type].status; 215 if (lastStatus !== status) { 216 if (['COMPLETE', 'FAILED'].includes(status)) { 217 this.flushCallbacks(type); 218 } 219 } 220 } 221} 222