Skip to content

Commit 17ad2a3

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 953c2e7 commit 17ad2a3

8 files changed

Lines changed: 467 additions & 4 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
@@ -169,6 +169,11 @@
169169
"minimum": 0,
170170
"maximum": 1024,
171171
"description": "Number of threads for running CodeQL tests."
172+
},
173+
"codeQL.telemetry.enableTelemetry": {
174+
"type": "boolean",
175+
"default": true,
176+
"markdownDescription": "Specifies whether to enable Code QL telemetry. This setting AND the global `#telemetry.enableTelemetry#` setting must be checked for telemetry to be sent."
172177
}
173178
}
174179
},
@@ -710,6 +715,7 @@
710715
"tmp-promise": "~3.0.2",
711716
"tree-kill": "~1.2.2",
712717
"unzipper": "~0.10.5",
718+
"vscode-extension-telemetry": "^0.1.6",
713719
"vscode-jsonrpc": "^5.0.1",
714720
"vscode-languageclient": "^6.1.3",
715721
"vscode-test-adapter-api": "~1.7.0",

extensions/ql-vscode/src/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,8 @@ export class QueryHistoryConfigListener extends ConfigListener implements QueryH
211211
}
212212
}
213213

214+
export const ENABLE_TELEMETRY = new Setting('telemetry.enableTelemetry', ROOT_SETTING);
215+
214216
// Enable experimental features
215217

216218
/**

extensions/ql-vscode/src/extension.ts

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

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

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

99-
const extensionId = 'GitHub.vscode-codeql'; // TODO: Is there a better way of obtaining this?
100-
const extension = extensions.getExtension(extensionId);
101103
if (extension === undefined) {
102104
throw new Error(`Can't find extension ${extensionId}`);
103105
}
@@ -113,9 +115,13 @@ function registerErrorStubs(excludedCommands: string[], stubGenerator: (command:
113115
}
114116

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

118123
initializeLogging(ctx);
124+
initializeTelemetry(extension, ctx);
119125
languageSupport.install();
120126

121127
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+
}

extensions/ql-vscode/src/vscode-tests/no-workspace/databases.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ describe('databases', () => {
1717
databaseManager = new DatabaseManager(
1818
{
1919
workspaceState: {
20-
update: updateSpy
20+
update: updateSpy,
21+
get: sinon.spy()
2122
}
2223
} as unknown as ExtensionContext,
2324
{} as QueryServerConfig,

0 commit comments

Comments
 (0)