// 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(value: A | null | undefined): A { if (value === null || value === undefined) { throw new Error("Value doesn't exist"); } return value; } export function assertIsInstance(value: unknown, clazz: Function): T { assertTrue(value instanceof clazz); return value as T; } 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 :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}`); }