Skip to content

Commit e97ef3a

Browse files
committed
Add telemetry for commands
This commit adds telemetry capturing for command execution. The data captured is only the command id. We also capture errors thrown by any command execution. There is a new config setting added that controls whether or not telemetry should be sent. This setting AND the global setting must be enabled in order for telemetry to be sent. Note that the global setting is handled inside the extension by default.
1 parent b5e7087 commit e97ef3a

7 files changed

Lines changed: 464 additions & 3 deletions

File tree

extensions/ql-vscode/package-lock.json

Lines changed: 107 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

extensions/ql-vscode/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,11 @@
174174
"minimum": 0,
175175
"maximum": 1024,
176176
"description": "Number of threads for running CodeQL tests."
177+
},
178+
"codeQL.telemetry.enableTelemetry": {
179+
"type": "boolean",
180+
"default": true,
181+
"markdownDescription": "Specifies whether to enable Code QL telemetry. This setting AND the global `#telemetry.enableTelemetry#` setting must be checked for telemetry to be sent."
177182
}
178183
}
179184
},
@@ -730,6 +735,7 @@
730735
"tmp-promise": "~3.0.2",
731736
"tree-kill": "~1.2.2",
732737
"unzipper": "~0.10.5",
738+
"vscode-extension-telemetry": "^0.1.6",
733739
"vscode-jsonrpc": "^5.0.1",
734740
"vscode-languageclient": "^6.1.3",
735741
"vscode-test-adapter-api": "~1.7.0",

extensions/ql-vscode/src/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ const DEBUG_SETTING = new Setting('debug', RUNNING_QUERIES_SETTING);
7171
const RUNNING_TESTS_SETTING = new Setting('runningTests', ROOT_SETTING);
7272
const RESULTS_DISPLAY_SETTING = new Setting('resultsDisplay', ROOT_SETTING);
7373

74+
export const ENABLE_TELEMETRY = new Setting('telemetry.enableTelemetry', ROOT_SETTING);
7475
export const NUMBER_OF_TEST_THREADS_SETTING = new Setting('numberOfThreads', RUNNING_TESTS_SETTING);
7576
export const MAX_QUERIES = new Setting('maxQueries', RUNNING_QUERIES_SETTING);
7677
export const AUTOSAVE_SETTING = new Setting('autoSave', RUNNING_QUERIES_SETTING);

extensions/ql-vscode/src/extension.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import { QLTestAdapterFactory } from './test-adapter';
5959
import { TestUIService } from './test-ui';
6060
import { CompareInterfaceManager } from './compare/compare-interface';
6161
import { gatherQlFiles } from './pure/files';
62+
import { initializeTelemetry } from './telemetry';
6263

6364
/**
6465
* extension.ts
@@ -87,6 +88,9 @@ const errorStubs: Disposable[] = [];
8788
*/
8889
let isInstallingOrUpdatingDistribution = false;
8990

91+
const extensionId = 'GitHub.vscode-codeql';
92+
const extension = extensions.getExtension(extensionId);
93+
9094
/**
9195
* If the user tries to execute vscode commands after extension activation is failed, give
9296
* a sensible error message.
@@ -97,8 +101,6 @@ function registerErrorStubs(excludedCommands: string[], stubGenerator: (command:
97101
// Remove existing stubs
98102
errorStubs.forEach(stub => stub.dispose());
99103

100-
const extensionId = 'GitHub.vscode-codeql'; // TODO: Is there a better way of obtaining this?
101-
const extension = extensions.getExtension(extensionId);
102104
if (extension === undefined) {
103105
throw new Error(`Can't find extension ${extensionId}`);
104106
}
@@ -137,10 +139,14 @@ export interface CodeQLExtensionInterface {
137139
* @returns CodeQLExtensionInterface
138140
*/
139141
export async function activate(ctx: ExtensionContext): Promise<CodeQLExtensionInterface | {}> {
140-
logger.log('Starting CodeQL extension');
142+
logger.log(`Starting ${extensionId} extension`);
143+
if (extension === undefined) {
144+
throw new Error(`Can't find extension ${extensionId}`);
145+
}
141146

142147
const distributionConfigListener = new DistributionConfigListener();
143148
initializeLogging(ctx);
149+
initializeTelemetry(extension, ctx);
144150
languageSupport.install();
145151

146152
ctx.subscriptions.push(distributionConfigListener);

extensions/ql-vscode/src/helpers.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
} from 'vscode';
1515
import { CodeQLCliServer } from './cli';
1616
import { logger } from './logging';
17+
import { sendCommandUsage } from './telemetry';
1718

1819
export class UserCancellationException extends Error {
1920
/**
@@ -120,9 +121,13 @@ export function commandRunner(
120121
task: NoProgressTask,
121122
): Disposable {
122123
return commands.registerCommand(commandId, async (...args: any[]) => {
124+
const startTIme = Date.now();
125+
let error: Error | undefined;
126+
123127
try {
124128
return await task(...args);
125129
} catch (e) {
130+
error = e;
126131
if (e instanceof UserCancellationException) {
127132
// User has cancelled this action manually
128133
if (e.silent) {
@@ -134,6 +139,9 @@ export function commandRunner(
134139
showAndLogErrorMessage(e.message || e);
135140
}
136141
return undefined;
142+
} finally {
143+
const executionTime = Date.now() - startTIme;
144+
sendCommandUsage(commandId, executionTime, error);
137145
}
138146
});
139147
}
@@ -154,13 +162,16 @@ export function commandRunnerWithProgress<R>(
154162
progressOptions: Partial<ProgressOptions>
155163
): Disposable {
156164
return commands.registerCommand(commandId, async (...args: any[]) => {
165+
const startTIme = Date.now();
166+
let error: Error | undefined;
157167
const progressOptionsWithDefaults = {
158168
location: ProgressLocation.Notification,
159169
...progressOptions
160170
};
161171
try {
162172
return await withProgress(progressOptionsWithDefaults, task, ...args);
163173
} catch (e) {
174+
error = e;
164175
if (e instanceof UserCancellationException) {
165176
// User has cancelled this action manually
166177
if (e.silent) {
@@ -172,6 +183,9 @@ export function commandRunnerWithProgress<R>(
172183
showAndLogErrorMessage(e.message || e);
173184
}
174185
return undefined;
186+
} finally {
187+
const executionTime = Date.now() - startTIme;
188+
sendCommandUsage(commandId, executionTime, error);
175189
}
176190
});
177191
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { Extension, ExtensionContext, workspace } from 'vscode';
2+
import TelemetryReporter from 'vscode-extension-telemetry';
3+
import { Disposable } from 'vscode-jsonrpc';
4+
import { ENABLE_TELEMETRY } from './config';
5+
import { UserCancellationException } from './helpers';
6+
7+
const key = '6f88c20e-2879-41ed-af73-218b82e1ff44';
8+
9+
let reporter: TelemetryReporter | undefined;
10+
let listener: Disposable | undefined;
11+
12+
export enum CommandCompletion {
13+
Success = 'Success',
14+
Failed = 'Failed',
15+
Cancelled = 'Cancelled'
16+
}
17+
18+
export function initializeTelemetry(extension: Extension<any>, ctx: ExtensionContext): void {
19+
registerListener(extension, ctx);
20+
if (reporter) {
21+
reporter.dispose();
22+
reporter = undefined;
23+
}
24+
if (ENABLE_TELEMETRY.getValue<boolean>()) {
25+
reporter = new TelemetryReporter(extension.id, extension.packageJSON.version, key, /* anonymize stack traces */ true);
26+
ctx.subscriptions.push(reporter);
27+
}
28+
}
29+
30+
export function sendCommandUsage(name: string, executionTime: number, error?: Error) {
31+
if (!reporter) {
32+
return;
33+
}
34+
const status = !error
35+
? CommandCompletion.Success
36+
: error instanceof UserCancellationException
37+
? CommandCompletion.Cancelled
38+
: CommandCompletion.Failed;
39+
40+
reporter.sendTelemetryEvent(
41+
'command-usage',
42+
{
43+
name,
44+
status,
45+
},
46+
{ executionTime }
47+
);
48+
49+
// if this is a true error, also report it
50+
if (status === CommandCompletion.Failed) {
51+
reporter.sendTelemetryException(
52+
error!,
53+
{
54+
type: 'command-usage',
55+
name,
56+
status,
57+
},
58+
{ executionTime }
59+
);
60+
}
61+
}
62+
63+
function registerListener(extension: Extension<any>, ctx: ExtensionContext) {
64+
if (!listener) {
65+
listener = workspace.onDidChangeConfiguration(e => {
66+
if (e.affectsConfiguration('codeQL.telemetry.enableTelemetry')) {
67+
initializeTelemetry(extension, ctx);
68+
}
69+
});
70+
ctx.subscriptions.push(listener);
71+
}
72+
}
73+
74+
// Exported for testing
75+
export function _dispose() {
76+
if (listener) {
77+
listener.dispose();
78+
listener = undefined;
79+
}
80+
if (reporter) {
81+
reporter.dispose();
82+
reporter = undefined;
83+
}
84+
}

0 commit comments

Comments
 (0)