The main problem this commit fixes is with vscode 1.67.0, an error is thrown when inside of integration tests and a dialog box is opened. We were opening the telemetry dialog box. Now, an env variable is set during cli-integration tests that prevents the dialog from being opened. There are also other cleanups and improvements with cli-integration tests that assist with running locally: - `vscode-test` dependency has been renamed to `@vscode/test-electron`, so use that instead and make the small API changes to support it. - Commit the codeql-pack.lock.yml file so it isn't recreated on each test run. - Ensure all databases are removed before _and after_ each test run that manipulates the set of installed databases - Similarly, for quick query files, delete them before and after each test. - Change some async `forEach` blocks to for loops in order to support sequential operations more easily.
221 lines
6.7 KiB
TypeScript
221 lines
6.7 KiB
TypeScript
import { ConfigurationTarget, Extension, ExtensionContext, ConfigurationChangeEvent } from 'vscode';
|
|
import TelemetryReporter from 'vscode-extension-telemetry';
|
|
import { ConfigListener, CANARY_FEATURES, ENABLE_TELEMETRY, GLOBAL_ENABLE_TELEMETRY, LOG_TELEMETRY, isIntegrationTestMode } from './config';
|
|
import * as appInsights from 'applicationinsights';
|
|
import { logger } from './logging';
|
|
import { UserCancellationException } from './commandRunner';
|
|
import { showBinaryChoiceWithUrlDialog } from './helpers';
|
|
|
|
// Key is injected at build time through the APP_INSIGHTS_KEY environment variable.
|
|
const key = 'REPLACE-APP-INSIGHTS-KEY';
|
|
|
|
export enum CommandCompletion {
|
|
Success = 'Success',
|
|
Failed = 'Failed',
|
|
Cancelled = 'Cancelled'
|
|
}
|
|
|
|
// Avoid sending the following data to App insights since we don't need it.
|
|
const tagsToRemove = [
|
|
'ai.application.ver',
|
|
'ai.device.id',
|
|
'ai.cloud.roleInstance',
|
|
'ai.cloud.role',
|
|
'ai.device.id',
|
|
'ai.device.osArchitecture',
|
|
'ai.device.osPlatform',
|
|
'ai.device.osVersion',
|
|
'ai.internal.sdkVersion',
|
|
'ai.session.id'
|
|
];
|
|
|
|
const baseDataPropertiesToRemove = [
|
|
'common.os',
|
|
'common.platformversion',
|
|
'common.remotename',
|
|
'common.uikind',
|
|
'common.vscodesessionid'
|
|
];
|
|
|
|
export class TelemetryListener extends ConfigListener {
|
|
|
|
static relevantSettings = [ENABLE_TELEMETRY, CANARY_FEATURES];
|
|
|
|
private reporter?: TelemetryReporter;
|
|
|
|
constructor(
|
|
private readonly id: string,
|
|
private readonly version: string,
|
|
private readonly key: string,
|
|
private readonly ctx: ExtensionContext
|
|
) {
|
|
super();
|
|
}
|
|
|
|
/**
|
|
* This function handles changes to relevant configuration elements. There are 2 configuration
|
|
* ids that this function cares about:
|
|
*
|
|
* * `codeQL.telemetry.enableTelemetry`: If this one has changed, then we need to re-initialize
|
|
* the reporter and the reporter may wind up being removed.
|
|
* * `codeQL.canary`: A change here could possibly re-trigger a dialog popup.
|
|
*
|
|
* Note that the global telemetry setting also gate-keeps whether or not to send telemetry events
|
|
* to Application Insights. However, this gatekeeping happens inside of the vscode-extension-telemetry
|
|
* package. So, this does not need to be handled here.
|
|
*
|
|
* @param e the configuration change event
|
|
*/
|
|
async handleDidChangeConfiguration(e: ConfigurationChangeEvent): Promise<void> {
|
|
if (
|
|
e.affectsConfiguration('codeQL.telemetry.enableTelemetry') ||
|
|
e.affectsConfiguration('telemetry.enableTelemetry')
|
|
) {
|
|
await this.initialize();
|
|
}
|
|
|
|
// Re-request telemetry so that users can see the dialog again.
|
|
// Re-request if codeQL.canary is being set to `true` and telemetry
|
|
// is not currently enabled.
|
|
if (
|
|
e.affectsConfiguration('codeQL.canary') &&
|
|
CANARY_FEATURES.getValue() &&
|
|
!ENABLE_TELEMETRY.getValue()
|
|
) {
|
|
await Promise.all([
|
|
this.setTelemetryRequested(false),
|
|
this.requestTelemetryPermission()
|
|
]);
|
|
}
|
|
}
|
|
|
|
async initialize() {
|
|
await this.requestTelemetryPermission();
|
|
|
|
this.disposeReporter();
|
|
|
|
if (ENABLE_TELEMETRY.getValue<boolean>()) {
|
|
this.createReporter();
|
|
}
|
|
}
|
|
|
|
private createReporter() {
|
|
this.reporter = new TelemetryReporter(
|
|
this.id,
|
|
this.version,
|
|
this.key,
|
|
/* anonymize stack traces */ true
|
|
);
|
|
this.push(this.reporter);
|
|
|
|
const client = (this.reporter as any).appInsightsClient as appInsights.TelemetryClient;
|
|
if (client) {
|
|
// add a telemetry processor to delete unwanted properties
|
|
client.addTelemetryProcessor((envelope: any) => {
|
|
tagsToRemove.forEach(tag => delete envelope.tags[tag]);
|
|
const baseDataProperties = (envelope.data as any)?.baseData?.properties;
|
|
if (baseDataProperties) {
|
|
baseDataPropertiesToRemove.forEach(prop => delete baseDataProperties[prop]);
|
|
}
|
|
|
|
if (LOG_TELEMETRY.getValue<boolean>()) {
|
|
void logger.log(`Telemetry: ${JSON.stringify(envelope)}`);
|
|
}
|
|
return true;
|
|
});
|
|
}
|
|
}
|
|
|
|
dispose() {
|
|
super.dispose();
|
|
void this.reporter?.dispose();
|
|
}
|
|
|
|
sendCommandUsage(name: string, executionTime: number, error?: Error) {
|
|
if (!this.reporter) {
|
|
return;
|
|
}
|
|
const status = !error
|
|
? CommandCompletion.Success
|
|
: error instanceof UserCancellationException
|
|
? CommandCompletion.Cancelled
|
|
: CommandCompletion.Failed;
|
|
|
|
const isCanary = (!!CANARY_FEATURES.getValue<boolean>()).toString();
|
|
|
|
this.reporter.sendTelemetryEvent(
|
|
'command-usage',
|
|
{
|
|
name,
|
|
status,
|
|
isCanary
|
|
},
|
|
{ executionTime }
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Displays a popup asking the user if they want to enable telemetry
|
|
* for this extension.
|
|
*/
|
|
async requestTelemetryPermission() {
|
|
if (!this.wasTelemetryRequested()) {
|
|
// if global telemetry is disabled, avoid showing the dialog or making any changes
|
|
let result = undefined;
|
|
if (
|
|
GLOBAL_ENABLE_TELEMETRY.getValue() &&
|
|
// Avoid showing the dialog if we are in integration test mode.
|
|
!isIntegrationTestMode()
|
|
) {
|
|
// Extension won't start until this completes.
|
|
result = await showBinaryChoiceWithUrlDialog(
|
|
'Does the CodeQL Extension by GitHub have your permission to collect usage data and metrics to help us improve CodeQL for VSCode?',
|
|
'https://codeql.github.com/docs/codeql-for-visual-studio-code/about-telemetry-in-codeql-for-visual-studio-code'
|
|
);
|
|
}
|
|
if (result !== undefined) {
|
|
await Promise.all([
|
|
this.setTelemetryRequested(true),
|
|
ENABLE_TELEMETRY.updateValue<boolean>(result, ConfigurationTarget.Global),
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Exposed for testing
|
|
*/
|
|
get _reporter() {
|
|
return this.reporter;
|
|
}
|
|
|
|
private disposeReporter() {
|
|
if (this.reporter) {
|
|
void this.reporter.dispose();
|
|
this.reporter = undefined;
|
|
}
|
|
}
|
|
|
|
private wasTelemetryRequested(): boolean {
|
|
return !!this.ctx.globalState.get<boolean>('telemetry-request-viewed');
|
|
}
|
|
|
|
private async setTelemetryRequested(newValue: boolean): Promise<void> {
|
|
await this.ctx.globalState.update('telemetry-request-viewed', newValue);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* The global Telemetry instance
|
|
*/
|
|
export let telemetryListener: TelemetryListener;
|
|
|
|
export async function initializeTelemetry(extension: Extension<any>, ctx: ExtensionContext): Promise<void> {
|
|
telemetryListener = new TelemetryListener(extension.id, extension.packageJSON.version, key, ctx);
|
|
// do not await initialization, since doing so will sometimes cause a modal popup.
|
|
// this is a particular problem during integration tests, which will hang if a modal popup is displayed.
|
|
void telemetryListener.initialize();
|
|
ctx.subscriptions.push(telemetryListener);
|
|
}
|