Path: blob/main/src/vs/workbench/services/assignment/common/assignmentService.ts
5221 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { localize } from '../../../../nls.js';6import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';7import type { IKeyValueStorage, IExperimentationTelemetry, ExperimentationService as TASClient } from 'tas-client';8import { Memento } from '../../../common/memento.js';9import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';10import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';11import { ITelemetryData } from '../../../../base/common/actions.js';12import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';13import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';14import { IProductService } from '../../../../platform/product/common/productService.js';15import { ASSIGNMENT_REFETCH_INTERVAL, ASSIGNMENT_STORAGE_KEY, AssignmentFilterProvider, IAssignmentService, TargetPopulation } from '../../../../platform/assignment/common/assignment.js';16import { Registry } from '../../../../platform/registry/common/platform.js';17import { workbenchConfigurationNodeBase } from '../../../common/configuration.js';18import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from '../../../../platform/configuration/common/configurationRegistry.js';19import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js';20import { importAMDNodeModule } from '../../../../amdX.js';21import { timeout } from '../../../../base/common/async.js';22import { CopilotAssignmentFilterProvider } from './assignmentFilters.js';23import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';24import { Emitter, Event } from '../../../../base/common/event.js';25import { experimentsEnabled } from '../../telemetry/common/workbenchTelemetryUtils.js';2627export interface IAssignmentFilter {28exclude(assignment: string): boolean;29onDidChange: Event<void>;30}3132export const IWorkbenchAssignmentService = createDecorator<IWorkbenchAssignmentService>('assignmentService');3334export interface IWorkbenchAssignmentService extends IAssignmentService {35getCurrentExperiments(): Promise<string[] | undefined>;36addTelemetryAssignmentFilter(filter: IAssignmentFilter): void;37}3839class MementoKeyValueStorage implements IKeyValueStorage {4041private readonly mementoObj: Record<string, unknown>;4243constructor(private readonly memento: Memento<Record<string, unknown>>) {44this.mementoObj = memento.getMemento(StorageScope.APPLICATION, StorageTarget.MACHINE);45}4647async getValue<T>(key: string, defaultValue?: T | undefined): Promise<T | undefined> {48const value = await this.mementoObj[key] as T | undefined;4950return value || defaultValue;51}5253setValue<T>(key: string, value: T): void {54this.mementoObj[key] = value;55this.memento.saveMemento();56}57}5859class WorkbenchAssignmentServiceTelemetry extends Disposable implements IExperimentationTelemetry {6061private readonly _onDidUpdateAssignmentContext = this._register(new Emitter<void>());62readonly onDidUpdateAssignmentContext = this._onDidUpdateAssignmentContext.event;6364private _previousAssignmentContext: string | undefined;65private _lastAssignmentContext: string | undefined;66get assignmentContext(): string[] | undefined {67return this._lastAssignmentContext?.split(';');68}6970private _assignmentFilters: IAssignmentFilter[] = [];71private _assignmentFilterDisposables = this._register(new DisposableStore());7273constructor(74private readonly telemetryService: ITelemetryService,75private readonly productService: IProductService76) {77super();78}7980private _filterAssignmentContext(assignmentContext: string): string {81const assignments = assignmentContext.split(';');8283const filteredAssignments = assignments.filter(assignment => {84for (const filter of this._assignmentFilters) {85if (filter.exclude(assignment)) {86return false;87}88}89return true;90});9192return filteredAssignments.join(';');93}9495private _setAssignmentContext(value: string): void {96const filteredValue = this._filterAssignmentContext(value);97this._lastAssignmentContext = filteredValue;98this._onDidUpdateAssignmentContext.fire();99100if (this.productService.tasConfig?.assignmentContextTelemetryPropertyName) {101this.telemetryService.setExperimentProperty(this.productService.tasConfig.assignmentContextTelemetryPropertyName, filteredValue);102}103}104105addAssignmentFilter(filter: IAssignmentFilter): void {106this._assignmentFilters.push(filter);107this._assignmentFilterDisposables.add(filter.onDidChange(() => {108if (this._previousAssignmentContext) {109this._setAssignmentContext(this._previousAssignmentContext);110}111}));112if (this._previousAssignmentContext) {113this._setAssignmentContext(this._previousAssignmentContext);114}115}116117// __GDPR__COMMON__ "abexp.assignmentcontext" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }118setSharedProperty(name: string, value: string): void {119if (name === this.productService.tasConfig?.assignmentContextTelemetryPropertyName) {120this._previousAssignmentContext = value;121return this._setAssignmentContext(value);122}123124this.telemetryService.setExperimentProperty(name, value);125}126127postEvent(eventName: string, props: Map<string, string>): void {128const data: ITelemetryData = {};129for (const [key, value] of props.entries()) {130data[key] = value;131}132133/* __GDPR__134"query-expfeature" : {135"owner": "sbatten",136"comment": "Logs queries to the experiment service by feature for metric calculations",137"ABExp.queriedFeature": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The experimental feature being queried" }138}139*/140this.telemetryService.publicLog(eventName, data);141}142}143144export class WorkbenchAssignmentService extends Disposable implements IAssignmentService {145146declare readonly _serviceBrand: undefined;147148private readonly tasClient: Promise<TASClient> | undefined;149private readonly tasSetupDisposables = new DisposableStore();150151private networkInitialized = false;152private readonly overrideInitDelay: Promise<void>;153154private readonly telemetry: WorkbenchAssignmentServiceTelemetry;155private readonly keyValueStorage: IKeyValueStorage;156157private readonly experimentsEnabled: boolean;158159private readonly _onDidRefetchAssignments = this._register(new Emitter<void>());160public readonly onDidRefetchAssignments = this._onDidRefetchAssignments.event;161162constructor(163@ITelemetryService private readonly telemetryService: ITelemetryService,164@IStorageService storageService: IStorageService,165@IConfigurationService private readonly configurationService: IConfigurationService,166@IProductService private readonly productService: IProductService,167@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,168@IInstantiationService private readonly instantiationService: IInstantiationService,169) {170super();171172this.experimentsEnabled = experimentsEnabled(configurationService, productService, environmentService);173174if (this.experimentsEnabled) {175this.tasClient = this.setupTASClient();176}177178this.telemetry = this._register(new WorkbenchAssignmentServiceTelemetry(telemetryService, productService));179this._register(this.telemetry.onDidUpdateAssignmentContext(() => this._onDidRefetchAssignments.fire()));180181this.keyValueStorage = new MementoKeyValueStorage(new Memento<Record<string, unknown>>('experiment.service.memento', storageService));182183// For development purposes, configure the delay until tas local tas treatment ovverrides are available184const overrideDelaySetting = configurationService.getValue('experiments.overrideDelay');185const overrideDelay = typeof overrideDelaySetting === 'number' ? overrideDelaySetting : 0;186this.overrideInitDelay = timeout(overrideDelay);187}188189async getTreatment<T extends string | number | boolean>(name: string): Promise<T | undefined> {190const result = await this.doGetTreatment<T>(name);191192type TASClientReadTreatmentData = {193treatmentName: string;194treatmentValue: string;195};196197type TASClientReadTreatmentClassification = {198owner: 'sbatten';199comment: 'Logged when a treatment value is read from the experiment service';200treatmentValue: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The value of the read treatment' };201treatmentName: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The name of the treatment that was read' };202};203204this.telemetryService.publicLog2<TASClientReadTreatmentData, TASClientReadTreatmentClassification>('tasClientReadTreatmentComplete', {205treatmentName: name,206treatmentValue: JSON.stringify(result)207});208209return result;210}211212private async doGetTreatment<T extends string | number | boolean>(name: string): Promise<T | undefined> {213await this.overrideInitDelay; // For development purposes, allow overriding tas assignments to test variants locally.214215const override = this.configurationService.getValue<T>(`experiments.override.${name}`);216if (override !== undefined) {217return override;218}219220if (!this.tasClient) {221return undefined;222}223224if (!this.experimentsEnabled) {225return undefined;226}227228let result: T | undefined;229const client = await this.tasClient;230231// The TAS client is initialized but we need to check if the initial fetch has completed yet232// If it is complete, return a cached value for the treatment233// If not, use the async call with `checkCache: true`. This will allow the module to return a cached value if it is present.234// Otherwise it will await the initial fetch to return the most up to date value.235if (this.networkInitialized) {236result = client.getTreatmentVariable<T>('vscode', name);237} else {238result = await client.getTreatmentVariableAsync<T>('vscode', name, true);239}240241result = client.getTreatmentVariable<T>('vscode', name);242return result;243}244245private async setupTASClient(): Promise<TASClient> {246this.tasSetupDisposables.clear();247248const targetPopulation = this.productService.quality === 'stable' ?249TargetPopulation.Public : (this.productService.quality === 'exploration' ?250TargetPopulation.Exploration : TargetPopulation.Insiders);251252const filterProvider = new AssignmentFilterProvider(253this.productService.version,254this.productService.nameLong,255this.telemetryService.machineId,256this.telemetryService.devDeviceId,257targetPopulation,258this.productService.date ?? ''259);260261const extensionsFilterProvider = this.instantiationService.createInstance(CopilotAssignmentFilterProvider);262this.tasSetupDisposables.add(extensionsFilterProvider);263this.tasSetupDisposables.add(extensionsFilterProvider.onDidChangeFilters(() => this.refetchAssignments()));264265const tasConfig = this.productService.tasConfig!;266const tasClient = new (await importAMDNodeModule<typeof import('tas-client')>('tas-client', 'dist/tas-client.min.js')).ExperimentationService({267filterProviders: [filterProvider, extensionsFilterProvider],268telemetry: this.telemetry,269storageKey: ASSIGNMENT_STORAGE_KEY,270keyValueStorage: this.keyValueStorage,271assignmentContextTelemetryPropertyName: tasConfig.assignmentContextTelemetryPropertyName,272telemetryEventName: tasConfig.telemetryEventName,273endpoint: tasConfig.endpoint,274refetchInterval: ASSIGNMENT_REFETCH_INTERVAL,275});276277await tasClient.initializePromise;278tasClient.initialFetch.then(() => {279this.networkInitialized = true;280});281282return tasClient;283}284285private async refetchAssignments(): Promise<void> {286if (!this.tasClient) {287return; // Setup has not started, assignments will use latest filters288}289290// Await the client to be setup and the initial fetch to complete291const tasClient = await this.tasClient;292await tasClient.initialFetch;293294// Refresh the assignments295await tasClient.getTreatmentVariableAsync('vscode', 'refresh', false);296}297298async getCurrentExperiments(): Promise<string[] | undefined> {299if (!this.tasClient) {300return undefined;301}302303if (!this.experimentsEnabled) {304return undefined;305}306307await this.tasClient;308309return this.telemetry.assignmentContext;310}311312addTelemetryAssignmentFilter(filter: IAssignmentFilter): void {313this.telemetry.addAssignmentFilter(filter);314}315}316317registerSingleton(IWorkbenchAssignmentService, WorkbenchAssignmentService, InstantiationType.Delayed);318319const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);320registry.registerConfiguration({321...workbenchConfigurationNodeBase,322'properties': {323'workbench.enableExperiments': {324'type': 'boolean',325'description': localize('workbench.enableExperiments', "Fetches experiments to run from a Microsoft online service."),326'default': true,327'scope': ConfigurationScope.APPLICATION,328'restricted': true,329'tags': ['usesOnlineServices']330}331}332});333334335