xref: /aosp_15_r20/external/perfetto/ui/src/base/logging.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
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