Skip to content

Commit 0da40a6

Browse files
committed
Use a CLI version changed listener for telemetry events
Set the CLI version in the telemetry listener whenever the version changes. A few things to note here: 1. In `CliServer::getVersion()`, avoid calling `supportsPerQueryEvalLog` directly. This avoids a recursive call to `CliServer::getVersion()`. Currently, it's always safe to do this, but I thought that it would be good to avoid recursion here in case we change things in the future. 2. Now, we are sending the CLI version with all telemetry events.
1 parent d6b3e51 commit 0da40a6

File tree

5 files changed

+115
-31
lines changed

5 files changed

+115
-31
lines changed

extensions/ql-vscode/src/cli.ts

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ export type OnLineCallback = (
155155
line: string,
156156
) => Promise<string | undefined> | string | undefined;
157157

158+
type VersionChangedListener = (newVersion: SemVer | undefined) => void;
159+
158160
/**
159161
* This class manages a cli server started by `codeql execute cli-server` to
160162
* run commands without the overhead of starting a new java
@@ -172,7 +174,9 @@ export class CodeQLCliServer implements Disposable {
172174
nullBuffer: Buffer;
173175

174176
/** Version of current cli, lazily computed by the `getVersion()` method */
175-
private _version: Promise<SemVer> | undefined;
177+
private _version: SemVer | undefined;
178+
179+
private _versionChangedListeners: VersionChangedListener[] = [];
176180

177181
/**
178182
* The languages supported by the current version of the CLI, computed by `getSupportedLanguages()`.
@@ -1373,15 +1377,36 @@ export class CodeQLCliServer implements Disposable {
13731377

13741378
public async getVersion() {
13751379
if (!this._version) {
1376-
this._version = this.refreshVersion();
1377-
// this._version is only undefined upon config change, so we reset CLI-based context key only when necessary.
1378-
await this.app.commands.execute(
1379-
"setContext",
1380-
"codeql.supportsEvalLog",
1381-
await this.cliConstraints.supportsPerQueryEvalLog(),
1382-
);
1380+
try {
1381+
const newVersion = await this.refreshVersion();
1382+
this._version = newVersion;
1383+
this._versionChangedListeners.forEach((listener) =>
1384+
listener(newVersion),
1385+
);
1386+
1387+
// this._version is only undefined upon config change, so we reset CLI-based context key only when necessary.
1388+
await this.app.commands.execute(
1389+
"setContext",
1390+
"codeql.supportsEvalLog",
1391+
newVersion.compare(
1392+
CliVersionConstraint.CLI_VERSION_WITH_PER_QUERY_EVAL_LOG,
1393+
) >= 0,
1394+
);
1395+
} catch (e) {
1396+
this._versionChangedListeners.forEach((listener) =>
1397+
listener(undefined),
1398+
);
1399+
throw e;
1400+
}
1401+
}
1402+
return this._version;
1403+
}
1404+
1405+
public addVersionChangedListener(listener: VersionChangedListener) {
1406+
if (this._version) {
1407+
listener(this._version);
13831408
}
1384-
return await this._version;
1409+
this._versionChangedListeners.push(listener);
13851410
}
13861411

13871412
private async refreshVersion() {

extensions/ql-vscode/src/common/vscode/commands.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export function registerCommandWithErrorHandling(
7575
return undefined;
7676
} finally {
7777
const executionTime = Date.now() - startTime;
78-
void telemetryListener?.sendCommandUsage(commandId, executionTime, error);
78+
telemetryListener?.sendCommandUsage(commandId, executionTime, error);
7979
}
8080
});
8181
}

extensions/ql-vscode/src/extension.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,9 @@ export async function activate(
395395
variantAnalysisViewSerializer.onExtensionLoaded(
396396
codeQlExtension.variantAnalysisManager,
397397
);
398-
telemetryListener.cli = codeQlExtension.cliServer;
398+
codeQlExtension.cliServer.addVersionChangedListener((ver) => {
399+
telemetryListener.cliVersion = ver;
400+
});
399401
}
400402

401403
return codeQlExtension;

extensions/ql-vscode/src/telemetry.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { extLogger } from "./common";
1919
import { UserCancellationException } from "./progress";
2020
import { showBinaryChoiceWithUrlDialog } from "./helpers";
2121
import { RedactableError } from "./pure/errors";
22-
import { CodeQLCliServer } from "./cli";
22+
import { SemVer } from "semver";
2323

2424
// Key is injected at build time through the APP_INSIGHTS_KEY environment variable.
2525
const key = "REPLACE-APP-INSIGHTS-KEY";
@@ -52,12 +52,14 @@ const baseDataPropertiesToRemove = [
5252
"common.vscodesessionid",
5353
];
5454

55+
const NOT_SET_CLI_VERSION = "not-set";
56+
5557
export class TelemetryListener extends ConfigListener {
5658
static relevantSettings = [ENABLE_TELEMETRY, CANARY_FEATURES];
5759

5860
private reporter?: TelemetryReporter;
5961

60-
private _cli?: CodeQLCliServer;
62+
private cliVersionStr = NOT_SET_CLI_VERSION;
6163

6264
constructor(
6365
private readonly id: string,
@@ -150,7 +152,7 @@ export class TelemetryListener extends ConfigListener {
150152
void this.reporter?.dispose();
151153
}
152154

153-
async sendCommandUsage(name: string, executionTime: number, error?: Error) {
155+
sendCommandUsage(name: string, executionTime: number, error?: Error) {
154156
if (!this.reporter) {
155157
return;
156158
}
@@ -160,18 +162,13 @@ export class TelemetryListener extends ConfigListener {
160162
? CommandCompletion.Cancelled
161163
: CommandCompletion.Failed;
162164

163-
const cliVersion = this._cli
164-
? (await this._cli.getVersion()).toString()
165-
: // telemetry events that are sent before the cli is initialized will not have a version number
166-
"not-set";
167-
168165
this.reporter.sendTelemetryEvent(
169166
"command-usage",
170167
{
171168
name,
172169
status,
173170
isCanary: isCanary().toString(),
174-
cliVersion,
171+
cliVersion: this.cliVersionStr,
175172
},
176173
{ executionTime },
177174
);
@@ -187,6 +184,7 @@ export class TelemetryListener extends ConfigListener {
187184
{
188185
name,
189186
isCanary: isCanary().toString(),
187+
cliVersion: this.cliVersionStr,
190188
},
191189
{},
192190
);
@@ -202,6 +200,7 @@ export class TelemetryListener extends ConfigListener {
202200

203201
const properties: { [key: string]: string } = {
204202
isCanary: isCanary().toString(),
203+
cliVersion: this.cliVersionStr,
205204
message: error.redactedMessage,
206205
...extraProperties,
207206
};
@@ -250,8 +249,8 @@ export class TelemetryListener extends ConfigListener {
250249
return this.reporter;
251250
}
252251

253-
set cli(cli: CodeQLCliServer) {
254-
this._cli = cli;
252+
set cliVersion(version: SemVer | undefined) {
253+
this.cliVersionStr = version ? version.toString() : NOT_SET_CLI_VERSION;
255254
}
256255

257256
private disposeReporter() {

extensions/ql-vscode/test/vscode-tests/no-workspace/telemetry.test.ts

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { ENABLE_TELEMETRY } from "../../../src/config";
1414
import { createMockExtensionContext } from "./index";
1515
import { vscodeGetConfigurationMock } from "../test-config";
1616
import { redactableError } from "../../../src/pure/errors";
17+
import { SemVer } from "semver";
1718

1819
// setting preferences can trigger lots of background activity
1920
// so need to bump up the timeout of this test.
@@ -185,7 +186,7 @@ describe("telemetry reporting", () => {
185186
it("should send an event", async () => {
186187
await telemetryListener.initialize();
187188

188-
await telemetryListener.sendCommandUsage("command-id", 1234, undefined);
189+
telemetryListener.sendCommandUsage("command-id", 1234, undefined);
189190

190191
expect(sendTelemetryEventSpy).toHaveBeenCalledWith(
191192
"command-usage",
@@ -204,7 +205,7 @@ describe("telemetry reporting", () => {
204205
it("should send a command usage event with an error", async () => {
205206
await telemetryListener.initialize();
206207

207-
await telemetryListener.sendCommandUsage(
208+
telemetryListener.sendCommandUsage(
208209
"command-id",
209210
1234,
210211
new UserCancellationException(),
@@ -226,11 +227,9 @@ describe("telemetry reporting", () => {
226227

227228
it("should send a command usage event with a cli version", async () => {
228229
await telemetryListener.initialize();
229-
telemetryListener.cli = {
230-
getVersion: () => Promise.resolve("1.2.3"),
231-
} as any;
230+
telemetryListener.cliVersion = new SemVer("1.2.3");
232231

233-
await telemetryListener.sendCommandUsage(
232+
telemetryListener.sendCommandUsage(
234233
"command-id",
235234
1234,
236235
new UserCancellationException(),
@@ -248,14 +247,35 @@ describe("telemetry reporting", () => {
248247
);
249248

250249
expect(sendTelemetryExceptionSpy).not.toBeCalled();
250+
251+
// Verify that if the cli version is not set, then the telemetry falls back to "not-set"
252+
sendTelemetryEventSpy.mockClear();
253+
telemetryListener.cliVersion = undefined;
254+
255+
telemetryListener.sendCommandUsage(
256+
"command-id",
257+
5678,
258+
new UserCancellationException(),
259+
);
260+
261+
expect(sendTelemetryEventSpy).toHaveBeenCalledWith(
262+
"command-usage",
263+
{
264+
name: "command-id",
265+
status: "Cancelled",
266+
isCanary,
267+
cliVersion: "not-set",
268+
},
269+
{ executionTime: 5678 },
270+
);
251271
});
252272

253273
it("should avoid sending an event when telemetry is disabled", async () => {
254274
await telemetryListener.initialize();
255275
await enableTelemetry("codeQL.telemetry", false);
256276

257-
await telemetryListener.sendCommandUsage("command-id", 1234, undefined);
258-
await telemetryListener.sendCommandUsage("command-id", 1234, new Error());
277+
telemetryListener.sendCommandUsage("command-id", 1234, undefined);
278+
telemetryListener.sendCommandUsage("command-id", 1234, new Error());
259279

260280
expect(sendTelemetryEventSpy).not.toBeCalled();
261281
expect(sendTelemetryExceptionSpy).not.toBeCalled();
@@ -266,7 +286,7 @@ describe("telemetry reporting", () => {
266286
await enableTelemetry("codeQL.telemetry", false);
267287
await enableTelemetry("codeQL.telemetry", true);
268288

269-
await telemetryListener.sendCommandUsage("command-id", 1234, undefined);
289+
telemetryListener.sendCommandUsage("command-id", 1234, undefined);
270290

271291
expect(sendTelemetryEventSpy).toHaveBeenCalledWith(
272292
"command-usage",
@@ -431,6 +451,24 @@ describe("telemetry reporting", () => {
431451
{
432452
name: "test",
433453
isCanary,
454+
cliVersion: "not-set",
455+
},
456+
{},
457+
);
458+
});
459+
460+
it("should send a ui-interaction telementry event with a cli version", async () => {
461+
await telemetryListener.initialize();
462+
463+
telemetryListener.cliVersion = new SemVer("1.2.3");
464+
telemetryListener.sendUIInteraction("test");
465+
466+
expect(sendTelemetryEventSpy).toHaveBeenCalledWith(
467+
"ui-interaction",
468+
{
469+
name: "test",
470+
isCanary,
471+
cliVersion: "1.2.3",
434472
},
435473
{},
436474
);
@@ -447,6 +485,25 @@ describe("telemetry reporting", () => {
447485
message: "test",
448486
isCanary,
449487
stack: expect.any(String),
488+
cliVersion: "not-set",
489+
},
490+
{},
491+
);
492+
});
493+
494+
it("should send an error telementry event with a cli version", async () => {
495+
await telemetryListener.initialize();
496+
telemetryListener.cliVersion = new SemVer("1.2.3");
497+
498+
telemetryListener.sendError(redactableError`test`);
499+
500+
expect(sendTelemetryEventSpy).toHaveBeenCalledWith(
501+
"error",
502+
{
503+
message: "test",
504+
isCanary,
505+
stack: expect.any(String),
506+
cliVersion: "1.2.3",
450507
},
451508
{},
452509
);
@@ -465,6 +522,7 @@ describe("telemetry reporting", () => {
465522
message:
466523
"test message with secret information: [REDACTED] and more [REDACTED] parts",
467524
isCanary,
525+
cliVersion: "not-set",
468526
stack: expect.any(String),
469527
},
470528
{},

0 commit comments

Comments
 (0)