Path: blob/main/src/vs/workbench/services/assignment/common/assignmentService.ts
3296 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-umd';8import { MementoObject, Memento } from '../../../common/memento.js';9import { ITelemetryService, TelemetryLevel } 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 { getTelemetryLevel } from '../../../../platform/telemetry/common/telemetryUtils.js';21import { importAMDNodeModule } from '../../../../amdX.js';22import { timeout } from '../../../../base/common/async.js';23import { CopilotAssignmentFilterProvider } from './assignmentFilters.js';24import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';25import { Emitter } from '../../../../base/common/event.js';2627export const IWorkbenchAssignmentService = createDecorator<IWorkbenchAssignmentService>('assignmentService');2829export interface IWorkbenchAssignmentService extends IAssignmentService {30getCurrentExperiments(): Promise<string[] | undefined>;31}3233class MementoKeyValueStorage implements IKeyValueStorage {3435private readonly mementoObj: MementoObject;3637constructor(private readonly memento: Memento) {38this.mementoObj = memento.getMemento(StorageScope.APPLICATION, StorageTarget.MACHINE);39}4041async getValue<T>(key: string, defaultValue?: T | undefined): Promise<T | undefined> {42const value = await this.mementoObj[key];4344return value || defaultValue;45}4647setValue<T>(key: string, value: T): void {48this.mementoObj[key] = value;49this.memento.saveMemento();50}51}5253class WorkbenchAssignmentServiceTelemetry extends Disposable implements IExperimentationTelemetry {5455private readonly _onDidUpdateAssignmentContext = this._register(new Emitter<void>());56readonly onDidUpdateAssignmentContext = this._onDidUpdateAssignmentContext.event;5758private _lastAssignmentContext: string | undefined;59get assignmentContext(): string[] | undefined {60return this._lastAssignmentContext?.split(';');61}6263constructor(64private readonly telemetryService: ITelemetryService,65private readonly productService: IProductService66) {67super();68}6970// __GDPR__COMMON__ "abexp.assignmentcontext" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }71setSharedProperty(name: string, value: string): void {72if (name === this.productService.tasConfig?.assignmentContextTelemetryPropertyName) {73this._lastAssignmentContext = value;74this._onDidUpdateAssignmentContext.fire();75}7677this.telemetryService.setExperimentProperty(name, value);78}7980postEvent(eventName: string, props: Map<string, string>): void {81const data: ITelemetryData = {};82for (const [key, value] of props.entries()) {83data[key] = value;84}8586/* __GDPR__87"query-expfeature" : {88"owner": "sbatten",89"comment": "Logs queries to the experiment service by feature for metric calculations",90"ABExp.queriedFeature": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The experimental feature being queried" }91}92*/93this.telemetryService.publicLog(eventName, data);94}95}9697export class WorkbenchAssignmentService extends Disposable implements IAssignmentService {9899declare readonly _serviceBrand: undefined;100101private readonly tasClient: Promise<TASClient> | undefined;102private readonly tasSetupDisposables = new DisposableStore();103104private networkInitialized = false;105private readonly overrideInitDelay: Promise<void>;106107private readonly telemetry: WorkbenchAssignmentServiceTelemetry;108private readonly keyValueStorage: IKeyValueStorage;109110private readonly experimentsEnabled: boolean;111112private readonly _onDidRefetchAssignments = this._register(new Emitter<void>());113public readonly onDidRefetchAssignments = this._onDidRefetchAssignments.event;114115constructor(116@ITelemetryService private readonly telemetryService: ITelemetryService,117@IStorageService storageService: IStorageService,118@IConfigurationService private readonly configurationService: IConfigurationService,119@IProductService private readonly productService: IProductService,120@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,121@IInstantiationService private readonly instantiationService: IInstantiationService,122) {123super();124125this.experimentsEnabled = getTelemetryLevel(configurationService) === TelemetryLevel.USAGE &&126!environmentService.disableExperiments &&127!environmentService.extensionTestsLocationURI &&128!environmentService.enableSmokeTestDriver &&129configurationService.getValue('workbench.enableExperiments') === true;130131if (productService.tasConfig && this.experimentsEnabled) {132this.tasClient = this.setupTASClient();133}134135this.telemetry = this._register(new WorkbenchAssignmentServiceTelemetry(telemetryService, productService));136this._register(this.telemetry.onDidUpdateAssignmentContext(() => this._onDidRefetchAssignments.fire()));137138this.keyValueStorage = new MementoKeyValueStorage(new Memento('experiment.service.memento', storageService));139140// For development purposes, configure the delay until tas local tas treatment ovverrides are available141const overrideDelaySetting = configurationService.getValue('experiments.overrideDelay');142const overrideDelay = typeof overrideDelaySetting === 'number' ? overrideDelaySetting : 0;143this.overrideInitDelay = timeout(overrideDelay);144}145146async getTreatment<T extends string | number | boolean>(name: string): Promise<T | undefined> {147const result = await this.doGetTreatment<T>(name);148149type TASClientReadTreatmentData = {150treatmentName: string;151treatmentValue: string;152};153154type TASClientReadTreatmentClassification = {155owner: 'sbatten';156comment: 'Logged when a treatment value is read from the experiment service';157treatmentValue: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The value of the read treatment' };158treatmentName: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The name of the treatment that was read' };159};160161this.telemetryService.publicLog2<TASClientReadTreatmentData, TASClientReadTreatmentClassification>('tasClientReadTreatmentComplete', {162treatmentName: name,163treatmentValue: JSON.stringify(result)164});165166return result;167}168169private async doGetTreatment<T extends string | number | boolean>(name: string): Promise<T | undefined> {170await this.overrideInitDelay; // For development purposes, allow overriding tas assignments to test variants locally.171172const override = this.configurationService.getValue<T>(`experiments.override.${name}`);173if (override !== undefined) {174return override;175}176177if (!this.tasClient) {178return undefined;179}180181if (!this.experimentsEnabled) {182return undefined;183}184185let result: T | undefined;186const client = await this.tasClient;187188// The TAS client is initialized but we need to check if the initial fetch has completed yet189// If it is complete, return a cached value for the treatment190// If not, use the async call with `checkCache: true`. This will allow the module to return a cached value if it is present.191// Otherwise it will await the initial fetch to return the most up to date value.192if (this.networkInitialized) {193result = client.getTreatmentVariable<T>('vscode', name);194} else {195result = await client.getTreatmentVariableAsync<T>('vscode', name, true);196}197198result = client.getTreatmentVariable<T>('vscode', name);199return result;200}201202private async setupTASClient(): Promise<TASClient> {203this.tasSetupDisposables.clear();204205const targetPopulation = this.productService.quality === 'stable' ?206TargetPopulation.Public : (this.productService.quality === 'exploration' ?207TargetPopulation.Exploration : TargetPopulation.Insiders);208209const filterProvider = new AssignmentFilterProvider(210this.productService.version,211this.productService.nameLong,212this.telemetryService.machineId,213targetPopulation214);215216const extensionsFilterProvider = this.instantiationService.createInstance(CopilotAssignmentFilterProvider);217this.tasSetupDisposables.add(extensionsFilterProvider);218this.tasSetupDisposables.add(extensionsFilterProvider.onDidChangeFilters(() => this.refetchAssignments()));219220const tasConfig = this.productService.tasConfig!;221const tasClient = new (await importAMDNodeModule<typeof import('tas-client-umd')>('tas-client-umd', 'lib/tas-client-umd.js')).ExperimentationService({222filterProviders: [filterProvider, extensionsFilterProvider],223telemetry: this.telemetry,224storageKey: ASSIGNMENT_STORAGE_KEY,225keyValueStorage: this.keyValueStorage,226assignmentContextTelemetryPropertyName: tasConfig.assignmentContextTelemetryPropertyName,227telemetryEventName: tasConfig.telemetryEventName,228endpoint: tasConfig.endpoint,229refetchInterval: ASSIGNMENT_REFETCH_INTERVAL,230});231232await tasClient.initializePromise;233tasClient.initialFetch.then(() => {234this.networkInitialized = true;235});236237return tasClient;238}239240private async refetchAssignments(): Promise<void> {241if (!this.tasClient) {242return; // Setup has not started, assignments will use latest filters243}244245// Await the client to be setup and the initial fetch to complete246const tasClient = await this.tasClient;247await tasClient.initialFetch;248249// Refresh the assignments250await tasClient.getTreatmentVariableAsync('vscode', 'refresh', false);251}252253async getCurrentExperiments(): Promise<string[] | undefined> {254if (!this.tasClient) {255return undefined;256}257258if (!this.experimentsEnabled) {259return undefined;260}261262await this.tasClient;263264return this.telemetry.assignmentContext;265}266}267268registerSingleton(IWorkbenchAssignmentService, WorkbenchAssignmentService, InstantiationType.Delayed);269270const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);271registry.registerConfiguration({272...workbenchConfigurationNodeBase,273'properties': {274'workbench.enableExperiments': {275'type': 'boolean',276'description': localize('workbench.enableExperiments', "Fetches experiments to run from a Microsoft online service."),277'default': true,278'scope': ConfigurationScope.APPLICATION,279'restricted': true,280'tags': ['usesOnlineServices']281}282}283});284285286