Skip to content

Commit 61cc919

Browse files
Merge pull request #2139 from github/robertbrignull/webview_error_telemetry
Add listeners for unhandled errors to web views
2 parents 9045253 + f2f1b1d commit 61cc919

7 files changed

Lines changed: 127 additions & 19 deletions

File tree

extensions/ql-vscode/src/compare/compare-view.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import { assertNever, getErrorMessage } from "../pure/helpers-pure";
2020
import { HistoryItemLabelProvider } from "../query-history/history-item-label-provider";
2121
import { AbstractWebview, WebviewPanelConfig } from "../abstract-webview";
2222
import { telemetryListener } from "../telemetry";
23+
import { redactableError } from "../pure/errors";
24+
import { showAndLogExceptionWithTelemetry } from "../helpers";
2325

2426
interface ComparePair {
2527
from: CompletedLocalQueryInfo;
@@ -139,6 +141,14 @@ export class CompareView extends AbstractWebview<
139141
telemetryListener?.sendUIInteraction(msg.action);
140142
break;
141143

144+
case "unhandledError":
145+
void showAndLogExceptionWithTelemetry(
146+
redactableError(
147+
msg.error,
148+
)`Unhandled error in result comparison view: ${msg.error.message}`,
149+
);
150+
break;
151+
142152
default:
143153
assertNever(msg);
144154
}

extensions/ql-vscode/src/interface.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,13 @@ export class ResultsView extends AbstractWebview<
295295
case "telemetry":
296296
telemetryListener?.sendUIInteraction(msg.action);
297297
break;
298+
case "unhandledError":
299+
void showAndLogExceptionWithTelemetry(
300+
redactableError(
301+
msg.error,
302+
)`Unhandled error in results view: ${msg.error.message}`,
303+
);
304+
break;
298305
default:
299306
assertNever(msg);
300307
}

extensions/ql-vscode/src/pure/errors.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export class RedactableError extends Error {
22
constructor(
3-
cause: Error | undefined,
3+
cause: ErrorLike | undefined,
44
private readonly strings: TemplateStringsArray,
55
private readonly values: unknown[],
66
) {
@@ -54,19 +54,34 @@ export function redactableError(
5454
...values: unknown[]
5555
): RedactableError;
5656
export function redactableError(
57-
error: Error,
57+
error: ErrorLike,
5858
): (strings: TemplateStringsArray, ...values: unknown[]) => RedactableError;
5959

6060
export function redactableError(
61-
errorOrStrings: Error | TemplateStringsArray,
61+
errorOrStrings: ErrorLike | TemplateStringsArray,
6262
...values: unknown[]
6363
):
6464
| ((strings: TemplateStringsArray, ...values: unknown[]) => RedactableError)
6565
| RedactableError {
66-
if (errorOrStrings instanceof Error) {
66+
if (isErrorLike(errorOrStrings)) {
6767
return (strings: TemplateStringsArray, ...values: unknown[]) =>
6868
new RedactableError(errorOrStrings, strings, values);
6969
} else {
7070
return new RedactableError(undefined, errorOrStrings, values);
7171
}
7272
}
73+
74+
export interface ErrorLike {
75+
message: string;
76+
stack?: string;
77+
}
78+
79+
function isErrorLike(error: any): error is ErrorLike {
80+
if (
81+
typeof error.message === "string" &&
82+
(error.stack === undefined || typeof error.stack === "string")
83+
) {
84+
return true;
85+
}
86+
return false;
87+
}

extensions/ql-vscode/src/pure/interface-types.ts

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
VariantAnalysisScannedRepositoryState,
1313
} from "../variant-analysis/shared/variant-analysis";
1414
import { RepositoriesFilterSortStateWithIds } from "./variant-analysis-filter-sort";
15+
import { ErrorLike } from "./errors";
1516

1617
/**
1718
* This module contains types and code that are shared between
@@ -182,14 +183,13 @@ export type IntoResultsViewMsg =
182183
* A message sent from the results view.
183184
*/
184185
export type FromResultsViewMsg =
186+
| CommonFromViewMessages
185187
| ViewSourceFileMsg
186188
| ToggleDiagnostics
187189
| ChangeRawResultsSortMsg
188190
| ChangeInterpretedResultsSortMsg
189-
| ViewLoadedMsg
190191
| ChangePage
191-
| OpenFileMsg
192-
| TelemetryMessage;
192+
| OpenFileMsg;
193193

194194
/**
195195
* Message from the results view to open a database source
@@ -231,6 +231,21 @@ interface ViewLoadedMsg {
231231
viewName: string;
232232
}
233233

234+
interface TelemetryMessage {
235+
t: "telemetry";
236+
action: string;
237+
}
238+
239+
interface UnhandledErrorMessage {
240+
t: "unhandledError";
241+
error: ErrorLike;
242+
}
243+
244+
type CommonFromViewMessages =
245+
| ViewLoadedMsg
246+
| TelemetryMessage
247+
| UnhandledErrorMessage;
248+
234249
/**
235250
* Message from the results view to signal a request to change the
236251
* page.
@@ -287,11 +302,10 @@ interface ChangeInterpretedResultsSortMsg {
287302
* Message from the compare view to the extension.
288303
*/
289304
export type FromCompareViewMessage =
290-
| ViewLoadedMsg
305+
| CommonFromViewMessages
291306
| ChangeCompareMessage
292307
| ViewSourceFileMsg
293-
| OpenQueryMessage
294-
| TelemetryMessage;
308+
| OpenQueryMessage;
295309

296310
/**
297311
* Message from the compare view to request opening a query.
@@ -434,23 +448,17 @@ export interface CancelVariantAnalysisMessage {
434448
t: "cancelVariantAnalysis";
435449
}
436450

437-
export interface TelemetryMessage {
438-
t: "telemetry";
439-
action: string;
440-
}
441-
442451
export type ToVariantAnalysisMessage =
443452
| SetVariantAnalysisMessage
444453
| SetRepoResultsMessage
445454
| SetRepoStatesMessage;
446455

447456
export type FromVariantAnalysisMessage =
448-
| ViewLoadedMsg
457+
| CommonFromViewMessages
449458
| RequestRepositoryResultsMessage
450459
| OpenQueryFileMessage
451460
| OpenQueryTextMessage
452461
| CopyRepositoryListMessage
453462
| ExportResultsMessage
454463
| OpenLogsMessage
455-
| CancelVariantAnalysisMessage
456-
| TelemetryMessage;
464+
| CancelVariantAnalysisMessage;

extensions/ql-vscode/src/variant-analysis/variant-analysis-view.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,12 @@ import {
1515
VariantAnalysisViewInterface,
1616
VariantAnalysisViewManager,
1717
} from "./variant-analysis-view-manager";
18-
import { showAndLogWarningMessage } from "../helpers";
18+
import {
19+
showAndLogExceptionWithTelemetry,
20+
showAndLogWarningMessage,
21+
} from "../helpers";
1922
import { telemetryListener } from "../telemetry";
23+
import { redactableError } from "../pure/errors";
2024

2125
export class VariantAnalysisView
2226
extends AbstractWebview<ToVariantAnalysisMessage, FromVariantAnalysisMessage>
@@ -153,6 +157,13 @@ export class VariantAnalysisView
153157
case "telemetry":
154158
telemetryListener?.sendUIInteraction(msg.action);
155159
break;
160+
case "unhandledError":
161+
void showAndLogExceptionWithTelemetry(
162+
redactableError(
163+
msg.error,
164+
)`Unhandled error in variant analysis results view: ${msg.error.message}`,
165+
);
166+
break;
156167
default:
157168
assertNever(msg);
158169
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { getErrorMessage, getErrorStack } from "../../pure/helpers-pure";
2+
import { vscode } from "../vscode-api";
3+
4+
// Keep track of previous errors that have happened.
5+
// The listeners for uncaught errors and rejections can get triggered
6+
// twice for each error. This is believed to be an effect caused
7+
// by React's error boundaries. Adding an error boundary stops
8+
// this duplicate reporting for errors that happen during component
9+
// rendering, but unfortunately errors from event handlers and
10+
// timeouts are still duplicated and there does not appear to be
11+
// a way around this.
12+
const previousErrors: Set<Error> = new Set();
13+
14+
function shouldReportError(error: Error): boolean {
15+
const seenBefore = previousErrors.has(error);
16+
previousErrors.add(error);
17+
setTimeout(() => {
18+
previousErrors.delete(error);
19+
}, 1000);
20+
return !seenBefore;
21+
}
22+
23+
const unhandledErrorListener = (event: ErrorEvent) => {
24+
if (shouldReportError(event.error)) {
25+
vscode.postMessage({
26+
t: "unhandledError",
27+
error: {
28+
message: getErrorMessage(event.error),
29+
stack: getErrorStack(event.error),
30+
},
31+
});
32+
}
33+
};
34+
35+
const unhandledRejectionListener = (event: PromiseRejectionEvent) => {
36+
if (shouldReportError(event.reason)) {
37+
vscode.postMessage({
38+
t: "unhandledError",
39+
error: {
40+
message: getErrorMessage(event.reason),
41+
stack: getErrorStack(event.reason),
42+
},
43+
});
44+
}
45+
};
46+
47+
/**
48+
* Adds listeners for unhandled errors / rejected promises.
49+
* When an error is detected a "unhandledError" message is posted to the view.
50+
*/
51+
export function registerUnhandledErrorListener() {
52+
window.addEventListener("error", unhandledErrorListener);
53+
window.addEventListener("unhandledrejection", unhandledRejectionListener);
54+
}

extensions/ql-vscode/src/view/webview.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@ import { WebviewDefinition } from "./webview-definition";
55

66
// Allow all views to use Codicons
77
import "@vscode/codicons/dist/codicon.css";
8+
import { registerUnhandledErrorListener } from "./common/errors";
89

910
const render = () => {
11+
registerUnhandledErrorListener();
12+
1013
const element = document.getElementById("root");
1114

1215
if (!element) {

0 commit comments

Comments
 (0)