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