blob: bfe794039ee38fa9f8679a35071eb4179adb4549 [file] [log] [blame]
// Copyright (C) 2018 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {VERSION} from '../gen/perfetto_version';
import {exists} from './utils';
export type ErrorType = 'ERROR' | 'PROMISE_REJ' | 'OTHER';
export interface ErrorStackEntry {
name: string; // e.g. renderCanvas
location: string; // e.g. frontend_bundle.js:12:3
}
export interface ErrorDetails {
errType: ErrorType;
message: string; // Uncaught StoreError: No such subtree: tracks,1374,state
stack: ErrorStackEntry[];
}
export type ErrorHandler = (err: ErrorDetails) => void;
const errorHandlers: ErrorHandler[] = [];
export function assertExists<A>(value: A | null | undefined): A {
if (value === null || value === undefined) {
throw new Error("Value doesn't exist");
}
return value;
}
export function assertTrue(value: boolean, optMsg?: string) {
if (!value) {
throw new Error(optMsg ?? 'Failed assertion');
}
}
export function assertFalse(value: boolean, optMsg?: string) {
assertTrue(!value, optMsg);
}
export function addErrorHandler(handler: ErrorHandler) {
if (!errorHandlers.includes(handler)) {
errorHandlers.push(handler);
}
}
export function reportError(err: ErrorEvent | PromiseRejectionEvent | {}) {
let errorObj = undefined;
let errMsg = '';
let errType: ErrorType;
const stack: ErrorStackEntry[] = [];
const baseUrl = `${location.protocol}//${location.host}`;
if (err instanceof ErrorEvent) {
errType = 'ERROR';
// In nominal cases the error is set in err.error{message,stack} and
// a toString() of the error object returns a meaningful one-line
// description. However, in the case of wasm errors, emscripten seems to
// wrap the error in an unusual way: err.error is null but err.message
// contains the whole one-line + stack trace.
if (err.error === null || err.error === undefined) {
// Wasm case.
const errLines = `${err.message}`.split('\n');
errMsg = errLines[0];
errorObj = {stack: errLines.slice(1).join('\n')};
} else {
// Standard JS case.
errMsg = `${err.error}`;
errorObj = err.error;
}
} else if (err instanceof PromiseRejectionEvent) {
errType = 'PROMISE_REJ';
errMsg = `${err.reason}`;
errorObj = err.reason;
} else {
errType = 'OTHER';
errMsg = `${err}`;
}
// Remove useless "Uncaught Error:" or "Error:" prefixes which just create
// noise in the bug tracker without adding any meaningful value.
errMsg = errMsg.replace(/^Uncaught Error:/, '');
errMsg = errMsg.replace(/^Error:/, '');
errMsg = errMsg.trim();
if (errorObj !== undefined && errorObj !== null) {
const maybeStack = (errorObj as {stack?: string}).stack;
let errStack = maybeStack !== undefined ? `${maybeStack}` : '';
errStack = errStack.replaceAll(/\r/g, ''); // Strip Windows CR.
for (let line of errStack.split('\n')) {
if (errMsg.includes(line)) continue;
// Chrome, Firefox and safari don't agree on the stack format:
// Chrome: prefixes entries with a ' at ' and uses the format
// function(https://url:line:col), e.g.
// ' at FooBar (https://.../frontend_bundle.js:2073:15)'
// however, if the function name is not known, it prints just:
// ' at https://.../frontend_bundle.js:2073:15'
// or also:
// ' at <anonymous>:5:11'
// Firefox and Safari: don't have any prefix and use @ as a separator:
// redrawCanvas@https://.../frontend_bundle.js:468814:26
// @debugger eval code:1:32
// Here we first normalize Chrome into the Firefox/Safari format by
// removing the ' at ' prefix and replacing (xxx)$ into @xxx.
line = line.replace(/^\s*at\s*/, '');
line = line.replace(/\s*\(([^)]+)\)$/, '@$1');
// This leaves us still with two possible options here:
// 1. FooBar@https://ui.perfetto.dev/v123/frontend_bundle.js:2073:15
// 2. https://ui.perfetto.dev/v123/frontend_bundle.js:2073:15
const lastAt = line.lastIndexOf('@');
let entryName = '';
let entryLocation = '';
if (lastAt >= 0) {
entryLocation = line.substring(lastAt + 1);
entryName = line.substring(0, lastAt);
} else {
entryLocation = line;
}
// Remove redundant https://ui.perfetto.dev/v38.0-d6ed090ee/ as we have
// that information already and don't need to repeat it on each line.
if (entryLocation.includes(baseUrl)) {
entryLocation = entryLocation.replace(baseUrl, '');
entryLocation = entryLocation.replace(`/${VERSION}/`, '');
}
stack.push({name: entryName, location: entryLocation});
} // for (line in stack)
// Beautify the Wasm error message if possible. Most Wasm errors are of the
// form RuntimeError: unreachable or RuntimeError: abort. Those lead to bug
// titles that are undistinguishable from each other. Instead try using the
// first entry of the stack that contains a perfetto:: function name.
const wasmFunc = stack.find((e) => e.name.includes('perfetto::'))?.name;
if (errMsg.includes('RuntimeError') && exists(wasmFunc)) {
errMsg += ` @ ${wasmFunc.trim()}`;
}
}
// Invoke all the handlers registered through addErrorHandler.
// There are usually two handlers registered, one for the UI (error_dialog.ts)
// and one for Analytics (analytics.ts).
for (const handler of errorHandlers) {
handler({
errType,
message: errMsg,
stack,
} as ErrorDetails);
}
}
// This function serves two purposes.
// 1) A runtime check - if we are ever called, we throw an exception.
// This is useful for checking that code we suspect should never be reached is
// actually never reached.
// 2) A compile time check where typescript asserts that the value passed can be
// cast to the "never" type.
// This is useful for ensuring we exhastively check union types.
export function assertUnreachable(value: never): never {
throw new Error(`This code should not be reachable ${value as unknown}`);
}