Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add survey and banner #7

Merged
merged 10 commits into from
Nov 6, 2017
47 changes: 47 additions & 0 deletions src/client/banner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

'use strict';

import * as child_process from 'child_process';
import * as os from 'os';
import { window } from 'vscode';
import { IPersistentStateFactory, PersistentState } from './common/persistentState';

const BANNER_URL = 'https://aka.ms/egv4z1';

export class BannerService {
private shouldShowBanner: PersistentState<boolean>;
constructor(persistentStateFactory: IPersistentStateFactory) {
this.shouldShowBanner = persistentStateFactory.createGlobalPersistentState('SHOW_NEW_EXT_BANNER', true);
this.showBanner();
}
private showBanner() {
if (!this.shouldShowBanner.value) {
return;
}
this.shouldShowBanner.value = false;

const message = 'Would you like to know what is new?';
const yesButton = 'Yes';
window.showInformationMessage(message, yesButton).then((value) => {
if (value === yesButton) {
this.displayBanner();
}
});
}
private displayBanner() {
let openCommand: string | undefined;
if (os.platform() === 'win32') {
openCommand = 'explorer';
} else if (os.platform() === 'darwin') {
openCommand = '/usr/bin/open';
} else {
openCommand = '/usr/bin/xdg-open';
}
if (!openCommand) {
console.error(`Unable open ${BANNER_URL} on platform '${os.platform()}'.`);
}
child_process.spawn(openCommand, [BANNER_URL]);
}
}
33 changes: 33 additions & 0 deletions src/client/common/persistentState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

'use strict';

import { Memento } from 'vscode';

export class PersistentState<T> {
constructor(private storage: Memento, private key: string, private defaultValue: T) { }

public get value(): T {
return this.storage.get<T>(this.key, this.defaultValue);
}

public set value(newValue: T) {
this.storage.update(this.key, newValue);
}
}

export interface IPersistentStateFactory {
createGlobalPersistentState<T>(key: string, defaultValue: T): PersistentState<T>;
createWorkspacePersistentState<T>(key: string, defaultValue: T): PersistentState<T>;
}

export class PersistentStateFactory implements IPersistentStateFactory {
constructor(private globalState: Memento, private workspaceState: Memento) { }
public createGlobalPersistentState<T>(key: string, defaultValue: T): PersistentState<T> {
return new PersistentState<T>(this.globalState, key, defaultValue);
}
public createWorkspacePersistentState<T>(key: string, defaultValue: T): PersistentState<T> {
return new PersistentState<T>(this.workspaceState, key, defaultValue);
}
}
4 changes: 2 additions & 2 deletions src/client/debugger/Main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import { CreateAttachDebugClient, CreateLaunchDebugClient } from "./DebugClients
import { LaunchRequestArguments, AttachRequestArguments, DebugOptions, TelemetryEvent, PythonEvaluationResultFlags } from "./Common/Contracts";
import { validatePath, getPythonExecutable } from './Common/Utils';
import { isNotInstalledError } from '../common/helpers';
import { DEBUGGER } from '../../client/common/telemetry/constants';
import { DebuggerTelemetry } from '../../client/common/telemetry/types';
import { DEBUGGER } from '../../client/telemetry/constants';
import { DebuggerTelemetry } from '../../client/telemetry/types';

const CHILD_ENUMEARATION_TIMEOUT = 5000;

Expand Down
31 changes: 24 additions & 7 deletions src/client/extension.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
'use strict';
import { EDITOR_LOAD } from './common/telemetry/constants';

import * as os from 'os';
import * as vscode from 'vscode';
import { BannerService } from './banner';
import * as settings from './common/configSettings';
import { Commands } from './common/constants';
import { createDeferred } from './common/helpers';
import { sendTelemetryEvent } from './common/telemetry';
import { StopWatch } from './common/telemetry/stopWatch';
import { PersistentStateFactory } from './common/persistentState';
import { SimpleConfigurationProvider } from './debugger';
import { FeedbackService } from './feedback';
import { InterpreterManager } from './interpreter';
import { SetInterpreterProvider } from './interpreter/configuration/setInterpreterProvider';
import { ShebangCodeLensProvider } from './interpreter/display/shebangCodeLensProvider';
Expand All @@ -33,6 +31,9 @@ import { activateSimplePythonRefactorProvider } from './providers/simpleRefactor
import { PythonSymbolProvider } from './providers/symbolProvider';
import { activateUpdateSparkLibraryProvider } from './providers/updateSparkLibraryProvider';
import * as sortImports from './sortImports';
import { sendTelemetryEvent } from './telemetry';
import { EDITOR_LOAD } from './telemetry/constants';
import { StopWatch } from './telemetry/stopWatch';
import { BlockFormatProviders } from './typeFormatters/blockFormatProvider';
import * as tests from './unittests/main';
import { WorkspaceSymbols } from './workspaceSymbols/main';
Expand All @@ -47,6 +48,7 @@ export const activated = activationDeferred.promise;
// tslint:disable-next-line:max-func-body-length
export async function activate(context: vscode.ExtensionContext) {
const pythonSettings = settings.PythonSettings.getInstance();
// tslint:disable-next-line:no-floating-promises
sendStartupTelemetry(activated);

lintingOutChannel = vscode.window.createOutputChannel(pythonSettings.linting.outputWindow);
Expand Down Expand Up @@ -77,7 +79,8 @@ export async function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(new ReplProvider());

// Enable indentAction
vscode.languages.setLanguageConfiguration(PYTHON.language, {
// tslint:disable-next-line:no-non-null-assertion
vscode.languages.setLanguageConfiguration(PYTHON.language!, {
onEnterRules: [
{
beforeText: /^\s*(?:def|class|for|if|elif|else|while|try|with|finally|except|async).*?:\s*$/,
Expand Down Expand Up @@ -116,23 +119,29 @@ export async function activate(context: vscode.ExtensionContext) {
}

const jupyterExtInstalled = vscode.extensions.getExtension('donjayamanne.jupyter');
// tslint:disable-next-line:promise-function-async
const linterProvider = new LintProvider(context, lintingOutChannel, (a, b) => Promise.resolve(false));
context.subscriptions.push();
if (jupyterExtInstalled) {
if (jupyterExtInstalled.isActive) {
// tslint:disable-next-line:no-unsafe-any
jupyterExtInstalled.exports.registerLanguageProvider(PYTHON.language, new JupyterProvider());
// tslint:disable-next-line:no-unsafe-any
linterProvider.documentHasJupyterCodeCells = jupyterExtInstalled.exports.hasCodeCells;
}

jupyterExtInstalled.activate().then(() => {
// tslint:disable-next-line:no-unsafe-any
jupyterExtInstalled.exports.registerLanguageProvider(PYTHON.language, new JupyterProvider());
// tslint:disable-next-line:no-unsafe-any
linterProvider.documentHasJupyterCodeCells = jupyterExtInstalled.exports.hasCodeCells;
});
} else {
jupMain = new jup.Jupyter(lintingOutChannel);
const documentHasJupyterCodeCells = jupMain.hasCodeCells.bind(jupMain);
jupMain.activate();
context.subscriptions.push(jupMain);
// tslint:disable-next-line:no-unsafe-any
linterProvider.documentHasJupyterCodeCells = documentHasJupyterCodeCells;
}
tests.activate(context, unitTestOutChannel, symbolProvider);
Expand All @@ -146,17 +155,25 @@ export async function activate(context: vscode.ExtensionContext) {

context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider('python', new SimpleConfigurationProvider()));
activationDeferred.resolve();

const persistentStateFactory = new PersistentStateFactory(context.globalState, context.workspaceState);
const feedbackService = new FeedbackService(persistentStateFactory);
context.subscriptions.push(feedbackService);
// tslint:disable-next-line:no-unused-expression
new BannerService(persistentStateFactory);
}

async function sendStartupTelemetry(activatedPromise: Promise<void>) {
const stopWatch = new StopWatch();
// tslint:disable-next-line:no-floating-promises
activatedPromise.then(async () => {
const duration = stopWatch.elapsedTime;
let condaVersion: string | undefined;
try {
condaVersion = await getCondaVersion();
// tslint:disable-next-line:no-empty
} catch { }
sendTelemetryEvent(EDITOR_LOAD, duration, { condaVersion });
const props = condaVersion ? { condaVersion } : undefined;
sendTelemetryEvent(EDITOR_LOAD, duration, props);
});
}
52 changes: 52 additions & 0 deletions src/client/feedback/counters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

'use strict';

import { EventEmitter } from 'events';

const THRESHOLD_FOR_FEATURE_USAGE = 1000;
const THRESHOLD_FOR_TEXT_EDIT = 5000;

const FEARTURES_USAGE_COUNTER = 'FEARTURES_USAGE';
const TEXT_EDIT_COUNTER = 'TEXT_EDIT';
type counters = 'FEARTURES_USAGE' | 'TEXT_EDIT';

export class FeedbackCounters extends EventEmitter {
private counters = new Map<string, { counter: number, threshold: number }>();
constructor() {
super();
this.createCounters();
}
public incrementEditCounter(): void {
this.incrementCounter(TEXT_EDIT_COUNTER);
}
public incrementFeatureUsageCounter(): void {
this.incrementCounter(FEARTURES_USAGE_COUNTER);
}
private createCounters() {
this.counters.set(TEXT_EDIT_COUNTER, { counter: 0, threshold: THRESHOLD_FOR_TEXT_EDIT });
this.counters.set(FEARTURES_USAGE_COUNTER, { counter: 0, threshold: THRESHOLD_FOR_FEATURE_USAGE });
}
private incrementCounter(counterName: counters): void {
if (!this.counters.has(counterName)) {
console.error(`Counter ${counterName} not supported in the feedback module of the Python Extension`);
return;
}

// tslint:disable-next-line:no-non-null-assertion
const value = this.counters.get(counterName)!;
value.counter += 1;

this.checkThreshold(counterName);
}
private checkThreshold(counterName: string) {
// tslint:disable-next-line:no-non-null-assertion
const value = this.counters.get(counterName)!;
if (value.counter < value.threshold) {
return;
}

this.emit('thresholdReached');
}
}
128 changes: 128 additions & 0 deletions src/client/feedback/feedbackService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

'use strict';

import * as child_process from 'child_process';
import * as os from 'os';
import { window } from 'vscode';
import { commands, Disposable, TextDocument, workspace } from 'vscode';
import { PythonLanguage } from '../common/constants';
import { IPersistentStateFactory, PersistentState } from '../common/persistentState';
import { FEEDBACK } from '../telemetry/constants';
import { captureTelemetry, sendTelemetryEvent } from '../telemetry/index';
import { FeedbackCounters } from './counters';

const FEEDBACK_URL = 'https://aka.ms/egv4z1';

export class FeedbackService implements Disposable {
private counters?: FeedbackCounters;
private showFeedbackPrompt: PersistentState<boolean>;
private userResponded: PersistentState<boolean>;
private promptDisplayed: boolean;
private disposables: Disposable[] = [];
private get canShowPrompt(): boolean {
return this.showFeedbackPrompt.value && !this.userResponded.value &&
!this.promptDisplayed && this.counters !== undefined;
}
constructor(persistentStateFactory: IPersistentStateFactory) {
this.showFeedbackPrompt = persistentStateFactory.createGlobalPersistentState('SHOW_FEEDBACK_PROMPT', true);
this.userResponded = persistentStateFactory.createGlobalPersistentState('RESPONDED_TO_FEEDBACK', false);
if (this.showFeedbackPrompt.value && !this.userResponded.value) {
this.initialize();
}
}
public dispose() {
this.counters = undefined;
this.disposables.forEach(disposable => {
// tslint:disable-next-line:no-unsafe-any
disposable.dispose();
});
this.disposables = [];
}
private initialize() {
// tslint:disable-next-line:no-void-expression
let commandDisable = commands.registerCommand('python.updateFeedbackCounter', (telemetryEventName: string) => this.updateFeedbackCounter(telemetryEventName));
this.disposables.push(commandDisable);
// tslint:disable-next-line:no-void-expression
commandDisable = workspace.onDidChangeTextDocument(changeEvent => this.handleChangesToTextDocument(changeEvent.document), this, this.disposables);
this.disposables.push(commandDisable);

this.counters = new FeedbackCounters();
this.counters.on('thresholdReached', () => {
this.thresholdHandler();
});
}
private handleChangesToTextDocument(textDocument: TextDocument) {
if (textDocument.languageId !== PythonLanguage.language) {
return;
}
if (!this.canShowPrompt) {
return;
}
this.counters.incrementEditCounter();
}
private updateFeedbackCounter(telemetryEventName: string): void {
// Ignore feedback events.
if (telemetryEventName === FEEDBACK) {
return;
}
if (!this.canShowPrompt) {
return;
}
this.counters.incrementFeatureUsageCounter();
}
private thresholdHandler() {
if (!this.canShowPrompt) {
return;
}
this.showPrompt();
}
private showPrompt() {
this.promptDisplayed = true;

const message = 'Would you tell us how likely you are to recommend the Python extension for VS Code to a friend or colleague?';
const yesButton = 'Yes';
const dontShowAgainButton = 'Don\'t Show Again';
window.showInformationMessage(message, yesButton, dontShowAgainButton).then((value) => {
switch (value) {
case yesButton: {
this.displaySurvey();
break;
}
case dontShowAgainButton: {
this.doNotShowFeedbackAgain();
break;
}
default: {
sendTelemetryEvent(FEEDBACK, undefined, { action: 'dismissed' });
break;
}
}
// Stop everything for this session.
this.dispose();
});
}
@captureTelemetry(FEEDBACK, { action: 'accepted' })
private displaySurvey() {
this.userResponded.value = true;

let openCommand: string | undefined;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this code common between here and the banner? Should it be factored out?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but the banner would go out (removed), hence didn't want to share any code.

if (os.platform() === 'win32') {
openCommand = 'explorer';
} else if (os.platform() === 'darwin') {
openCommand = '/usr/bin/open';
} else {
openCommand = '/usr/bin/xdg-open';
}
if (!openCommand) {
console.error(`Unable to determine platform to capture user feedback in Python extension ${os.platform()}`);
console.error(`Survey link is: ${FEEDBACK_URL}`);
}
child_process.spawn(openCommand, [FEEDBACK_URL]);
}
@captureTelemetry(FEEDBACK, { action: 'doNotShowAgain' })
private doNotShowFeedbackAgain() {
this.showFeedbackPrompt.value = false;
}
}
6 changes: 6 additions & 0 deletions src/client/feedback/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

'use strict';

export * from './feedbackService';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this file for? Simplifying the namespace?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes,

6 changes: 3 additions & 3 deletions src/client/formatters/autoPep8Formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import * as vscode from 'vscode';
import { PythonSettings } from '../common/configSettings';
import { Product } from '../common/installer';
import { sendTelemetryWhenDone } from '../common/telemetry';
import { FORMAT } from '../common/telemetry/constants';
import { StopWatch } from '../common/telemetry/stopWatch';
import { sendTelemetryWhenDone } from '../telemetry';
import { FORMAT } from '../telemetry/constants';
import { StopWatch } from '../telemetry/stopWatch';
import { BaseFormatter } from './baseFormatter';

export class AutoPep8Formatter extends BaseFormatter {
Expand Down
Loading