Skip to content

Commit

Permalink
Initial assignment service work (#136238)
Browse files Browse the repository at this point in the history
* Initial assignment service work

* Modify useragent for experiment

* minor refactor

* remove extraneous comment

Co-authored-by: SteVen Batten <sbatten@microsoft.com>
  • Loading branch information
lramos15 and sbatten committed Nov 1, 2021
1 parent 1775f26 commit 78ea034
Show file tree
Hide file tree
Showing 7 changed files with 260 additions and 119 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,8 @@
"vs/nls",
"**/vs/base/common/**",
"**/vs/base/parts/*/common/**",
"**/vs/platform/*/common/**"
"**/vs/platform/*/common/**",
"tas-client-umd"
]
},
{
Expand Down
3 changes: 2 additions & 1 deletion src/tsconfig.monaco.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
],
"exclude": [
"node_modules/*",
"vs/platform/files/browser/htmlFileSystemProvider.ts"
"vs/platform/files/browser/htmlFileSystemProvider.ts",
"vs/platform/assignment/*"
]
}
15 changes: 14 additions & 1 deletion src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ import { SharedProcessTunnelService } from 'vs/platform/remote/node/sharedProces
import { ipcSharedProcessWorkerChannelName, ISharedProcessWorkerConfiguration, ISharedProcessWorkerService } from 'vs/platform/sharedProcess/common/sharedProcessWorkerService';
import { SharedProcessWorkerService } from 'vs/platform/sharedProcess/electron-browser/sharedProcessWorkerService';
import { IUserConfigurationFileService, UserConfigurationFileServiceId } from 'vs/platform/configuration/common/userConfigurationFileService';
import { AssignmentService } from 'vs/platform/assignment/common/assignmentService';

class SharedProcessMain extends Disposable {

Expand Down Expand Up @@ -240,6 +241,9 @@ class SharedProcessMain extends Disposable {
const activeWindowRouter = new StaticRouter(ctx => activeWindowManager.getActiveClientId().then(id => ctx === id));
services.set(IExtensionRecommendationNotificationService, new ExtensionRecommendationNotificationServiceChannelClient(this.server.getChannel('extensionRecommendationNotification', activeWindowRouter)));

// Assignment Service (Experiment service w/out scorecards)
const assignmentService = new AssignmentService(this.configuration.machineId, configurationService, productService);

// Telemetry
let telemetryService: ITelemetryService;
const appenders: ITelemetryAppender[] = [];
Expand All @@ -250,7 +254,16 @@ class SharedProcessMain extends Disposable {

// Application Insights
if (productService.aiConfig && productService.aiConfig.asimovKey) {
const appInsightsAppender = new AppInsightsAppender('monacoworkbench', null, productService.aiConfig.asimovKey);
const testCollector = await assignmentService.getTreatment<boolean>('vscode.telemetryMigration') ?? false;
const insiders = productService.quality !== 'stable';
// Insiders send to both collector and vortex if assigned.
// Stable only send to one
if (insiders && testCollector) {
const collectorAppender = new AppInsightsAppender('monacoworkbench', null, productService.aiConfig.asimovKey, testCollector, true);
this._register(toDisposable(() => collectorAppender.flush())); // Ensure the AI appender is disposed so that it flushes remaining data
appenders.push(collectorAppender);
}
const appInsightsAppender = new AppInsightsAppender('monacoworkbench', null, productService.aiConfig.asimovKey, insiders ? false : testCollector);
this._register(toDisposable(() => appInsightsAppender.flush())); // Ensure the AI appender is disposed so that it flushes remaining data
appenders.push(appInsightsAppender);
}
Expand Down
116 changes: 116 additions & 0 deletions src/vs/platform/assignment/common/assignment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*---------------------------------------------------------------------------------------------
* 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 { IExperimentationFilterProvider } from 'tas-client-umd';

export const ASSIGNMENT_STORAGE_KEY = 'VSCode.ABExp.FeatureData';
export const ASSIGNMENT_REFETCH_INTERVAL = 0; // no polling

export interface IAssignmentService {
readonly _serviceBrand: undefined;
getTreatment<T extends string | number | boolean>(name: string): Promise<T | undefined>;
}

export enum TargetPopulation {
Team = 'team',
Internal = 'internal',
Insiders = 'insider',
Public = 'public',
}

/*
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-ExtensionName": "extensionname",
"X-VSCode-TargetPopulation": "targetpopulation",
"X-VSCode-Language": "language"
*/
export 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',

/**
* Extension header.
*/
ExtensionName = 'X-VSCode-ExtensionName',

/**
* 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',
}

export class AssignmentFilterProvider 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.ExtensionName:
return 'vscode-core'; // always return vscode-core for exp service
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;
}
}
110 changes: 110 additions & 0 deletions src/vs/platform/assignment/common/assignmentService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import type { IExperimentationTelemetry, ExperimentationService as TASClient } from 'tas-client-umd';
import { TelemetryLevel } from 'vs/platform/telemetry/common/telemetry';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IProductService } from 'vs/platform/product/common/productService';
import { getTelemetryLevel } from 'vs/platform/telemetry/common/telemetryUtils';
import { AssignmentFilterProvider, ASSIGNMENT_REFETCH_INTERVAL, ASSIGNMENT_STORAGE_KEY, IAssignmentService, TargetPopulation } from 'vs/platform/assignment/common/assignment';

class AssignmentServiceTelemetry implements IExperimentationTelemetry {
constructor(
) { }

setSharedProperty(name: string, value: string): void {
// noop due to lack of telemetry service
}

postEvent(eventName: string, props: Map<string, string>): void {
// noop due to lack of telemetry service
}
}

export class AssignmentService implements IAssignmentService {
_serviceBrand: undefined;
private tasClient: Promise<TASClient> | undefined;
private telemetry: AssignmentServiceTelemetry | undefined;
private networkInitialized = false;

private overrideInitDelay: Promise<void>;

private get experimentsEnabled(): boolean {
return this.configurationService.getValue('workbench.enableExperiments') === true;
}

constructor(
private readonly machineId: string,
@IConfigurationService private configurationService: IConfigurationService,
@IProductService private productService: IProductService
) {

if (productService.tasConfig && this.experimentsEnabled && getTelemetryLevel(this.configurationService) === TelemetryLevel.USAGE) {
this.tasClient = this.setupTASClient();
}

// For development purposes, configure the delay until tas local tas treatment ovverrides are available
const overrideDelaySetting = this.configurationService.getValue('experiments.overrideDelay');
const overrideDelay = typeof overrideDelaySetting === 'number' ? overrideDelaySetting : 0;
this.overrideInitDelay = new Promise(resolve => setTimeout(resolve, overrideDelay));
}

async getTreatment<T extends string | number | boolean>(name: string): Promise<T | undefined> {
// For development purposes, allow overriding tas assignments to test variants locally.
await this.overrideInitDelay;
const override = this.configurationService.getValue<T>('experiments.override.' + name);
if (override !== undefined) {
return override;
}

if (!this.tasClient) {
return undefined;
}

if (!this.experimentsEnabled) {
return undefined;
}

let result: T | undefined;
const client = await this.tasClient;
if (this.networkInitialized) {
result = client.getTreatmentVariable<T>('vscode', name);
} else {
result = await client.getTreatmentVariableAsync<T>('vscode', name, true);
}
return result;
}

private async setupTASClient(): Promise<TASClient> {
const targetPopulation = this.productService.quality === 'stable' ? TargetPopulation.Public : TargetPopulation.Insiders;
const machineId = this.machineId;
const filterProvider = new AssignmentFilterProvider(
this.productService.version,
this.productService.nameLong,
machineId,
targetPopulation
);

this.telemetry = new AssignmentServiceTelemetry();

const tasConfig = this.productService.tasConfig!;
const tasClient = new (await import('tas-client-umd')).ExperimentationService({
filterProviders: [filterProvider],
telemetry: this.telemetry,
storageKey: ASSIGNMENT_STORAGE_KEY,
keyValueStorage: undefined,
featuresTelemetryPropertyName: tasConfig.featuresTelemetryPropertyName,
assignmentContextTelemetryPropertyName: tasConfig.assignmentContextTelemetryPropertyName,
telemetryEventName: tasConfig.telemetryEventName,
endpoint: tasConfig.endpoint,
refetchInterval: ASSIGNMENT_REFETCH_INTERVAL,
});

await tasClient.initializePromise;

tasClient.initialFetch.then(() => this.networkInitialized = true);
return tasClient;
}
}
12 changes: 9 additions & 3 deletions src/vs/platform/telemetry/node/appInsightsAppender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { onUnexpectedError } from 'vs/base/common/errors';
import { mixin } from 'vs/base/common/objects';
import { ITelemetryAppender, validateTelemetryData } from 'vs/platform/telemetry/common/telemetryUtils';

async function getClient(aiKey: string): Promise<TelemetryClient> {
async function getClient(aiKey: string, testCollector: boolean): Promise<TelemetryClient> {
const appInsights = await import('applicationinsights');
let client: TelemetryClient;
if (appInsights.defaultClient) {
Expand All @@ -29,7 +29,7 @@ async function getClient(aiKey: string): Promise<TelemetryClient> {
}

if (aiKey.indexOf('AIF-') === 0) {
client.config.endpointUrl = 'https://vortex.data.microsoft.com/collect/v1';
client.config.endpointUrl = testCollector ? 'https://mobile.events.data.microsoft.com/collect/v1' : 'https://vortex.data.microsoft.com/collect/v1';
}
return client;
}
Expand All @@ -44,6 +44,8 @@ export class AppInsightsAppender implements ITelemetryAppender {
private _eventPrefix: string,
private _defaultData: { [key: string]: any } | null,
aiKeyOrClientFactory: string | (() => TelemetryClient), // allow factory function for testing
private readonly testCollector?: boolean,
private readonly mirrored?: boolean
) {
if (!this._defaultData) {
this._defaultData = Object.create(null);
Expand All @@ -68,7 +70,7 @@ export class AppInsightsAppender implements ITelemetryAppender {
}

if (!this._asyncAIClient) {
this._asyncAIClient = getClient(this._aiClient);
this._asyncAIClient = getClient(this._aiClient, this.testCollector ?? false);
}

this._asyncAIClient.then(
Expand All @@ -89,6 +91,10 @@ export class AppInsightsAppender implements ITelemetryAppender {
data = mixin(data, this._defaultData);
data = validateTelemetryData(data);

if (this.testCollector) {
data.properties['common.useragent'] = this.mirrored ? 'mirror-collector++' : 'collector++';
}

this._withAIClient((aiClient) => aiClient.trackEvent({
name: this._eventPrefix + '/' + eventName,
properties: data.properties,
Expand Down

0 comments on commit 78ea034

Please sign in to comment.