TAS-based experiment service (#103177)
* new experimentation service based on tas-client * fixes for exp service * add event classifications * leverage product.json
This commit is contained in:
parent
a7fdaf4fbc
commit
4cdb2dfa3c
13 changed files with 278 additions and 1 deletions
|
@ -62,6 +62,7 @@
|
|||
"semver-umd": "^5.5.7",
|
||||
"spdlog": "^0.11.1",
|
||||
"sudo-prompt": "9.1.1",
|
||||
"tas-client": "^0.0.950",
|
||||
"v8-inspect-profiler": "^0.0.20",
|
||||
"vscode-nsfw": "1.2.8",
|
||||
"vscode-oniguruma": "1.3.1",
|
||||
|
|
|
@ -552,6 +552,9 @@ export class StandaloneTelemetryService implements ITelemetryService {
|
|||
public setEnabled(value: boolean): void {
|
||||
}
|
||||
|
||||
public setExperimentProperty(name: string, value: string): void {
|
||||
}
|
||||
|
||||
public publicLog(eventName: string, data?: any): Promise<void> {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
|
|
@ -49,6 +49,13 @@ export interface IProductConfiguration {
|
|||
readonly settingsSearchBuildId?: number;
|
||||
readonly settingsSearchUrl?: string;
|
||||
|
||||
readonly tasConfig?: {
|
||||
endpoint: string;
|
||||
telemetryEventName: string;
|
||||
featuresTelemetryPropertyName: string;
|
||||
assignmentContextTelemetryPropertyName: string;
|
||||
};
|
||||
|
||||
readonly experimentsUrl?: string;
|
||||
|
||||
readonly extensionsGallery?: {
|
||||
|
|
|
@ -46,6 +46,8 @@ export interface ITelemetryService {
|
|||
|
||||
getTelemetryInfo(): Promise<ITelemetryInfo>;
|
||||
|
||||
setExperimentProperty(name: string, value: string): void;
|
||||
|
||||
isOptedIn: boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ export class TelemetryService implements ITelemetryService {
|
|||
|
||||
private _appender: ITelemetryAppender;
|
||||
private _commonProperties: Promise<{ [name: string]: any; }>;
|
||||
private _experimentProperties: { [name: string]: string } = {};
|
||||
private _piiPaths: string[];
|
||||
private _userOptIn: boolean;
|
||||
private _enabled: boolean;
|
||||
|
@ -79,6 +80,10 @@ export class TelemetryService implements ITelemetryService {
|
|||
}
|
||||
}
|
||||
|
||||
setExperimentProperty(name: string, value: string): void {
|
||||
this._experimentProperties[name] = value;
|
||||
}
|
||||
|
||||
setEnabled(value: boolean): void {
|
||||
this._enabled = value;
|
||||
}
|
||||
|
@ -119,6 +124,9 @@ export class TelemetryService implements ITelemetryService {
|
|||
// (first) add common properties
|
||||
data = mixin(data, values);
|
||||
|
||||
// (next) add experiment properties
|
||||
data = mixin(data, this._experimentProperties);
|
||||
|
||||
// (last) remove all PII from data
|
||||
data = cloneAndChange(data, value => {
|
||||
if (typeof value === 'string') {
|
||||
|
|
|
@ -28,6 +28,7 @@ export const NullTelemetryService = new class implements ITelemetryService {
|
|||
return this.publicLogError(eventName, data as ITelemetryData);
|
||||
}
|
||||
|
||||
setExperimentProperty() { }
|
||||
setEnabled() { }
|
||||
isOptedIn = true;
|
||||
getTelemetryInfo(): Promise<ITelemetryInfo> {
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
export const ITASExperimentService = createDecorator<ITASExperimentService>('TASExperimentService');
|
||||
|
||||
export interface ITASExperimentService {
|
||||
readonly _serviceBrand: undefined;
|
||||
getTreatment<T extends string | number | boolean>(name: string): Promise<T | undefined>;
|
||||
}
|
|
@ -0,0 +1,209 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { IKeyValueStorage, IExperimentationTelemetry, IExperimentationFilterProvider, ExperimentationService as TASClient } from 'tas-client';
|
||||
import { MementoObject, Memento } from 'vs/workbench/common/memento';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { ITelemetryData } from 'vs/base/common/actions';
|
||||
import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
|
||||
const storageKey = 'VSCode.ABExp.FeatureData';
|
||||
const refetchInterval = 1000 * 60 * 30; // By default it's set up to 30 minutes.
|
||||
|
||||
class MementoKeyValueStorage implements IKeyValueStorage {
|
||||
constructor(private mementoObj: MementoObject) { }
|
||||
|
||||
async getValue<T>(key: string, defaultValue?: T | undefined): Promise<T | undefined> {
|
||||
const value = await this.mementoObj[key];
|
||||
return value || defaultValue;
|
||||
}
|
||||
|
||||
setValue<T>(key: string, value: T): void {
|
||||
this.mementoObj[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
class ExperimentServiceTelemetry implements IExperimentationTelemetry {
|
||||
constructor(private telemetryService: ITelemetryService) { }
|
||||
|
||||
// __GDPR__COMMON__ "VSCode.ABExp.Features" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
||||
// __GDPR__COMMON__ "abexp.assignmentcontext" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
||||
setSharedProperty(name: string, value: string): void {
|
||||
this.telemetryService.setExperimentProperty(name, value);
|
||||
}
|
||||
|
||||
postEvent(eventName: string, props: Map<string, string>): void {
|
||||
const data: ITelemetryData = {};
|
||||
for (const [key, value] of props.entries()) {
|
||||
data[key] = value;
|
||||
}
|
||||
|
||||
/* __GDPR__
|
||||
"query-expfeature" : {
|
||||
"ABExp.queriedFeature": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
this.telemetryService.publicLog(eventName, data);
|
||||
}
|
||||
}
|
||||
|
||||
class ExperimentServiceFilterProvider implements IExperimentationFilterProvider {
|
||||
constructor(
|
||||
private version: string,
|
||||
private appName: string,
|
||||
private machineId: string,
|
||||
private targetPopulation: TargetPopulation
|
||||
) { }
|
||||
|
||||
getFilterValue(filter: string): string | null {
|
||||
switch (filter) {
|
||||
case Filters.ApplicationVersion:
|
||||
return this.version; // productService.version
|
||||
case Filters.Build:
|
||||
return this.appName; // productService.nameLong
|
||||
case Filters.ClientId:
|
||||
return this.machineId;
|
||||
case Filters.Language:
|
||||
return platform.language;
|
||||
case Filters.TargetPopulation:
|
||||
return this.targetPopulation;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
getFilters(): Map<string, any> {
|
||||
let filters: Map<string, any> = new Map<string, any>();
|
||||
let filterValues = Object.values(Filters);
|
||||
for (let value of filterValues) {
|
||||
filters.set(value, this.getFilterValue(value));
|
||||
}
|
||||
|
||||
return filters;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Based upon the official VSCode currently existing filters in the
|
||||
ExP backend for the VSCode cluster.
|
||||
https://experimentation.visualstudio.com/Analysis%20and%20Experimentation/_git/AnE.ExP.TAS.TachyonHost.Configuration?path=%2FConfigurations%2Fvscode%2Fvscode.json&version=GBmaster
|
||||
"X-MSEdge-Market": "detection.market",
|
||||
"X-FD-Corpnet": "detection.corpnet",
|
||||
"X-VSCode–AppVersion": "appversion",
|
||||
"X-VSCode-Build": "build",
|
||||
"X-MSEdge-ClientId": "clientid",
|
||||
"X-VSCode-TargetPopulation": "targetpopulation",
|
||||
"X-VSCode-Language": "language"
|
||||
*/
|
||||
|
||||
enum Filters {
|
||||
/**
|
||||
* The market in which the extension is distributed.
|
||||
*/
|
||||
Market = 'X-MSEdge-Market',
|
||||
|
||||
/**
|
||||
* The corporation network.
|
||||
*/
|
||||
CorpNet = 'X-FD-Corpnet',
|
||||
|
||||
/**
|
||||
* Version of the application which uses experimentation service.
|
||||
*/
|
||||
ApplicationVersion = 'X-VSCode-AppVersion',
|
||||
|
||||
/**
|
||||
* Insiders vs Stable.
|
||||
*/
|
||||
Build = 'X-VSCode-Build',
|
||||
|
||||
/**
|
||||
* Client Id which is used as primary unit for the experimentation.
|
||||
*/
|
||||
ClientId = 'X-MSEdge-ClientId',
|
||||
|
||||
/**
|
||||
* The language in use by VS Code
|
||||
*/
|
||||
Language = 'X-VSCode-Language',
|
||||
|
||||
/**
|
||||
* The target population.
|
||||
* This is used to separate internal, early preview, GA, etc.
|
||||
*/
|
||||
TargetPopulation = 'X-VSCode-TargetPopulation',
|
||||
}
|
||||
|
||||
enum TargetPopulation {
|
||||
Team = 'team',
|
||||
Internal = 'internal',
|
||||
Insiders = 'insider',
|
||||
Public = 'public',
|
||||
}
|
||||
|
||||
export class ExperimentService implements ITASExperimentService {
|
||||
_serviceBrand: undefined;
|
||||
private tasClient: Promise<TASClient> | undefined;
|
||||
private static MEMENTO_ID = 'experiment.service.memento';
|
||||
|
||||
constructor(
|
||||
@IProductService private productService: IProductService,
|
||||
@ITelemetryService private telemetryService: ITelemetryService,
|
||||
@IStorageService private storageService: IStorageService
|
||||
) {
|
||||
|
||||
if (this.productService.tasConfig) {
|
||||
this.tasClient = this.setupTASClient();
|
||||
}
|
||||
}
|
||||
|
||||
async getTreatment<T extends string | number | boolean>(name: string): Promise<T | undefined> {
|
||||
if (!this.tasClient) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (await this.tasClient).getTreatmentVariable<T>('vscode', name);
|
||||
}
|
||||
|
||||
private async setupTASClient(): Promise<TASClient> {
|
||||
const telemetryInfo = await this.telemetryService.getTelemetryInfo();
|
||||
const targetPopulation = telemetryInfo.msftInternal ? TargetPopulation.Internal : (this.productService.quality === 'stable' ? TargetPopulation.Public : TargetPopulation.Insiders);
|
||||
const machineId = telemetryInfo.machineId;
|
||||
const filterProvider = new ExperimentServiceFilterProvider(
|
||||
this.productService.version,
|
||||
this.productService.nameLong,
|
||||
machineId,
|
||||
targetPopulation
|
||||
);
|
||||
|
||||
const memento = new Memento(ExperimentService.MEMENTO_ID, this.storageService);
|
||||
const keyValueStorage = new MementoKeyValueStorage(memento.getMemento(StorageScope.GLOBAL));
|
||||
|
||||
const telemetry = new ExperimentServiceTelemetry(this.telemetryService);
|
||||
|
||||
const tasConfig = this.productService.tasConfig!;
|
||||
const tasClient = new TASClient({
|
||||
filterProviders: [filterProvider],
|
||||
telemetry: telemetry,
|
||||
storageKey: storageKey,
|
||||
keyValueStorage: keyValueStorage,
|
||||
featuresTelemetryPropertyName: tasConfig.featuresTelemetryPropertyName,
|
||||
assignmentContextTelemetryPropertyName: tasConfig.assignmentContextTelemetryPropertyName,
|
||||
telemetryEventName: tasConfig.telemetryEventName,
|
||||
endpoint: tasConfig.endpoint,
|
||||
refetchInterval: refetchInterval,
|
||||
});
|
||||
|
||||
await tasClient.initializePromise;
|
||||
return tasClient;
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(ITASExperimentService, ExperimentService, false);
|
||||
|
|
@ -65,6 +65,10 @@ export class TelemetryService extends Disposable implements ITelemetryService {
|
|||
return this.impl.setEnabled(value);
|
||||
}
|
||||
|
||||
setExperimentProperty(name: string, value: string): void {
|
||||
return this.impl.setExperimentProperty(name, value);
|
||||
}
|
||||
|
||||
get isOptedIn(): boolean {
|
||||
return this.impl.isOptedIn;
|
||||
}
|
||||
|
|
|
@ -57,6 +57,10 @@ export class TelemetryService extends Disposable implements ITelemetryService {
|
|||
return this.impl.setEnabled(value);
|
||||
}
|
||||
|
||||
setExperimentProperty(name: string, value: string): void {
|
||||
return this.impl.setExperimentProperty(name, value);
|
||||
}
|
||||
|
||||
get isOptedIn(): boolean {
|
||||
return this.impl.isOptedIn;
|
||||
}
|
||||
|
|
|
@ -176,6 +176,9 @@ class TestTelemetryService implements ITelemetryService {
|
|||
public setEnabled(value: boolean): void {
|
||||
}
|
||||
|
||||
public setExperimentProperty(name: string, value: string): void {
|
||||
}
|
||||
|
||||
public publicLog(eventName: string, data?: any): Promise<void> {
|
||||
const event = { name: eventName, data: data };
|
||||
this.events.push(event);
|
||||
|
|
|
@ -58,6 +58,7 @@ import 'vs/workbench/services/userDataSync/electron-browser/userDataSyncAccountS
|
|||
import 'vs/workbench/services/sharedProcess/electron-browser/sharedProcessService';
|
||||
import 'vs/workbench/services/localizations/electron-browser/localizationsService';
|
||||
import 'vs/workbench/services/path/electron-browser/pathService';
|
||||
import 'vs/workbench/services/experiment/electron-browser/experimentService';
|
||||
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { ICredentialsService } from 'vs/platform/credentials/common/credentials';
|
||||
|
|
23
yarn.lock
23
yarn.lock
|
@ -1108,6 +1108,13 @@ aws4@^1.8.0:
|
|||
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
|
||||
integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==
|
||||
|
||||
axios@^0.19.0:
|
||||
version "0.19.2"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27"
|
||||
integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==
|
||||
dependencies:
|
||||
follow-redirects "1.5.10"
|
||||
|
||||
azure-storage@^2.10.2:
|
||||
version "2.10.2"
|
||||
resolved "https://registry.yarnpkg.com/azure-storage/-/azure-storage-2.10.2.tgz#3bcabdbf10e72fd0990db81116e49023c4a675b6"
|
||||
|
@ -2371,7 +2378,7 @@ debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3:
|
|||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
debug@3.1.0:
|
||||
debug@3.1.0, debug@=3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
|
||||
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
|
||||
|
@ -3566,6 +3573,13 @@ flush-write-stream@^1.0.0, flush-write-stream@^1.0.2:
|
|||
inherits "^2.0.1"
|
||||
readable-stream "^2.0.4"
|
||||
|
||||
follow-redirects@1.5.10:
|
||||
version "1.5.10"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
|
||||
integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==
|
||||
dependencies:
|
||||
debug "=3.1.0"
|
||||
|
||||
for-in@^0.1.5:
|
||||
version "0.1.5"
|
||||
resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.5.tgz#007374e2b6d5c67420a1479bdb75a04872b738c4"
|
||||
|
@ -9027,6 +9041,13 @@ tar@^4:
|
|||
safe-buffer "^5.1.2"
|
||||
yallist "^3.0.2"
|
||||
|
||||
tas-client@^0.0.950:
|
||||
version "0.0.950"
|
||||
resolved "https://registry.yarnpkg.com/tas-client/-/tas-client-0.0.950.tgz#0fadc684721d5bc6d6af03b09e1ff5a83a5186fc"
|
||||
integrity sha512-AvCNjvfouxJyKln+TsobOBO5KmXklL9+FlxrEPlIgaixy1TxCC2v2Vs/MflCiyHlGl+BeIStP4oAVPqo5c0pIA==
|
||||
dependencies:
|
||||
axios "^0.19.0"
|
||||
|
||||
temp@^0.8.3:
|
||||
version "0.8.3"
|
||||
resolved "https://registry.yarnpkg.com/temp/-/temp-0.8.3.tgz#e0c6bc4d26b903124410e4fed81103014dfc1f59"
|
||||
|
|
Loading…
Reference in a new issue