Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/services/assignment/common/assignmentService.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { localize } from '../../../../nls.js';
7
import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
8
import type { IKeyValueStorage, IExperimentationTelemetry, ExperimentationService as TASClient } from 'tas-client-umd';
9
import { MementoObject, Memento } from '../../../common/memento.js';
10
import { ITelemetryService, TelemetryLevel } from '../../../../platform/telemetry/common/telemetry.js';
11
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
12
import { ITelemetryData } from '../../../../base/common/actions.js';
13
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
14
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
15
import { IProductService } from '../../../../platform/product/common/productService.js';
16
import { ASSIGNMENT_REFETCH_INTERVAL, ASSIGNMENT_STORAGE_KEY, AssignmentFilterProvider, IAssignmentService, TargetPopulation } from '../../../../platform/assignment/common/assignment.js';
17
import { Registry } from '../../../../platform/registry/common/platform.js';
18
import { workbenchConfigurationNodeBase } from '../../../common/configuration.js';
19
import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from '../../../../platform/configuration/common/configurationRegistry.js';
20
import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js';
21
import { getTelemetryLevel } from '../../../../platform/telemetry/common/telemetryUtils.js';
22
import { importAMDNodeModule } from '../../../../amdX.js';
23
import { timeout } from '../../../../base/common/async.js';
24
import { CopilotAssignmentFilterProvider } from './assignmentFilters.js';
25
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
26
import { Emitter } from '../../../../base/common/event.js';
27
28
export const IWorkbenchAssignmentService = createDecorator<IWorkbenchAssignmentService>('assignmentService');
29
30
export interface IWorkbenchAssignmentService extends IAssignmentService {
31
getCurrentExperiments(): Promise<string[] | undefined>;
32
}
33
34
class MementoKeyValueStorage implements IKeyValueStorage {
35
36
private readonly mementoObj: MementoObject;
37
38
constructor(private readonly memento: Memento) {
39
this.mementoObj = memento.getMemento(StorageScope.APPLICATION, StorageTarget.MACHINE);
40
}
41
42
async getValue<T>(key: string, defaultValue?: T | undefined): Promise<T | undefined> {
43
const value = await this.mementoObj[key];
44
45
return value || defaultValue;
46
}
47
48
setValue<T>(key: string, value: T): void {
49
this.mementoObj[key] = value;
50
this.memento.saveMemento();
51
}
52
}
53
54
class WorkbenchAssignmentServiceTelemetry extends Disposable implements IExperimentationTelemetry {
55
56
private readonly _onDidUpdateAssignmentContext = this._register(new Emitter<void>());
57
readonly onDidUpdateAssignmentContext = this._onDidUpdateAssignmentContext.event;
58
59
private _lastAssignmentContext: string | undefined;
60
get assignmentContext(): string[] | undefined {
61
return this._lastAssignmentContext?.split(';');
62
}
63
64
constructor(
65
private readonly telemetryService: ITelemetryService,
66
private readonly productService: IProductService
67
) {
68
super();
69
}
70
71
// __GDPR__COMMON__ "abexp.assignmentcontext" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
72
setSharedProperty(name: string, value: string): void {
73
if (name === this.productService.tasConfig?.assignmentContextTelemetryPropertyName) {
74
this._lastAssignmentContext = value;
75
this._onDidUpdateAssignmentContext.fire();
76
}
77
78
this.telemetryService.setExperimentProperty(name, value);
79
}
80
81
postEvent(eventName: string, props: Map<string, string>): void {
82
const data: ITelemetryData = {};
83
for (const [key, value] of props.entries()) {
84
data[key] = value;
85
}
86
87
/* __GDPR__
88
"query-expfeature" : {
89
"owner": "sbatten",
90
"comment": "Logs queries to the experiment service by feature for metric calculations",
91
"ABExp.queriedFeature": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The experimental feature being queried" }
92
}
93
*/
94
this.telemetryService.publicLog(eventName, data);
95
}
96
}
97
98
export class WorkbenchAssignmentService extends Disposable implements IAssignmentService {
99
100
declare readonly _serviceBrand: undefined;
101
102
private readonly tasClient: Promise<TASClient> | undefined;
103
private readonly tasSetupDisposables = new DisposableStore();
104
105
private networkInitialized = false;
106
private readonly overrideInitDelay: Promise<void>;
107
108
private readonly telemetry: WorkbenchAssignmentServiceTelemetry;
109
private readonly keyValueStorage: IKeyValueStorage;
110
111
private readonly experimentsEnabled: boolean;
112
113
private readonly _onDidRefetchAssignments = this._register(new Emitter<void>());
114
public readonly onDidRefetchAssignments = this._onDidRefetchAssignments.event;
115
116
constructor(
117
@ITelemetryService private readonly telemetryService: ITelemetryService,
118
@IStorageService storageService: IStorageService,
119
@IConfigurationService private readonly configurationService: IConfigurationService,
120
@IProductService private readonly productService: IProductService,
121
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
122
@IInstantiationService private readonly instantiationService: IInstantiationService,
123
) {
124
super();
125
126
this.experimentsEnabled = getTelemetryLevel(configurationService) === TelemetryLevel.USAGE &&
127
!environmentService.disableExperiments &&
128
!environmentService.extensionTestsLocationURI &&
129
!environmentService.enableSmokeTestDriver &&
130
configurationService.getValue('workbench.enableExperiments') === true;
131
132
if (productService.tasConfig && this.experimentsEnabled) {
133
this.tasClient = this.setupTASClient();
134
}
135
136
this.telemetry = this._register(new WorkbenchAssignmentServiceTelemetry(telemetryService, productService));
137
this._register(this.telemetry.onDidUpdateAssignmentContext(() => this._onDidRefetchAssignments.fire()));
138
139
this.keyValueStorage = new MementoKeyValueStorage(new Memento('experiment.service.memento', storageService));
140
141
// For development purposes, configure the delay until tas local tas treatment ovverrides are available
142
const overrideDelaySetting = configurationService.getValue('experiments.overrideDelay');
143
const overrideDelay = typeof overrideDelaySetting === 'number' ? overrideDelaySetting : 0;
144
this.overrideInitDelay = timeout(overrideDelay);
145
}
146
147
async getTreatment<T extends string | number | boolean>(name: string): Promise<T | undefined> {
148
const result = await this.doGetTreatment<T>(name);
149
150
type TASClientReadTreatmentData = {
151
treatmentName: string;
152
treatmentValue: string;
153
};
154
155
type TASClientReadTreatmentClassification = {
156
owner: 'sbatten';
157
comment: 'Logged when a treatment value is read from the experiment service';
158
treatmentValue: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The value of the read treatment' };
159
treatmentName: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The name of the treatment that was read' };
160
};
161
162
this.telemetryService.publicLog2<TASClientReadTreatmentData, TASClientReadTreatmentClassification>('tasClientReadTreatmentComplete', {
163
treatmentName: name,
164
treatmentValue: JSON.stringify(result)
165
});
166
167
return result;
168
}
169
170
private async doGetTreatment<T extends string | number | boolean>(name: string): Promise<T | undefined> {
171
await this.overrideInitDelay; // For development purposes, allow overriding tas assignments to test variants locally.
172
173
const override = this.configurationService.getValue<T>(`experiments.override.${name}`);
174
if (override !== undefined) {
175
return override;
176
}
177
178
if (!this.tasClient) {
179
return undefined;
180
}
181
182
if (!this.experimentsEnabled) {
183
return undefined;
184
}
185
186
let result: T | undefined;
187
const client = await this.tasClient;
188
189
// The TAS client is initialized but we need to check if the initial fetch has completed yet
190
// If it is complete, return a cached value for the treatment
191
// If not, use the async call with `checkCache: true`. This will allow the module to return a cached value if it is present.
192
// Otherwise it will await the initial fetch to return the most up to date value.
193
if (this.networkInitialized) {
194
result = client.getTreatmentVariable<T>('vscode', name);
195
} else {
196
result = await client.getTreatmentVariableAsync<T>('vscode', name, true);
197
}
198
199
result = client.getTreatmentVariable<T>('vscode', name);
200
return result;
201
}
202
203
private async setupTASClient(): Promise<TASClient> {
204
this.tasSetupDisposables.clear();
205
206
const targetPopulation = this.productService.quality === 'stable' ?
207
TargetPopulation.Public : (this.productService.quality === 'exploration' ?
208
TargetPopulation.Exploration : TargetPopulation.Insiders);
209
210
const filterProvider = new AssignmentFilterProvider(
211
this.productService.version,
212
this.productService.nameLong,
213
this.telemetryService.machineId,
214
targetPopulation
215
);
216
217
const extensionsFilterProvider = this.instantiationService.createInstance(CopilotAssignmentFilterProvider);
218
this.tasSetupDisposables.add(extensionsFilterProvider);
219
this.tasSetupDisposables.add(extensionsFilterProvider.onDidChangeFilters(() => this.refetchAssignments()));
220
221
const tasConfig = this.productService.tasConfig!;
222
const tasClient = new (await importAMDNodeModule<typeof import('tas-client-umd')>('tas-client-umd', 'lib/tas-client-umd.js')).ExperimentationService({
223
filterProviders: [filterProvider, extensionsFilterProvider],
224
telemetry: this.telemetry,
225
storageKey: ASSIGNMENT_STORAGE_KEY,
226
keyValueStorage: this.keyValueStorage,
227
assignmentContextTelemetryPropertyName: tasConfig.assignmentContextTelemetryPropertyName,
228
telemetryEventName: tasConfig.telemetryEventName,
229
endpoint: tasConfig.endpoint,
230
refetchInterval: ASSIGNMENT_REFETCH_INTERVAL,
231
});
232
233
await tasClient.initializePromise;
234
tasClient.initialFetch.then(() => {
235
this.networkInitialized = true;
236
});
237
238
return tasClient;
239
}
240
241
private async refetchAssignments(): Promise<void> {
242
if (!this.tasClient) {
243
return; // Setup has not started, assignments will use latest filters
244
}
245
246
// Await the client to be setup and the initial fetch to complete
247
const tasClient = await this.tasClient;
248
await tasClient.initialFetch;
249
250
// Refresh the assignments
251
await tasClient.getTreatmentVariableAsync('vscode', 'refresh', false);
252
}
253
254
async getCurrentExperiments(): Promise<string[] | undefined> {
255
if (!this.tasClient) {
256
return undefined;
257
}
258
259
if (!this.experimentsEnabled) {
260
return undefined;
261
}
262
263
await this.tasClient;
264
265
return this.telemetry.assignmentContext;
266
}
267
}
268
269
registerSingleton(IWorkbenchAssignmentService, WorkbenchAssignmentService, InstantiationType.Delayed);
270
271
const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
272
registry.registerConfiguration({
273
...workbenchConfigurationNodeBase,
274
'properties': {
275
'workbench.enableExperiments': {
276
'type': 'boolean',
277
'description': localize('workbench.enableExperiments', "Fetches experiments to run from a Microsoft online service."),
278
'default': true,
279
'scope': ConfigurationScope.APPLICATION,
280
'restricted': true,
281
'tags': ['usesOnlineServices']
282
}
283
}
284
});
285
286