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