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