Skip to content

Commit 8580ff8

Browse files
Kartik RajDonJayamanne
andauthored
Implemented prompt for python extension survey (microsoft#6825)
* Added functionality * News entry * Added tests * Code reviews * Add tests * Update src/client/activation/types.ts Co-Authored-By: Don Jayamanne <don.jayamanne@yahoo.com>
1 parent d4ac309 commit 8580ff8

10 files changed

Lines changed: 456 additions & 4 deletions

File tree

news/1 Enhancements/6752.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Implemented prompt for survey

package.nls.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@
122122
"OutputChannelNames.languageServer": "Python Language Server",
123123
"OutputChannelNames.python": "Python",
124124
"OutputChannelNames.pythonTest": "Python Test Log",
125+
"ExtensionSurveyBanner.bannerMessage": "Can you please take 2 minutes to tell us how the Python extension is working for you?",
126+
"ExtensionSurveyBanner.maybeLater": "Maybe later",
125127
"ExtensionChannels.installingInsidersMessage": "Installing Insiders... ",
126128
"ExtensionChannels.installingStableMessage": "Installing Stable... ",
127129
"ExtensionChannels.installationCompleteMessage": "complete.",
@@ -329,5 +331,5 @@
329331
"DataScience.cellStopOnErrorFormatMessage": "{0} cells were canceled due to an error in the previous cell.",
330332
"DataScience.instructionComments": "# To add a new cell, type '#%%'\n# To add a new markdown cell, type '#%% [markdown]'\n",
331333
"DataScience.scrollToCellTitleFormatMessage": "Go to [{0}]",
332-
"DataScience.remoteDebuggerNotSupported" : "Debugging while attached to a remote server is not currently supported."
334+
"DataScience.remoteDebuggerNotSupported": "Debugging while attached to a remote server is not currently supported."
333335
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { inject, injectable, optional } from 'inversify';
7+
import { IApplicationShell } from '../common/application/types';
8+
import '../common/extensions';
9+
import { traceDecorators } from '../common/logger';
10+
import {
11+
IBrowserService, IPersistentStateFactory, IRandom
12+
} from '../common/types';
13+
import { Common, ExtensionSurveyBanner, LanguageService } from '../common/utils/localize';
14+
import { sendTelemetryEvent } from '../telemetry';
15+
import { EventName } from '../telemetry/constants';
16+
import { IExtensionSingleActivationService } from './types';
17+
18+
// persistent state names, exported to make use of in testing
19+
export enum extensionSurveyStateKeys {
20+
doNotShowAgain = 'doNotShowExtensionSurveyAgain',
21+
disableSurveyForTime = 'doNotShowExtensionSurveyUntilTime'
22+
}
23+
24+
const timeToDisableSurveyFor = 1000 * 60 * 60 * 24 * 7 * 12; // 12 weeks
25+
const WAIT_TIME_TO_SHOW_SURVEY = 1000 * 60 * 60 * 3; // 3 hours
26+
27+
@injectable()
28+
export class ExtensionSurveyPrompt implements IExtensionSingleActivationService {
29+
constructor(
30+
@inject(IApplicationShell) private appShell: IApplicationShell,
31+
@inject(IBrowserService) private browserService: IBrowserService,
32+
@inject(IPersistentStateFactory) private persistentState: IPersistentStateFactory,
33+
@inject(IRandom) private random: IRandom,
34+
@optional() private sampleSizePerOneHundredUsers: number = 10,
35+
@optional() private waitTimeToShowSurvey: number = WAIT_TIME_TO_SHOW_SURVEY) { }
36+
37+
public async activate(): Promise<void> {
38+
const show = this.shouldShowBanner();
39+
if (!show) {
40+
return;
41+
}
42+
setTimeout(() => this.showSurvey().ignoreErrors(), this.waitTimeToShowSurvey);
43+
}
44+
45+
@traceDecorators.error('Failed to check whether to display prompt for extension survey')
46+
public shouldShowBanner(): boolean {
47+
const doNotShowSurveyAgain = this.persistentState.createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false);
48+
if (doNotShowSurveyAgain.value) {
49+
return false;
50+
}
51+
const isSurveyDisabledForTimeState = this.persistentState.createGlobalPersistentState(extensionSurveyStateKeys.disableSurveyForTime, false, timeToDisableSurveyFor);
52+
if (isSurveyDisabledForTimeState.value) {
53+
return false;
54+
}
55+
// we only want 10% of folks to see this survey.
56+
const randomSample: number = this.random.getRandomInt(0, 100);
57+
if (randomSample >= this.sampleSizePerOneHundredUsers) {
58+
return false;
59+
}
60+
return true;
61+
}
62+
63+
@traceDecorators.error('Failed to display prompt for extension survey')
64+
public async showSurvey() {
65+
const prompts = [LanguageService.bannerLabelYes(), ExtensionSurveyBanner.maybeLater(), Common.doNotShowAgain()];
66+
const telemetrySelections: ['Yes', 'Maybe later', 'Do not show again'] = ['Yes', 'Maybe later', 'Do not show again'];
67+
const selection = await this.appShell.showInformationMessage(ExtensionSurveyBanner.bannerMessage(), ...prompts);
68+
sendTelemetryEvent(EventName.EXTENSION_SURVEY_PROMPT, undefined, { selection: selection ? telemetrySelections[prompts.indexOf(selection)] : undefined });
69+
if (!selection) {
70+
return;
71+
}
72+
if (selection === LanguageService.bannerLabelYes()) {
73+
this.launchSurvey();
74+
// Disable survey for a few weeks
75+
await this.persistentState.createGlobalPersistentState(extensionSurveyStateKeys.disableSurveyForTime, false, timeToDisableSurveyFor).updateValue(true);
76+
} else if (selection === Common.doNotShowAgain()) {
77+
// Never show the survey again
78+
await this.persistentState.createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false).updateValue(true);
79+
}
80+
}
81+
82+
private launchSurvey() {
83+
this.browserService.launch('https://aka.ms/AA5rjx5');
84+
}
85+
}

src/client/activation/serviceRegistry.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { LanguageServerSurveyBanner } from '../languageServices/languageServerSu
1212
import { ProposeLanguageServerBanner } from '../languageServices/proposeLanguageServerBanner';
1313
import { ExtensionActivationManager } from './activationManager';
1414
import { LanguageServerExtensionActivationService } from './activationService';
15+
import { ExtensionSurveyPrompt } from './extensionSurvey';
1516
import { JediExtensionActivator } from './jedi';
1617
import { LanguageServerExtensionActivator } from './languageServer/activator';
1718
import { LanguageServerAnalysisOptions } from './languageServer/analysisOptions';
@@ -27,7 +28,7 @@ import { LanguageServerPackageService } from './languageServer/languageServerPac
2728
import { LanguageServerManager } from './languageServer/manager';
2829
import { LanguageServerOutputChannel } from './languageServer/outputChannel';
2930
import { PlatformData } from './languageServer/platformData';
30-
import { IDownloadChannelRule, IExtensionActivationManager, IExtensionActivationService, ILanguageClientFactory, ILanguageServer, ILanguageServerActivator, ILanguageServerAnalysisOptions, ILanguageServerCompatibilityService as ILanagueServerCompatibilityService, ILanguageServerDownloader, ILanguageServerExtension, ILanguageServerFolderService, ILanguageServerManager, ILanguageServerOutputChannel, ILanguageServerPackageService, IPlatformData, LanguageClientFactory, LanguageServerActivator } from './types';
31+
import { IDownloadChannelRule, IExtensionActivationManager, IExtensionActivationService, IExtensionSingleActivationService, ILanguageClientFactory, ILanguageServer, ILanguageServerActivator, ILanguageServerAnalysisOptions, ILanguageServerCompatibilityService as ILanagueServerCompatibilityService, ILanguageServerDownloader, ILanguageServerExtension, ILanguageServerFolderService, ILanguageServerManager, ILanguageServerPackageService, IPlatformData, LanguageClientFactory, LanguageServerActivator, ILanguageServerOutputChannel } from './types';
3132

3233
export function registerTypes(serviceManager: IServiceManager) {
3334
serviceManager.addSingleton<IExtensionActivationService>(IExtensionActivationService, LanguageServerExtensionActivationService);
@@ -57,4 +58,5 @@ export function registerTypes(serviceManager: IServiceManager) {
5758
serviceManager.addSingleton<ILanguageServer>(ILanguageServer, LanguageServer);
5859
serviceManager.add<ILanguageServerManager>(ILanguageServerManager, LanguageServerManager);
5960
serviceManager.addSingleton<ILanguageServerOutputChannel>(ILanguageServerOutputChannel, LanguageServerOutputChannel);
61+
serviceManager.addSingleton<IExtensionSingleActivationService>(IExtensionSingleActivationService, ExtensionSurveyPrompt);
6062
}

src/client/activation/types.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export interface IExtensionActivationManager extends IDisposable {
1818
export const IExtensionActivationService = Symbol('IExtensionActivationService');
1919
/**
2020
* Classes implementing this interface will have their `activate` methods
21-
* invoked during the actiavtion of the extension.
21+
* invoked for every workspace folder (in multi-root workspace folders) during the activation of the extension.
2222
* This is a great hook for extension activation code, i.e. you don't need to modify
2323
* the `extension.ts` file to invoke some code when extension gets activated.
2424
* @export
@@ -134,3 +134,16 @@ export interface ILanguageServerOutputChannel {
134134
*/
135135
readonly channel: IOutputChannel;
136136
}
137+
138+
export const IExtensionSingleActivationService = Symbol('IExtensionSingleActivationService');
139+
/**
140+
* Classes implementing this interface will have their `activate` methods
141+
* invoked during the activation of the extension.
142+
* This is a great hook for extension activation code, i.e. you don't need to modify
143+
* the `extension.ts` file to invoke some code when extension gets activated.
144+
* @export
145+
* @interface IExtensionSingleActivationService
146+
*/
147+
export interface IExtensionSingleActivationService {
148+
activate(): Promise<void>;
149+
}

src/client/common/utils/localize.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@ export namespace DataScienceSurveyBanner {
103103
export const bannerLabelNo = localize('DataScienceSurveyBanner.bannerLabelNo', 'No, thanks');
104104
}
105105

106+
export namespace ExtensionSurveyBanner {
107+
export const bannerMessage = localize('ExtensionSurveyBanner.bannerMessage', 'Can you please take 2 minutes to tell us how the Python extension is working for you?');
108+
export const maybeLater = localize('ExtensionSurveyBanner.maybeLater', 'Maybe later');
109+
}
110+
106111
export namespace DataScience {
107112
export const historyTitle = localize('DataScience.historyTitle', 'Python Interactive');
108113
export const dataExplorerTitle = localize('DataScience.dataExplorerTitle', 'Data Viewer');

src/client/extension.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import {
3434
} from 'vscode';
3535

3636
import { registerTypes as activationRegisterTypes } from './activation/serviceRegistry';
37-
import { IExtensionActivationManager, ILanguageServerExtension } from './activation/types';
37+
import { IExtensionActivationManager, IExtensionSingleActivationService, ILanguageServerExtension } from './activation/types';
3838
import { buildApi, IExtensionApi } from './api';
3939
import { registerTypes as appRegisterTypes } from './application/serviceRegistry';
4040
import { IApplicationDiagnostics } from './application/types';
@@ -266,6 +266,8 @@ function registerServices(context: ExtensionContext, serviceManager: ServiceMana
266266
async function initializeServices(context: ExtensionContext, serviceManager: ServiceManager, serviceContainer: ServiceContainer) {
267267
const abExperiments = serviceContainer.get<IExperimentsManager>(IExperimentsManager);
268268
await abExperiments.activate();
269+
const singleActivationServices = serviceContainer.getAll<IExtensionSingleActivationService>(IExtensionSingleActivationService);
270+
Promise.all(singleActivationServices.map(item => item.activate())).ignoreErrors();
269271
const selector = serviceContainer.get<IInterpreterSelector>(IInterpreterSelector);
270272
selector.initialize();
271273
context.subscriptions.push(selector);

src/client/telemetry/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export enum EventName {
6363
PYTHON_LANGUAGE_SERVER_TELEMETRY = 'PYTHON_LANGUAGE_SERVER.EVENT',
6464
PYTHON_EXPERIMENTS = 'PYTHON_EXPERIMENTS',
6565
PYTHON_EXPERIMENTS_DOWNLOAD_SUCCESS_RATE = 'PYTHON_EXPERIMENTS_DOWNLOAD_SUCCESS_RATE',
66+
EXTENSION_SURVEY_PROMPT = 'EXTENSION_SURVEY_PROMPT',
6667

6768
TERMINAL_CREATE = 'TERMINAL.CREATE',
6869
PYTHON_LANGUAGE_SERVER_LIST_BLOB_STORE_PACKAGES = 'PYTHON_LANGUAGE_SERVER.LIST_BLOB_PACKAGES',

src/client/telemetry/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,15 @@ export interface IEventNamePropertyMapping {
459459
*/
460460
error?: string;
461461
};
462+
/**
463+
* When user clicks a button in the python extension survey prompt, this telemetry event is sent with details
464+
*/
465+
[EventName.EXTENSION_SURVEY_PROMPT]: {
466+
/**
467+
* Carries the selection of user when they are asked to take the extension survey
468+
*/
469+
selection: 'Yes' | 'Maybe later' | 'Do not show again' | undefined;
470+
};
462471
[EventName.REFACTOR_EXTRACT_FUNCTION]: never | undefined;
463472
[EventName.REFACTOR_EXTRACT_VAR]: never | undefined;
464473
[EventName.REFACTOR_RENAME]: never | undefined;

0 commit comments

Comments
 (0)