126ad19fcSEvan Bacon/**
226ad19fcSEvan Bacon * Copyright (c) 650 Industries.
326ad19fcSEvan Bacon * Copyright (c) Meta Platforms, Inc. and affiliates.
426ad19fcSEvan Bacon *
526ad19fcSEvan Bacon * This source code is licensed under the MIT license found in the
626ad19fcSEvan Bacon * LICENSE file in the root directory of this source tree.
726ad19fcSEvan Bacon */
826ad19fcSEvan Bacon
98a424bebSJames Ideimport type { LogBoxLogData } from './LogBoxLog';
1026ad19fcSEvan Baconimport parseErrorStack from '../modules/parseErrorStack';
1126ad19fcSEvan Baconimport stringifySafe from '../modules/stringifySafe';
1226ad19fcSEvan Bacontype ExceptionData = any;
1326ad19fcSEvan Bacon
1426ad19fcSEvan Baconconst BABEL_TRANSFORM_ERROR_FORMAT =
1526ad19fcSEvan Bacon  /^(?:TransformError )?(?:SyntaxError: |ReferenceError: )(.*): (.*) \((\d+):(\d+)\)\n\n([\s\S]+)/;
1626ad19fcSEvan Baconconst BABEL_CODE_FRAME_ERROR_FORMAT =
1726ad19fcSEvan Bacon  /^(?:TransformError )?(?:.*):? (?:.*?)(\/.*): ([\s\S]+?)\n([ >]{2}[\d\s]+ \|[\s\S]+|\u{001b}[\s\S]+)/u;
1826ad19fcSEvan Baconconst METRO_ERROR_FORMAT =
1926ad19fcSEvan Bacon  /^(?:InternalError Metro has encountered an error:) (.*): (.*) \((\d+):(\d+)\)\n\n([\s\S]+)/u;
2026ad19fcSEvan Bacon
2126ad19fcSEvan Baconexport type ExtendedExceptionData = ExceptionData & {
2226ad19fcSEvan Bacon  isComponentError: boolean;
2326ad19fcSEvan Bacon  [key: string]: any;
2426ad19fcSEvan Bacon};
2526ad19fcSEvan Baconexport type Category = string;
2626ad19fcSEvan Baconexport type CodeFrame = {
2726ad19fcSEvan Bacon  content: string;
2826ad19fcSEvan Bacon  location?: {
2926ad19fcSEvan Bacon    row: number;
3026ad19fcSEvan Bacon    column: number;
3126ad19fcSEvan Bacon    [key: string]: any;
3226ad19fcSEvan Bacon  } | null;
3326ad19fcSEvan Bacon  fileName: string;
3426ad19fcSEvan Bacon
3526ad19fcSEvan Bacon  // TODO: When React switched to using call stack frames,
3626ad19fcSEvan Bacon  // we gained the ability to use the collapse flag, but
3726ad19fcSEvan Bacon  // it is not integrated into the LogBox UI.
3826ad19fcSEvan Bacon  collapse?: boolean;
3926ad19fcSEvan Bacon};
4026ad19fcSEvan Bacon
4126ad19fcSEvan Baconexport type Message = {
4226ad19fcSEvan Bacon  content: string;
4326ad19fcSEvan Bacon  substitutions: {
4426ad19fcSEvan Bacon    length: number;
4526ad19fcSEvan Bacon    offset: number;
4626ad19fcSEvan Bacon  }[];
4726ad19fcSEvan Bacon};
4826ad19fcSEvan Bacon
4926ad19fcSEvan Baconexport type ComponentStack = CodeFrame[];
5026ad19fcSEvan Bacon
51*4a8c0978SEvan Baconconst SUBSTITUTION = '\ufeff%s';
5226ad19fcSEvan Bacon
5326ad19fcSEvan Baconexport function parseInterpolation(args: readonly any[]): {
5426ad19fcSEvan Bacon  category: Category;
5526ad19fcSEvan Bacon  message: Message;
5626ad19fcSEvan Bacon} {
5726ad19fcSEvan Bacon  const categoryParts: string[] = [];
5826ad19fcSEvan Bacon  const contentParts: string[] = [];
5926ad19fcSEvan Bacon  const substitutionOffsets: { length: number; offset: number }[] = [];
6026ad19fcSEvan Bacon
6126ad19fcSEvan Bacon  const remaining = [...args];
6226ad19fcSEvan Bacon  if (typeof remaining[0] === 'string') {
6326ad19fcSEvan Bacon    const formatString = String(remaining.shift());
6426ad19fcSEvan Bacon    const formatStringParts = formatString.split('%s');
6526ad19fcSEvan Bacon    const substitutionCount = formatStringParts.length - 1;
6626ad19fcSEvan Bacon    const substitutions = remaining.splice(0, substitutionCount);
6726ad19fcSEvan Bacon
6826ad19fcSEvan Bacon    let categoryString = '';
6926ad19fcSEvan Bacon    let contentString = '';
7026ad19fcSEvan Bacon
7126ad19fcSEvan Bacon    let substitutionIndex = 0;
7226ad19fcSEvan Bacon    for (const formatStringPart of formatStringParts) {
7326ad19fcSEvan Bacon      categoryString += formatStringPart;
7426ad19fcSEvan Bacon      contentString += formatStringPart;
7526ad19fcSEvan Bacon
7626ad19fcSEvan Bacon      if (substitutionIndex < substitutionCount) {
7726ad19fcSEvan Bacon        if (substitutionIndex < substitutions.length) {
7826ad19fcSEvan Bacon          // Don't stringify a string type.
7926ad19fcSEvan Bacon          // It adds quotation mark wrappers around the string,
8026ad19fcSEvan Bacon          // which causes the LogBox to look odd.
8126ad19fcSEvan Bacon          const substitution =
8226ad19fcSEvan Bacon            typeof substitutions[substitutionIndex] === 'string'
8326ad19fcSEvan Bacon              ? substitutions[substitutionIndex]
8426ad19fcSEvan Bacon              : stringifySafe(substitutions[substitutionIndex]);
8526ad19fcSEvan Bacon          substitutionOffsets.push({
8626ad19fcSEvan Bacon            length: substitution.length,
8726ad19fcSEvan Bacon            offset: contentString.length,
8826ad19fcSEvan Bacon          });
8926ad19fcSEvan Bacon
9026ad19fcSEvan Bacon          categoryString += SUBSTITUTION;
9126ad19fcSEvan Bacon          contentString += substitution;
9226ad19fcSEvan Bacon        } else {
9326ad19fcSEvan Bacon          substitutionOffsets.push({
9426ad19fcSEvan Bacon            length: 2,
9526ad19fcSEvan Bacon            offset: contentString.length,
9626ad19fcSEvan Bacon          });
9726ad19fcSEvan Bacon
9826ad19fcSEvan Bacon          categoryString += '%s';
9926ad19fcSEvan Bacon          contentString += '%s';
10026ad19fcSEvan Bacon        }
10126ad19fcSEvan Bacon
10226ad19fcSEvan Bacon        substitutionIndex++;
10326ad19fcSEvan Bacon      }
10426ad19fcSEvan Bacon    }
10526ad19fcSEvan Bacon
10626ad19fcSEvan Bacon    categoryParts.push(categoryString);
10726ad19fcSEvan Bacon    contentParts.push(contentString);
10826ad19fcSEvan Bacon  }
10926ad19fcSEvan Bacon
11026ad19fcSEvan Bacon  const remainingArgs = remaining.map((arg) => {
11126ad19fcSEvan Bacon    // Don't stringify a string type.
11226ad19fcSEvan Bacon    // It adds quotation mark wrappers around the string,
11326ad19fcSEvan Bacon    // which causes the LogBox to look odd.
11426ad19fcSEvan Bacon    return typeof arg === 'string' ? arg : stringifySafe(arg);
11526ad19fcSEvan Bacon  });
11626ad19fcSEvan Bacon  categoryParts.push(...remainingArgs);
11726ad19fcSEvan Bacon  contentParts.push(...remainingArgs);
11826ad19fcSEvan Bacon
11926ad19fcSEvan Bacon  return {
12026ad19fcSEvan Bacon    category: categoryParts.join(' '),
12126ad19fcSEvan Bacon    message: {
12226ad19fcSEvan Bacon      content: contentParts.join(' '),
12326ad19fcSEvan Bacon      substitutions: substitutionOffsets,
12426ad19fcSEvan Bacon    },
12526ad19fcSEvan Bacon  };
12626ad19fcSEvan Bacon}
12726ad19fcSEvan Bacon
12826ad19fcSEvan Baconfunction isComponentStack(consoleArgument: string) {
12926ad19fcSEvan Bacon  const isOldComponentStackFormat = / {4}in/.test(consoleArgument);
13026ad19fcSEvan Bacon  const isNewComponentStackFormat = / {4}at/.test(consoleArgument);
13126ad19fcSEvan Bacon  const isNewJSCComponentStackFormat = /@.*\n/.test(consoleArgument);
13226ad19fcSEvan Bacon
13326ad19fcSEvan Bacon  return isOldComponentStackFormat || isNewComponentStackFormat || isNewJSCComponentStackFormat;
13426ad19fcSEvan Bacon}
13526ad19fcSEvan Bacon
13626ad19fcSEvan Baconexport function parseComponentStack(message: string): ComponentStack {
13726ad19fcSEvan Bacon  // In newer versions of React, the component stack is formatted as a call stack frame.
13826ad19fcSEvan Bacon  // First try to parse the component stack as a call stack frame, and if that doesn't
13926ad19fcSEvan Bacon  // work then we'll fallback to the old custom component stack format parsing.
14026ad19fcSEvan Bacon  const stack = parseErrorStack(message);
14126ad19fcSEvan Bacon  if (stack && stack.length > 0) {
14226ad19fcSEvan Bacon    return stack.map((frame) => ({
14326ad19fcSEvan Bacon      content: frame.methodName,
14426ad19fcSEvan Bacon      collapse: frame.collapse || false,
14526ad19fcSEvan Bacon      fileName: frame.file == null ? 'unknown' : frame.file,
14626ad19fcSEvan Bacon      location: {
14726ad19fcSEvan Bacon        column: frame.column == null ? -1 : frame.column,
14826ad19fcSEvan Bacon        row: frame.lineNumber == null ? -1 : frame.lineNumber,
14926ad19fcSEvan Bacon      },
15026ad19fcSEvan Bacon    }));
15126ad19fcSEvan Bacon  }
15226ad19fcSEvan Bacon
15326ad19fcSEvan Bacon  return message
15426ad19fcSEvan Bacon    .split(/\n {4}in /g)
15526ad19fcSEvan Bacon    .map((s) => {
15626ad19fcSEvan Bacon      if (!s) {
15726ad19fcSEvan Bacon        return null;
15826ad19fcSEvan Bacon      }
15926ad19fcSEvan Bacon      const match = s.match(/(.*) \(at (.*\.js):([\d]+)\)/);
16026ad19fcSEvan Bacon      if (!match) {
16126ad19fcSEvan Bacon        return null;
16226ad19fcSEvan Bacon      }
16326ad19fcSEvan Bacon
16426ad19fcSEvan Bacon      const [content, fileName, row] = match.slice(1);
16526ad19fcSEvan Bacon      return {
16626ad19fcSEvan Bacon        content,
16726ad19fcSEvan Bacon        fileName,
16826ad19fcSEvan Bacon        location: { column: -1, row: parseInt(row, 10) },
16926ad19fcSEvan Bacon      };
17026ad19fcSEvan Bacon    })
17126ad19fcSEvan Bacon    .filter(Boolean) as ComponentStack;
17226ad19fcSEvan Bacon}
17326ad19fcSEvan Bacon
17426ad19fcSEvan Baconexport function parseLogBoxException(error: ExtendedExceptionData): LogBoxLogData {
17526ad19fcSEvan Bacon  const message = error.originalMessage != null ? error.originalMessage : 'Unknown';
17626ad19fcSEvan Bacon
17726ad19fcSEvan Bacon  const metroInternalError = message.match(METRO_ERROR_FORMAT);
17826ad19fcSEvan Bacon  if (metroInternalError) {
17926ad19fcSEvan Bacon    const [content, fileName, row, column, codeFrame] = metroInternalError.slice(1);
18026ad19fcSEvan Bacon
18126ad19fcSEvan Bacon    return {
18226ad19fcSEvan Bacon      level: 'fatal',
18326ad19fcSEvan Bacon      type: 'Metro Error',
18426ad19fcSEvan Bacon      stack: [],
18526ad19fcSEvan Bacon      isComponentError: false,
18626ad19fcSEvan Bacon      componentStack: [],
18726ad19fcSEvan Bacon      codeFrame: {
18826ad19fcSEvan Bacon        fileName,
18926ad19fcSEvan Bacon        location: {
19026ad19fcSEvan Bacon          row: parseInt(row, 10),
19126ad19fcSEvan Bacon          column: parseInt(column, 10),
19226ad19fcSEvan Bacon        },
19326ad19fcSEvan Bacon        content: codeFrame,
19426ad19fcSEvan Bacon      },
19526ad19fcSEvan Bacon      message: {
19626ad19fcSEvan Bacon        content,
19726ad19fcSEvan Bacon        substitutions: [],
19826ad19fcSEvan Bacon      },
19926ad19fcSEvan Bacon      category: `${fileName}-${row}-${column}`,
20026ad19fcSEvan Bacon    };
20126ad19fcSEvan Bacon  }
20226ad19fcSEvan Bacon
20326ad19fcSEvan Bacon  const babelTransformError = message.match(BABEL_TRANSFORM_ERROR_FORMAT);
20426ad19fcSEvan Bacon  if (babelTransformError) {
20526ad19fcSEvan Bacon    // Transform errors are thrown from inside the Babel transformer.
20626ad19fcSEvan Bacon    const [fileName, content, row, column, codeFrame] = babelTransformError.slice(1);
20726ad19fcSEvan Bacon
20826ad19fcSEvan Bacon    return {
20926ad19fcSEvan Bacon      level: 'syntax',
21026ad19fcSEvan Bacon      stack: [],
21126ad19fcSEvan Bacon      isComponentError: false,
21226ad19fcSEvan Bacon      componentStack: [],
21326ad19fcSEvan Bacon      codeFrame: {
21426ad19fcSEvan Bacon        fileName,
21526ad19fcSEvan Bacon        location: {
21626ad19fcSEvan Bacon          row: parseInt(row, 10),
21726ad19fcSEvan Bacon          column: parseInt(column, 10),
21826ad19fcSEvan Bacon        },
21926ad19fcSEvan Bacon        content: codeFrame,
22026ad19fcSEvan Bacon      },
22126ad19fcSEvan Bacon      message: {
22226ad19fcSEvan Bacon        content,
22326ad19fcSEvan Bacon        substitutions: [],
22426ad19fcSEvan Bacon      },
22526ad19fcSEvan Bacon      category: `${fileName}-${row}-${column}`,
22626ad19fcSEvan Bacon    };
22726ad19fcSEvan Bacon  }
22826ad19fcSEvan Bacon
22926ad19fcSEvan Bacon  const babelCodeFrameError = message.match(BABEL_CODE_FRAME_ERROR_FORMAT);
23026ad19fcSEvan Bacon
23126ad19fcSEvan Bacon  if (babelCodeFrameError) {
23226ad19fcSEvan Bacon    // Codeframe errors are thrown from any use of buildCodeFrameError.
23326ad19fcSEvan Bacon    const [fileName, content, codeFrame] = babelCodeFrameError.slice(1);
23426ad19fcSEvan Bacon    return {
23526ad19fcSEvan Bacon      level: 'syntax',
23626ad19fcSEvan Bacon      stack: [],
23726ad19fcSEvan Bacon      isComponentError: false,
23826ad19fcSEvan Bacon      componentStack: [],
23926ad19fcSEvan Bacon      codeFrame: {
24026ad19fcSEvan Bacon        fileName,
24126ad19fcSEvan Bacon        location: null, // We are not given the location.
24226ad19fcSEvan Bacon        content: codeFrame,
24326ad19fcSEvan Bacon      },
24426ad19fcSEvan Bacon      message: {
24526ad19fcSEvan Bacon        content,
24626ad19fcSEvan Bacon        substitutions: [],
24726ad19fcSEvan Bacon      },
24826ad19fcSEvan Bacon      category: `${fileName}-${1}-${1}`,
24926ad19fcSEvan Bacon    };
25026ad19fcSEvan Bacon  }
25126ad19fcSEvan Bacon
25226ad19fcSEvan Bacon  if (message.match(/^TransformError /)) {
25326ad19fcSEvan Bacon    return {
25426ad19fcSEvan Bacon      level: 'syntax',
25526ad19fcSEvan Bacon      stack: error.stack,
25626ad19fcSEvan Bacon      isComponentError: error.isComponentError,
25726ad19fcSEvan Bacon      componentStack: [],
25826ad19fcSEvan Bacon      message: {
25926ad19fcSEvan Bacon        content: message,
26026ad19fcSEvan Bacon        substitutions: [],
26126ad19fcSEvan Bacon      },
26226ad19fcSEvan Bacon      category: message,
26326ad19fcSEvan Bacon    };
26426ad19fcSEvan Bacon  }
26526ad19fcSEvan Bacon
26626ad19fcSEvan Bacon  const componentStack = error.componentStack;
26726ad19fcSEvan Bacon  if (error.isFatal || error.isComponentError) {
26826ad19fcSEvan Bacon    return {
26926ad19fcSEvan Bacon      level: 'fatal',
27026ad19fcSEvan Bacon      stack: error.stack,
27126ad19fcSEvan Bacon      isComponentError: error.isComponentError,
27226ad19fcSEvan Bacon      componentStack: componentStack != null ? parseComponentStack(componentStack) : [],
27326ad19fcSEvan Bacon      ...parseInterpolation([message]),
27426ad19fcSEvan Bacon    };
27526ad19fcSEvan Bacon  }
27626ad19fcSEvan Bacon
27726ad19fcSEvan Bacon  if (componentStack != null) {
27826ad19fcSEvan Bacon    // It is possible that console errors have a componentStack.
27926ad19fcSEvan Bacon    return {
28026ad19fcSEvan Bacon      level: 'error',
28126ad19fcSEvan Bacon      stack: error.stack,
28226ad19fcSEvan Bacon      isComponentError: error.isComponentError,
28326ad19fcSEvan Bacon      componentStack: parseComponentStack(componentStack),
28426ad19fcSEvan Bacon      ...parseInterpolation([message]),
28526ad19fcSEvan Bacon    };
28626ad19fcSEvan Bacon  }
28726ad19fcSEvan Bacon
28826ad19fcSEvan Bacon  // Most `console.error` calls won't have a componentStack. We parse them like
28926ad19fcSEvan Bacon  // regular logs which have the component stack burried in the message.
29026ad19fcSEvan Bacon  return {
29126ad19fcSEvan Bacon    level: 'error',
29226ad19fcSEvan Bacon    stack: error.stack,
29326ad19fcSEvan Bacon    isComponentError: error.isComponentError,
29426ad19fcSEvan Bacon    ...parseLogBoxLog([message]),
29526ad19fcSEvan Bacon  };
29626ad19fcSEvan Bacon}
29726ad19fcSEvan Bacon
29826ad19fcSEvan Baconexport function parseLogBoxLog(args: readonly any[]): {
29926ad19fcSEvan Bacon  componentStack: ComponentStack;
30026ad19fcSEvan Bacon  category: Category;
30126ad19fcSEvan Bacon  message: Message;
30226ad19fcSEvan Bacon} {
30326ad19fcSEvan Bacon  const message = args[0];
30426ad19fcSEvan Bacon  let argsWithoutComponentStack: any[] = [];
30526ad19fcSEvan Bacon  let componentStack: ComponentStack = [];
30626ad19fcSEvan Bacon
30726ad19fcSEvan Bacon  // Extract component stack from warnings like "Some warning%s".
30826ad19fcSEvan Bacon  if (typeof message === 'string' && message.slice(-2) === '%s' && args.length > 0) {
30926ad19fcSEvan Bacon    const lastArg = args[args.length - 1];
31026ad19fcSEvan Bacon    if (typeof lastArg === 'string' && isComponentStack(lastArg)) {
31126ad19fcSEvan Bacon      argsWithoutComponentStack = args.slice(0, -1);
31226ad19fcSEvan Bacon      argsWithoutComponentStack[0] = message.slice(0, -2);
31326ad19fcSEvan Bacon      componentStack = parseComponentStack(lastArg);
31426ad19fcSEvan Bacon    }
31526ad19fcSEvan Bacon  }
31626ad19fcSEvan Bacon
31726ad19fcSEvan Bacon  if (componentStack.length === 0) {
31826ad19fcSEvan Bacon    // Try finding the component stack elsewhere.
31926ad19fcSEvan Bacon    for (const arg of args) {
32026ad19fcSEvan Bacon      if (typeof arg === 'string' && isComponentStack(arg)) {
32126ad19fcSEvan Bacon        // Strip out any messages before the component stack.
32226ad19fcSEvan Bacon        let messageEndIndex = arg.search(/\n {4}(in|at) /);
32326ad19fcSEvan Bacon        if (messageEndIndex < 0) {
32426ad19fcSEvan Bacon          // Handle JSC component stacks.
32526ad19fcSEvan Bacon          messageEndIndex = arg.search(/\n/);
32626ad19fcSEvan Bacon        }
32726ad19fcSEvan Bacon        if (messageEndIndex > 0) {
32826ad19fcSEvan Bacon          argsWithoutComponentStack.push(arg.slice(0, messageEndIndex));
32926ad19fcSEvan Bacon        }
33026ad19fcSEvan Bacon
33126ad19fcSEvan Bacon        componentStack = parseComponentStack(arg);
33226ad19fcSEvan Bacon      } else {
33326ad19fcSEvan Bacon        argsWithoutComponentStack.push(arg);
33426ad19fcSEvan Bacon      }
33526ad19fcSEvan Bacon    }
33626ad19fcSEvan Bacon  }
33726ad19fcSEvan Bacon
33826ad19fcSEvan Bacon  return {
33926ad19fcSEvan Bacon    ...parseInterpolation(argsWithoutComponentStack),
34026ad19fcSEvan Bacon    componentStack,
34126ad19fcSEvan Bacon  };
34226ad19fcSEvan Bacon}
343