Merge pull request #2307 from github/aeisenberg/cli-version-telemetry

Add the CLI version to telemetry events
This commit is contained in:
Andrew Eisenberg
2023-04-18 08:27:46 -07:00
committed by GitHub
4 changed files with 139 additions and 11 deletions

View File

@@ -171,6 +171,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
@@ -188,7 +190,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()`.
@@ -1417,15 +1421,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 await this._version;
return this._version;
}
public addVersionChangedListener(listener: VersionChangedListener) {
if (this._version) {
listener(this._version);
}
this._versionChangedListeners.push(listener);
}
private async refreshVersion() {

View File

@@ -317,7 +317,7 @@ export async function activate(
const distributionConfigListener = new DistributionConfigListener();
await initializeLogging(ctx);
await initializeTelemetry(extension, ctx);
const telemetryListener = await initializeTelemetry(extension, ctx);
addUnhandledRejectionListener();
install();
@@ -406,6 +406,9 @@ export async function activate(
variantAnalysisViewSerializer.onExtensionLoaded(
codeQlExtension.variantAnalysisManager,
);
codeQlExtension.cliServer.addVersionChangedListener((ver) => {
telemetryListener.cliVersion = ver;
});
}
return codeQlExtension;

View File

@@ -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";
@@ -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,
@@ -163,6 +168,7 @@ export class TelemetryListener extends ConfigListener {
name,
status,
isCanary: isCanary().toString(),
cliVersion: this.cliVersionStr,
},
{ executionTime },
);
@@ -178,6 +184,7 @@ export class TelemetryListener extends ConfigListener {
{
name,
isCanary: isCanary().toString(),
cliVersion: this.cliVersionStr,
},
{},
);
@@ -193,6 +200,7 @@ export class TelemetryListener extends ConfigListener {
const properties: { [key: string]: string } = {
isCanary: isCanary().toString(),
cliVersion: this.cliVersionStr,
message: error.redactedMessage,
...extraProperties,
};
@@ -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();
@@ -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");
}
@@ -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;
}

View File

@@ -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.
@@ -193,6 +194,7 @@ describe("telemetry reporting", () => {
name: "command-id",
status: "Success",
isCanary,
cliVersion: "not-set",
},
{ executionTime: 1234 },
);
@@ -215,6 +217,7 @@ describe("telemetry reporting", () => {
name: "command-id",
status: "Cancelled",
isCanary,
cliVersion: "not-set",
},
{ executionTime: 1234 },
);
@@ -222,6 +225,51 @@ describe("telemetry reporting", () => {
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);
@@ -246,6 +294,7 @@ describe("telemetry reporting", () => {
name: "command-id",
status: "Success",
isCanary,
cliVersion: "not-set",
},
{ executionTime: 1234 },
);
@@ -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",
},
{},
);
@@ -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",
},
{},
);
@@ -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),
},
{},