diff --git a/extensions/typescript-language-features/src/experimentTelemetryReporter.ts b/extensions/typescript-language-features/src/experimentTelemetryReporter.ts new file mode 100644 index 00000000000..d7561300761 --- /dev/null +++ b/extensions/typescript-language-features/src/experimentTelemetryReporter.ts @@ -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 = {}; + private _reporter: VsCodeTelemetryReporter; + constructor(reporter: VsCodeTelemetryReporter) { + this._reporter = reporter; + } + + setSharedProperty(name: string, value: string): void { + this._sharedProperties[name] = value; + } + + postEvent(eventName: string, props: Map): 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(); + } +} + diff --git a/extensions/typescript-language-features/src/experimentationService.ts b/extensions/typescript-language-features/src/experimentationService.ts index f739872a824..1f3b1fbd73b 100644 --- a/extensions/typescript-language-features/src/experimentationService.ts +++ b/extensions/typescript-language-features/src/experimentationService.ts @@ -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; - 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(name: K, defaultValue: ExperimentTypes[K]): Promise { @@ -29,70 +30,33 @@ export class ExperimentationService implements vscode.Disposable { return defaultValue; } } - - private async createExperimentationService(): Promise { - 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 = {}; - 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 { + 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): 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; } diff --git a/extensions/typescript-language-features/src/extension.browser.ts b/extensions/typescript-language-features/src/extension.browser.ts index 06b650d2375..ad947fc0302 100644 --- a/extensions/typescript-language-features/src/extension.browser.ts +++ b/extensions/typescript-language-features/src/extension.browser.ts @@ -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); }); diff --git a/extensions/typescript-language-features/src/extension.ts b/extensions/typescript-language-features/src/extension.ts index f6576dabc0d..e6b971260d3 100644 --- a/extensions/typescript-language-features/src/extension.ts +++ b/extensions/typescript-language-features/src/extension.ts @@ -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))); }); diff --git a/extensions/typescript-language-features/src/lazyClientHost.ts b/extensions/typescript-language-features/src/lazyClientHost.ts index 8ad2efac84c..377d9c09fa1 100644 --- a/extensions/typescript-language-features/src/lazyClientHost.ts +++ b/extensions/typescript-language-features/src/lazyClientHost.ts @@ -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 { diff --git a/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts b/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts index c8635b2f668..6c2168a380c 100644 --- a/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts +++ b/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts @@ -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, ) { diff --git a/extensions/typescript-language-features/src/typescriptServiceClient.ts b/extensions/typescript-language-features/src/typescriptServiceClient.ts index a42f5c29f06..169df72d203 100644 --- a/extensions/typescript-language-features/src/typescriptServiceClient.ts +++ b/extensions/typescript-language-features/src/typescriptServiceClient.ts @@ -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); diff --git a/extensions/typescript-language-features/src/utils/packageInfo.ts b/extensions/typescript-language-features/src/utils/packageInfo.ts new file mode 100644 index 00000000000..09536ab4141 --- /dev/null +++ b/extensions/typescript-language-features/src/utils/packageInfo.ts @@ -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; +} diff --git a/extensions/typescript-language-features/src/utils/telemetry.ts b/extensions/typescript-language-features/src/utils/telemetry.ts index da7ee73cad0..4b2d3867f6d 100644 --- a/extensions/typescript-language-features/src/utils/telemetry.ts +++ b/extensions/typescript-language-features/src/utils/telemetry.ts @@ -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); } }