forked from DonJayamanne/pythonVSCode
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add data science survey banner (#3224)
* Add data science survey banner * fix hygiene that didn't show up on local run * don't await on survey call
- Loading branch information
1 parent
cd78a30
commit 044f768
Showing
7 changed files
with
269 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
'use strict'; | ||
|
||
import { inject, injectable } from 'inversify'; | ||
import { IApplicationShell } from '../common/application/types'; | ||
import '../common/extensions'; | ||
import { | ||
IBrowserService, IPersistentStateFactory, | ||
IPythonExtensionBanner | ||
} from '../common/types'; | ||
import * as localize from '../common/utils/localize'; | ||
|
||
export enum DSSurveyStateKeys { | ||
ShowBanner = 'ShowDSSurveyBanner', | ||
ShowAttemptCounter = 'DSSurveyShowAttempt' | ||
} | ||
|
||
enum DSSurveyLabelIndex { | ||
Yes, | ||
No | ||
} | ||
|
||
@injectable() | ||
export class DataScienceSurveyBanner implements IPythonExtensionBanner { | ||
private disabledInCurrentSession: boolean = false; | ||
private isInitialized: boolean = false; | ||
private bannerMessage: string = localize.DataScienceSurveyBanner.bannerMessage(); | ||
private bannerLabels: string[] = [localize.DataScienceSurveyBanner.bannerLabelYes(), localize.DataScienceSurveyBanner.bannerLabelNo()]; | ||
private readonly commandThreshold: number; | ||
private readonly surveyLink: string; | ||
|
||
constructor( | ||
@inject(IApplicationShell) private appShell: IApplicationShell, | ||
@inject(IPersistentStateFactory) private persistentState: IPersistentStateFactory, | ||
@inject(IBrowserService) private browserService: IBrowserService, | ||
commandThreshold: number = 500, | ||
surveyLink: string = 'https://aka.ms/pyaisurvey') { | ||
this.commandThreshold = commandThreshold; | ||
this.surveyLink = surveyLink; | ||
this.initialize(); | ||
} | ||
|
||
public initialize(): void { | ||
if (this.isInitialized) { | ||
return; | ||
} | ||
this.isInitialized = true; | ||
} | ||
|
||
public get optionLabels(): string[] { | ||
return this.bannerLabels; | ||
} | ||
|
||
public get shownCount(): Promise<number> { | ||
return this.getPythonDSCommandCounter(); | ||
} | ||
|
||
public get enabled(): boolean { | ||
return this.persistentState.createGlobalPersistentState<boolean>(DSSurveyStateKeys.ShowBanner, true).value; | ||
} | ||
|
||
public async showBanner(): Promise<void> { | ||
if (!this.enabled || this.disabledInCurrentSession) { | ||
return; | ||
} | ||
|
||
const launchCounter: number = await this.incrementPythonDataScienceCommandCounter(); | ||
const show = await this.shouldShowBanner(launchCounter); | ||
if (!show) { | ||
return; | ||
} | ||
|
||
const response = await this.appShell.showInformationMessage(this.bannerMessage, ...this.bannerLabels); | ||
switch (response) { | ||
case this.bannerLabels[DSSurveyLabelIndex.Yes]: | ||
{ | ||
await this.launchSurvey(); | ||
await this.disable(); | ||
break; | ||
} | ||
case this.bannerLabels[DSSurveyLabelIndex.No]: { | ||
await this.disable(); | ||
break; | ||
} | ||
default: { | ||
// Disable for the current session. | ||
this.disabledInCurrentSession = true; | ||
} | ||
} | ||
} | ||
|
||
public async shouldShowBanner(launchCounter?: number): Promise<boolean> { | ||
if (!this.enabled || this.disabledInCurrentSession) { | ||
return false; | ||
} | ||
|
||
if (!launchCounter) { | ||
launchCounter = await this.getPythonDSCommandCounter(); | ||
} | ||
|
||
return launchCounter >= this.commandThreshold; | ||
} | ||
|
||
public async disable(): Promise<void> { | ||
await this.persistentState.createGlobalPersistentState<boolean>(DSSurveyStateKeys.ShowBanner, false).updateValue(false); | ||
} | ||
|
||
public async launchSurvey(): Promise<void> { | ||
this.browserService.launch(this.surveyLink); | ||
} | ||
|
||
private async getPythonDSCommandCounter(): Promise<number> { | ||
const state = this.persistentState.createGlobalPersistentState<number>(DSSurveyStateKeys.ShowAttemptCounter, 0); | ||
return state.value; | ||
} | ||
|
||
private async incrementPythonDataScienceCommandCounter(): Promise<number> { | ||
const state = this.persistentState.createGlobalPersistentState<number>(DSSurveyStateKeys.ShowAttemptCounter, 0); | ||
await state.updateValue(state.value + 1); | ||
return state.value; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
111 changes: 111 additions & 0 deletions
111
src/test/datascience/datascienceSurveyBanner.unit.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
'use strict'; | ||
|
||
// tslint:disable:no-any max-func-body-length | ||
|
||
import { expect } from 'chai'; | ||
import * as typemoq from 'typemoq'; | ||
import { IApplicationShell } from '../../client/common/application/types'; | ||
import { IBrowserService, IPersistentState, IPersistentStateFactory } from '../../client/common/types'; | ||
import { DataScienceSurveyBanner, DSSurveyStateKeys } from '../../client/datascience/dataScienceSurveyBanner'; | ||
|
||
suite('Data Science Survey Banner', () => { | ||
let appShell: typemoq.IMock<IApplicationShell>; | ||
let browser: typemoq.IMock<IBrowserService>; | ||
const targetUri: string = 'https://microsoft.com'; | ||
|
||
const message = 'Can you please take 2 minutes to tell us how the Python Data Science features are working for you?'; | ||
const yes = 'Yes, take survey now'; | ||
const no = 'No, thanks'; | ||
|
||
setup(() => { | ||
appShell = typemoq.Mock.ofType<IApplicationShell>(); | ||
browser = typemoq.Mock.ofType<IBrowserService>(); | ||
}); | ||
test('Data science banner should be enabled after we hit our command execution count', async () => { | ||
const enabledValue: boolean = true; | ||
const attemptCounter: number = 1000; | ||
const testBanner: DataScienceSurveyBanner = preparePopup(attemptCounter, enabledValue, 0, appShell.object, browser.object, targetUri); | ||
const expectedUri: string = targetUri; | ||
let receivedUri: string = ''; | ||
browser.setup(b => b.launch( | ||
typemoq.It.is((a: string) => { | ||
receivedUri = a; | ||
return a === expectedUri; | ||
})) | ||
).verifiable(typemoq.Times.once()); | ||
await testBanner.launchSurvey(); | ||
// This is technically not necessary, but it gives | ||
// better output than the .verifyAll messages do. | ||
expect(receivedUri).is.equal(expectedUri, 'Uri given to launch mock is incorrect.'); | ||
|
||
// verify that the calls expected were indeed made. | ||
browser.verifyAll(); | ||
browser.reset(); | ||
}); | ||
test('Do not show data science banner when it is disabled', () => { | ||
appShell.setup(a => a.showInformationMessage(typemoq.It.isValue(message), | ||
typemoq.It.isValue(yes), | ||
typemoq.It.isValue(no))) | ||
.verifiable(typemoq.Times.never()); | ||
const enabledValue: boolean = false; | ||
const attemptCounter: number = 0; | ||
const testBanner: DataScienceSurveyBanner = preparePopup(attemptCounter, enabledValue, 0, appShell.object, browser.object, targetUri); | ||
testBanner.showBanner().ignoreErrors(); | ||
}); | ||
test('Do not show data science banner if we have not hit our command count', () => { | ||
appShell.setup(a => a.showInformationMessage(typemoq.It.isValue(message), | ||
typemoq.It.isValue(yes), | ||
typemoq.It.isValue(no))) | ||
.verifiable(typemoq.Times.never()); | ||
const enabledValue: boolean = true; | ||
const attemptCounter: number = 100; | ||
const testBanner: DataScienceSurveyBanner = preparePopup(attemptCounter, enabledValue, 1000, appShell.object, browser.object, targetUri); | ||
testBanner.showBanner().ignoreErrors(); | ||
}); | ||
}); | ||
|
||
function preparePopup( | ||
commandCounter: number, | ||
enabledValue: boolean, | ||
commandThreshold: number, | ||
appShell: IApplicationShell, | ||
browser: IBrowserService, | ||
targetUri: string | ||
): DataScienceSurveyBanner { | ||
const myfactory: typemoq.IMock<IPersistentStateFactory> = typemoq.Mock.ofType<IPersistentStateFactory>(); | ||
const enabledValState: typemoq.IMock<IPersistentState<boolean>> = typemoq.Mock.ofType<IPersistentState<boolean>>(); | ||
const attemptCountState: typemoq.IMock<IPersistentState<number>> = typemoq.Mock.ofType<IPersistentState<number>>(); | ||
enabledValState.setup(a => a.updateValue(typemoq.It.isValue(true))).returns(() => { | ||
enabledValue = true; | ||
return Promise.resolve(); | ||
}); | ||
enabledValState.setup(a => a.updateValue(typemoq.It.isValue(false))).returns(() => { | ||
enabledValue = false; | ||
return Promise.resolve(); | ||
}); | ||
|
||
attemptCountState.setup(a => a.updateValue(typemoq.It.isAnyNumber())).returns(() => { | ||
commandCounter += 1; | ||
return Promise.resolve(); | ||
}); | ||
|
||
enabledValState.setup(a => a.value).returns(() => enabledValue); | ||
attemptCountState.setup(a => a.value).returns(() => commandCounter); | ||
|
||
myfactory.setup(a => a.createGlobalPersistentState(typemoq.It.isValue(DSSurveyStateKeys.ShowBanner), | ||
typemoq.It.isValue(true))).returns(() => { | ||
return enabledValState.object; | ||
}); | ||
myfactory.setup(a => a.createGlobalPersistentState(typemoq.It.isValue(DSSurveyStateKeys.ShowBanner), | ||
typemoq.It.isValue(false))).returns(() => { | ||
return enabledValState.object; | ||
}); | ||
myfactory.setup(a => a.createGlobalPersistentState(typemoq.It.isValue(DSSurveyStateKeys.ShowAttemptCounter), | ||
typemoq.It.isAnyNumber())).returns(() => { | ||
return attemptCountState.object; | ||
}); | ||
return new DataScienceSurveyBanner(appShell, myfactory.object, browser, commandThreshold, targetUri); | ||
} |