chromium/ash/webui/camera_app_ui/resources/js/error.ts

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {assertInstanceof} from './assert.js';
import * as metrics from './metrics.js';
import {isLocalDev} from './models/load_time_data.js';
import {
  ErrorLevel,
  ErrorType,
} from './type.js';

/**
 * Code location of stack frame.
 */
export interface StackFrame {
  fileName: string;
  funcName: string;
  lineNo: number;
  colNo: number;
}

const PRODUCT_NAME = 'ChromeOS_CameraApp';

function parseTopFrameInfo(stackTrace: string): StackFrame {
  const regex = /at (\[?\w+\]? |)\(?(.+):(\d+):(\d+)/;
  const match = stackTrace.match(regex) ?? ['', '', '', '-1', '-1'] as const;
  return {
    funcName: match[1].trim(),
    fileName: match[2],
    lineNo: Number(match[3]),
    colNo: Number(match[4]),
  };
}

/**
 * Initializes error collecting functions.
 */
export function initialize(): void {
  window.addEventListener('unhandledrejection', (e) => {
    reportError(ErrorType.UNCAUGHT_PROMISE, ErrorLevel.ERROR, e.reason);
  });
  window.addEventListener('error', (e) => {
    reportError(ErrorType.UNCAUGHT_ERROR, ErrorLevel.ERROR, e.error);
  });
}

/**
 * All triggered error will be hashed and saved in this set to prevent the same
 * error being triggered multiple times.
 */
const triggeredErrorSet = new Set<string>();

/**
 * Reports error either through test error callback in test run or to error
 * metrics in non test run.
 */
export function reportError(
    errorType: ErrorType, level: ErrorLevel, errorRaw: unknown): void {
  const error = assertInstanceof(errorRaw, Error);
  // Uncaught errors will be logged to the console by browser.
  if (![ErrorType.UNCAUGHT_ERROR, ErrorType.UNCAUGHT_PROMISE].includes(
          errorType)) {
    if (level === ErrorLevel.ERROR) {
      console.error(errorType, error);
    } else if (level === ErrorLevel.WARNING) {
      console.warn(errorType, error);
    }
  }

  const time = Date.now();
  const errorName = error.name;
  const stackStr = error.stack ?? '';
  const {fileName, lineNo, colNo, funcName} = parseTopFrameInfo(stackStr);

  const hash = `${errorName},${fileName},${lineNo},${colNo}`;
  if (triggeredErrorSet.has(hash)) {
    return;
  }
  triggeredErrorSet.add(hash);

  if (window.appWindow !== null) {
    void window.appWindow.reportError({
      type: errorType,
      level,
      stack: stackStr,
      time,
      name: errorName,
    });
    return;
  }
  metrics.sendErrorEvent({
    type: errorType,
    level,
    errorName,
    fileName,
    funcName,
    lineNo: String(lineNo),
    colNo: String(colNo),
  });

  // Only reports the error to crash server if it reaches "error" level.
  if (level !== ErrorLevel.ERROR) {
    return;
  }

  const params = {
    product: PRODUCT_NAME,
    url: self.location.href,
    message: `${errorType}: ${errorName}: ${error.message}`,
    lineNumber: lineNo,
    stackTrace: stackStr,
    columnNumber: colNo,
  };

  if (isLocalDev()) {
    // eslint-disable-next-line no-console
    console.info('crashReportPrivate called with:', params);
  } else {
    chrome.crashReportPrivate.reportError(
        params,
        () => {
            // Do nothing after error reported.
        });
  }
}