Merge pull request #2139 from github/robertbrignull/webview_error_telemetry

Add listeners for unhandled errors to web views
This commit is contained in:
Robert
2023-03-16 11:18:52 +00:00
committed by GitHub
7 changed files with 127 additions and 19 deletions

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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);
}

View 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);
}

View File

@@ -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) {