Skip to content

Commit 0640e82

Browse files
author
Kartik Raj
authored
Implemented prompt for survey using A/B test framework (microsoft#7090)
* Add experiment * Added tests * News entry
1 parent a0094d4 commit 0640e82

File tree

5 files changed

+78
-10
lines changed

5 files changed

+78
-10
lines changed

experiments.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,17 @@
2828
"salt": "ShowPlayIcon",
2929
"min": 0,
3030
"max": 20
31+
},
32+
{
33+
"name": "ShowExtensionSurveyPrompt - enabled",
34+
"salt": "ShowExtensionSurveyPrompt",
35+
"max": 100,
36+
"min": 80
37+
},
38+
{
39+
"name": "ShowExtensionSurveyPrompt - control",
40+
"salt": "ShowExtensionSurveyPrompt",
41+
"min": 0,
42+
"max": 20
3143
}
3244
]

news/1 Enhancements/6957.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Implemented prompt for survey using A/B test framework

src/client/activation/extensionSurvey.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55

66
import { inject, injectable, optional } from 'inversify';
77
import { IApplicationShell } from '../common/application/types';
8+
import { ShowExtensionSurveyPrompt } from '../common/experimentGroups';
89
import '../common/extensions';
910
import { traceDecorators } from '../common/logger';
1011
import {
11-
IBrowserService, IPersistentStateFactory, IRandom
12+
IBrowserService, IExperimentsManager, IPersistentStateFactory, IRandom
1213
} from '../common/types';
1314
import { Common, ExtensionSurveyBanner, LanguageService } from '../common/utils/localize';
1415
import { sendTelemetryEvent } from '../telemetry';
@@ -31,10 +32,15 @@ export class ExtensionSurveyPrompt implements IExtensionSingleActivationService
3132
@inject(IBrowserService) private browserService: IBrowserService,
3233
@inject(IPersistentStateFactory) private persistentState: IPersistentStateFactory,
3334
@inject(IRandom) private random: IRandom,
35+
@inject(IExperimentsManager) private experiments: IExperimentsManager,
3436
@optional() private sampleSizePerOneHundredUsers: number = 10,
3537
@optional() private waitTimeToShowSurvey: number = WAIT_TIME_TO_SHOW_SURVEY) { }
3638

3739
public async activate(): Promise<void> {
40+
if (!this.experiments.inExperiment(ShowExtensionSurveyPrompt.enabled)) {
41+
this.experiments.sendTelemetryIfInExperiment(ShowExtensionSurveyPrompt.control);
42+
return;
43+
}
3844
const show = this.shouldShowBanner();
3945
if (!show) {
4046
return;

src/client/common/experimentGroups.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,9 @@ export enum ShowPlayIcon {
1313
icon1 = 'ShowPlayIcon - start',
1414
icon2 = 'ShowPlayIcon - runFile'
1515
}
16+
17+
// Experiment to check whether to show "Extension Survey prompt" or not.
18+
export enum ShowExtensionSurveyPrompt {
19+
control = 'ShowExtensionSurveyPrompt - control',
20+
enabled = 'ShowExtensionSurveyPrompt - enabled'
21+
}

src/test/activation/extensionSurvey.unit.test.ts

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import { anything, instance, mock, verify, when } from 'ts-mockito';
99
import * as TypeMoq from 'typemoq';
1010
import { ExtensionSurveyPrompt, extensionSurveyStateKeys } from '../../client/activation/extensionSurvey';
1111
import { IApplicationShell } from '../../client/common/application/types';
12+
import { ShowExtensionSurveyPrompt } from '../../client/common/experimentGroups';
1213
import { PersistentStateFactory } from '../../client/common/persistentState';
13-
import { IBrowserService, IPersistentState, IPersistentStateFactory, IRandom } from '../../client/common/types';
14+
import { IBrowserService, IExperimentsManager, IPersistentState, IPersistentStateFactory, IRandom } from '../../client/common/types';
1415
import { createDeferred } from '../../client/common/utils/async';
1516
import { Common, ExtensionSurveyBanner, LanguageService } from '../../client/common/utils/localize';
1617
import { sleep } from '../core';
@@ -23,10 +24,12 @@ suite('Extension survey prompt - shouldShowBanner()', () => {
2324
let browserService: TypeMoq.IMock<IBrowserService>;
2425
let random: TypeMoq.IMock<IRandom>;
2526
let persistentStateFactory: IPersistentStateFactory;
27+
let experiments: TypeMoq.IMock<IExperimentsManager>;
2628
let disableSurveyForTime: TypeMoq.IMock<IPersistentState<any>>;
2729
let doNotShowAgain: TypeMoq.IMock<IPersistentState<any>>;
2830
let extensionSurveyPrompt: ExtensionSurveyPrompt;
2931
setup(() => {
32+
experiments = TypeMoq.Mock.ofType<IExperimentsManager>();
3033
appShell = TypeMoq.Mock.ofType<IApplicationShell>();
3134
browserService = TypeMoq.Mock.ofType<IBrowserService>();
3235
random = TypeMoq.Mock.ofType<IRandom>();
@@ -35,7 +38,7 @@ suite('Extension survey prompt - shouldShowBanner()', () => {
3538
doNotShowAgain = TypeMoq.Mock.ofType<IPersistentState<any>>();
3639
when(persistentStateFactory.createGlobalPersistentState(extensionSurveyStateKeys.disableSurveyForTime, false, anything())).thenReturn(disableSurveyForTime.object);
3740
when(persistentStateFactory.createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false)).thenReturn(doNotShowAgain.object);
38-
extensionSurveyPrompt = new ExtensionSurveyPrompt(appShell.object, browserService.object, instance(persistentStateFactory), random.object, 10);
41+
extensionSurveyPrompt = new ExtensionSurveyPrompt(appShell.object, browserService.object, instance(persistentStateFactory), random.object, experiments.object, 10);
3942
});
4043
test('Returns false if do not show again is clicked', async () => {
4144
random
@@ -107,7 +110,7 @@ suite('Extension survey prompt - shouldShowBanner()', () => {
107110
});
108111

109112
test('Always return true if sample size is 100', async () => {
110-
extensionSurveyPrompt = new ExtensionSurveyPrompt(appShell.object, browserService.object, instance(persistentStateFactory), random.object, 100);
113+
extensionSurveyPrompt = new ExtensionSurveyPrompt(appShell.object, browserService.object, instance(persistentStateFactory), random.object, experiments.object, 100);
111114
disableSurveyForTime
112115
.setup(d => d.value)
113116
.returns(() => false);
@@ -124,7 +127,7 @@ suite('Extension survey prompt - shouldShowBanner()', () => {
124127
});
125128

126129
test('Always return false if sample size is 0', async () => {
127-
extensionSurveyPrompt = new ExtensionSurveyPrompt(appShell.object, browserService.object, instance(persistentStateFactory), random.object, 0);
130+
extensionSurveyPrompt = new ExtensionSurveyPrompt(appShell.object, browserService.object, instance(persistentStateFactory), random.object, experiments.object, 0);
128131
disableSurveyForTime
129132
.setup(d => d.value)
130133
.returns(() => false);
@@ -144,6 +147,7 @@ suite('Extension survey prompt - shouldShowBanner()', () => {
144147

145148
// tslint:disable-next-line: max-func-body-length
146149
suite('Extension survey prompt - showSurvey()', () => {
150+
let experiments: TypeMoq.IMock<IExperimentsManager>;
147151
let appShell: TypeMoq.IMock<IApplicationShell>;
148152
let browserService: TypeMoq.IMock<IBrowserService>;
149153
let random: TypeMoq.IMock<IRandom>;
@@ -160,7 +164,8 @@ suite('Extension survey prompt - showSurvey()', () => {
160164
doNotShowAgain = TypeMoq.Mock.ofType<IPersistentState<any>>();
161165
when(persistentStateFactory.createGlobalPersistentState(extensionSurveyStateKeys.disableSurveyForTime, false, anything())).thenReturn(disableSurveyForTime.object);
162166
when(persistentStateFactory.createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false)).thenReturn(doNotShowAgain.object);
163-
extensionSurveyPrompt = new ExtensionSurveyPrompt(appShell.object, browserService.object, instance(persistentStateFactory), random.object, 10);
167+
experiments = TypeMoq.Mock.ofType<IExperimentsManager>();
168+
extensionSurveyPrompt = new ExtensionSurveyPrompt(appShell.object, browserService.object, instance(persistentStateFactory), random.object, experiments.object, 10);
164169
});
165170

166171
test('Launch survey if \'Yes\' option is clicked', async () => {
@@ -280,19 +285,39 @@ suite('Extension survey prompt - activate()', () => {
280285
let persistentStateFactory: IPersistentStateFactory;
281286
let shouldShowBanner: sinon.SinonStub<any>;
282287
let showSurvey: sinon.SinonStub<any>;
288+
let experiments: TypeMoq.IMock<IExperimentsManager>;
283289
let extensionSurveyPrompt: ExtensionSurveyPrompt;
284290
setup(() => {
285291
appShell = TypeMoq.Mock.ofType<IApplicationShell>();
286292
browserService = TypeMoq.Mock.ofType<IBrowserService>();
287293
random = TypeMoq.Mock.ofType<IRandom>();
288294
persistentStateFactory = mock(PersistentStateFactory);
295+
experiments = TypeMoq.Mock.ofType<IExperimentsManager>();
289296
});
290297

291298
teardown(() => {
292299
sinon.restore();
293300
});
294301

295-
test('No survey is shown if shouldShowBanner() returns false', async () => {
302+
test('If user is not in \'ShowExtensionPrompt\' experiment, send telemetry if in control group & return', async () => {
303+
shouldShowBanner = sinon.stub(ExtensionSurveyPrompt.prototype, 'shouldShowBanner');
304+
shouldShowBanner.callsFake(() => false);
305+
showSurvey = sinon.stub(ExtensionSurveyPrompt.prototype, 'showSurvey');
306+
extensionSurveyPrompt = new ExtensionSurveyPrompt(appShell.object, browserService.object, instance(persistentStateFactory), random.object, experiments.object, 10);
307+
experiments
308+
.setup(exp => exp.inExperiment(ShowExtensionSurveyPrompt.enabled))
309+
.returns(() => false)
310+
.verifiable(TypeMoq.Times.once());
311+
experiments
312+
.setup(exp => exp.sendTelemetryIfInExperiment(ShowExtensionSurveyPrompt.control))
313+
.returns(() => undefined)
314+
.verifiable(TypeMoq.Times.once());
315+
await extensionSurveyPrompt.activate();
316+
assert.ok(shouldShowBanner.notCalled);
317+
experiments.verifyAll();
318+
});
319+
320+
test('No survey is shown if shouldShowBanner() returns false and user is in \'ShowExtensionPrompt\' experiment', async () => {
296321
const deferred = createDeferred<true>();
297322
shouldShowBanner = sinon.stub(ExtensionSurveyPrompt.prototype, 'shouldShowBanner');
298323
shouldShowBanner.callsFake(() => false);
@@ -302,16 +327,25 @@ suite('Extension survey prompt - activate()', () => {
302327
return Promise.resolve();
303328
});
304329
// waitTimeToShowSurvey = 50 ms
305-
extensionSurveyPrompt = new ExtensionSurveyPrompt(appShell.object, browserService.object, instance(persistentStateFactory), random.object, 10, 50);
330+
extensionSurveyPrompt = new ExtensionSurveyPrompt(appShell.object, browserService.object, instance(persistentStateFactory), random.object, experiments.object, 10, 50);
331+
experiments
332+
.setup(exp => exp.inExperiment(ShowExtensionSurveyPrompt.enabled))
333+
.returns(() => true)
334+
.verifiable(TypeMoq.Times.once());
335+
experiments
336+
.setup(exp => exp.sendTelemetryIfInExperiment(TypeMoq.It.isAny()))
337+
.returns(() => undefined)
338+
.verifiable(TypeMoq.Times.never());
306339
await extensionSurveyPrompt.activate();
307340
assert.ok(shouldShowBanner.calledOnce);
308341

309342
const doesSurveyShowUp = await Promise.race([deferred.promise, sleep(100).then(() => false)]);
310343
assert.ok(showSurvey.notCalled);
311344
expect(doesSurveyShowUp).to.equal(false, 'Survey should not appear');
345+
experiments.verifyAll();
312346
});
313347

314-
test('Survey is shown after waitTimeToShowSurvey if shouldShowBanner() returns true', async () => {
348+
test('Survey is shown after waitTimeToShowSurvey if shouldShowBanner() returns true and user is in \'ShowExtensionPrompt\' experiment', async () => {
315349
const deferred = createDeferred<true>();
316350
shouldShowBanner = sinon.stub(ExtensionSurveyPrompt.prototype, 'shouldShowBanner');
317351
shouldShowBanner.callsFake(() => true);
@@ -321,12 +355,21 @@ suite('Extension survey prompt - activate()', () => {
321355
return Promise.resolve();
322356
});
323357
// waitTimeToShowSurvey = 50 ms
324-
extensionSurveyPrompt = new ExtensionSurveyPrompt(appShell.object, browserService.object, instance(persistentStateFactory), random.object, 10, 50);
358+
extensionSurveyPrompt = new ExtensionSurveyPrompt(appShell.object, browserService.object, instance(persistentStateFactory), random.object, experiments.object, 10, 50);
359+
experiments
360+
.setup(exp => exp.inExperiment(ShowExtensionSurveyPrompt.enabled))
361+
.returns(() => true)
362+
.verifiable(TypeMoq.Times.once());
363+
experiments
364+
.setup(exp => exp.sendTelemetryIfInExperiment(TypeMoq.It.isAny()))
365+
.returns(() => undefined)
366+
.verifiable(TypeMoq.Times.never());
325367
await extensionSurveyPrompt.activate();
326368
assert.ok(shouldShowBanner.calledOnce);
327369

328370
const doesSurveyShowUp = await Promise.race([deferred.promise, sleep(200).then(() => false)]);
329371
expect(doesSurveyShowUp).to.equal(true, 'Survey should appear');
330372
assert.ok(showSurvey.calledOnce);
373+
experiments.verifyAll();
331374
});
332375
});

0 commit comments

Comments
 (0)