Skip to content

Commit

Permalink
Add data science survey banner (#3224)
Browse files Browse the repository at this point in the history
* Add data science survey banner

* fix hygiene that didn't show up on local run

* don't await on survey call
  • Loading branch information
IanMatthewHuff authored Nov 6, 2018
1 parent cd78a30 commit 044f768
Show file tree
Hide file tree
Showing 7 changed files with 269 additions and 8 deletions.
5 changes: 4 additions & 1 deletion package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,5 +83,8 @@
"Interpreters.LoadingInterpreters": "Loading Python Interpreters",
"DataScience.restartKernelMessage" : "Do you want to restart the Jupter kernel? All variables will be lost.",
"DataScience.restartKernelMessageYes" : "Restart",
"DataScience.restartKernelMessageNo" : "Cancel"
"DataScience.restartKernelMessageNo" : "Cancel",
"DataScienceSurveyBanner.bannerMessage": "Can you please take 2 minutes to tell us how the Python Data Science features are working for you?",
"DataScienceSurveyBanner.bannerLabelYes": "Yes, take survey now",
"DataScienceSurveyBanner.bannerLabelNo": "No, thanks"
}
4 changes: 3 additions & 1 deletion src/client/activation/serviceRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
'use strict';

import { INugetRepository } from '../common/nuget/types';
import { BANNER_NAME_LS_SURVEY, BANNER_NAME_PROPOSE_LS, IPythonExtensionBanner } from '../common/types';
import { BANNER_NAME_DS_SURVEY, BANNER_NAME_LS_SURVEY, BANNER_NAME_PROPOSE_LS, IPythonExtensionBanner } from '../common/types';
import { DataScienceSurveyBanner } from '../datascience/dataScienceSurveyBanner';
import { IServiceManager } from '../ioc/types';
import { LanguageServerSurveyBanner } from '../languageServices/languageServerSurveyBanner';
import { ProposeLanguageServerBanner } from '../languageServices/proposeLanguageServerBanner';
Expand All @@ -23,6 +24,7 @@ export function registerTypes(serviceManager: IServiceManager) {
serviceManager.add<IExtensionActivator>(IExtensionActivator, LanguageServerExtensionActivator, ExtensionActivators.DotNet);
serviceManager.addSingleton<IPythonExtensionBanner>(IPythonExtensionBanner, LanguageServerSurveyBanner, BANNER_NAME_LS_SURVEY);
serviceManager.addSingleton<IPythonExtensionBanner>(IPythonExtensionBanner, ProposeLanguageServerBanner, BANNER_NAME_PROPOSE_LS);
serviceManager.addSingleton<IPythonExtensionBanner>(IPythonExtensionBanner, DataScienceSurveyBanner, BANNER_NAME_DS_SURVEY);
serviceManager.addSingleton<ILanguageServerFolderService>(ILanguageServerFolderService, LanguageServerFolderService);
serviceManager.addSingleton<ILanguageServerPackageService>(ILanguageServerPackageService, LanguageServerPackageService);
serviceManager.addSingleton<INugetRepository>(INugetRepository, StableLanguageServerPackageRepository, LanguageServerDownloadChannel.stable);
Expand Down
1 change: 1 addition & 0 deletions src/client/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ export interface IPythonExtensionBanner {
}
export const BANNER_NAME_LS_SURVEY: string = 'LSSurveyBanner';
export const BANNER_NAME_PROPOSE_LS: string = 'ProposeLS';
export const BANNER_NAME_DS_SURVEY: string = 'DSSurveyBanner';

export type DeprecatedSettingAndValue = {
setting: string;
Expand Down
6 changes: 6 additions & 0 deletions src/client/common/utils/localize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ export namespace Interpreters {
export const refreshing = localize('Interpreters.RefreshingInterpreters', 'Refreshing Python Interpreters');
}

export namespace DataScienceSurveyBanner {
export const bannerMessage = localize('DataScienceSurveyBanner.bannerMessage', 'Can you please take 2 minutes to tell us how the Python Data Science features are working for you?');
export const bannerLabelYes = localize('DataScienceSurveyBanner.bannerLabelYes', 'Yes, take survey now');
export const bannerLabelNo = localize('DataScienceSurveyBanner.bannerLabelNo', 'No, thanks');
}

export namespace DataScience {
export const historyTitle = localize('DataScience.historyTitle', 'Python Interactive');
export const badWebPanelFormatString = localize('DataScience.badWebPanelFormatString', '<html><body><h1>{0} is not a valid file name</h1></body></html>');
Expand Down
124 changes: 124 additions & 0 deletions src/client/datascience/dataScienceSurveyBanner.ts
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;
}
}
26 changes: 20 additions & 6 deletions src/client/datascience/datascience.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import { inject, injectable } from 'inversify';
import * as vscode from 'vscode';
import { ICommandManager } from '../common/application/types';
import { PythonSettings } from '../common/configSettings';
import { PYTHON } from '../common/constants';
import { isTestExecution, PYTHON } from '../common/constants';
import { ContextKey } from '../common/contextKey';
import { IConfigurationService, IDisposableRegistry, IExtensionContext } from '../common/types';
import { BANNER_NAME_DS_SURVEY, IConfigurationService, IDisposableRegistry, IExtensionContext, IPythonExtensionBanner } from '../common/types';
import { IServiceContainer } from '../ioc/types';
import { Commands, EditorContexts } from './constants';
import { ICodeWatcher, IDataScience, IDataScienceCodeLensProvider, IDataScienceCommandListener } from './types';
Expand All @@ -22,6 +22,7 @@ export class DataScience implements IDataScience {
private readonly dataScienceCodeLensProvider: IDataScienceCodeLensProvider;
private readonly commandListeners: IDataScienceCommandListener[];
private readonly configuration: IConfigurationService;
private readonly dataScienceSurveyBanner: IPythonExtensionBanner;
constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer)
{
this.commandManager = this.serviceContainer.get<ICommandManager>(ICommandManager);
Expand All @@ -30,6 +31,7 @@ export class DataScience implements IDataScience {
this.dataScienceCodeLensProvider = this.serviceContainer.get<IDataScienceCodeLensProvider>(IDataScienceCodeLensProvider);
this.commandListeners = this.serviceContainer.getAll<IDataScienceCommandListener>(IDataScienceCommandListener);
this.configuration = this.serviceContainer.get<IConfigurationService>(IConfigurationService);
this.dataScienceSurveyBanner = this.serviceContainer.get<IPythonExtensionBanner>(IPythonExtensionBanner, BANNER_NAME_DS_SURVEY);
}

public async activate(): Promise<void> {
Expand All @@ -54,7 +56,10 @@ export class DataScience implements IDataScience {
}
}

public runAllCells(codeWatcher: ICodeWatcher): Promise<void> {
public async runAllCells(codeWatcher: ICodeWatcher): Promise<void> {
if (!isTestExecution()) {
this.dataScienceSurveyBanner.showBanner().ignoreErrors();
}
let activeCodeWatcher: ICodeWatcher | undefined = codeWatcher;
if (!activeCodeWatcher) {
activeCodeWatcher = this.getCurrentCodeWatcher();
Expand All @@ -66,15 +71,21 @@ export class DataScience implements IDataScience {
}
}

public runCell(codeWatcher: ICodeWatcher, range: vscode.Range): Promise<void> {
public async runCell(codeWatcher: ICodeWatcher, range: vscode.Range): Promise<void> {
if (!isTestExecution()) {
this.dataScienceSurveyBanner.showBanner().ignoreErrors();
}
if (codeWatcher) {
return codeWatcher.runCell(range);
} else {
return this.runCurrentCell();
}
}

public runCurrentCell(): Promise<void> {
public async runCurrentCell(): Promise<void> {
if (!isTestExecution()) {
this.dataScienceSurveyBanner.showBanner().ignoreErrors();
}
const activeCodeWatcher = this.getCurrentCodeWatcher();
if (activeCodeWatcher) {
return activeCodeWatcher.runCurrentCell();
Expand All @@ -83,7 +94,10 @@ export class DataScience implements IDataScience {
}
}

public runCurrentCellAndAdvance(): Promise<void> {
public async runCurrentCellAndAdvance(): Promise<void> {
if (!isTestExecution()) {
this.dataScienceSurveyBanner.showBanner().ignoreErrors();
}
const activeCodeWatcher = this.getCurrentCodeWatcher();
if (activeCodeWatcher) {
return activeCodeWatcher.runCurrentCellAndAdvance();
Expand Down
111 changes: 111 additions & 0 deletions src/test/datascience/datascienceSurveyBanner.unit.test.ts
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);
}

0 comments on commit 044f768

Please sign in to comment.