diff --git a/extensions/ql-vscode/CHANGELOG.md b/extensions/ql-vscode/CHANGELOG.md index 74f53c12968..cf3ee87e104 100644 --- a/extensions/ql-vscode/CHANGELOG.md +++ b/extensions/ql-vscode/CHANGELOG.md @@ -2,6 +2,8 @@ ## [UNRELEASED] +- Send telemetry about unhandled errors happening within the extension. [#2125](https://github.com/github/vscode-codeql/pull/2125) + ## 1.7.11 - 1 March 2023 - Enable collection of telemetry concerning interactions with UI elements, including buttons, links, and other inputs. [#2114](https://github.com/github/vscode-codeql/pull/2114) diff --git a/extensions/ql-vscode/src/extension.ts b/extensions/ql-vscode/src/extension.ts index e9143b129fc..18d77817ed3 100644 --- a/extensions/ql-vscode/src/extension.ts +++ b/extensions/ql-vscode/src/extension.ts @@ -72,7 +72,12 @@ import { tmpDir, tmpDirDisposal, } from "./helpers"; -import { asError, assertNever, getErrorMessage } from "./pure/helpers-pure"; +import { + asError, + assertNever, + getErrorMessage, + getErrorStack, +} from "./pure/helpers-pure"; import { spawnIdeServer } from "./ide-server"; import { ResultsView } from "./interface"; import { WebviewReveal } from "./interface-utils"; @@ -235,6 +240,7 @@ export async function activate( const distributionConfigListener = new DistributionConfigListener(); await initializeLogging(ctx); await initializeTelemetry(extension, ctx); + addUnhandledRejectionListener(); install(); const codelensProvider = new QuickEvalCodeLensProvider(); @@ -1569,6 +1575,49 @@ async function activateWithInstalledDistribution( }; } +function addUnhandledRejectionListener() { + const handler = (error: unknown) => { + // This listener will be triggered for errors from other extensions as + // well as errors from this extension. We don't want to flood the user + // with popups about errors from other extensions, and we don't want to + // report them in our telemetry. + // + // The stack trace gets redacted before being sent as telemetry, but at + // this point in the code we have the full unredacted information. + const isFromThisExtension = + extension && getErrorStack(error).includes(extension.extensionPath); + + if (isFromThisExtension) { + const message = redactableError( + asError(error), + )`Unhandled error: ${getErrorMessage(error)}`; + // Add a catch so that showAndLogExceptionWithTelemetry fails, we avoid + // triggering "unhandledRejection" and avoid an infinite loop + showAndLogExceptionWithTelemetry(message).catch( + (telemetryError: unknown) => { + void extLogger.log( + `Failed to send error telemetry: ${getErrorMessage( + telemetryError, + )}`, + ); + void extLogger.log(message.fullMessage); + }, + ); + } + }; + + // "uncaughtException" will trigger whenever an exception reaches the top level. + // This covers extension initialization and any code within a `setTimeout`. + // Notably this does not include exceptions thrown when executing commands, + // because `commandRunner` wraps the command body and handles errors. + process.addListener("uncaughtException", handler); + + // "unhandledRejection" will trigger whenever any promise is rejected and it is + // not handled by a "catch" somewhere in the promise chain. This includes when + // a promise is used with the "void" operator. + process.addListener("unhandledRejection", handler); +} + async function createQueryServer( qlConfigurationListener: QueryServerConfigListener, cliServer: CodeQLCliServer,