Skip to content

Commit c821c9e

Browse files
Add support for VS Code's experiment service (#11979)
* Add npm package * News entry * experiments.ts -> experiments/manager.ts * Wrong issue number * eexperimentGroups -> experiments/experimentGroups * Move experiments.unit.tests.ts -> experiments/ * Commit new files * Merge gone sideways * Add types * Add opt in/out handling * Activation (service registry) * Don't pin tas-client to one version * Unit tests + fixes * Lol forgot to remove a comment + add headers * Forgot 'use strict' in service.ts * Use IApplicationEnvironment instead of IExtensions * Apply suggestions from code review Co-authored-by: Don Jayamanne <[email protected]> * Remove unnecessary formatted props * n e v e r m i n d * Aight fixed it * flight -> experiment * Check stub calls instead of ctor impl * removed getExperimentService stub check * Set shared properties for all telemetry events * Add test for shared properties Co-authored-by: Don Jayamanne <[email protected]>
1 parent 5cef0d9 commit c821c9e

File tree

13 files changed

+565
-3
lines changed

13 files changed

+565
-3
lines changed

news/1 Enhancements/10790.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Integrate VS Code experiment framework in the extension.

package-lock.json

Lines changed: 47 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3076,6 +3076,7 @@
30763076
"vscode-languageclient": "^6.2.0-next.2",
30773077
"vscode-languageserver": "^6.2.0-next.2",
30783078
"vscode-languageserver-protocol": "^3.16.0-next.2",
3079+
"vscode-tas-client": "^0.0.757",
30793080
"vsls": "^0.3.1291",
30803081
"winreg": "^1.2.4",
30813082
"winston": "^3.2.1",
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { inject, named } from 'inversify';
7+
import { Memento } from 'vscode';
8+
import { getExperimentationService, IExperimentationService, TargetPopulation } from 'vscode-tas-client';
9+
import { sendTelemetryEvent } from '../../telemetry';
10+
import { EventName } from '../../telemetry/constants';
11+
import { IApplicationEnvironment } from '../application/types';
12+
import { GLOBAL_MEMENTO, IConfigurationService, IExperimentService, IMemento, IPythonSettings } from '../types';
13+
import { ExperimentationTelemetry } from './telemetry';
14+
15+
export class ExperimentService implements IExperimentService {
16+
/**
17+
* Experiments the user requested to opt into manually.
18+
*/
19+
public _optInto: string[] = [];
20+
/**
21+
* Experiments the user requested to opt out from manually.
22+
*/
23+
public _optOutFrom: string[] = [];
24+
25+
private readonly experimentationService?: IExperimentationService;
26+
private readonly settings: IPythonSettings;
27+
28+
constructor(
29+
@inject(IConfigurationService) readonly configurationService: IConfigurationService,
30+
@inject(IApplicationEnvironment) private readonly appEnvironment: IApplicationEnvironment,
31+
@inject(IMemento) @named(GLOBAL_MEMENTO) readonly globalState: Memento
32+
) {
33+
this.settings = configurationService.getSettings(undefined);
34+
35+
// Users can only opt in or out of experiment groups, not control groups.
36+
const optInto = this.settings.experiments.optInto;
37+
const optOutFrom = this.settings.experiments.optOutFrom;
38+
this._optInto = optInto.filter((exp) => !exp.endsWith('control'));
39+
this._optOutFrom = optOutFrom.filter((exp) => !exp.endsWith('control'));
40+
41+
// Don't initialize the experiment service if the extension's experiments setting is disabled.
42+
const enabled = this.settings.experiments.enabled;
43+
if (!enabled) {
44+
return;
45+
}
46+
47+
let targetPopulation: TargetPopulation;
48+
49+
if (this.appEnvironment.channel === 'insiders') {
50+
targetPopulation = TargetPopulation.Insiders;
51+
} else {
52+
targetPopulation = TargetPopulation.Public;
53+
}
54+
55+
const telemetryReporter = new ExperimentationTelemetry();
56+
57+
this.experimentationService = getExperimentationService(
58+
this.appEnvironment.extensionName,
59+
this.appEnvironment.packageJson.version!,
60+
targetPopulation,
61+
telemetryReporter,
62+
globalState
63+
);
64+
}
65+
66+
public async inExperiment(experiment: string): Promise<boolean> {
67+
if (!this.experimentationService) {
68+
return false;
69+
}
70+
71+
// Currently the service doesn't support opting in and out of experiments,
72+
// so we need to perform these checks and send the corresponding telemetry manually.
73+
if (this._optOutFrom.includes('All') || this._optOutFrom.includes(experiment)) {
74+
sendTelemetryEvent(EventName.PYTHON_EXPERIMENTS_OPT_IN_OUT, undefined, {
75+
expNameOptedOutOf: experiment
76+
});
77+
78+
return false;
79+
}
80+
81+
if (this._optInto.includes('All') || this._optInto.includes(experiment)) {
82+
sendTelemetryEvent(EventName.PYTHON_EXPERIMENTS_OPT_IN_OUT, undefined, {
83+
expNameOptedInto: experiment
84+
});
85+
86+
return true;
87+
}
88+
89+
return this.experimentationService.isCachedFlightEnabled(experiment);
90+
}
91+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { IExperimentationTelemetry } from 'vscode-tas-client';
7+
import { sendTelemetryEvent, setSharedProperty } from '../../telemetry';
8+
9+
export class ExperimentationTelemetry implements IExperimentationTelemetry {
10+
public setSharedProperty(name: string, value: string): void {
11+
// Add the shared property to all telemetry being sent, not just events being sent by the experimentation package.
12+
setSharedProperty(name, value);
13+
}
14+
15+
public postEvent(eventName: string, properties: Map<string, string>): void {
16+
const formattedProperties: { [key: string]: string } = {};
17+
properties.forEach((value, key) => {
18+
formattedProperties[key] = value;
19+
});
20+
21+
// tslint:disable-next-line: no-any
22+
sendTelemetryEvent(eventName as any, undefined, formattedProperties);
23+
}
24+
}

src/client/common/serviceRegistry.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33
import { IExtensionSingleActivationService } from '../activation/types';
4-
import { IFileDownloader, IHttpClient, IInterpreterPathService } from '../common/types';
4+
import { IExperimentService, IFileDownloader, IHttpClient, IInterpreterPathService } from '../common/types';
55
import { LiveShareApi } from '../datascience/liveshare/liveshare';
66
import { INotebookExecutionLogger } from '../datascience/types';
77
import { IServiceManager } from '../ioc/types';
@@ -42,6 +42,7 @@ import { ConfigurationService } from './configuration/service';
4242
import { CryptoUtils } from './crypto';
4343
import { EditorUtils } from './editor';
4444
import { ExperimentsManager } from './experiments/manager';
45+
import { ExperimentService } from './experiments/service';
4546
import { FeatureDeprecationManager } from './featureDeprecationManager';
4647
import {
4748
ExtensionInsidersDailyChannelRule,
@@ -149,6 +150,7 @@ export function registerTypes(serviceManager: IServiceManager) {
149150
serviceManager.addSingleton<ILiveShareApi>(ILiveShareApi, LiveShareApi);
150151
serviceManager.addSingleton<ICryptoUtils>(ICryptoUtils, CryptoUtils);
151152
serviceManager.addSingleton<IExperimentsManager>(IExperimentsManager, ExperimentsManager);
153+
serviceManager.addSingleton<IExperimentService>(IExperimentService, ExperimentService);
152154

153155
serviceManager.addSingleton<ITerminalHelper>(ITerminalHelper, TerminalHelper);
154156
serviceManager.addSingleton<ITerminalActivationCommandProvider>(

src/client/common/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,14 @@ export interface IExperimentsManager {
618618
sendTelemetryIfInExperiment(experimentName: string): void;
619619
}
620620

621+
/**
622+
* Experiment service leveraging VS Code's experiment framework.
623+
*/
624+
export const IExperimentService = Symbol('IExperimentService');
625+
export interface IExperimentService {
626+
inExperiment(experimentName: string): Promise<boolean>;
627+
}
628+
621629
export type InterpreterConfigurationScope = { uri: Resource; configTarget: ConfigurationTarget };
622630
export type InspectInterpreterSettingType = {
623631
globalValue?: string;

src/client/telemetry/index.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,24 @@ export function isTelemetryDisabled(workspaceService: IWorkspaceService): boolea
5858
return settings.globalValue === false ? true : false;
5959
}
6060

61+
// Shared properties set by the IExperimentationTelemetry implementation.
62+
const sharedProperties: Record<string, string> = {};
63+
/**
64+
* Set shared properties for all telemetry events.
65+
*/
66+
export function setSharedProperty(name: string, value: string): void {
67+
sharedProperties[name] = value;
68+
}
69+
70+
/**
71+
* Reset shared properties for testing purposes.
72+
*/
73+
export function _resetSharedProperties(): void {
74+
for (const key of Object.keys(sharedProperties)) {
75+
delete sharedProperties[key];
76+
}
77+
}
78+
6179
let telemetryReporter: TelemetryReporter | undefined;
6280
function getTelemetryReporter() {
6381
if (!isTestExecution() && telemetryReporter) {
@@ -123,6 +141,9 @@ export function sendTelemetryEvent<P extends IEventNamePropertyMapping, E extend
123141
});
124142
}
125143

144+
// Add shared properties to telemetry props (we may overwrite existing ones).
145+
Object.assign(customProperties, sharedProperties);
146+
126147
reporter.sendTelemetryEvent(eventNameSent, customProperties, measures);
127148
}
128149

0 commit comments

Comments
 (0)