Tag existing events with experimentation info in typescript-language-features (#161463)

* Associate experiment info with existing telemetry.

* Move the ExperimentTelemetryReporter to its own file.

* Roughly copied the 'getPackageInfo' function from other extensions.

* Addressed code review feedback.
This commit is contained in:
Daniel Rosenwasser 2022-09-23 15:18:57 -07:00 committed by GitHub
parent 25d51ed33f
commit da506e9b8d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 147 additions and 119 deletions

View file

@ -0,0 +1,50 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import VsCodeTelemetryReporter from '@vscode/extension-telemetry';
import * as tas from 'vscode-tas-client';
export interface IExperimentationTelemetryReporter extends tas.IExperimentationTelemetry, vscode.Disposable {
postEventObj(eventName: string, props: { [prop: string]: string }): void;
}
/**
* This reporter *supports* experimentation telemetry,
* but will only do so when passed to an {@link ExperimentationService}.
*/
export class ExperimentationTelemetryReporter
implements IExperimentationTelemetryReporter {
private _sharedProperties: Record<string, string> = {};
private _reporter: VsCodeTelemetryReporter;
constructor(reporter: VsCodeTelemetryReporter) {
this._reporter = reporter;
}
setSharedProperty(name: string, value: string): void {
this._sharedProperties[name] = value;
}
postEvent(eventName: string, props: Map<string, string>): void {
const propsObject = {
...this._sharedProperties,
...Object.fromEntries(props),
};
this._reporter.sendTelemetryEvent(eventName, propsObject);
}
postEventObj(eventName: string, props: { [prop: string]: string }) {
this._reporter.sendTelemetryEvent(eventName, {
...this._sharedProperties,
...props,
});
}
dispose() {
this._reporter.dispose();
}
}

View file

@ -4,20 +4,21 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import VsCodeTelemetryReporter from '@vscode/extension-telemetry';
import * as tas from 'vscode-tas-client';
import { IExperimentationTelemetryReporter } from './experimentTelemetryReporter';
interface ExperimentTypes {
// None for now.
}
export class ExperimentationService implements vscode.Disposable {
export class ExperimentationService {
private _experimentationServicePromise: Promise<tas.IExperimentationService>;
private _telemetryReporter: ExperimentTelemetryReporter;
private _telemetryReporter: IExperimentationTelemetryReporter;
constructor(private readonly _extensionContext: vscode.ExtensionContext) {
this._telemetryReporter = new ExperimentTelemetryReporter(_extensionContext);
this._experimentationServicePromise = this.createExperimentationService();
constructor(telemetryReporter: IExperimentationTelemetryReporter, id: string, version: string, globalState: vscode.Memento) {
this._telemetryReporter = telemetryReporter;
this._experimentationServicePromise = createExperimentationService(this._telemetryReporter, id, version, globalState);
}
public async getTreatmentVariable<K extends keyof ExperimentTypes>(name: K, defaultValue: ExperimentTypes[K]): Promise<ExperimentTypes[K]> {
@ -29,70 +30,33 @@ export class ExperimentationService implements vscode.Disposable {
return defaultValue;
}
}
private async createExperimentationService(): Promise<tas.IExperimentationService> {
let targetPopulation: tas.TargetPopulation;
switch (vscode.env.uriScheme) {
case 'vscode':
targetPopulation = tas.TargetPopulation.Public;
break;
case 'vscode-insiders':
targetPopulation = tas.TargetPopulation.Insiders;
break;
case 'vscode-exploration':
targetPopulation = tas.TargetPopulation.Internal;
break;
case 'code-oss':
targetPopulation = tas.TargetPopulation.Team;
break;
default:
targetPopulation = tas.TargetPopulation.Public;
break;
}
const id = this._extensionContext.extension.id;
const version = this._extensionContext.extension.packageJSON.version || '';
const experimentationService = tas.getExperimentationService(id, version, targetPopulation, this._telemetryReporter, this._extensionContext.globalState);
await experimentationService.initialFetch;
return experimentationService;
}
/**
* @inheritdoc
*/
public dispose() {
this._telemetryReporter.dispose();
}
}
export class ExperimentTelemetryReporter
implements tas.IExperimentationTelemetry, vscode.Disposable {
private _sharedProperties: Record<string, string> = {};
private _reporter: VsCodeTelemetryReporter;
constructor(ctxt: vscode.ExtensionContext) {
const extension = ctxt.extension;
const packageJSON = extension.packageJSON;
this._reporter = new VsCodeTelemetryReporter(
extension.id,
packageJSON.version || '',
packageJSON.aiKey || '');
export async function createExperimentationService(
reporter: IExperimentationTelemetryReporter,
id: string,
version: string,
globalState: vscode.Memento): Promise<tas.IExperimentationService> {
let targetPopulation: tas.TargetPopulation;
switch (vscode.env.uriScheme) {
case 'vscode':
targetPopulation = tas.TargetPopulation.Public;
break;
case 'vscode-insiders':
targetPopulation = tas.TargetPopulation.Insiders;
break;
case 'vscode-exploration':
targetPopulation = tas.TargetPopulation.Internal;
break;
case 'code-oss':
targetPopulation = tas.TargetPopulation.Team;
break;
default:
targetPopulation = tas.TargetPopulation.Public;
break;
}
setSharedProperty(name: string, value: string): void {
this._sharedProperties[name] = value;
}
postEvent(eventName: string, props: Map<string, string>): void {
const propsObject = {
...this._sharedProperties,
...Object.fromEntries(props),
};
this._reporter.sendTelemetryEvent(eventName, propsObject);
}
dispose() {
this._reporter.dispose();
}
const experimentationService = tas.getExperimentationService(id, version, targetPopulation, reporter, globalState);
await experimentationService.initialFetch;
return experimentationService;
}

View file

@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import VsCodeTelemetryReporter from '@vscode/extension-telemetry';
import { Api, getExtensionApi } from './api';
import { CommandManager } from './commands/commandManager';
import { registerBaseCommands } from './commands/index';
@ -17,6 +18,8 @@ import API from './utils/api';
import { TypeScriptServiceConfiguration } from './utils/configuration';
import { BrowserServiceConfigurationProvider } from './utils/configuration.browser';
import { PluginManager } from './utils/plugins';
import { ExperimentationTelemetryReporter, IExperimentationTelemetryReporter } from './experimentTelemetryReporter';
import { getPackageInfo } from './utils/packageInfo';
class StaticVersionProvider implements ITypeScriptVersionProvider {
@ -57,6 +60,15 @@ export function activate(
vscode.Uri.joinPath(context.extensionUri, 'dist/browser/typescript/tsserver.web.js').toString(),
API.fromSimpleString('4.8.2')));
let experimentTelemetryReporter: IExperimentationTelemetryReporter | undefined;
const packageInfo = getPackageInfo(context);
if (packageInfo) {
const { name: id, version, aiKey } = packageInfo;
const vscTelemetryReporter = new VsCodeTelemetryReporter(id, version, aiKey);
experimentTelemetryReporter = new ExperimentationTelemetryReporter(vscTelemetryReporter);
context.subscriptions.push(experimentTelemetryReporter);
}
const lazyClientHost = createLazyClientHost(context, false, {
pluginManager,
commandManager,
@ -66,6 +78,7 @@ export function activate(
processFactory: WorkerServerProcess,
activeJsTsEditorTracker,
serviceConfigurationProvider: new BrowserServiceConfigurationProvider(),
experimentTelemetryReporter,
}, item => {
onCompletionAccepted.fire(item);
});

View file

@ -5,10 +5,12 @@
import * as fs from 'fs';
import * as vscode from 'vscode';
import VsCodeTelemetryReporter from '@vscode/extension-telemetry';
import { Api, getExtensionApi } from './api';
import { CommandManager } from './commands/commandManager';
import { registerBaseCommands } from './commands/index';
import { ExperimentationService } from './experimentationService';
import { ExperimentationTelemetryReporter, IExperimentationTelemetryReporter } from './experimentTelemetryReporter';
import { createLazyClientHost, lazilyActivateClient } from './lazyClientHost';
import { nodeRequestCancellerFactory } from './tsServer/cancellation.electron';
import { NodeLogDirectoryProvider } from './tsServer/logDirectoryProvider.electron';
@ -20,6 +22,7 @@ import { ElectronServiceConfigurationProvider } from './utils/configuration.elec
import { onCaseInsensitiveFileSystem } from './utils/fileSystem.electron';
import { PluginManager } from './utils/plugins';
import * as temp from './utils/temp.electron';
import { getPackageInfo } from './utils/packageInfo';
export function activate(
context: vscode.ExtensionContext
@ -42,6 +45,19 @@ export function activate(
const jsWalkthroughState = new JsWalkthroughState();
context.subscriptions.push(jsWalkthroughState);
let experimentTelemetryReporter: IExperimentationTelemetryReporter | undefined;
const packageInfo = getPackageInfo(context);
if (packageInfo) {
const { name: id, version, aiKey } = packageInfo;
const vscTelemetryReporter = new VsCodeTelemetryReporter(id, version, aiKey);
experimentTelemetryReporter = new ExperimentationTelemetryReporter(vscTelemetryReporter);
context.subscriptions.push(experimentTelemetryReporter);
// Currently we have no experiments, but creating the service adds the appropriate
// shared properties to the ExperimentationTelemetryReporter we just created.
new ExperimentationService(experimentTelemetryReporter, id, version, context.globalState);
}
const lazyClientHost = createLazyClientHost(context, onCaseInsensitiveFileSystem(), {
pluginManager,
commandManager,
@ -51,6 +67,7 @@ export function activate(
processFactory: new ElectronServiceProcessFactory(),
activeJsTsEditorTracker,
serviceConfigurationProvider: new ElectronServiceConfigurationProvider(),
experimentTelemetryReporter,
}, item => {
onCompletionAccepted.fire(item);
});
@ -58,9 +75,6 @@ export function activate(
registerBaseCommands(commandManager, lazyClientHost, pluginManager, activeJsTsEditorTracker);
registerJsNodeWalkthrough(commandManager, jsWalkthroughState);
// Currently no variables in use.
context.subscriptions.push(new ExperimentationService(context));
import('./task/taskProvider').then(module => {
context.subscriptions.push(module.register(lazyClientHost.map(x => x.serviceClient)));
});

View file

@ -5,6 +5,7 @@
import * as vscode from 'vscode';
import { CommandManager } from './commands/commandManager';
import { IExperimentationTelemetryReporter } from './experimentTelemetryReporter';
import { OngoingRequestCancellerFactory } from './tsServer/cancellation';
import { ILogDirectoryProvider } from './tsServer/logDirectoryProvider';
import { TsServerProcessFactory } from './tsServer/server';
@ -30,6 +31,7 @@ export function createLazyClientHost(
processFactory: TsServerProcessFactory;
activeJsTsEditorTracker: ActiveJsTsEditorTracker;
serviceConfigurationProvider: ServiceConfigurationProvider;
experimentTelemetryReporter: IExperimentationTelemetryReporter | undefined;
},
onCompletionAccepted: (item: vscode.CompletionItem) => void,
): Lazy<TypeScriptServiceClientHost> {

View file

@ -10,6 +10,7 @@
import * as vscode from 'vscode';
import { CommandManager } from './commands/commandManager';
import { IExperimentationTelemetryReporter } from './experimentTelemetryReporter';
import { DiagnosticKind } from './languageFeatures/diagnostics';
import FileConfigurationManager from './languageFeatures/fileConfigurationManager';
import LanguageProvider from './languageProvider';
@ -72,6 +73,7 @@ export default class TypeScriptServiceClientHost extends Disposable {
processFactory: TsServerProcessFactory;
activeJsTsEditorTracker: ActiveJsTsEditorTracker;
serviceConfigurationProvider: ServiceConfigurationProvider;
experimentTelemetryReporter: IExperimentationTelemetryReporter | undefined;
},
onCompletionAccepted: (item: vscode.CompletionItem) => void,
) {

View file

@ -6,6 +6,7 @@
import * as path from 'path';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { IExperimentationTelemetryReporter } from './experimentTelemetryReporter';
import { DiagnosticKind, DiagnosticsManager } from './languageFeatures/diagnostics';
import * as Proto from './protocol';
import { EventName } from './protocol.const';
@ -137,6 +138,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType
versionProvider: ITypeScriptVersionProvider;
processFactory: TsServerProcessFactory;
serviceConfigurationProvider: ServiceConfigurationProvider;
experimentTelemetryReporter: IExperimentationTelemetryReporter | undefined;
},
allModeIds: readonly string[]
) {
@ -205,14 +207,14 @@ export default class TypeScriptServiceClient extends Disposable implements IType
}
}, this, this._disposables);
this.telemetryReporter = this._register(new VSCodeTelemetryReporter(() => {
this.telemetryReporter = new VSCodeTelemetryReporter(services.experimentTelemetryReporter, () => {
if (this.serverState.type === ServerState.Type.Running) {
if (this.serverState.tsserverVersion) {
return this.serverState.tsserverVersion;
}
}
return this.apiVersion.fullVersionString;
}));
});
this.typescriptServerSpawner = new TypeScriptServerSpawner(this.versionProvider, this._versionManager, this.logDirectoryProvider, this.pluginPathsProvider, this.logger, this.telemetryReporter, this.tracer, this.processFactory);

View file

@ -0,0 +1,24 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
export interface PackageInfo {
name: string;
version: string;
aiKey: string;
}
export function getPackageInfo(context: vscode.ExtensionContext) {
const packageJSON = context.extension.packageJSON;
if (packageJSON && typeof packageJSON === 'object') {
return {
name: packageJSON.name ?? '',
version: packageJSON.version ?? '',
aiKey: packageJSON.aiKey ?? '',
};
}
return null;
}

View file

@ -3,15 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import VsCodeTelemetryReporter from '@vscode/extension-telemetry';
import { memoize } from './memoize';
interface PackageInfo {
readonly name: string;
readonly version: string;
readonly aiKey: string;
}
import { IExperimentationTelemetryReporter } from '../experimentTelemetryReporter';
export interface TelemetryProperties {
readonly [prop: string]: string | number | boolean | undefined;
@ -19,14 +11,11 @@ export interface TelemetryProperties {
export interface TelemetryReporter {
logTelemetry(eventName: string, properties?: TelemetryProperties): void;
dispose(): void;
}
export class VSCodeTelemetryReporter implements TelemetryReporter {
private _reporter: VsCodeTelemetryReporter | null = null;
constructor(
private readonly reporter: IExperimentationTelemetryReporter | undefined,
private readonly clientVersionDelegate: () => string
) { }
@ -43,38 +32,6 @@ export class VSCodeTelemetryReporter implements TelemetryReporter {
*/
properties['version'] = this.clientVersionDelegate();
reporter.sendTelemetryEvent(eventName, properties);
}
public dispose() {
if (this._reporter) {
this._reporter.dispose();
this._reporter = null;
}
}
@memoize
private get reporter(): VsCodeTelemetryReporter | null {
if (this.packageInfo?.aiKey) {
this._reporter = new VsCodeTelemetryReporter(
this.packageInfo.name,
this.packageInfo.version,
this.packageInfo.aiKey);
return this._reporter;
}
return null;
}
@memoize
private get packageInfo(): PackageInfo | null {
const { packageJSON } = vscode.extensions.getExtension('vscode.typescript-language-features')!;
if (packageJSON) {
return {
name: packageJSON.name,
version: packageJSON.version,
aiKey: packageJSON.aiKey
};
}
return null;
reporter.postEventObj(eventName, properties);
}
}