Skip to content

Commit c9170af

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 b7b5a6e commit c9170af

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
},
@@ -729,6 +734,7 @@
729734
"tmp-promise": "~3.0.2",
730735
"tree-kill": "~1.2.2",
731736
"unzipper": "~0.10.5",
737+
"vscode-extension-telemetry": "^0.1.6",
732738
"vscode-jsonrpc": "^5.0.1",
733739
"vscode-languageclient": "^6.1.3",
734740
"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
@@ -70,6 +70,7 @@ const DEBUG_SETTING = new Setting('debug', RUNNING_QUERIES_SETTING);
7070
const RUNNING_TESTS_SETTING = new Setting('runningTests', ROOT_SETTING);
7171
const RESULTS_DISPLAY_SETTING = new Setting('resultsDisplay', ROOT_SETTING);
7272

73+
export const ENABLE_TELEMETRY = new Setting('telemetry.enableTelemetry', ROOT_SETTING);
7374
export const NUMBER_OF_TEST_THREADS_SETTING = new Setting('numberOfThreads', RUNNING_TESTS_SETTING);
7475
export const MAX_QUERIES = new Setting('maxQueries', RUNNING_QUERIES_SETTING);
7576
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
}
@@ -114,9 +116,13 @@ function registerErrorStubs(excludedCommands: string[], stubGenerator: (command:
114116
}
115117

116118
export async function activate(ctx: ExtensionContext): Promise<void> {
117-
logger.log('Starting CodeQL extension');
119+
logger.log(`Starting ${extensionId} extension`);
120+
if (extension === undefined) {
121+
throw new Error(`Can't find extension ${extensionId}`);
122+
}
118123

119124
initializeLogging(ctx);
125+
initializeTelemetry(extension, ctx);
120126
languageSupport.install();
121127

122128
const distributionConfigListener = new 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
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) {
@@ -133,6 +138,9 @@ export function commandRunner(
133138
} else {
134139
showAndLogErrorMessage(e.message || e);
135140
}
141+
} finally {
142+
const executionTime = Date.now() - startTIme;
143+
sendCommandUsage(commandId, executionTime, error);
136144
}
137145
});
138146
}
@@ -153,13 +161,16 @@ export function commandRunnerWithProgress<R>(
153161
progressOptions: Partial<ProgressOptions>
154162
): Disposable {
155163
return commands.registerCommand(commandId, async (...args: any[]) => {
164+
const startTIme = Date.now();
165+
let error: Error | undefined;
156166
const progressOptionsWithDefaults = {
157167
location: ProgressLocation.Notification,
158168
...progressOptions
159169
};
160170
try {
161171
await withProgress(progressOptionsWithDefaults, task, ...args);
162172
} catch (e) {
173+
error = e;
163174
if (e instanceof UserCancellationException) {
164175
// User has cancelled this action manually
165176
if (e.silent) {
@@ -170,6 +181,9 @@ export function commandRunnerWithProgress<R>(
170181
} else {
171182
showAndLogErrorMessage(e.message || e);
172183
}
184+
} finally {
185+
const executionTime = Date.now() - startTIme;
186+
sendCommandUsage(commandId, executionTime, error);
173187
}
174188
});
175189
}
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)