|
| 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 | + |
0 commit comments