Skip to content

Commit

Permalink
More work on telemetry
Browse files Browse the repository at this point in the history
* Extract commandRunner to its own file
* Add an option to log all telemetry commands to the console
* Add a popup (text to be determined) that requires users to accept
  telemetry before using.
* Convert telemetry to opt-in instead of opt-out
* Add a "canary" setting that users can use to opt-in to unreleased
  features.
  • Loading branch information
aeisenberg committed Oct 30, 2020
1 parent fd0f0df commit 497e492
Show file tree
Hide file tree
Showing 25 changed files with 669 additions and 319 deletions.
21 changes: 21 additions & 0 deletions .github/TELEMETRY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Telemetry in the CodeQL for VS Code

## Why do you collect data?

GitHub collects usage data and metrics to help us improve Salesforce Extensions for VS Code.

## What data is collected

GitHub collects anonymous information related to the usage of the extensions. The data collected are:

- Which commands are run.
- The time taken for each command.
- Anonymized stack trace and error message of any errors that are thrown from inside the extension.
- Anonymous GUID to uniquely identify an installation.
- IP address of the client sending the telemetry data. This IP address is not stored. It is discarded immediately after the telemetry data is received.

## How do I disable telemetry reporting?

You can disable telemetry reporting by setting `codeQL.telemetry.enableTelemetry` to `false` in your settings.

Additionally, telemetry will be disabled if the global `telemetry.enableTelemetry` is set to `false`. For more information see [Microsoft’s documentation](https://code.visualstudio.com/docs/supporting/faq#_how-to-disable-telemetry-reporting).
59 changes: 47 additions & 12 deletions extensions/ql-vscode/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion extensions/ql-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,15 @@
},
"codeQL.telemetry.enableTelemetry": {
"type": "boolean",
"default": true,
"default": false,
"scope": "application",
"markdownDescription": "Specifies whether to enable Code QL telemetry. This setting AND the global `#telemetry.enableTelemetry#` setting must be checked for telemetry to be sent."
},
"codeQL.telemetry.logTelemetry": {
"type": "boolean",
"default": false,
"scope": "application",
"markdownDescription": "Specifies whether or not to write telemetry events to the extension log."
}
}
},
Expand Down Expand Up @@ -751,6 +758,7 @@
"@typescript-eslint/eslint-plugin": "~2.23.0",
"@typescript-eslint/parser": "~2.23.0",
"ansi-colors": "^4.1.1",
"applicationinsights": "^1.8.7",
"chai": "^4.2.0",
"chai-as-promised": "~7.1.1",
"css-loader": "~3.1.0",
Expand Down
2 changes: 1 addition & 1 deletion extensions/ql-vscode/src/astViewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { DatabaseItem } from './databases';
import { UrlValue, BqrsId } from './bqrs-cli-types';
import { showLocation } from './interface-utils';
import { isStringLoc, isWholeFileLoc, isLineColumnLoc } from './bqrs-utils';
import { commandRunner } from './helpers';
import { commandRunner } from './commandRunner';
import { DisposableObject } from './vscode-utils/disposable-object';

export interface AstItem {
Expand Down
183 changes: 183 additions & 0 deletions extensions/ql-vscode/src/commandRunner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import {
CancellationToken,
ProgressOptions,
window as Window,
commands,
Disposable,
ProgressLocation
} from 'vscode';
import { showAndLogErrorMessage, showAndLogWarningMessage } from './helpers';
import { logger } from './logging';
import { sendCommandUsage } from './telemetry';

export class UserCancellationException extends Error {
/**
* @param message The error message
* @param silent If silent is true, then this exception will avoid showing a warning message to the user.
*/
constructor(message?: string, public readonly silent = false) {
super(message);
}
}

export interface ProgressUpdate {
/**
* The current step
*/
step: number;
/**
* The maximum step. This *should* be constant for a single job.
*/
maxStep: number;
/**
* The current progress message
*/
message: string;
}

export type ProgressCallback = (p: ProgressUpdate) => void;

/**
* A task that handles command invocations from `commandRunner`
* and includes a progress monitor.
*
*
* Arguments passed to the command handler are passed along,
* untouched to this `ProgressTask` instance.
*
* @param progress a progress handler function. Call this
* function with a `ProgressUpdate` instance in order to
* denote some progress being achieved on this task.
* @param token a cencellation token
* @param args arguments passed to this task passed on from
* `commands.registerCommand`.
*/
export type ProgressTask<R> = (
progress: ProgressCallback,
token: CancellationToken,
...args: any[]
) => Thenable<R>;

/**
* A task that handles command invocations from `commandRunner`.
* Arguments passed to the command handler are passed along,
* untouched to this `NoProgressTask` instance.
*
* @param args arguments passed to this task passed on from
* `commands.registerCommand`.
*/
type NoProgressTask = ((...args: any[]) => Promise<any>);

/**
* This mediates between the kind of progress callbacks we want to
* write (where we *set* current progress position and give
* `maxSteps`) and the kind vscode progress api expects us to write
* (which increment progress by a certain amount out of 100%).
*
* Where possible, the `commandRunner` function below should be used
* instead of this function. The commandRunner is meant for wrapping
* top-level commands and provides error handling and other support
* automatically.
*
* Only use this function if you need a progress monitor and the
* control flow does not always come from a command (eg- during
* extension activation, or from an internal language server
* request).
*/
export function withProgress<R>(
options: ProgressOptions,
task: ProgressTask<R>,
...args: any[]
): Thenable<R> {
let progressAchieved = 0;
return Window.withProgress(options,
(progress, token) => {
return task(p => {
const { message, step, maxStep } = p;
const increment = 100 * (step - progressAchieved) / maxStep;
progressAchieved = step;
progress.report({ message, increment });
}, token, ...args);
});
}

/**
* A generic wrapper for command registration. This wrapper adds uniform error handling for commands.
*
* In this variant of the command runner, no progress monitor is used.
*
* @param commandId The ID of the command to register.
* @param task The task to run. It is passed directly to `commands.registerCommand`. Any
* arguments to the command handler are passed on to the task.
*/
export function commandRunner(
commandId: string,
task: NoProgressTask,
): Disposable {
return commands.registerCommand(commandId, async (...args: any[]) => {
const startTIme = Date.now();
let error: Error | undefined;

try {
await task(...args);
} catch (e) {
error = e;
if (e instanceof UserCancellationException) {
// User has cancelled this action manually
if (e.silent) {
logger.log(e.message);
} else {
showAndLogWarningMessage(e.message);
}
} else {
showAndLogErrorMessage(e.message || e);
}
} finally {
const executionTime = Date.now() - startTIme;
sendCommandUsage(commandId, executionTime, error);
}
});
}

/**
* A generic wrapper for command registration. This wrapper adds uniform error handling,
* progress monitoring, and cancellation for commands.
*
* @param commandId The ID of the command to register.
* @param task The task to run. It is passed directly to `commands.registerCommand`. Any
* arguments to the command handler are passed on to the task after the progress callback
* and cancellation token.
* @param progressOptions Progress options to be sent to the progress monitor.
*/
export function commandRunnerWithProgress<R>(
commandId: string,
task: ProgressTask<R>,
progressOptions: Partial<ProgressOptions>
): Disposable {
return commands.registerCommand(commandId, async (...args: any[]) => {
const startTIme = Date.now();
let error: Error | undefined;
const progressOptionsWithDefaults = {
location: ProgressLocation.Notification,
...progressOptions
};
try {
await withProgress(progressOptionsWithDefaults, task, ...args);
} catch (e) {
error = e;
if (e instanceof UserCancellationException) {
// User has cancelled this action manually
if (e.silent) {
logger.log(e.message);
} else {
showAndLogWarningMessage(e.message);
}
} else {
showAndLogErrorMessage(e.message || e);
}
} finally {
const executionTime = Date.now() - startTIme;
sendCommandUsage(commandId, executionTime, error);
}
});
}
6 changes: 6 additions & 0 deletions extensions/ql-vscode/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const MEMORY_SETTING = new Setting('memory', RUNNING_QUERIES_SETTING);
const DEBUG_SETTING = new Setting('debug', RUNNING_QUERIES_SETTING);
const RUNNING_TESTS_SETTING = new Setting('runningTests', ROOT_SETTING);

export const LOG_TELEMETRY = new Setting('telemetry.logTelemetry', ROOT_SETTING);
export const ENABLE_TELEMETRY = new Setting('telemetry.enableTelemetry', ROOT_SETTING);
export const NUMBER_OF_TEST_THREADS_SETTING = new Setting('numberOfThreads', RUNNING_TESTS_SETTING);
export const MAX_QUERIES = new Setting('maxQueries', RUNNING_QUERIES_SETTING);
Expand Down Expand Up @@ -234,3 +235,8 @@ export class CliConfigListener extends ConfigListener implements CliConfig {
* want to enable experimental features, they can add them directly in
* their vscode settings json file.
*/

/**
* Enables canary features of this extension. Recommended for all internal users.
*/
export const CANARY_FEATURES = new Setting('canary', ROOT_SETTING);
2 changes: 1 addition & 1 deletion extensions/ql-vscode/src/contextual/locationFinder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import fileRangeFromURI from './fileRangeFromURI';
import * as messages from '../messages';
import { QueryServerClient } from '../queryserver-client';
import { QueryWithResults, compileAndRunQueryAgainstDatabase } from '../run-queries';
import { ProgressCallback } from '../helpers';
import { ProgressCallback } from '../commandRunner';
import { KeyType } from './keyType';
import { qlpackOfDatabase, resolveQueries } from './queryResolver';

Expand Down
3 changes: 2 additions & 1 deletion extensions/ql-vscode/src/contextual/templateProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import * as vscode from 'vscode';

import { decodeSourceArchiveUri, encodeArchiveBasePath, zipArchiveScheme } from '../archive-filesystem-provider';
import { CodeQLCliServer } from '../cli';
import { ProgressCallback, withProgress } from '../commandRunner';
import { DatabaseManager } from '../databases';
import { CachedOperation, ProgressCallback, withProgress } from '../helpers';
import { CachedOperation } from '../helpers';
import * as messages from '../messages';
import { QueryServerClient } from '../queryserver-client';
import { compileAndRunQueryAgainstDatabase, QueryWithResults } from '../run-queries';
Expand Down
2 changes: 1 addition & 1 deletion extensions/ql-vscode/src/databaseFetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import * as path from 'path';

import { DatabaseManager, DatabaseItem } from './databases';
import {
ProgressCallback,
showAndLogInformationMessage,
} from './helpers';
import { logger } from './logging';
import { ProgressCallback } from './commandRunner';

/**
* Prompts a user to fetch a database from a remote location. Database is assumed to be an archive file.
Expand Down
4 changes: 3 additions & 1 deletion extensions/ql-vscode/src/databases-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ import {
import {
commandRunner,
commandRunnerWithProgress,
getOnDiskWorkspaceFolders,
ProgressCallback,
} from './commandRunner';
import {
getOnDiskWorkspaceFolders,
showAndLogErrorMessage
} from './helpers';
import { logger } from './logging';
Expand Down
Loading

0 comments on commit 497e492

Please sign in to comment.