1// Copyright (C) 2018 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import {VERSION} from '../gen/perfetto_version'; 16import {exists} from './utils'; 17 18export type ErrorType = 'ERROR' | 'PROMISE_REJ' | 'OTHER'; 19export interface ErrorStackEntry { 20 name: string; // e.g. renderCanvas 21 location: string; // e.g. frontend_bundle.js:12:3 22} 23export interface ErrorDetails { 24 errType: ErrorType; 25 message: string; // Uncaught StoreError: No such subtree: tracks,1374,state 26 stack: ErrorStackEntry[]; 27} 28 29export type ErrorHandler = (err: ErrorDetails) => void; 30const errorHandlers: ErrorHandler[] = []; 31 32export function assertExists<A>(value: A | null | undefined): A { 33 if (value === null || value === undefined) { 34 throw new Error("Value doesn't exist"); 35 } 36 return value; 37} 38 39export function assertIsInstance<T>(value: unknown, clazz: Function): T { 40 assertTrue(value instanceof clazz); 41 return value as T; 42} 43 44export function assertTrue(value: boolean, optMsg?: string) { 45 if (!value) { 46 throw new Error(optMsg ?? 'Failed assertion'); 47 } 48} 49 50export function assertFalse(value: boolean, optMsg?: string) { 51 assertTrue(!value, optMsg); 52} 53 54export function addErrorHandler(handler: ErrorHandler) { 55 if (!errorHandlers.includes(handler)) { 56 errorHandlers.push(handler); 57 } 58} 59 60export function reportError(err: ErrorEvent | PromiseRejectionEvent | {}) { 61 let errorObj = undefined; 62 let errMsg = ''; 63 let errType: ErrorType; 64 const stack: ErrorStackEntry[] = []; 65 const baseUrl = `${location.protocol}//${location.host}`; 66 67 if (err instanceof ErrorEvent) { 68 errType = 'ERROR'; 69 // In nominal cases the error is set in err.error{message,stack} and 70 // a toString() of the error object returns a meaningful one-line 71 // description. However, in the case of wasm errors, emscripten seems to 72 // wrap the error in an unusual way: err.error is null but err.message 73 // contains the whole one-line + stack trace. 74 if (err.error === null || err.error === undefined) { 75 // Wasm case. 76 const errLines = `${err.message}`.split('\n'); 77 errMsg = errLines[0]; 78 errorObj = {stack: errLines.slice(1).join('\n')}; 79 } else { 80 // Standard JS case. 81 errMsg = `${err.error}`; 82 errorObj = err.error; 83 } 84 } else if (err instanceof PromiseRejectionEvent) { 85 errType = 'PROMISE_REJ'; 86 errMsg = `${err.reason}`; 87 errorObj = err.reason; 88 } else { 89 errType = 'OTHER'; 90 errMsg = `${err}`; 91 } 92 93 // Remove useless "Uncaught Error:" or "Error:" prefixes which just create 94 // noise in the bug tracker without adding any meaningful value. 95 errMsg = errMsg.replace(/^Uncaught Error:/, ''); 96 errMsg = errMsg.replace(/^Error:/, ''); 97 errMsg = errMsg.trim(); 98 99 if (errorObj !== undefined && errorObj !== null) { 100 const maybeStack = (errorObj as {stack?: string}).stack; 101 let errStack = maybeStack !== undefined ? `${maybeStack}` : ''; 102 errStack = errStack.replaceAll(/\r/g, ''); // Strip Windows CR. 103 for (let line of errStack.split('\n')) { 104 if (errMsg.includes(line)) continue; 105 // Chrome, Firefox and safari don't agree on the stack format: 106 // Chrome: prefixes entries with a ' at ' and uses the format 107 // function(https://url:line:col), e.g. 108 // ' at FooBar (https://.../frontend_bundle.js:2073:15)' 109 // however, if the function name is not known, it prints just: 110 // ' at https://.../frontend_bundle.js:2073:15' 111 // or also: 112 // ' at <anonymous>:5:11' 113 // Firefox and Safari: don't have any prefix and use @ as a separator: 114 // redrawCanvas@https://.../frontend_bundle.js:468814:26 115 // @debugger eval code:1:32 116 117 // Here we first normalize Chrome into the Firefox/Safari format by 118 // removing the ' at ' prefix and replacing (xxx)$ into @xxx. 119 line = line.replace(/^\s*at\s*/, ''); 120 line = line.replace(/\s*\(([^)]+)\)$/, '@$1'); 121 122 // This leaves us still with two possible options here: 123 // 1. FooBar@https://ui.perfetto.dev/v123/frontend_bundle.js:2073:15 124 // 2. https://ui.perfetto.dev/v123/frontend_bundle.js:2073:15 125 const lastAt = line.lastIndexOf('@'); 126 let entryName = ''; 127 let entryLocation = ''; 128 if (lastAt >= 0) { 129 entryLocation = line.substring(lastAt + 1); 130 entryName = line.substring(0, lastAt); 131 } else { 132 entryLocation = line; 133 } 134 135 // Remove redundant https://ui.perfetto.dev/v38.0-d6ed090ee/ as we have 136 // that information already and don't need to repeat it on each line. 137 if (entryLocation.includes(baseUrl)) { 138 entryLocation = entryLocation.replace(baseUrl, ''); 139 entryLocation = entryLocation.replace(`/${VERSION}/`, ''); 140 } 141 stack.push({name: entryName, location: entryLocation}); 142 } // for (line in stack) 143 144 // Beautify the Wasm error message if possible. Most Wasm errors are of the 145 // form RuntimeError: unreachable or RuntimeError: abort. Those lead to bug 146 // titles that are undistinguishable from each other. Instead try using the 147 // first entry of the stack that contains a perfetto:: function name. 148 const wasmFunc = stack.find((e) => e.name.includes('perfetto::'))?.name; 149 if (errMsg.includes('RuntimeError') && exists(wasmFunc)) { 150 errMsg += ` @ ${wasmFunc.trim()}`; 151 } 152 } 153 // Invoke all the handlers registered through addErrorHandler. 154 // There are usually two handlers registered, one for the UI (error_dialog.ts) 155 // and one for Analytics (analytics.ts). 156 for (const handler of errorHandlers) { 157 handler({ 158 errType, 159 message: errMsg, 160 stack, 161 } as ErrorDetails); 162 } 163} 164 165// This function serves two purposes. 166// 1) A runtime check - if we are ever called, we throw an exception. 167// This is useful for checking that code we suspect should never be reached is 168// actually never reached. 169// 2) A compile time check where typescript asserts that the value passed can be 170// cast to the "never" type. 171// This is useful for ensuring we exhastively check union types. 172export function assertUnreachable(value: never): never { 173 throw new Error(`This code should not be reachable ${value as unknown}`); 174} 175