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
5221 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';
9
import { Memento } from '../../../common/memento.js';
10
import { ITelemetryService } 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 { importAMDNodeModule } from '../../../../amdX.js';
22
import { timeout } from '../../../../base/common/async.js';
23
import { CopilotAssignmentFilterProvider } from './assignmentFilters.js';
24
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
25
import { Emitter, Event } from '../../../../base/common/event.js';
26
import { experimentsEnabled } from '../../telemetry/common/workbenchTelemetryUtils.js';
27
28
export interface IAssignmentFilter {
29
exclude(assignment: string): boolean;
30
onDidChange: Event<void>;
31
}
32
33
export const IWorkbenchAssignmentService = createDecorator<IWorkbenchAssignmentService>('assignmentService');
34
35
export interface IWorkbenchAssignmentService extends IAssignmentService {
36
getCurrentExperiments(): Promise<string[] | undefined>;
37
addTelemetryAssignmentFilter(filter: IAssignmentFilter): void;
38
}
39
40
class MementoKeyValueStorage implements IKeyValueStorage {
41
42
private readonly mementoObj: Record<string, unknown>;
43
44
constructor(private readonly memento: Memento<Record<string, unknown>>) {
45
this.mementoObj = memento.getMemento(StorageScope.APPLICATION, StorageTarget.MACHINE);
46
}
47
48
async getValue<T>(key: string, defaultValue?: T | undefined): Promise<T | undefined> {
49
const value = await this.mementoObj[key] as T | undefined;
50
51
return value || defaultValue;
52
}
53
54
setValue<T>(key: string, value: T): void {
55
this.mementoObj[key] = value;
56
this.memento.saveMemento();
57
}
58
}
59
60
class WorkbenchAssignmentServiceTelemetry extends Disposable implements IExperimentationTelemetry {
61
62
private readonly _onDidUpdateAssignmentContext = this._register(new Emitter<void>());
63
readonly onDidUpdateAssignmentContext = this._onDidUpdateAssignmentContext.event;
64
65
private _previousAssignmentContext: string | undefined;
66
private _lastAssignmentContext: string | undefined;
67
get assignmentContext(): string[] | undefined {
68
return this._lastAssignmentContext?.split(';');
69
}
70
71
private _assignmentFilters: IAssignmentFilter[] = [];
72
private _assignmentFilterDisposables = this._register(new DisposableStore());
73
74
constructor(
75
private readonly telemetryService: ITelemetryService,
76
private readonly productService: IProductService
77
) {
78
super();
79
}
80
81
private _filterAssignmentContext(assignmentContext: string): string {
82
const assignments = assignmentContext.split(';');
83
84
const filteredAssignments = assignments.filter(assignment => {
85
for (const filter of this._assignmentFilters) {
86
if (filter.exclude(assignment)) {
87
return false;
88
}
89
}
90
return true;
91
});
92
93
return filteredAssignments.join(';');
94
}
95
96
private _setAssignmentContext(value: string): void {
97
const filteredValue = this._filterAssignmentContext(value);
98
this._lastAssignmentContext = filteredValue;
99
this._onDidUpdateAssignmentContext.fire();
100
101
if (this.productService.tasConfig?.assignmentContextTelemetryPropertyName) {
102
this.telemetryService.setExperimentProperty(this.productService.tasConfig.assignmentContextTelemetryPropertyName, filteredValue);
103
}
104
}
105
106
addAssignmentFilter(filter: IAssignmentFilter): void {
107
this._assignmentFilters.push(filter);
108
this._assignmentFilterDisposables.add(filter.onDidChange(() => {
109
if (this._previousAssignmentContext) {
110
this._setAssignmentContext(this._previousAssignmentContext);
111
}
112
}));
113
if (this._previousAssignmentContext) {
114
this._setAssignmentContext(this._previousAssignmentContext);
115
}
116
}
117
118
// __GDPR__COMMON__ "abexp.assignmentcontext" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
119
setSharedProperty(name: string, value: string): void {
120
if (name === this.productService.tasConfig?.assignmentContextTelemetryPropertyName) {
121
this._previousAssignmentContext = value;
122
return this._setAssignmentContext(value);
123
}
124
125
this.telemetryService.setExperimentProperty(name, value);
126
}
127
128
postEvent(eventName: string, props: Map<string, string>): void {
129
const data: ITelemetryData = {};
130
for (const [key, value] of props.entries()) {
131
data[key] = value;
132
}
133
134
/* __GDPR__
135
"query-expfeature" : {
136
"owner": "sbatten",
137
"comment": "Logs queries to the experiment service by feature for metric calculations",
138
"ABExp.queriedFeature": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The experimental feature being queried" }
139
}
140
*/
141
this.telemetryService.publicLog(eventName, data);
142
}
143
}
144
145
export class WorkbenchAssignmentService extends Disposable implements IAssignmentService {
146
147
declare readonly _serviceBrand: undefined;
148
149
private readonly tasClient: Promise<TASClient> | undefined;
150
private readonly tasSetupDisposables = new DisposableStore();
151
152
private networkInitialized = false;
153
private readonly overrideInitDelay: Promise<void>;
154
155
private readonly telemetry: WorkbenchAssignmentServiceTelemetry;
156
private readonly keyValueStorage: IKeyValueStorage;
157
158
private readonly experimentsEnabled: boolean;
159
160
private readonly _onDidRefetchAssignments = this._register(new Emitter<void>());
161
public readonly onDidRefetchAssignments = this._onDidRefetchAssignments.event;
162
163
constructor(
164
@ITelemetryService private readonly telemetryService: ITelemetryService,
165
@IStorageService storageService: IStorageService,
166
@IConfigurationService private readonly configurationService: IConfigurationService,
167
@IProductService private readonly productService: IProductService,
168
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
169
@IInstantiationService private readonly instantiationService: IInstantiationService,
170
) {
171
super();
172
173
this.experimentsEnabled = experimentsEnabled(configurationService, productService, environmentService);
174
175
if (this.experimentsEnabled) {
176
this.tasClient = this.setupTASClient();
177
}
178
179
this.telemetry = this._register(new WorkbenchAssignmentServiceTelemetry(telemetryService, productService));
180
this._register(this.telemetry.onDidUpdateAssignmentContext(() => this._onDidRefetchAssignments.fire()));
181
182
this.keyValueStorage = new MementoKeyValueStorage(new Memento<Record<string, unknown>>('experiment.service.memento', storageService));
183
184
// For development purposes, configure the delay until tas local tas treatment ovverrides are available
185
const overrideDelaySetting = configurationService.getValue('experiments.overrideDelay');
186
const overrideDelay = typeof overrideDelaySetting === 'number' ? overrideDelaySetting : 0;
187
this.overrideInitDelay = timeout(overrideDelay);
188
}
189
190
async getTreatment<T extends string | number | boolean>(name: string): Promise<T | undefined> {
191
const result = await this.doGetTreatment<T>(name);
192
193
type TASClientReadTreatmentData = {
194
treatmentName: string;
195
treatmentValue: string;
196
};
197
198
type TASClientReadTreatmentClassification = {
199
owner: 'sbatten';
200
comment: 'Logged when a treatment value is read from the experiment service';
201
treatmentValue: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The value of the read treatment' };
202
treatmentName: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The name of the treatment that was read' };
203
};
204
205
this.telemetryService.publicLog2<TASClientReadTreatmentData, TASClientReadTreatmentClassification>('tasClientReadTreatmentComplete', {
206
treatmentName: name,
207
treatmentValue: JSON.stringify(result)
208
});
209
210
return result;
211
}
212
213
private async doGetTreatment<T extends string | number | boolean>(name: string): Promise<T | undefined> {
214
await this.overrideInitDelay; // For development purposes, allow overriding tas assignments to test variants locally.
215
216
const override = this.configurationService.getValue<T>(`experiments.override.${name}`);
217
if (override !== undefined) {
218
return override;
219
}
220
221
if (!this.tasClient) {
222
return undefined;
223
}
224
225
if (!this.experimentsEnabled) {
226
return undefined;
227
}
228
229
let result: T | undefined;
230
const client = await this.tasClient;
231
232
// The TAS client is initialized but we need to check if the initial fetch has completed yet
233
// If it is complete, return a cached value for the treatment
234
// If not, use the async call with `checkCache: true`. This will allow the module to return a cached value if it is present.
235
// Otherwise it will await the initial fetch to return the most up to date value.
236
if (this.networkInitialized) {
237
result = client.getTreatmentVariable<T>('vscode', name);
238
} else {
239
result = await client.getTreatmentVariableAsync<T>('vscode', name, true);
240
}
241
242
result = client.getTreatmentVariable<T>('vscode', name);
243
return result;
244
}
245
246
private async setupTASClient(): Promise<TASClient> {
247
this.tasSetupDisposables.clear();
248
249
const targetPopulation = this.productService.quality === 'stable' ?
250
TargetPopulation.Public : (this.productService.quality === 'exploration' ?
251
TargetPopulation.Exploration : TargetPopulation.Insiders);
252
253
const filterProvider = new AssignmentFilterProvider(
254
this.productService.version,
255
this.productService.nameLong,
256
this.telemetryService.machineId,
257
this.telemetryService.devDeviceId,
258
targetPopulation,
259
this.productService.date ?? ''
260
);
261
262
const extensionsFilterProvider = this.instantiationService.createInstance(CopilotAssignmentFilterProvider);
263
this.tasSetupDisposables.add(extensionsFilterProvider);
264
this.tasSetupDisposables.add(extensionsFilterProvider.onDidChangeFilters(() => this.refetchAssignments()));
265
266
const tasConfig = this.productService.tasConfig!;
267
const tasClient = new (await importAMDNodeModule<typeof import('tas-client')>('tas-client', 'dist/tas-client.min.js')).ExperimentationService({
268
filterProviders: [filterProvider, extensionsFilterProvider],
269
telemetry: this.telemetry,
270
storageKey: ASSIGNMENT_STORAGE_KEY,
271
keyValueStorage: this.keyValueStorage,
272
assignmentContextTelemetryPropertyName: tasConfig.assignmentContextTelemetryPropertyName,
273
telemetryEventName: tasConfig.telemetryEventName,
274
endpoint: tasConfig.endpoint,
275
refetchInterval: ASSIGNMENT_REFETCH_INTERVAL,
276
});
277
278
await tasClient.initializePromise;
279
tasClient.initialFetch.then(() => {
280
this.networkInitialized = true;
281
});
282
283
return tasClient;
284
}
285
286
private async refetchAssignments(): Promise<void> {
287
if (!this.tasClient) {
288
return; // Setup has not started, assignments will use latest filters
289
}
290
291
// Await the client to be setup and the initial fetch to complete
292
const tasClient = await this.tasClient;
293
await tasClient.initialFetch;
294
295
// Refresh the assignments
296
await tasClient.getTreatmentVariableAsync('vscode', 'refresh', false);
297
}
298
299
async getCurrentExperiments(): Promise<string[] | undefined> {
300
if (!this.tasClient) {
301
return undefined;
302
}
303
304
if (!this.experimentsEnabled) {
305
return undefined;
306
}
307
308
await this.tasClient;
309
310
return this.telemetry.assignmentContext;
311
}
312
313
addTelemetryAssignmentFilter(filter: IAssignmentFilter): void {
314
this.telemetry.addAssignmentFilter(filter);
315
}
316
}
317
318
registerSingleton(IWorkbenchAssignmentService, WorkbenchAssignmentService, InstantiationType.Delayed);
319
320
const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
321
registry.registerConfiguration({
322
...workbenchConfigurationNodeBase,
323
'properties': {
324
'workbench.enableExperiments': {
325
'type': 'boolean',
326
'description': localize('workbench.enableExperiments', "Fetches experiments to run from a Microsoft online service."),
327
'default': true,
328
'scope': ConfigurationScope.APPLICATION,
329
'restricted': true,
330
'tags': ['usesOnlineServices']
331
}
332
}
333
});
334
335