import { isArray } from '@/utils' import { getFilenameToChunkIdMap } from './chunk-ids' import { ErrorProperties, ExceptionLike, ExceptionList, CoercingContext, StackFrame, StackFrameModifierFn, StackParser, ErrorTrackingCoercer, EventHint, ParsingContext, ChunkIdMapType, Mechanism, ParsedException, Exception, } from './types' const MAX_CAUSE_RECURSION = 4 export class ErrorPropertiesBuilder { constructor( private coercers: ErrorTrackingCoercer[], private stackParser: StackParser, private modifiers: StackFrameModifierFn[] = [] ) {} buildFromUnknown(input: unknown, hint: EventHint = {}): ErrorProperties { const providedMechanism = hint && hint.mechanism const mechanism = providedMechanism || { handled: true, type: 'generic', } const coercingContext: CoercingContext = this.buildCoercingContext(mechanism, hint, 0) const exceptionWithCause = coercingContext.apply(input) const parsingContext: ParsingContext = this.buildParsingContext(hint) const exceptionWithStack = this.parseStacktrace(exceptionWithCause, parsingContext) const exceptionList = this.convertToExceptionList(exceptionWithStack, mechanism) return { $exception_list: exceptionList, $exception_level: 'error', } } async modifyFrames(exceptionList: ErrorProperties['$exception_list']): Promise { for (const exc of exceptionList) { if (exc.stacktrace && exc.stacktrace.frames && isArray(exc.stacktrace.frames)) { exc.stacktrace.frames = await this.applyModifiers(exc.stacktrace.frames) } } return exceptionList } private coerceFallback(ctx: CoercingContext): ExceptionLike { return { type: 'Error', value: 'Unknown error', stack: ctx.syntheticException?.stack, synthetic: true, } } private parseStacktrace(err: ExceptionLike, ctx: ParsingContext): ParsedException { let cause: ParsedException | undefined = undefined if (err.cause != null) { cause = this.parseStacktrace(err.cause, ctx) } let stack: StackFrame[] | undefined = undefined if (err.stack != '' && err.stack != null) { stack = this.applyChunkIds(this.stackParser(err.stack, err.synthetic ? ctx.skipFirstLines : 0), ctx.chunkIdMap) } return { ...err, cause, stack } } private applyChunkIds(frames: StackFrame[], chunkIdMap?: ChunkIdMapType): StackFrame[] { return frames.map((frame) => { if (frame.filename && chunkIdMap) { frame.chunk_id = chunkIdMap[frame.filename] } return frame }) } private applyCoercers(input: unknown, ctx: CoercingContext): ExceptionLike | undefined { for (const adapter of this.coercers) { if (adapter.match(input)) { return adapter.coerce(input, ctx) } } return this.coerceFallback(ctx) } private async applyModifiers(frames: StackFrame[]): Promise { let newFrames = frames for (const modifier of this.modifiers) { newFrames = await modifier(newFrames) } return newFrames } private convertToExceptionList(exceptionWithStack: ParsedException, mechanism: Mechanism): ExceptionList { const currentException: Exception = { type: exceptionWithStack.type, value: exceptionWithStack.value, mechanism: { type: mechanism.type ?? 'generic', handled: mechanism.handled ?? true, synthetic: exceptionWithStack.synthetic ?? false, }, } if (exceptionWithStack.stack) { currentException.stacktrace = { type: 'raw', frames: exceptionWithStack.stack, } } const exceptionList: ExceptionList = [currentException] if (exceptionWithStack.cause != null) { // Cause errors are necessarily handled exceptionList.push( ...this.convertToExceptionList(exceptionWithStack.cause, { ...mechanism, handled: true, }) ) } return exceptionList } private buildParsingContext(hint: EventHint): ParsingContext { const context = { chunkIdMap: getFilenameToChunkIdMap(this.stackParser), skipFirstLines: hint.skipFirstLines ?? 1, } as ParsingContext return context } public buildCoercingContext(mechanism: Mechanism, hint: EventHint, depth: number = 0): CoercingContext { const coerce = (input: unknown, depth: number) => { if (depth <= MAX_CAUSE_RECURSION) { const ctx = this.buildCoercingContext(mechanism, hint, depth) return this.applyCoercers(input, ctx) } else { return undefined } } const context = { ...hint, // Do not propagate synthetic exception as it doesn't make sense syntheticException: depth == 0 ? hint.syntheticException : undefined, mechanism, apply: (input: unknown) => { return coerce(input, depth) }, next: (input: unknown) => { return coerce(input, depth + 1) }, } as CoercingContext return context } }