Merge pull request #2139 from github/robertbrignull/webview_error_telemetry
Add listeners for unhandled errors to web views
This commit is contained in:
@@ -20,6 +20,8 @@ import { assertNever, getErrorMessage } from "../pure/helpers-pure";
|
||||
import { HistoryItemLabelProvider } from "../query-history/history-item-label-provider";
|
||||
import { AbstractWebview, WebviewPanelConfig } from "../abstract-webview";
|
||||
import { telemetryListener } from "../telemetry";
|
||||
import { redactableError } from "../pure/errors";
|
||||
import { showAndLogExceptionWithTelemetry } from "../helpers";
|
||||
|
||||
interface ComparePair {
|
||||
from: CompletedLocalQueryInfo;
|
||||
@@ -139,6 +141,14 @@ export class CompareView extends AbstractWebview<
|
||||
telemetryListener?.sendUIInteraction(msg.action);
|
||||
break;
|
||||
|
||||
case "unhandledError":
|
||||
void showAndLogExceptionWithTelemetry(
|
||||
redactableError(
|
||||
msg.error,
|
||||
)`Unhandled error in result comparison view: ${msg.error.message}`,
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
assertNever(msg);
|
||||
}
|
||||
|
||||
@@ -295,6 +295,13 @@ export class ResultsView extends AbstractWebview<
|
||||
case "telemetry":
|
||||
telemetryListener?.sendUIInteraction(msg.action);
|
||||
break;
|
||||
case "unhandledError":
|
||||
void showAndLogExceptionWithTelemetry(
|
||||
redactableError(
|
||||
msg.error,
|
||||
)`Unhandled error in results view: ${msg.error.message}`,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
assertNever(msg);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export class RedactableError extends Error {
|
||||
constructor(
|
||||
cause: Error | undefined,
|
||||
cause: ErrorLike | undefined,
|
||||
private readonly strings: TemplateStringsArray,
|
||||
private readonly values: unknown[],
|
||||
) {
|
||||
@@ -54,19 +54,34 @@ export function redactableError(
|
||||
...values: unknown[]
|
||||
): RedactableError;
|
||||
export function redactableError(
|
||||
error: Error,
|
||||
error: ErrorLike,
|
||||
): (strings: TemplateStringsArray, ...values: unknown[]) => RedactableError;
|
||||
|
||||
export function redactableError(
|
||||
errorOrStrings: Error | TemplateStringsArray,
|
||||
errorOrStrings: ErrorLike | TemplateStringsArray,
|
||||
...values: unknown[]
|
||||
):
|
||||
| ((strings: TemplateStringsArray, ...values: unknown[]) => RedactableError)
|
||||
| RedactableError {
|
||||
if (errorOrStrings instanceof Error) {
|
||||
if (isErrorLike(errorOrStrings)) {
|
||||
return (strings: TemplateStringsArray, ...values: unknown[]) =>
|
||||
new RedactableError(errorOrStrings, strings, values);
|
||||
} else {
|
||||
return new RedactableError(undefined, errorOrStrings, values);
|
||||
}
|
||||
}
|
||||
|
||||
export interface ErrorLike {
|
||||
message: string;
|
||||
stack?: string;
|
||||
}
|
||||
|
||||
function isErrorLike(error: any): error is ErrorLike {
|
||||
if (
|
||||
typeof error.message === "string" &&
|
||||
(error.stack === undefined || typeof error.stack === "string")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
VariantAnalysisScannedRepositoryState,
|
||||
} from "../variant-analysis/shared/variant-analysis";
|
||||
import { RepositoriesFilterSortStateWithIds } from "./variant-analysis-filter-sort";
|
||||
import { ErrorLike } from "./errors";
|
||||
|
||||
/**
|
||||
* This module contains types and code that are shared between
|
||||
@@ -182,14 +183,13 @@ export type IntoResultsViewMsg =
|
||||
* A message sent from the results view.
|
||||
*/
|
||||
export type FromResultsViewMsg =
|
||||
| CommonFromViewMessages
|
||||
| ViewSourceFileMsg
|
||||
| ToggleDiagnostics
|
||||
| ChangeRawResultsSortMsg
|
||||
| ChangeInterpretedResultsSortMsg
|
||||
| ViewLoadedMsg
|
||||
| ChangePage
|
||||
| OpenFileMsg
|
||||
| TelemetryMessage;
|
||||
| OpenFileMsg;
|
||||
|
||||
/**
|
||||
* Message from the results view to open a database source
|
||||
@@ -231,6 +231,21 @@ interface ViewLoadedMsg {
|
||||
viewName: string;
|
||||
}
|
||||
|
||||
interface TelemetryMessage {
|
||||
t: "telemetry";
|
||||
action: string;
|
||||
}
|
||||
|
||||
interface UnhandledErrorMessage {
|
||||
t: "unhandledError";
|
||||
error: ErrorLike;
|
||||
}
|
||||
|
||||
type CommonFromViewMessages =
|
||||
| ViewLoadedMsg
|
||||
| TelemetryMessage
|
||||
| UnhandledErrorMessage;
|
||||
|
||||
/**
|
||||
* Message from the results view to signal a request to change the
|
||||
* page.
|
||||
@@ -287,11 +302,10 @@ interface ChangeInterpretedResultsSortMsg {
|
||||
* Message from the compare view to the extension.
|
||||
*/
|
||||
export type FromCompareViewMessage =
|
||||
| ViewLoadedMsg
|
||||
| CommonFromViewMessages
|
||||
| ChangeCompareMessage
|
||||
| ViewSourceFileMsg
|
||||
| OpenQueryMessage
|
||||
| TelemetryMessage;
|
||||
| OpenQueryMessage;
|
||||
|
||||
/**
|
||||
* Message from the compare view to request opening a query.
|
||||
@@ -434,23 +448,17 @@ export interface CancelVariantAnalysisMessage {
|
||||
t: "cancelVariantAnalysis";
|
||||
}
|
||||
|
||||
export interface TelemetryMessage {
|
||||
t: "telemetry";
|
||||
action: string;
|
||||
}
|
||||
|
||||
export type ToVariantAnalysisMessage =
|
||||
| SetVariantAnalysisMessage
|
||||
| SetRepoResultsMessage
|
||||
| SetRepoStatesMessage;
|
||||
|
||||
export type FromVariantAnalysisMessage =
|
||||
| ViewLoadedMsg
|
||||
| CommonFromViewMessages
|
||||
| RequestRepositoryResultsMessage
|
||||
| OpenQueryFileMessage
|
||||
| OpenQueryTextMessage
|
||||
| CopyRepositoryListMessage
|
||||
| ExportResultsMessage
|
||||
| OpenLogsMessage
|
||||
| CancelVariantAnalysisMessage
|
||||
| TelemetryMessage;
|
||||
| CancelVariantAnalysisMessage;
|
||||
|
||||
@@ -15,8 +15,12 @@ import {
|
||||
VariantAnalysisViewInterface,
|
||||
VariantAnalysisViewManager,
|
||||
} from "./variant-analysis-view-manager";
|
||||
import { showAndLogWarningMessage } from "../helpers";
|
||||
import {
|
||||
showAndLogExceptionWithTelemetry,
|
||||
showAndLogWarningMessage,
|
||||
} from "../helpers";
|
||||
import { telemetryListener } from "../telemetry";
|
||||
import { redactableError } from "../pure/errors";
|
||||
|
||||
export class VariantAnalysisView
|
||||
extends AbstractWebview<ToVariantAnalysisMessage, FromVariantAnalysisMessage>
|
||||
@@ -153,6 +157,13 @@ export class VariantAnalysisView
|
||||
case "telemetry":
|
||||
telemetryListener?.sendUIInteraction(msg.action);
|
||||
break;
|
||||
case "unhandledError":
|
||||
void showAndLogExceptionWithTelemetry(
|
||||
redactableError(
|
||||
msg.error,
|
||||
)`Unhandled error in variant analysis results view: ${msg.error.message}`,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
assertNever(msg);
|
||||
}
|
||||
|
||||
54
extensions/ql-vscode/src/view/common/errors.ts
Normal file
54
extensions/ql-vscode/src/view/common/errors.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { getErrorMessage, getErrorStack } from "../../pure/helpers-pure";
|
||||
import { vscode } from "../vscode-api";
|
||||
|
||||
// Keep track of previous errors that have happened.
|
||||
// The listeners for uncaught errors and rejections can get triggered
|
||||
// twice for each error. This is believed to be an effect caused
|
||||
// by React's error boundaries. Adding an error boundary stops
|
||||
// this duplicate reporting for errors that happen during component
|
||||
// rendering, but unfortunately errors from event handlers and
|
||||
// timeouts are still duplicated and there does not appear to be
|
||||
// a way around this.
|
||||
const previousErrors: Set<Error> = new Set();
|
||||
|
||||
function shouldReportError(error: Error): boolean {
|
||||
const seenBefore = previousErrors.has(error);
|
||||
previousErrors.add(error);
|
||||
setTimeout(() => {
|
||||
previousErrors.delete(error);
|
||||
}, 1000);
|
||||
return !seenBefore;
|
||||
}
|
||||
|
||||
const unhandledErrorListener = (event: ErrorEvent) => {
|
||||
if (shouldReportError(event.error)) {
|
||||
vscode.postMessage({
|
||||
t: "unhandledError",
|
||||
error: {
|
||||
message: getErrorMessage(event.error),
|
||||
stack: getErrorStack(event.error),
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const unhandledRejectionListener = (event: PromiseRejectionEvent) => {
|
||||
if (shouldReportError(event.reason)) {
|
||||
vscode.postMessage({
|
||||
t: "unhandledError",
|
||||
error: {
|
||||
message: getErrorMessage(event.reason),
|
||||
stack: getErrorStack(event.reason),
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds listeners for unhandled errors / rejected promises.
|
||||
* When an error is detected a "unhandledError" message is posted to the view.
|
||||
*/
|
||||
export function registerUnhandledErrorListener() {
|
||||
window.addEventListener("error", unhandledErrorListener);
|
||||
window.addEventListener("unhandledrejection", unhandledRejectionListener);
|
||||
}
|
||||
@@ -5,8 +5,11 @@ import { WebviewDefinition } from "./webview-definition";
|
||||
|
||||
// Allow all views to use Codicons
|
||||
import "@vscode/codicons/dist/codicon.css";
|
||||
import { registerUnhandledErrorListener } from "./common/errors";
|
||||
|
||||
const render = () => {
|
||||
registerUnhandledErrorListener();
|
||||
|
||||
const element = document.getElementById("root");
|
||||
|
||||
if (!element) {
|
||||
|
||||
Reference in New Issue
Block a user