UI: add error handler
Show a civilized error dialog when an error is raised.
Handles both errors in the frontend and in the controller.
Example: https://screenshot.googleplex.com/CktD3ag8PEh
Change-Id: I8956e3c06961420f3ef4fd4d5881e617b665327c
diff --git a/ui/src/assets/modal.scss b/ui/src/assets/modal.scss
index 99f2e6a..24a8bfd 100644
--- a/ui/src/assets/modal.scss
+++ b/ui/src/assets/modal.scss
@@ -29,7 +29,7 @@
display: flex;
justify-content: center;
align-items: center;
- z-index: 4;
+ z-index: 999;
}
.modal-container {
@@ -186,4 +186,17 @@
.modal-pre {
white-space: pre-line;
+}
+
+.modal-logs {
+ white-space: pre-wrap;
+ border: 1px solid #999;
+ background: #eee;
+ font-size: 10px;
+ font-family: monospace;
+ -webkit-user-select: text;
+ margin-top: 10px;
+ min-height: 50px;
+ max-height: 40vh;
+ overflow: scroll;
}
\ No newline at end of file
diff --git a/ui/src/base/logging.ts b/ui/src/base/logging.ts
index c3279c1..caefe56 100644
--- a/ui/src/base/logging.ts
+++ b/ui/src/base/logging.ts
@@ -12,6 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+export type ErrorHandler = (err: string) => void;
+
+let errorHandler: ErrorHandler = (_: string) => {};
+
export function assertExists<A>(value: A | null | undefined): A {
if (value === null || value === undefined) {
throw new Error('Value doesn\'t exist');
@@ -28,3 +32,31 @@
export function assertFalse(value: boolean, optMsg?: string) {
assertTrue(!value, optMsg);
}
+
+export function setErrorHandler(handler: ErrorHandler) {
+ errorHandler = handler;
+}
+
+export function reportError(err: ErrorEvent|PromiseRejectionEvent|{}) {
+ let errLog = '';
+ let errorObj = undefined;
+
+ if (err instanceof ErrorEvent) {
+ errLog = err.message;
+ errorObj = err.error;
+ } else if (err instanceof PromiseRejectionEvent) {
+ errLog = `${err.reason}`;
+ errorObj = err.reason;
+ } else {
+ errLog = `${err}`;
+ }
+ if (errorObj !== undefined) {
+ const errStack = (errorObj as {stack?: string}).stack;
+ errLog += '\n';
+ errLog += errStack !== undefined ? errStack : JSON.stringify(errorObj);
+ }
+ errLog += `\n\nUA: ${navigator.userAgent}\n`;
+
+ console.error(errLog, err);
+ errorHandler(errLog);
+}
\ No newline at end of file
diff --git a/ui/src/controller/index.ts b/ui/src/controller/index.ts
index e8e7d71..24e4800 100644
--- a/ui/src/controller/index.ts
+++ b/ui/src/controller/index.ts
@@ -14,6 +14,7 @@
import '../tracks/all_controller';
+import {reportError, setErrorHandler} from '../base/logging';
import {Remote} from '../base/remote';
import {warmupWasmEngine} from '../common/wasm_engine_proxy';
@@ -21,6 +22,8 @@
import {globals} from './globals';
function main() {
+ self.addEventListener('error', e => reportError(e));
+ self.addEventListener('unhandledrejection', e => reportError(e));
warmupWasmEngine();
let initialized = false;
self.onmessage = ({data}) => {
@@ -32,7 +35,8 @@
const frontendPort = data.frontendPort as MessagePort;
const controllerPort = data.controllerPort as MessagePort;
const extensionPort = data.extensionPort as MessagePort;
-
+ const errorReportingPort = data.errorReportingPort as MessagePort;
+ setErrorHandler((err: string) => errorReportingPort.postMessage(err));
const frontend = new Remote(frontendPort);
controllerPort.onmessage = ({data}) => globals.dispatch(data);
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index e9632ad..8c93273 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -110,8 +110,7 @@
})
.catch(err => {
this.updateStatus(`${err}`);
- console.error(err);
- return;
+ throw err;
});
this.updateStatus('Opening trace');
this.setState('loading_trace');
@@ -185,7 +184,7 @@
engine.errorHandler = (err) => {
globals.dispatch(
Actions.setEngineFailed({mode: 'HTTP_RPC', failure: `${err}`}));
- console.error(err);
+ throw err;
};
this.engine = engine;
} else {
diff --git a/ui/src/frontend/error_dialog.ts b/ui/src/frontend/error_dialog.ts
new file mode 100644
index 0000000..565cafc
--- /dev/null
+++ b/ui/src/frontend/error_dialog.ts
@@ -0,0 +1,66 @@
+// Copyright (C) 2019 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 * as m from 'mithril';
+
+import {showModal} from './modal';
+
+// Never show more than one dialog per minute.
+const MIN_REPORT_PERIOD_MS = 60000;
+let timeLastReport = 0;
+
+// Keeps the last ERR_QUEUE_MAX_LEN errors while the dialog is throttled.
+const queuedErrors = new Array<string>();
+const ERR_QUEUE_MAX_LEN = 10;
+
+export function maybeShowErrorDialog(errLog: string) {
+ const now = performance.now();
+ if (timeLastReport > 0 && now - timeLastReport <= MIN_REPORT_PERIOD_MS) {
+ queuedErrors.unshift(errLog);
+ if (queuedErrors.length > ERR_QUEUE_MAX_LEN) queuedErrors.pop();
+ console.log('Suppressing crash dialog, last error notified too soon.');
+ return;
+ }
+ timeLastReport = now;
+
+ // Append queued errors.
+ while (queuedErrors.length > 0) {
+ const queuedErr = queuedErrors.shift();
+ errLog += `\n\n---------------------------------------\n${queuedErr}`;
+ }
+
+ const errTitle = errLog.split('\n', 1)[0].substr(0, 80);
+ let link = 'https://goto.google.com/perfetto-ui-bug';
+ link += '?title=' + encodeURIComponent(`UI Error: ${errTitle}`);
+ link += '&description=' + encodeURIComponent(errLog.substr(0, 32768));
+
+ showModal({
+ title: 'Oops, something went wrong. Please file a bug',
+ content: m(
+ 'div',
+ m('span', 'An unexpected crash occurred:'),
+ m('.modal-logs', errLog),
+ ),
+ buttons: [
+ {
+ text: 'File a bug (Googlers only)',
+ primary: true,
+ id: 'file_bug',
+ action: () => {
+ window.open(link, '_blank');
+ }
+ },
+ ]
+ });
+}
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index dceb528..22515ee 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -18,7 +18,7 @@
import * as MicroModal from 'micromodal';
import * as m from 'mithril';
-import {assertExists} from '../base/logging';
+import {assertExists, reportError, setErrorHandler} from '../base/logging';
import {forwardRemoteCalls} from '../base/remote';
import {Actions} from '../common/actions';
import {
@@ -32,6 +32,7 @@
HeapProfileFlamegraphKey
} from '../tracks/heap_profile_flamegraph/common';
+import {maybeShowErrorDialog} from './error_dialog';
import {
CounterDetails,
globals,
@@ -210,25 +211,32 @@
}
function main() {
+ // Add Error handlers for JS error and for uncaught exceptions in promises.
+ setErrorHandler((err: string) => maybeShowErrorDialog(err));
+ window.addEventListener('error', e => reportError(e));
+ window.addEventListener('unhandledrejection', e => reportError(e));
+
const controller = new Worker('controller_bundle.js');
- controller.onerror = e => {
- console.error(e);
- };
const frontendChannel = new MessageChannel();
const controllerChannel = new MessageChannel();
const extensionLocalChannel = new MessageChannel();
+ const errorReportingChannel = new MessageChannel();
+ errorReportingChannel.port2.onmessage = (e) =>
+ maybeShowErrorDialog(`${e.data}`);
controller.postMessage(
{
frontendPort: frontendChannel.port1,
controllerPort: controllerChannel.port1,
- extensionPort: extensionLocalChannel.port1
+ extensionPort: extensionLocalChannel.port1,
+ errorReportingPort: errorReportingChannel.port1,
},
[
frontendChannel.port1,
controllerChannel.port1,
- extensionLocalChannel.port1
+ extensionLocalChannel.port1,
+ errorReportingChannel.port1,
]);
const dispatch =