Skip to content

Commit

Permalink
Added a prompt asking users to enroll back in the insiders program (#…
Browse files Browse the repository at this point in the history
…7484)

* Added functionality

* Added telemetry

* Added tests

* Some code reviews

* Refactored functionality

* Added doc

* Refactored the tests

* Kode reviews

* Code reviews
  • Loading branch information
Kartik Raj committed Sep 20, 2019
1 parent 613a0d1 commit 574a9e3
Show file tree
Hide file tree
Showing 10 changed files with 490 additions and 195 deletions.
1 change: 1 addition & 0 deletions news/2 Fixes/7473.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added a prompt asking users to enroll back in the insiders program
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@
"ExtensionChannels.reloadToUseInsidersMessage": "Please reload Visual Studio Code to use the insiders build of the Python extension.",
"ExtensionChannels.downloadCompletedOutputMessage": "Insiders build download complete.",
"ExtensionChannels.startingDownloadOutputMessage": "Starting download for Insiders build.",
"ExtensionChannels.optIntoProgramAgainMessage": "It looks like you were previously in the Insiders Program of the Python extension. Would you like to opt into the program again?",
"Interpreters.environmentPromptMessage": "We noticed a new virtual environment has been created. Do you want to select it for the workspace folder?",
"DataScience.restartKernelMessage": "Do you want to restart the IPython kernel? All variables will be lost.",
"DataScience.restartKernelMessageYes": "Yes",
Expand Down
41 changes: 27 additions & 14 deletions src/client/common/insidersBuild/insidersExtensionPrompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,33 +14,30 @@ import { noop } from '../utils/misc';
import { IExtensionChannelService, IInsiderExtensionPrompt } from './types';

export const insidersPromptStateKey = 'INSIDERS_PROMPT_STATE_KEY';
export const optIntoInsidersPromptAgainStateKey = 'OPT_INTO_INSIDERS_PROGRAM_AGAIN_STATE_KEY';

@injectable()
export class InsidersExtensionPrompt implements IInsiderExtensionPrompt {
public readonly hasUserBeenNotified: IPersistentState<boolean>;
public readonly hasUserBeenAskedToOptInAgain: IPersistentState<boolean>;
constructor(
@inject(IApplicationShell) private readonly appShell: IApplicationShell,
@inject(IExtensionChannelService) private readonly insidersDownloadChannelService: IExtensionChannelService,
@inject(ICommandManager) private readonly cmdManager: ICommandManager,
@inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory
) {
this.hasUserBeenNotified = this.persistentStateFactory.createGlobalPersistentState(insidersPromptStateKey, false);
this.hasUserBeenAskedToOptInAgain = this.persistentStateFactory.createGlobalPersistentState(optIntoInsidersPromptAgainStateKey, false);
}

@traceDecorators.error('Error in prompting to install insiders')
public async notifyToInstallInsiders(): Promise<void> {
const prompts = [ExtensionChannels.yesWeekly(), ExtensionChannels.yesDaily(), DataScienceSurveyBanner.bannerLabelNo()];
const telemetrySelections: ['Yes, weekly', 'Yes, daily', 'No, thanks'] = ['Yes, weekly', 'Yes, daily', 'No, thanks'];
const selection = await this.appShell.showInformationMessage(ExtensionChannels.promptMessage(), ...prompts);
sendTelemetryEvent(EventName.INSIDERS_PROMPT, undefined, { selection: selection ? telemetrySelections[prompts.indexOf(selection)] : undefined });
await this.hasUserBeenNotified.updateValue(true);
if (!selection) {
return;
}
if (selection === ExtensionChannels.yesWeekly()) {
await this.insidersDownloadChannelService.updateChannel('weekly');
} else if (selection === ExtensionChannels.yesDaily()) {
await this.insidersDownloadChannelService.updateChannel('daily');
}
public async promptToInstallInsiders(): Promise<void> {
await this.promptAndUpdate(ExtensionChannels.promptMessage(), this.hasUserBeenNotified, EventName.INSIDERS_PROMPT);
}

@traceDecorators.error('Error in prompting to enroll back to insiders program')
public async promptToEnrollBackToInsiders(): Promise<void> {
await this.promptAndUpdate(ExtensionChannels.optIntoProgramAgainMessage(), this.hasUserBeenAskedToOptInAgain, EventName.OPT_INTO_INSIDERS_AGAIN_PROMPT);
}

@traceDecorators.error('Error in prompting to reload')
Expand All @@ -54,4 +51,20 @@ export class InsidersExtensionPrompt implements IInsiderExtensionPrompt {
this.cmdManager.executeCommand('workbench.action.reloadWindow').then(noop);
}
}

private async promptAndUpdate(message: string, hasPromptBeenShownAlreadyState: IPersistentState<boolean>, telemetryEventKey: EventName.INSIDERS_PROMPT | EventName.OPT_INTO_INSIDERS_AGAIN_PROMPT) {
const prompts = [ExtensionChannels.yesWeekly(), ExtensionChannels.yesDaily(), DataScienceSurveyBanner.bannerLabelNo()];
const telemetrySelections: ['Yes, weekly', 'Yes, daily', 'No, thanks'] = ['Yes, weekly', 'Yes, daily', 'No, thanks'];
const selection = await this.appShell.showInformationMessage(message, ...prompts);
sendTelemetryEvent(telemetryEventKey, undefined, { selection: selection ? telemetrySelections[prompts.indexOf(selection)] : undefined });
await hasPromptBeenShownAlreadyState.updateValue(true);
if (!selection) {
return;
}
if (selection === ExtensionChannels.yesWeekly()) {
await this.insidersDownloadChannelService.updateChannel('weekly');
} else if (selection === ExtensionChannels.yesDaily()) {
await this.insidersDownloadChannelService.updateChannel('daily');
}
}
}
62 changes: 53 additions & 9 deletions src/client/common/insidersBuild/insidersExtensionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ export class InsidersExtensionService implements IExtensionSingleActivationServi
public async activate() {
this.registerCommandsAndHandlers();
const installChannel = this.extensionChannelService.getChannel();
await this.handleEdgeCases(installChannel);
const alreadyHandled = await this.handleEdgeCases(installChannel);
if (alreadyHandled) {
// Simply return if channel is already handled and doesn't need further handling
return;
}
this.handleChannel(installChannel).ignoreErrors();
}

Expand All @@ -45,15 +49,17 @@ export class InsidersExtensionService implements IExtensionSingleActivationServi

/**
* Choose what to do in miscellaneous situations
* * 'Notify to install insiders prompt' - Only when using VSC insiders and if they have not been notified before (usually the first session)
* * 'Resolve discrepency' - When install channel is not in sync with what is installed.
* @returns `true` if install channel is handled in these miscellaneous cases, `false` if install channel needs further handling
*/
public async handleEdgeCases(installChannel: ExtensionChannels): Promise<void> {
if (this.appEnvironment.channel === 'insiders' && !this.insidersPrompt.hasUserBeenNotified.value && this.extensionChannelService.isChannelUsingDefaultConfiguration) {
await this.insidersPrompt.notifyToInstallInsiders();
} else if (installChannel !== 'off' && this.appEnvironment.extensionChannel === 'stable') {
// Install channel is set to "weekly" or "daily" but stable version of extension is installed. Switch channel to "off" to use the installed version
await this.extensionChannelService.updateChannel('off');
public async handleEdgeCases(installChannel: ExtensionChannels): Promise<boolean> {
if (await this.promptToEnrollBackToInsidersIfApplicable(installChannel)) {
return true;
} else if (await this.promptToInstallInsidersIfApplicable()) {
return true;
} else if (await this.setInsidersChannelToOffIfApplicable(installChannel)) {
return true;
} else {
return false;
}
}

Expand All @@ -63,4 +69,42 @@ export class InsidersExtensionService implements IExtensionSingleActivationServi
this.disposables.push(this.cmdManager.registerCommand(Commands.SwitchToInsidersDaily, () => this.extensionChannelService.updateChannel('daily')));
this.disposables.push(this.cmdManager.registerCommand(Commands.SwitchToInsidersWeekly, () => this.extensionChannelService.updateChannel('weekly')));
}

/**
* If previously in the Insiders Program but not now, request them enroll in the program again
* @returns `true` if prompt is shown, `false` otherwise
*/
private async promptToEnrollBackToInsidersIfApplicable(installChannel: ExtensionChannels): Promise<boolean> {
if (installChannel === 'off' && !this.extensionChannelService.isChannelUsingDefaultConfiguration) {
// If install channel is explicitly set to off, it means that user has used the insiders program before
await this.insidersPrompt.promptToEnrollBackToInsiders();
return true;
}
return false;
}

/**
* Only when using VSC insiders and if they have not been notified before (usually the first session), notify to enroll into the insiders program
* @returns `true` if prompt is shown, `false` otherwise
*/
private async promptToInstallInsidersIfApplicable(): Promise<boolean> {
if (this.appEnvironment.channel === 'insiders' && !this.insidersPrompt.hasUserBeenNotified.value && this.extensionChannelService.isChannelUsingDefaultConfiguration) {
await this.insidersPrompt.promptToInstallInsiders();
return true;
}
return false;
}

/**
* When install channel is not in sync with what is installed, resolve discrepency by setting channel to "off"
* @returns `true` if channel is set to off, `false` otherwise
*/
private async setInsidersChannelToOffIfApplicable(installChannel: ExtensionChannels): Promise<boolean> {
if (installChannel !== 'off' && this.appEnvironment.extensionChannel === 'stable') {
// Install channel is set to "weekly" or "daily" but stable version of extension is installed. Switch channel to "off" to use the installed version
await this.extensionChannelService.updateChannel('off');
return true;
}
return false;
}
}
3 changes: 2 additions & 1 deletion src/client/common/insidersBuild/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ export interface IInsiderExtensionPrompt {
* Gets updated to `true` once user has been prompted to install insiders.
*/
readonly hasUserBeenNotified: IPersistentState<boolean>;
notifyToInstallInsiders(): Promise<void>;
promptToInstallInsiders(): Promise<void>;
promptToEnrollBackToInsiders(): Promise<void>;
promptToReload(): Promise<void>;
}

Expand Down
1 change: 1 addition & 0 deletions src/client/common/utils/localize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export namespace ExtensionChannels {
export const yesWeekly = localize('ExtensionChannels.yesWeekly', 'Yes, weekly');
export const yesDaily = localize('ExtensionChannels.yesDaily', 'Yes, daily');
export const promptMessage = localize('ExtensionChannels.promptMessage', 'We noticed you are using Visual Studio Code Insiders. Would you like to use the Insiders build of the Python extension?');
export const optIntoProgramAgainMessage = localize('ExtensionChannels.optIntoProgramAgainMessage', 'It looks like you were previously in the Insiders Program of the Python extension. Would you like to opt into the program again?');
export const reloadToUseInsidersMessage = localize('ExtensionChannels.reloadToUseInsidersMessage', 'Please reload Visual Studio Code to use the insiders build of the Python extension.');
export const downloadCompletedOutputMessage = localize('ExtensionChannels.downloadCompletedOutputMessage', 'Insiders build download complete.');
export const startingDownloadOutputMessage = localize('ExtensionChannels.startingDownloadOutputMessage', 'Starting download for Insiders build.');
Expand Down
1 change: 1 addition & 0 deletions src/client/telemetry/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export enum EventName {
PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT = 'PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT',
INSIDERS_RELOAD_PROMPT = 'INSIDERS_RELOAD_PROMPT',
INSIDERS_PROMPT = 'INSIDERS_PROMPT',
OPT_INTO_INSIDERS_AGAIN_PROMPT = 'OPT_INTO_INSIDERS_AGAIN_PROMPT',
ENVFILE_VARIABLE_SUBSTITUTION = 'ENVFILE_VARIABLE_SUBSTITUTION',
WORKSPACE_SYMBOLS_BUILD = 'WORKSPACE_SYMBOLS.BUILD',
WORKSPACE_SYMBOLS_GO_TO = 'WORKSPACE_SYMBOLS.GO_TO',
Expand Down
20 changes: 15 additions & 5 deletions src/client/telemetry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -942,11 +942,21 @@ export interface IEventNamePropertyMapping {
*/
[EventName.INSIDERS_PROMPT]: {
/**
* @type {'Yes, weekly'} When user selects to use "weekly" as extension channel in insiders prompt
* @type {'Yes, daily'} When user selects to use "daily" as extension channel in insiders prompt
* @type {'No, thanks'} When user decides to keep using the same extension channel as before
*
* @type {('Yes, weekly' | 'Yes, daily' | 'No, thanks' | undefined)}
* `Yes, weekly` When user selects to use "weekly" as extension channel in insiders prompt
* `Yes, daily` When user selects to use "daily" as extension channel in insiders prompt
* `No, thanks` When user decides to keep using the same extension channel as before
*/
selection: 'Yes, weekly' | 'Yes, daily' | 'No, thanks' | undefined;
};
/**
* Telemetry event sent with details when user clicks a button in the following prompt
* `Prompt message` :- 'It looks like you were previously in the Insiders Program of the Python extension. Would you like to opt into the program again?'
*/
[EventName.OPT_INTO_INSIDERS_AGAIN_PROMPT]: {
/**
* `Yes, weekly` When user selects to use "weekly" as extension channel
* `Yes, daily` When user selects to use "daily" as extension channel
* `No, thanks` When user decides to keep using the same extension channel as before
*/
selection: 'Yes, weekly' | 'Yes, daily' | 'No, thanks' | undefined;
};
Expand Down
Loading

0 comments on commit 574a9e3

Please sign in to comment.