Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions extensions/ql-vscode/src/common/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import { AppEventEmitter } from "./events";
import { NotificationLogger } from "./logging";
import { Memento } from "./memento";
import { AppCommandManager } from "./commands";
import { AppTelemetry } from "./telemetry";

export interface App {
createEventEmitter<T>(): AppEventEmitter<T>;
readonly mode: AppMode;
readonly logger: NotificationLogger;
readonly telemetry?: AppTelemetry;
readonly subscriptions: Disposable[];
readonly extensionPath: string;
readonly globalStoragePath: string;
Expand Down
27 changes: 27 additions & 0 deletions extensions/ql-vscode/src/common/logging/notifications.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { NotificationLogger } from "./notification-logger";
import { AppTelemetry } from "../telemetry";
import { RedactableError } from "../../pure/errors";

export interface ShowAndLogOptions {
/**
Expand Down Expand Up @@ -87,3 +89,28 @@ async function internalShowAndLog(
void logger.log(fullMessage || message);
await fn.bind(logger)(message);
}

interface ShowAndLogExceptionOptions extends ShowAndLogOptions {
/** Custom properties to include in the telemetry report. */
extraTelemetryProperties?: { [key: string]: string };
}

/**
* Show an error message, log it to the console, and emit redacted information as telemetry
*
* @param logger The logger that will receive the message.
* @param telemetry The telemetry instance to use for reporting.
* @param error The error to show. Only redacted information will be included in the telemetry.
* @param options See individual fields on `ShowAndLogExceptionOptions` type.
*
* @return A promise that resolves to the selected item or undefined when being dismissed.
*/
export async function showAndLogExceptionWithTelemetry(
logger: NotificationLogger,
telemetry: AppTelemetry | undefined,
error: RedactableError,
options: ShowAndLogExceptionOptions = {},
): Promise<void> {
telemetry?.sendError(error, options.extraTelemetryProperties);
return showAndLogErrorMessage(logger, error.fullMessage, options);
}
10 changes: 10 additions & 0 deletions extensions/ql-vscode/src/common/telemetry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { RedactableError } from "../pure/errors";

export interface AppTelemetry {
sendCommandUsage(name: string, executionTime: number, error?: Error): void;
sendUIInteraction(name: string): void;
sendError(
error: RedactableError,
extraProperties?: { [key: string]: string },
): void;
}
16 changes: 11 additions & 5 deletions extensions/ql-vscode/src/common/vscode/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
extLogger,
NotificationLogger,
showAndLogWarningMessage,
showAndLogExceptionWithTelemetry,
} from "../logging";
import {
asError,
Expand All @@ -12,18 +13,21 @@ import {
} from "../../pure/helpers-pure";
import { redactableError } from "../../pure/errors";
import { UserCancellationException } from "./progress";
import { telemetryListener } from "../../telemetry";
import { showAndLogExceptionWithTelemetry } from "./logging";
import { telemetryListener } from "./telemetry";
import { AppTelemetry } from "../telemetry";

/**
* Create a command manager for VSCode, wrapping registerCommandWithErrorHandling
* and vscode.executeCommand.
*/
export function createVSCodeCommandManager<
Commands extends Record<string, CommandFunction>,
>(logger?: NotificationLogger): CommandManager<Commands> {
>(
logger?: NotificationLogger,
telemetry?: AppTelemetry,
): CommandManager<Commands> {
return new CommandManager((commandId, task) => {
return registerCommandWithErrorHandling(commandId, task, logger);
return registerCommandWithErrorHandling(commandId, task, logger, telemetry);
}, wrapExecuteCommand);
}

Expand All @@ -34,11 +38,13 @@ export function createVSCodeCommandManager<
* @param task The task to run. It is passed directly to `commands.registerCommand`. Any
* arguments to the command handler are passed on to the task.
* @param logger The logger to use for error reporting.
* @param telemetry The telemetry listener to use for error reporting.
*/
export function registerCommandWithErrorHandling(
commandId: string,
task: (...args: any[]) => Promise<any>,
logger: NotificationLogger = extLogger,
telemetry: AppTelemetry | undefined = telemetryListener,
): Disposable {
return commands.registerCommand(commandId, async (...args: any[]) => {
const startTime = Date.now();
Expand All @@ -64,7 +70,7 @@ export function registerCommandWithErrorHandling(
const fullMessage = errorStack
? `${errorMessage.fullMessage}\n${errorStack}`
: errorMessage.fullMessage;
void showAndLogExceptionWithTelemetry(logger, errorMessage, {
void showAndLogExceptionWithTelemetry(logger, telemetry, errorMessage, {
fullMessage,
extraTelemetryProperties: {
command: commandId,
Expand Down
6 changes: 4 additions & 2 deletions extensions/ql-vscode/src/common/vscode/external-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import {
getErrorMessage,
getErrorStack,
} from "../../pure/helpers-pure";
import { extLogger } from "../logging";
import { showAndLogExceptionWithTelemetry } from "./logging";
import { extLogger, showAndLogExceptionWithTelemetry } from "../logging";
import { telemetryListener } from "./telemetry";

export async function tryOpenExternalFile(
commandManager: AppCommandManager,
Expand Down Expand Up @@ -36,6 +36,7 @@ the file in the file explorer and dragging it into the workspace.`,
} catch (e) {
void showAndLogExceptionWithTelemetry(
extLogger,
telemetryListener,
redactableError(
asError(e),
)`Failed to reveal file in OS: ${getErrorMessage(e)}`,
Expand All @@ -45,6 +46,7 @@ the file in the file explorer and dragging it into the workspace.`,
} else {
void showAndLogExceptionWithTelemetry(
extLogger,
telemetryListener,
redactableError(asError(e))`Could not open file ${fileLocation}`,
{
fullMessage: `${getErrorMessage(e)}\n${getErrorStack(e)}`,
Expand Down
30 changes: 0 additions & 30 deletions extensions/ql-vscode/src/common/vscode/logging.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ import {
LOG_TELEMETRY,
isIntegrationTestMode,
isCanary,
} from "./config";
} from "../../config";
import * as appInsights from "applicationinsights";
import { extLogger } from "./common";
import { UserCancellationException } from "./common/vscode/progress";
import { showBinaryChoiceWithUrlDialog } from "./common/vscode/dialog";
import { RedactableError } from "./pure/errors";
import { extLogger } from "../index";
import { UserCancellationException } from "./progress";
import { showBinaryChoiceWithUrlDialog } from "./dialog";
import { RedactableError } from "../../pure/errors";
import { SemVer } from "semver";
import { AppTelemetry } from "../telemetry";

// Key is injected at build time through the APP_INSIGHTS_KEY environment variable.
const key = "REPLACE-APP-INSIGHTS-KEY";
Expand Down Expand Up @@ -54,7 +55,10 @@ const baseDataPropertiesToRemove = [

const NOT_SET_CLI_VERSION = "not-set";

export class TelemetryListener extends ConfigListener {
export class ExtensionTelemetryListener
extends ConfigListener
implements AppTelemetry
{
static relevantSettings = [ENABLE_TELEMETRY, CANARY_FEATURES];

private reporter?: TelemetryReporter;
Expand Down Expand Up @@ -152,7 +156,7 @@ export class TelemetryListener extends ConfigListener {
void this.reporter?.dispose();
}

sendCommandUsage(name: string, executionTime: number, error?: Error) {
sendCommandUsage(name: string, executionTime: number, error?: Error): void {
if (!this.reporter) {
return;
}
Expand All @@ -174,7 +178,7 @@ export class TelemetryListener extends ConfigListener {
);
}

sendUIInteraction(name: string) {
sendUIInteraction(name: string): void {
if (!this.reporter) {
return;
}
Expand All @@ -193,7 +197,7 @@ export class TelemetryListener extends ConfigListener {
sendError(
error: RedactableError,
extraProperties?: { [key: string]: string },
) {
): void {
if (!this.reporter) {
return;
}
Expand Down Expand Up @@ -272,16 +276,16 @@ export class TelemetryListener extends ConfigListener {
/**
* The global Telemetry instance
*/
export let telemetryListener: TelemetryListener | undefined;
export let telemetryListener: ExtensionTelemetryListener | undefined;

export async function initializeTelemetry(
extension: Extension<any>,
ctx: ExtensionContext,
): Promise<TelemetryListener> {
): Promise<ExtensionTelemetryListener> {
if (telemetryListener !== undefined) {
throw new Error("Telemetry is already initialized");
}
telemetryListener = new TelemetryListener(
telemetryListener = new ExtensionTelemetryListener(
extension.id,
extension.packageJSON.version,
key,
Expand Down
6 changes: 6 additions & 0 deletions extensions/ql-vscode/src/common/vscode/vscode-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { VSCodeAppEventEmitter } from "./events";
import { AppCommandManager, QueryServerCommandManager } from "../commands";
import { createVSCodeCommandManager } from "./commands";
import { AppEnvironmentContext } from "./environment-context";
import { AppTelemetry } from "../telemetry";
import { telemetryListener } from "./telemetry";

export class ExtensionApp implements App {
public readonly credentials: VSCodeCredentials;
Expand Down Expand Up @@ -59,6 +61,10 @@ export class ExtensionApp implements App {
return extLogger;
}

public get telemetry(): AppTelemetry | undefined {
return telemetryListener;
}

public createEventEmitter<T>(): AppEventEmitter<T> {
return new VSCodeAppEventEmitter<T>();
}
Expand Down
5 changes: 3 additions & 2 deletions extensions/ql-vscode/src/compare/compare-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
QueryCompareResult,
} from "../pure/interface-types";
import { extLogger, Logger } from "../common";
import { showAndLogExceptionWithTelemetry } from "../common/vscode/logging";
import { showAndLogExceptionWithTelemetry } from "../common/logging";
import { CodeQLCliServer } from "../codeql-cli/cli";
import { DatabaseManager } from "../databases/local-databases";
import { jumpToLocation } from "../databases/local-databases/locations";
Expand All @@ -23,7 +23,7 @@ import {
AbstractWebview,
WebviewPanelConfig,
} from "../common/vscode/abstract-webview";
import { telemetryListener } from "../telemetry";
import { telemetryListener } from "../common/vscode/telemetry";
import { redactableError } from "../pure/errors";

interface ComparePair {
Expand Down Expand Up @@ -152,6 +152,7 @@ export class CompareView extends AbstractWebview<
case "unhandledError":
void showAndLogExceptionWithTelemetry(
extLogger,
telemetryListener,
redactableError(
msg.error,
)`Unhandled error in result comparison view: ${msg.error.message}`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ import {
} from "../pure/interface-types";
import { ProgressUpdate } from "../common/vscode/progress";
import { QueryRunner } from "../query-server";
import { showAndLogExceptionWithTelemetry } from "../common/vscode/logging";
import {
showAndLogExceptionWithTelemetry,
showAndLogErrorMessage,
} from "../common/logging";
import { outputFile, pathExists, readFile } from "fs-extra";
import { load as loadYaml } from "js-yaml";
import { DatabaseItem, DatabaseManager } from "../databases/local-databases";
Expand All @@ -42,7 +45,6 @@ import {
} from "./auto-model";
import { showLlmGeneration } from "../config";
import { getAutoModelUsages } from "./auto-model-usages-query";
import { showAndLogErrorMessage } from "../common/logging";

export class DataExtensionsEditorView extends AbstractWebview<
ToDataExtensionsEditorMessage,
Expand Down Expand Up @@ -276,6 +278,7 @@ export class DataExtensionsEditorView extends AbstractWebview<
} catch (err) {
void showAndLogExceptionWithTelemetry(
this.app.logger,
this.app.telemetry,
redactableError(
asError(err),
)`Failed to load external API usages: ${getErrorMessage(err)}`,
Expand Down Expand Up @@ -342,6 +345,7 @@ export class DataExtensionsEditorView extends AbstractWebview<
} catch (e: unknown) {
void showAndLogExceptionWithTelemetry(
this.app.logger,
this.app.telemetry,
redactableError(
asError(e),
)`Failed to generate flow model: ${getErrorMessage(e)}`,
Expand Down Expand Up @@ -476,6 +480,7 @@ export class DataExtensionsEditorView extends AbstractWebview<
if (e instanceof RequestError && e.status === 429) {
void showAndLogExceptionWithTelemetry(
this.app.logger,
this.app.telemetry,
redactableError(e)`Rate limit hit, please try again soon.`,
);
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { writeFile } from "fs-extra";
import { dump as dumpYaml } from "js-yaml";
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
import { extLogger, TeeLogger } from "../common";
import { showAndLogExceptionWithTelemetry } from "../common/vscode/logging";
import { showAndLogExceptionWithTelemetry } from "../common/logging";
import { isQueryLanguage } from "../common/query-language";
import { CancellationToken } from "vscode";
import { CodeQLCliServer } from "../codeql-cli/cli";
Expand All @@ -14,6 +14,7 @@ import { fetchExternalApiQueries } from "./queries";
import { QueryResultType } from "../pure/new-messages";
import { join } from "path";
import { redactableError } from "../pure/errors";
import { telemetryListener } from "../common/vscode/telemetry";

export type RunQueryOptions = {
cliServer: Pick<CodeQLCliServer, "resolveQlpacks">;
Expand Down Expand Up @@ -42,6 +43,7 @@ export async function runQuery({
if (!isQueryLanguage(databaseItem.language)) {
void showAndLogExceptionWithTelemetry(
extLogger,
telemetryListener,
redactableError`Unsupported database language ${databaseItem.language}`,
);
return;
Expand All @@ -51,6 +53,7 @@ export async function runQuery({
if (!query) {
void showAndLogExceptionWithTelemetry(
extLogger,
telemetryListener,
redactableError`No external API usage query found for language ${databaseItem.language}`,
);
return;
Expand Down Expand Up @@ -107,6 +110,7 @@ export async function runQuery({
if (completedQuery.resultType !== QueryResultType.SUCCESS) {
void showAndLogExceptionWithTelemetry(
extLogger,
telemetryListener,
redactableError`External API usage query failed: ${
completedQuery.message ?? "No message"
}`,
Expand All @@ -130,6 +134,7 @@ export async function readQueryResults({
if (bqrsInfo["result-sets"].length !== 1) {
void showAndLogExceptionWithTelemetry(
extLogger,
telemetryListener,
redactableError`Expected exactly one result set, got ${bqrsInfo["result-sets"].length}`,
);
return undefined;
Expand Down
Loading