diff --git a/extensions/ql-vscode/src/common/vscode/abstract-webview.ts b/extensions/ql-vscode/src/common/vscode/abstract-webview.ts index a1ac1c7ce..c38590e4f 100644 --- a/extensions/ql-vscode/src/common/vscode/abstract-webview.ts +++ b/extensions/ql-vscode/src/common/vscode/abstract-webview.ts @@ -13,6 +13,8 @@ import { tmpDir } from "../../tmp-dir"; import type { WebviewMessage, WebviewKind } from "./webview-html"; import { getHtmlForWebview } from "./webview-html"; import type { DeepReadonly } from "../readonly"; +import { runWithErrorHandling } from "./error-handling"; +import { telemetryListener } from "./telemetry"; export type WebviewPanelConfig = { viewId: string; @@ -117,7 +119,12 @@ export abstract class AbstractWebview< ); this.push( panel.webview.onDidReceiveMessage( - async (e) => this.onMessage(e), + async (e) => + runWithErrorHandling( + () => this.onMessage(e), + this.app.logger, + telemetryListener, + ), undefined, ), ); diff --git a/extensions/ql-vscode/src/common/vscode/commands.ts b/extensions/ql-vscode/src/common/vscode/commands.ts index 1c71aac5c..be6d57ca3 100644 --- a/extensions/ql-vscode/src/common/vscode/commands.ts +++ b/extensions/ql-vscode/src/common/vscode/commands.ts @@ -3,18 +3,10 @@ import { commands } from "vscode"; import type { CommandFunction } from "../../packages/commands"; import { CommandManager } from "../../packages/commands"; import type { NotificationLogger } from "../logging"; -import { - showAndLogWarningMessage, - showAndLogExceptionWithTelemetry, -} from "../logging"; import { extLogger } from "../logging/vscode"; -import { asError, getErrorMessage } from "../../common/helpers-pure"; -import { redactableError } from "../../common/errors"; -import { UserCancellationException } from "./progress"; import { telemetryListener } from "./telemetry"; import type { AppTelemetry } from "../telemetry"; -import { CliError } from "../../codeql-cli/cli-errors"; -import { EOL } from "os"; +import { runWithErrorHandling } from "./error-handling"; /** * Create a command manager for VSCode, wrapping registerCommandWithErrorHandling @@ -48,50 +40,9 @@ export function registerCommandWithErrorHandling< logger: NotificationLogger = extLogger, telemetry: AppTelemetry | undefined = telemetryListener, ): Disposable { - return commands.registerCommand(commandId, async (...args: Parameters) => { - const startTime = Date.now(); - let error: Error | undefined; - - try { - return await task(...args); - } catch (e) { - error = asError(e); - const errorMessage = redactableError(error)`${ - getErrorMessage(e) || e - } (${commandId})`; - if (e instanceof UserCancellationException) { - // User has cancelled this action manually - if (e.silent) { - void logger.log(errorMessage.fullMessage); - } else { - void showAndLogWarningMessage(logger, errorMessage.fullMessage); - } - } else if (e instanceof CliError) { - const fullMessage = `${e.commandDescription} failed with args:${EOL} ${e.commandArgs.join(" ")}${EOL}${ - e.stderr ?? e.cause - }`; - void showAndLogExceptionWithTelemetry(logger, telemetry, errorMessage, { - fullMessage, - extraTelemetryProperties: { - command: commandId, - }, - }); - } else { - // Include the full stack in the error log only. - const fullMessage = errorMessage.fullMessageWithStack; - void showAndLogExceptionWithTelemetry(logger, telemetry, errorMessage, { - fullMessage, - extraTelemetryProperties: { - command: commandId, - }, - }); - } - return undefined; - } finally { - const executionTime = Date.now() - startTime; - telemetryListener?.sendCommandUsage(commandId, executionTime, error); - } - }); + return commands.registerCommand(commandId, async (...args: Parameters) => + runWithErrorHandling(task, logger, telemetry, commandId, ...args), + ); } /** diff --git a/extensions/ql-vscode/src/common/vscode/error-handling.ts b/extensions/ql-vscode/src/common/vscode/error-handling.ts new file mode 100644 index 000000000..907424474 --- /dev/null +++ b/extensions/ql-vscode/src/common/vscode/error-handling.ts @@ -0,0 +1,81 @@ +import { + showAndLogWarningMessage, + showAndLogExceptionWithTelemetry, +} from "../logging"; +import type { NotificationLogger } from "../logging"; +import { extLogger } from "../logging/vscode"; +import type { AppTelemetry } from "../telemetry"; +import { telemetryListener } from "./telemetry"; +import { asError, getErrorMessage } from "../helpers-pure"; +import { redactableError } from "../errors"; +import { UserCancellationException } from "./progress"; +import { CliError } from "../../codeql-cli/cli-errors"; +import { EOL } from "os"; + +/** + * Executes a task with error handling. It provides a uniform way to handle errors. + * + * @template T - A function type that takes an unknown number of arguments and returns a Promise. + * @param {T} task - The task to be executed. + * @param {NotificationLogger} [logger=extLogger] - The logger to use for error reporting. + * @param {AppTelemetry | undefined} [telemetry=telemetryListener] - The telemetry listener to use for error reporting. + * @param {string} [commandId] - The optional command id associated with the task. + * @param {...unknown} args - The arguments to be passed to the task. + * @returns {Promise} The result of the task, or undefined if an error occurred. + * @throws {Error} If an error occurs during the execution of the task. + */ +export async function runWithErrorHandling< + T extends (...args: unknown[]) => Promise, +>( + task: T, + logger: NotificationLogger = extLogger, + telemetry: AppTelemetry | undefined = telemetryListener, + commandId?: string, + ...args: unknown[] +): Promise { + const startTime = Date.now(); + let error: Error | undefined; + + try { + return await task(...args); + } catch (e) { + error = asError(e); + const errorMessage = redactableError(error)`${ + getErrorMessage(e) || e + }${commandId ? ` (${commandId})` : ""}`; + + const extraTelemetryProperties = commandId + ? { command: commandId } + : undefined; + + if (e instanceof UserCancellationException) { + // User has cancelled this action manually + if (e.silent) { + void logger.log(errorMessage.fullMessage); + } else { + void showAndLogWarningMessage(logger, errorMessage.fullMessage); + } + } else if (e instanceof CliError) { + const fullMessage = `${e.commandDescription} failed with args:${EOL} ${e.commandArgs.join(" ")}${EOL}${ + e.stderr ?? e.cause + }`; + void showAndLogExceptionWithTelemetry(logger, telemetry, errorMessage, { + fullMessage, + extraTelemetryProperties, + }); + } else { + // Include the full stack in the error log only. + const fullMessage = errorMessage.fullMessageWithStack; + void showAndLogExceptionWithTelemetry(logger, telemetry, errorMessage, { + fullMessage, + extraTelemetryProperties, + }); + } + return undefined; + } finally { + if (commandId) { + const executionTime = Date.now() - startTime; + telemetryListener?.sendCommandUsage(commandId, executionTime, error); + } + } +}