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,35 @@ 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 (
|
||||
error.message !== undefined &&
|
||||
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
|
||||
@@ -189,7 +190,8 @@ export type FromResultsViewMsg =
|
||||
| ViewLoadedMsg
|
||||
| ChangePage
|
||||
| OpenFileMsg
|
||||
| TelemetryMessage;
|
||||
| TelemetryMessage
|
||||
| UnhandledErrorMessage;
|
||||
|
||||
/**
|
||||
* Message from the results view to open a database source
|
||||
@@ -291,7 +293,8 @@ export type FromCompareViewMessage =
|
||||
| ChangeCompareMessage
|
||||
| ViewSourceFileMsg
|
||||
| OpenQueryMessage
|
||||
| TelemetryMessage;
|
||||
| TelemetryMessage
|
||||
| UnhandledErrorMessage;
|
||||
|
||||
/**
|
||||
* Message from the compare view to request opening a query.
|
||||
@@ -439,6 +442,11 @@ export interface TelemetryMessage {
|
||||
action: string;
|
||||
}
|
||||
|
||||
export interface UnhandledErrorMessage {
|
||||
t: "unhandledError";
|
||||
error: ErrorLike;
|
||||
}
|
||||
|
||||
export type ToVariantAnalysisMessage =
|
||||
| SetVariantAnalysisMessage
|
||||
| SetRepoResultsMessage
|
||||
@@ -453,4 +461,5 @@ export type FromVariantAnalysisMessage =
|
||||
| ExportResultsMessage
|
||||
| OpenLogsMessage
|
||||
| CancelVariantAnalysisMessage
|
||||
| TelemetryMessage;
|
||||
| TelemetryMessage
|
||||
| UnhandledErrorMessage;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
51
extensions/ql-vscode/src/view/common/errors.ts
Normal file
51
extensions/ql-vscode/src/view/common/errors.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { useEffect } from "react";
|
||||
import { getErrorMessage, getErrorStack } from "../../pure/helpers-pure";
|
||||
import { vscode } from "../vscode-api";
|
||||
|
||||
const unhandledErrorListener = (event: ErrorEvent) => {
|
||||
vscode.postMessage({
|
||||
t: "unhandledError",
|
||||
error: {
|
||||
message: getErrorMessage(event.error),
|
||||
stack: getErrorStack(event.error),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const unhandledRejectionListener = (event: PromiseRejectionEvent) => {
|
||||
vscode.postMessage({
|
||||
t: "unhandledError",
|
||||
error: {
|
||||
message: getErrorMessage(event.reason),
|
||||
stack: getErrorStack(event.reason),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* A react effect that handles adding listeners for unhandled errors / rejected promises.
|
||||
* When an error is detected a "unhandledError" message is posted to the view.
|
||||
*/
|
||||
export function useUnhandledErrorListener() {
|
||||
useEffect(() => {
|
||||
registerUnhandledErrorListener();
|
||||
return unregisterUnhandledErrorListener;
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove listeners for unhandled errors / rejected promises.
|
||||
*/
|
||||
export function unregisterUnhandledErrorListener() {
|
||||
window.removeEventListener("error", unhandledErrorListener);
|
||||
window.removeEventListener("unhandledrejection", unhandledRejectionListener);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { vscode } from "../vscode-api";
|
||||
import CompareTable from "./CompareTable";
|
||||
|
||||
import "../results/resultsView.css";
|
||||
import { useUnhandledErrorListener } from "../common/errors";
|
||||
|
||||
const emptyComparison: SetComparisonsMessage = {
|
||||
t: "setComparisons",
|
||||
@@ -23,6 +24,8 @@ const emptyComparison: SetComparisonsMessage = {
|
||||
};
|
||||
|
||||
export function Compare(_: Record<string, never>): JSX.Element {
|
||||
useUnhandledErrorListener();
|
||||
|
||||
const [comparison, setComparison] =
|
||||
useState<SetComparisonsMessage>(emptyComparison);
|
||||
|
||||
|
||||
@@ -14,6 +14,10 @@ import {
|
||||
NavigateMsg,
|
||||
ResultSet,
|
||||
} from "../../pure/interface-types";
|
||||
import {
|
||||
registerUnhandledErrorListener,
|
||||
unregisterUnhandledErrorListener,
|
||||
} from "../common/errors";
|
||||
import { EventHandlers as EventHandlerList } from "./event-handler-list";
|
||||
import { ResultTables } from "./result-tables";
|
||||
|
||||
@@ -307,12 +311,14 @@ export class ResultsApp extends React.Component<
|
||||
componentDidMount(): void {
|
||||
this.vscodeMessageHandler = this.vscodeMessageHandler.bind(this);
|
||||
window.addEventListener("message", this.vscodeMessageHandler);
|
||||
registerUnhandledErrorListener();
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
if (this.vscodeMessageHandler) {
|
||||
window.removeEventListener("message", this.vscodeMessageHandler);
|
||||
}
|
||||
unregisterUnhandledErrorListener();
|
||||
}
|
||||
|
||||
private vscodeMessageHandler(evt: MessageEvent) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import { ToVariantAnalysisMessage } from "../../pure/interface-types";
|
||||
import { vscode } from "../vscode-api";
|
||||
import { defaultFilterSortState } from "../../pure/variant-analysis-filter-sort";
|
||||
import { useTelemetryOnChange } from "../common/telemetry";
|
||||
import { useUnhandledErrorListener } from "../common/errors";
|
||||
|
||||
export type VariantAnalysisProps = {
|
||||
variantAnalysis?: VariantAnalysisDomainModel;
|
||||
@@ -50,6 +51,8 @@ export function VariantAnalysis({
|
||||
repoStates: initialRepoStates = [],
|
||||
repoResults: initialRepoResults = [],
|
||||
}: VariantAnalysisProps): JSX.Element {
|
||||
useUnhandledErrorListener();
|
||||
|
||||
const [variantAnalysis, setVariantAnalysis] = useState<
|
||||
VariantAnalysisDomainModel | undefined
|
||||
>(initialVariantAnalysis);
|
||||
|
||||
Reference in New Issue
Block a user