Skip to content

Add the CLI version to telemetry events #2307

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 18, 2023
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
43 changes: 34 additions & 9 deletions extensions/ql-vscode/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ export type OnLineCallback = (
line: string,
) => Promise<string | undefined> | string | undefined;

type VersionChangedListener = (newVersion: SemVer | undefined) => void;

/**
* This class manages a cli server started by `codeql execute cli-server` to
* run commands without the overhead of starting a new java
Expand All @@ -187,7 +189,9 @@ export class CodeQLCliServer implements Disposable {
nullBuffer: Buffer;

/** Version of current cli, lazily computed by the `getVersion()` method */
private _version: Promise<SemVer> | undefined;
private _version: SemVer | undefined;

private _versionChangedListeners: VersionChangedListener[] = [];

/**
* The languages supported by the current version of the CLI, computed by `getSupportedLanguages()`.
Expand Down Expand Up @@ -1411,15 +1415,36 @@ export class CodeQLCliServer implements Disposable {

public async getVersion() {
if (!this._version) {
this._version = this.refreshVersion();
// this._version is only undefined upon config change, so we reset CLI-based context key only when necessary.
await this.app.commands.execute(
"setContext",
"codeql.supportsEvalLog",
await this.cliConstraints.supportsPerQueryEvalLog(),
);
try {
const newVersion = await this.refreshVersion();
this._version = newVersion;
this._versionChangedListeners.forEach((listener) =>
listener(newVersion),
);

// this._version is only undefined upon config change, so we reset CLI-based context key only when necessary.
await this.app.commands.execute(
"setContext",
"codeql.supportsEvalLog",
newVersion.compare(
CliVersionConstraint.CLI_VERSION_WITH_PER_QUERY_EVAL_LOG,
) >= 0,
);
} catch (e) {
this._versionChangedListeners.forEach((listener) =>
listener(undefined),
);
throw e;
}
}
return this._version;
}

public addVersionChangedListener(listener: VersionChangedListener) {
if (this._version) {
listener(this._version);
}
return await this._version;
this._versionChangedListeners.push(listener);
}

private async refreshVersion() {
Expand Down
5 changes: 4 additions & 1 deletion extensions/ql-vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ export async function activate(

const distributionConfigListener = new DistributionConfigListener();
await initializeLogging(ctx);
await initializeTelemetry(extension, ctx);
const telemetryListener = await initializeTelemetry(extension, ctx);
addUnhandledRejectionListener();
install();

Expand Down Expand Up @@ -401,6 +401,9 @@ export async function activate(
variantAnalysisViewSerializer.onExtensionLoaded(
codeQlExtension.variantAnalysisManager,
);
codeQlExtension.cliServer.addVersionChangedListener((ver) => {
telemetryListener.cliVersion = ver;
});
}

return codeQlExtension;
Expand Down
15 changes: 14 additions & 1 deletion extensions/ql-vscode/src/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { extLogger } from "./common";
import { UserCancellationException } from "./progress";
import { showBinaryChoiceWithUrlDialog } from "./helpers";
import { RedactableError } from "./pure/errors";
import { SemVer } from "semver";

// Key is injected at build time through the APP_INSIGHTS_KEY environment variable.
const key = "REPLACE-APP-INSIGHTS-KEY";
Expand Down Expand Up @@ -51,11 +52,15 @@ const baseDataPropertiesToRemove = [
"common.vscodesessionid",
];

const NOT_SET_CLI_VERSION = "not-set";

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

private reporter?: TelemetryReporter;

private cliVersionStr = NOT_SET_CLI_VERSION;

constructor(
private readonly id: string,
private readonly version: string,
Expand Down Expand Up @@ -163,6 +168,7 @@ export class TelemetryListener extends ConfigListener {
name,
status,
isCanary: isCanary().toString(),
cliVersion: this.cliVersionStr,
},
{ executionTime },
);
Expand All @@ -178,6 +184,7 @@ export class TelemetryListener extends ConfigListener {
{
name,
isCanary: isCanary().toString(),
cliVersion: this.cliVersionStr,
},
{},
);
Expand All @@ -193,6 +200,7 @@ export class TelemetryListener extends ConfigListener {

const properties: { [key: string]: string } = {
isCanary: isCanary().toString(),
cliVersion: this.cliVersionStr,
message: error.redactedMessage,
...extraProperties,
};
Expand Down Expand Up @@ -241,6 +249,10 @@ export class TelemetryListener extends ConfigListener {
return this.reporter;
}

set cliVersion(version: SemVer | undefined) {
this.cliVersionStr = version ? version.toString() : NOT_SET_CLI_VERSION;
}

private disposeReporter() {
if (this.reporter) {
void this.reporter.dispose();
Expand All @@ -265,7 +277,7 @@ export let telemetryListener: TelemetryListener | undefined;
export async function initializeTelemetry(
extension: Extension<any>,
ctx: ExtensionContext,
): Promise<void> {
): Promise<TelemetryListener> {
if (telemetryListener !== undefined) {
throw new Error("Telemetry is already initialized");
}
Expand All @@ -279,4 +291,5 @@ export async function initializeTelemetry(
// this is a particular problem during integration tests, which will hang if a modal popup is displayed.
void telemetryListener.initialize();
ctx.subscriptions.push(telemetryListener);
return telemetryListener;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ENABLE_TELEMETRY } from "../../../src/config";
import { createMockExtensionContext } from "./index";
import { vscodeGetConfigurationMock } from "../test-config";
import { redactableError } from "../../../src/pure/errors";
import { SemVer } from "semver";

// setting preferences can trigger lots of background activity
// so need to bump up the timeout of this test.
Expand Down Expand Up @@ -193,6 +194,7 @@ describe("telemetry reporting", () => {
name: "command-id",
status: "Success",
isCanary,
cliVersion: "not-set",
},
{ executionTime: 1234 },
);
Expand All @@ -215,13 +217,59 @@ describe("telemetry reporting", () => {
name: "command-id",
status: "Cancelled",
isCanary,
cliVersion: "not-set",
},
{ executionTime: 1234 },
);

expect(sendTelemetryExceptionSpy).not.toBeCalled();
});

it("should send a command usage event with a cli version", async () => {
await telemetryListener.initialize();
telemetryListener.cliVersion = new SemVer("1.2.3");

telemetryListener.sendCommandUsage(
"command-id",
1234,
new UserCancellationException(),
);

expect(sendTelemetryEventSpy).toHaveBeenCalledWith(
"command-usage",
{
name: "command-id",
status: "Cancelled",
isCanary,
cliVersion: "1.2.3",
},
{ executionTime: 1234 },
);

expect(sendTelemetryExceptionSpy).not.toBeCalled();

// Verify that if the cli version is not set, then the telemetry falls back to "not-set"
sendTelemetryEventSpy.mockClear();
telemetryListener.cliVersion = undefined;

telemetryListener.sendCommandUsage(
"command-id",
5678,
new UserCancellationException(),
);

expect(sendTelemetryEventSpy).toHaveBeenCalledWith(
"command-usage",
{
name: "command-id",
status: "Cancelled",
isCanary,
cliVersion: "not-set",
},
{ executionTime: 5678 },
);
});

it("should avoid sending an event when telemetry is disabled", async () => {
await telemetryListener.initialize();
await enableTelemetry("codeQL.telemetry", false);
Expand All @@ -246,6 +294,7 @@ describe("telemetry reporting", () => {
name: "command-id",
status: "Success",
isCanary,
cliVersion: "not-set",
},
{ executionTime: 1234 },
);
Expand Down Expand Up @@ -402,6 +451,24 @@ describe("telemetry reporting", () => {
{
name: "test",
isCanary,
cliVersion: "not-set",
},
{},
);
});

it("should send a ui-interaction telementry event with a cli version", async () => {
await telemetryListener.initialize();

telemetryListener.cliVersion = new SemVer("1.2.3");
telemetryListener.sendUIInteraction("test");

expect(sendTelemetryEventSpy).toHaveBeenCalledWith(
"ui-interaction",
{
name: "test",
isCanary,
cliVersion: "1.2.3",
},
{},
);
Expand All @@ -418,6 +485,25 @@ describe("telemetry reporting", () => {
message: "test",
isCanary,
stack: expect.any(String),
cliVersion: "not-set",
},
{},
);
});

it("should send an error telementry event with a cli version", async () => {
await telemetryListener.initialize();
telemetryListener.cliVersion = new SemVer("1.2.3");

telemetryListener.sendError(redactableError`test`);

expect(sendTelemetryEventSpy).toHaveBeenCalledWith(
"error",
{
message: "test",
isCanary,
stack: expect.any(String),
cliVersion: "1.2.3",
},
{},
);
Expand All @@ -436,6 +522,7 @@ describe("telemetry reporting", () => {
message:
"test message with secret information: [REDACTED] and more [REDACTED] parts",
isCanary,
cliVersion: "not-set",
stack: expect.any(String),
},
{},
Expand Down