Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial assignment service work #136238

Merged
merged 4 commits into from Nov 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion .eslintrc.json
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
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
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
@@ -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
@@ -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
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