Skip to content

Commit 4cdb2df

Browse files
authored
TAS-based experiment service (microsoft#103177)
* new experimentation service based on tas-client * fixes for exp service * add event classifications * leverage product.json
1 parent a7fdaf4 commit 4cdb2df

13 files changed

Lines changed: 278 additions & 1 deletion

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
"semver-umd": "^5.5.7",
6363
"spdlog": "^0.11.1",
6464
"sudo-prompt": "9.1.1",
65+
"tas-client": "^0.0.950",
6566
"v8-inspect-profiler": "^0.0.20",
6667
"vscode-nsfw": "1.2.8",
6768
"vscode-oniguruma": "1.3.1",

src/vs/editor/standalone/browser/simpleServices.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,9 @@ export class StandaloneTelemetryService implements ITelemetryService {
552552
public setEnabled(value: boolean): void {
553553
}
554554

555+
public setExperimentProperty(name: string, value: string): void {
556+
}
557+
555558
public publicLog(eventName: string, data?: any): Promise<void> {
556559
return Promise.resolve(undefined);
557560
}

src/vs/platform/product/common/productService.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ export interface IProductConfiguration {
4949
readonly settingsSearchBuildId?: number;
5050
readonly settingsSearchUrl?: string;
5151

52+
readonly tasConfig?: {
53+
endpoint: string;
54+
telemetryEventName: string;
55+
featuresTelemetryPropertyName: string;
56+
assignmentContextTelemetryPropertyName: string;
57+
};
58+
5259
readonly experimentsUrl?: string;
5360

5461
readonly extensionsGallery?: {

src/vs/platform/telemetry/common/telemetry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ export interface ITelemetryService {
4646

4747
getTelemetryInfo(): Promise<ITelemetryInfo>;
4848

49+
setExperimentProperty(name: string, value: string): void;
50+
4951
isOptedIn: boolean;
5052
}
5153

src/vs/platform/telemetry/common/telemetryService.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export class TelemetryService implements ITelemetryService {
3131

3232
private _appender: ITelemetryAppender;
3333
private _commonProperties: Promise<{ [name: string]: any; }>;
34+
private _experimentProperties: { [name: string]: string } = {};
3435
private _piiPaths: string[];
3536
private _userOptIn: boolean;
3637
private _enabled: boolean;
@@ -79,6 +80,10 @@ export class TelemetryService implements ITelemetryService {
7980
}
8081
}
8182

83+
setExperimentProperty(name: string, value: string): void {
84+
this._experimentProperties[name] = value;
85+
}
86+
8287
setEnabled(value: boolean): void {
8388
this._enabled = value;
8489
}
@@ -119,6 +124,9 @@ export class TelemetryService implements ITelemetryService {
119124
// (first) add common properties
120125
data = mixin(data, values);
121126

127+
// (next) add experiment properties
128+
data = mixin(data, this._experimentProperties);
129+
122130
// (last) remove all PII from data
123131
data = cloneAndChange(data, value => {
124132
if (typeof value === 'string') {

src/vs/platform/telemetry/common/telemetryUtils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export const NullTelemetryService = new class implements ITelemetryService {
2828
return this.publicLogError(eventName, data as ITelemetryData);
2929
}
3030

31+
setExperimentProperty() { }
3132
setEnabled() { }
3233
isOptedIn = true;
3334
getTelemetryInfo(): Promise<ITelemetryInfo> {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
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 { createDecorator } from 'vs/platform/instantiation/common/instantiation';
7+
8+
export const ITASExperimentService = createDecorator<ITASExperimentService>('TASExperimentService');
9+
10+
export interface ITASExperimentService {
11+
readonly _serviceBrand: undefined;
12+
getTreatment<T extends string | number | boolean>(name: string): Promise<T | undefined>;
13+
}
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
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 * as platform from 'vs/base/common/platform';
7+
import { IKeyValueStorage, IExperimentationTelemetry, IExperimentationFilterProvider, ExperimentationService as TASClient } from 'tas-client';
8+
import { MementoObject, Memento } from 'vs/workbench/common/memento';
9+
import { IProductService } from 'vs/platform/product/common/productService';
10+
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
11+
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
12+
import { ITelemetryData } from 'vs/base/common/actions';
13+
import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService';
14+
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
15+
16+
const storageKey = 'VSCode.ABExp.FeatureData';
17+
const refetchInterval = 1000 * 60 * 30; // By default it's set up to 30 minutes.
18+
19+
class MementoKeyValueStorage implements IKeyValueStorage {
20+
constructor(private mementoObj: MementoObject) { }
21+
22+
async getValue<T>(key: string, defaultValue?: T | undefined): Promise<T | undefined> {
23+
const value = await this.mementoObj[key];
24+
return value || defaultValue;
25+
}
26+
27+
setValue<T>(key: string, value: T): void {
28+
this.mementoObj[key] = value;
29+
}
30+
}
31+
32+
class ExperimentServiceTelemetry implements IExperimentationTelemetry {
33+
constructor(private telemetryService: ITelemetryService) { }
34+
35+
// __GDPR__COMMON__ "VSCode.ABExp.Features" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
36+
// __GDPR__COMMON__ "abexp.assignmentcontext" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
37+
setSharedProperty(name: string, value: string): void {
38+
this.telemetryService.setExperimentProperty(name, value);
39+
}
40+
41+
postEvent(eventName: string, props: Map<string, string>): void {
42+
const data: ITelemetryData = {};
43+
for (const [key, value] of props.entries()) {
44+
data[key] = value;
45+
}
46+
47+
/* __GDPR__
48+
"query-expfeature" : {
49+
"ABExp.queriedFeature": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
50+
}
51+
*/
52+
this.telemetryService.publicLog(eventName, data);
53+
}
54+
}
55+
56+
class ExperimentServiceFilterProvider implements IExperimentationFilterProvider {
57+
constructor(
58+
private version: string,
59+
private appName: string,
60+
private machineId: string,
61+
private targetPopulation: TargetPopulation
62+
) { }
63+
64+
getFilterValue(filter: string): string | null {
65+
switch (filter) {
66+
case Filters.ApplicationVersion:
67+
return this.version; // productService.version
68+
case Filters.Build:
69+
return this.appName; // productService.nameLong
70+
case Filters.ClientId:
71+
return this.machineId;
72+
case Filters.Language:
73+
return platform.language;
74+
case Filters.TargetPopulation:
75+
return this.targetPopulation;
76+
default:
77+
return '';
78+
}
79+
}
80+
81+
getFilters(): Map<string, any> {
82+
let filters: Map<string, any> = new Map<string, any>();
83+
let filterValues = Object.values(Filters);
84+
for (let value of filterValues) {
85+
filters.set(value, this.getFilterValue(value));
86+
}
87+
88+
return filters;
89+
}
90+
}
91+
92+
/*
93+
Based upon the official VSCode currently existing filters in the
94+
ExP backend for the VSCode cluster.
95+
https://experimentation.visualstudio.com/Analysis%20and%20Experimentation/_git/AnE.ExP.TAS.TachyonHost.Configuration?path=%2FConfigurations%2Fvscode%2Fvscode.json&version=GBmaster
96+
"X-MSEdge-Market": "detection.market",
97+
"X-FD-Corpnet": "detection.corpnet",
98+
"X-VSCode–AppVersion": "appversion",
99+
"X-VSCode-Build": "build",
100+
"X-MSEdge-ClientId": "clientid",
101+
"X-VSCode-TargetPopulation": "targetpopulation",
102+
"X-VSCode-Language": "language"
103+
*/
104+
105+
enum Filters {
106+
/**
107+
* The market in which the extension is distributed.
108+
*/
109+
Market = 'X-MSEdge-Market',
110+
111+
/**
112+
* The corporation network.
113+
*/
114+
CorpNet = 'X-FD-Corpnet',
115+
116+
/**
117+
* Version of the application which uses experimentation service.
118+
*/
119+
ApplicationVersion = 'X-VSCode-AppVersion',
120+
121+
/**
122+
* Insiders vs Stable.
123+
*/
124+
Build = 'X-VSCode-Build',
125+
126+
/**
127+
* Client Id which is used as primary unit for the experimentation.
128+
*/
129+
ClientId = 'X-MSEdge-ClientId',
130+
131+
/**
132+
* The language in use by VS Code
133+
*/
134+
Language = 'X-VSCode-Language',
135+
136+
/**
137+
* The target population.
138+
* This is used to separate internal, early preview, GA, etc.
139+
*/
140+
TargetPopulation = 'X-VSCode-TargetPopulation',
141+
}
142+
143+
enum TargetPopulation {
144+
Team = 'team',
145+
Internal = 'internal',
146+
Insiders = 'insider',
147+
Public = 'public',
148+
}
149+
150+
export class ExperimentService implements ITASExperimentService {
151+
_serviceBrand: undefined;
152+
private tasClient: Promise<TASClient> | undefined;
153+
private static MEMENTO_ID = 'experiment.service.memento';
154+
155+
constructor(
156+
@IProductService private productService: IProductService,
157+
@ITelemetryService private telemetryService: ITelemetryService,
158+
@IStorageService private storageService: IStorageService
159+
) {
160+
161+
if (this.productService.tasConfig) {
162+
this.tasClient = this.setupTASClient();
163+
}
164+
}
165+
166+
async getTreatment<T extends string | number | boolean>(name: string): Promise<T | undefined> {
167+
if (!this.tasClient) {
168+
return undefined;
169+
}
170+
171+
return (await this.tasClient).getTreatmentVariable<T>('vscode', name);
172+
}
173+
174+
private async setupTASClient(): Promise<TASClient> {
175+
const telemetryInfo = await this.telemetryService.getTelemetryInfo();
176+
const targetPopulation = telemetryInfo.msftInternal ? TargetPopulation.Internal : (this.productService.quality === 'stable' ? TargetPopulation.Public : TargetPopulation.Insiders);
177+
const machineId = telemetryInfo.machineId;
178+
const filterProvider = new ExperimentServiceFilterProvider(
179+
this.productService.version,
180+
this.productService.nameLong,
181+
machineId,
182+
targetPopulation
183+
);
184+
185+
const memento = new Memento(ExperimentService.MEMENTO_ID, this.storageService);
186+
const keyValueStorage = new MementoKeyValueStorage(memento.getMemento(StorageScope.GLOBAL));
187+
188+
const telemetry = new ExperimentServiceTelemetry(this.telemetryService);
189+
190+
const tasConfig = this.productService.tasConfig!;
191+
const tasClient = new TASClient({
192+
filterProviders: [filterProvider],
193+
telemetry: telemetry,
194+
storageKey: storageKey,
195+
keyValueStorage: keyValueStorage,
196+
featuresTelemetryPropertyName: tasConfig.featuresTelemetryPropertyName,
197+
assignmentContextTelemetryPropertyName: tasConfig.assignmentContextTelemetryPropertyName,
198+
telemetryEventName: tasConfig.telemetryEventName,
199+
endpoint: tasConfig.endpoint,
200+
refetchInterval: refetchInterval,
201+
});
202+
203+
await tasClient.initializePromise;
204+
return tasClient;
205+
}
206+
}
207+
208+
registerSingleton(ITASExperimentService, ExperimentService, false);
209+

src/vs/workbench/services/telemetry/browser/telemetryService.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ export class TelemetryService extends Disposable implements ITelemetryService {
6565
return this.impl.setEnabled(value);
6666
}
6767

68+
setExperimentProperty(name: string, value: string): void {
69+
return this.impl.setExperimentProperty(name, value);
70+
}
71+
6872
get isOptedIn(): boolean {
6973
return this.impl.isOptedIn;
7074
}

src/vs/workbench/services/telemetry/electron-browser/telemetryService.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ export class TelemetryService extends Disposable implements ITelemetryService {
5757
return this.impl.setEnabled(value);
5858
}
5959

60+
setExperimentProperty(name: string, value: string): void {
61+
return this.impl.setExperimentProperty(name, value);
62+
}
63+
6064
get isOptedIn(): boolean {
6165
return this.impl.isOptedIn;
6266
}

0 commit comments

Comments
 (0)