10000 Add survey and banner (#7) · microsoft/vscode-python@8c038f9 · GitHub
[go: up one dir, main page]

Skip to content

Commit 8c038f9

Browse files
authored
Add survey and banner (#7)
* added feeback and survey * moved telemetry files into its own folder * track whether user responded to feedback * introduce constants * fix formatting of type * track whether user responded to feedback prompt * check whether to display banner * code review comments * code review comments
1 parent 45530ab commit 8c038f9

37 files changed

+369
-70
lines changed

src/client/banner.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import * as child_process from 'child_process';
7+
import * as os from 'os';
8+
import { window } from 'vscode';
9+
import { IPersistentStateFactory, PersistentState } from './common/persistentState';
10+
11+
const BANNER_URL = 'https://aka.ms/egv4z1';
12+
13+
export class BannerService {
14+
private shouldShowBanner: PersistentState<boolean>;
15+
constructor(persistentStateFactory: IPersistentStateFactory) {
16+
this.shouldShowBanner = persistentStateFactory.createGlobalPersistentState('SHOW_NEW_EXT_BANNER', true);
17+
this.showBanner();
18+
}
19+
private showBanner() {
20+
if (!this.shouldShowBanner.value) {
21+
return;
22+
}
23+
this.shouldShowBanner.value = false;
24+
25+
const message = 'Would you like to know what is new?';
26+
const yesButton = 'Yes';
27+
window.showInformationMessage(message, yesButton).then((value) => {
28+
if (value === yesButton) {
29+
this.displayBanner();
30+
}
31+
});
32+
}
33+
private displayBanner() {
34+
let openCommand: string | undefined;
35+
if (os.platform() === 'win32') {
36+
openCommand = 'explorer';
37+
} else if (os.platform() === 'darwin') {
38+
openCommand = '/usr/bin/open';
39+
} else {
40+
openCommand = '/usr/bin/xdg-open';
41+
}
42+
if (!openCommand) {
43+
console.error(`Unable open ${BANNER_URL} on platform '${os.platform()}'.`);
44+
}
45+
child_process.spawn(openCommand, [BANNER_URL]);
46+
}
47+
}

src/client/common/persistentState.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { Memento } from 'vscode';
7+
8+
export class PersistentState<T> {
9+
constructor(private storage: Memento, private key: string, private defaultValue: T) { }
10+
11+
public get value(): T {
12+
return this.storage.get<T>(this.key, this.defaultValue);
13+
}
14+
15+
public set value(newValue: T) {
16+
this.storage.update(this.key, newValue);
17+
}
18+
}
19+
20+
export interface IPersistentStateFactory {
21+
createGlobalPersistentState<T>(key: string, defaultValue: T): PersistentState<T>;
22+
createWorkspacePersistentState<T>(key: string, defaultValue: T): PersistentState<T>;
23+
}
24+
25+
export class PersistentStateFactory implements IPersistentStateFactory {
26+
constructor(private globalState: Memento, private workspaceState: Memento) { }
27+
public createGlobalPersistentState<T>(key: string, defaultValue: T): PersistentState<T> {
28+
return new PersistentState<T>(this.globalState, key, defaultValue);
29+
}
30+
public createWorkspacePersistentState<T>(key: string, defaultValue: T): PersistentState<T> {
31+
return new PersistentState<T>(this.workspaceState, key, defaultValue);
32+
}
33+
}

src/client/debugger/Main.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import { CreateAttachDebugClient, CreateLaunchDebugClient } from "./DebugClients
1414
import { LaunchRequestArguments, AttachRequestArguments, DebugOptions, TelemetryEvent, PythonEvaluationResultFlags } from "./Common/Contracts";
1515
import { validatePath, getPythonExecutable } from './Common/Utils';
1616
import { isNotInstalledError } from '../common/helpers';
17-
import { DEBUGGER } from '../../client/common/telemetry/constants';
18-
import { DebuggerTelemetry } from '../../client/common/telemetry/types';
17+
import { DEBUGGER } from '../../client/telemetry/constants';
18+
import { DebuggerTelemetry } from '../../client/telemetry/types';
1919

2020
const CHILD_ENUMEARATION_TIMEOUT = 5000;
2121

src/client/extension.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
'use strict';
2-
import { EDITOR_LOAD } from './common/telemetry/constants';
3-
42
import * as os from 'os';
53
import * as vscode from 'vscode';
4+
import { BannerService } from './banner';
65
import * as settings from './common/configSettings';
7-
import { Commands } from './common/constants';
86
import { createDeferred } from './common/helpers';
9-
import { sendTelemetryEvent } from './common/telemetry';
10-
import { StopWatch } from './common/telemetry/stopWatch';
7+
import { PersistentStateFactory } from './common/persistentState';
118
import { SimpleConfigurationProvider } from './debugger';
9+
import { FeedbackService } from './feedback';
1210
import { InterpreterManager } from './interpreter';
1311
import { SetInterpreterProvider } from './interpreter/configuration/setInterpreterProvider';
1412
import { ShebangCodeLensProvider } from './interpreter/display/shebangCodeLensProvider';
@@ -33,6 +31,9 @@ import { activateSimplePythonRefactorProvider } from './providers/simpleRefactor
3331
import { PythonSymbolProvider } from './providers/symbolProvider';
3432
import { activateUpdateSparkLibraryProvider } from './providers/updateSparkLibraryProvider';
3533
import * as sortImports from './sortImports';
34+
import { sendTelemetryEvent } from './telemetry';
35+
import { EDITOR_LOAD } from './telemetry/constants';
36+
import { StopWatch } from './telemetry/stopWatch';
3637
import { BlockFormatProviders } from './typeFormatters/blockFormatProvider';
3738
import * as tests from './unittests/main';
3839
import { WorkspaceSymbols } from './workspaceSymbols/main';
@@ -47,6 +48,7 @@ export const activated = activationDeferred.promise;
4748
// tslint:disable-next-line:max-func-body-length
4849
export async function activate(context: vscode.ExtensionContext) {
4950
const pythonSettings = settings.PythonSettings.getInstance();
51+
// tslint:disable-next-line:no-floating-promises
5052
sendStartupTelemetry(activated);
5153

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

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

118121
const jupyterExtInstalled = vscode.extensions.getExtension('donjayamanne.jupyter');
122+
// tslint:disable-next-line:promise-function-async
119123
const linterProvider = new LintProvider(context, lintingOutChannel, (a, b) => Promise.resolve(false));
120124
context.subscriptions.push();
121125
if (jupyterExtInstalled) {
122126
if (jupyterExtInstalled.isActive) {
127+
// tslint:disable-next-line:no-unsafe-any
123128
jupyterExtInstalled.exports.registerLanguageProvider(PYTHON.language, new JupyterProvider());
129+
// tslint:disable-next-line:no-unsafe-any
124130
linterProvider.documentHasJupyterCodeCells = jupyterExtInstalled.exports.hasCodeCells;
125131
}
126132

127133
jupyterExtInstalled.activate().then(() => {
134+
// tslint:disable-next-line:no-unsafe-any
128135
jupyterExtInstalled.exports.registerLanguageProvider(PYTHON.language, new JupyterProvider());
136+
// tslint:disable-next-line:no-unsafe-any
129137
linterProvider.documentHasJupyterCodeCells = jupyterExtInstalled.exports.hasCodeCells;
130138
});
131139
} else {
132140
jupMain = new jup.Jupyter(lintingOutChannel);
133141
const documentHasJupyterCodeCells = jupMain.hasCodeCells.bind(jupMain);
134142
jupMain.activate();
135143
context.subscriptions.push(jupMain);
144+
// tslint:disable-next-line:no-unsafe-any
136145
linterProvider.documentHasJupyterCodeCells = documentHasJupyterCodeCells;
137146
}
138147
tests.activate(context, unitTestOutChannel, symbolProvider);
@@ -146,17 +155,25 @@ export async function activate(context: vscode.ExtensionContext) {
146155

147156
context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider('python', new SimpleConfigurationProvider()));
148157
activationDeferred.resolve();
158+
159+
const persistentStateFactory = new PersistentStateFactory(context.globalState, context.workspaceState);
160+
const feedbackService = new FeedbackService(persistentStateFactory);
161+
context.subscriptions.push(feedbackService);
162+
// tslint:disable-next-line:no-unused-expression
163+
new BannerService(persistentStateFactory);
149164
}
150165

151166
async function sendStartupTelemetry(activatedPromise: Promise<void>) {
152167
const stopWatch = new StopWatch();
168+
// tslint:disable-next-line:no-floating-promises
153169
activatedPromise.then(async () => {
154170
const duration = stopWatch.elapsedTime;
155171
let condaVersion: string | undefined;
156172
try {
157173
condaVersion = await getCondaVersion();
158174
// tslint:disable-next-line:no-empty
159175
} catch { }
160-
sendTelemetryEvent(EDITOR_LOAD, duration, { condaVersion });
176+
const props = condaVersion ? { condaVersion } : undefined;
177+
sendTelemetryEvent(EDITOR_LOAD, duration, props);
161178
});
162179
}

src/client/feedback/counters.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { EventEmitter } from 'events';
7+
8+
const THRESHOLD_FOR_FEATURE_USAGE = 1000;
9+
const THRESHOLD_FOR_TEXT_EDIT = 5000;
10+
11+
const FEARTURES_USAGE_COUNTER = 'FEARTURES_USAGE';
12+
const TEXT_EDIT_COUNTER = 'TEXT_EDIT';
13+
type counters = 'FEARTURES_USAGE' | 'TEXT_EDIT';
14+
15+
export class FeedbackCounters extends EventEmitter {
16+
private counters = new Map<string, { counter: number, threshold: number }>();
17+
constructor() {
18+
super();
19+
this.createCounters();
20+
}
21+
public incrementEditCounter(): void {
22+
this.incrementCounter(TEXT_EDIT_COUNTER);
23+
}
24+
public incrementFeatureUsageCounter(): void {
25+
this.incrementCounter(FEARTURES_USAGE_COUNTER);
26+
}
27+
private createCounters() {
28+
this.counters.set(TEXT_EDIT_COUNTER, { counter: 0, threshold: THRESHOLD_FOR_TEXT_EDIT });
29+
this.counters.set(FEARTURES_USAGE_COUNTER, { counter: 0, threshold: THRESHOLD_FOR_FEATURE_USAGE });
30+
}
31+
private incrementCounter(counterName: counters): void {
32+
if (!this.counters.has(counterName)) {
33+
console.error(`Counter ${counterName} not supported in the feedback module of the Python Extension`);
34+
return;
35+
}
36+
37+
// tslint:disable-next-line:no-non-null-assertion
38+
const value = this.counters.get(counterName)!;
39+
value.counter += 1;
40+
41+
this.checkThreshold(counterName);
42+
}
43+
private checkThreshold(counterName: string) {
44+
// tslint:disable-next-line:no-non-null-assertion
45+
const value = this.counters.get(counterName)!;
46+
if (value.counter < value.threshold) {
47+
return;
48+
}
49+
50+
this.emit('thresholdReached');
51+
}
52+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import * as child_process from 'child_process';
7+
import * as os from 'os';
8+
import { window } from 'vscode';
9+
import { commands, Disposable, TextDocument, workspace } from 'vscode';
10+
import { PythonLanguage } from '../common/constants';
11+
import { IPersistentStateFactory, PersistentState } from '../common/persistentState';
12+
import { FEEDBACK } from '../telemetry/constants';
13+
import { captureTelemetry, sendTelemetryEvent } from '../telemetry/index';
14+
import { FeedbackCounters } from './counters';
15+
16+
const FEEDBACK_URL = 'https://aka.ms/egv4z1';
17+
18+
export class FeedbackService implements Disposable {
19+
private counters?: FeedbackCounters;
20+
private showFeedbackPrompt: PersistentState<boolean>;
21+
private userResponded: PersistentState<boolean>;
22+
private promptDisplayed: boolean;
23+
private disposables: Disposable[] = [];
24+
private get canShowPrompt(): boolean {
25+
return this.showFeedbackPrompt.value && !this.userResponded.value &&
26+
!this.promptDisplayed && this.counters !== undefined;
27+
}
28+
constructor(persistentStateFactory: IPersistentStateFactory) {
29+
this.showFeedbackPrompt = persistentStateFactory.createGlobalPersistentState('SHOW_FEEDBACK_PROMPT', true);
30+
this.userResponded = persistentStateFactory.createGlobalPersistentState('RESPONDED_TO_FEEDBACK', false);
31+
if (this.showFeedbackPrompt.value && !this.userResponded.value) {
32+
this.initialize();
33+
}
34+
}
35+
public dispose() {
36+
this.counters = undefined;
37+
this.disposables.forEach(disposable => {
38+
// tslint:disable-next-line:no-unsafe-any
39+
disposable.dispose();
40+
});
41+
this.disposables = [];
42+
}
43+
private initialize() {
44+
// tslint:disable-next-line:no-void-expression
45+
let commandDisable = commands.registerCommand('python.updateFeedbackCounter', (telemetryEventName: string) => this.updateFeedbackCounter(telemetryEventName));
46+
this.disposables.push(commandDisable);
47+
// tslint:disable-next-line:no-void-expression
48+
commandDisable = workspace.onDidChangeTextDocument(changeEvent => this.handleChangesToTextDocument(changeEvent.document), this, this.disposables);
49+
this.disposables.push(commandDisable);
50+
51+
this.counters = new FeedbackCounters();
52+
this.counters.on('thresholdReached', () => {
53+
this.thresholdHandler();
54+
});
55+
}
56+
private handleChangesToTextDocument(textDocument: TextDocument) {
57+
if (textDocument.languageId !== PythonLanguage.language) {
58+
return;
59+
}
60+
if (!this.canShowPrompt) {
61+
return;
62+
}
63+
this.counters.incrementEditCounter();
64+
}
65+
private updateFeedbackCounter(telemetryEventName: string): void {
66+
// Ignore feedback events.
67+
if (telemetryEventName === FEEDBACK) {
68+
return;
69+
}
70+
if (!this.canShowPrompt) {
71+
return;
72+
}
73+
this.counters.incrementFeatureUsageCounter();
74+
}
75+
private thresholdHandler() {
76+
if (!this.canShowPrompt) {
77+
return;
78+
}
79+
this.showPrompt();
80+
}
81+
private showPrompt() {
82+
this.promptDisplayed = true;
83+
84+
const message = 'Would you tell us how likely you are to recommend the Python extension for VS Code to a friend or colleague?';
85+
const yesButton = 'Yes';
86+
const dontShowAgainButton = 'Don\'t Show Again';
87+
window.showInformationMessage(message, yesButton, dontShowAgainButton).then((value) => {
88+
switch (value) {
89+
case yesButton: {
90+
this.displaySurvey();
91+
break;
92+
}
93+
case dontShowAgainButton: {
94+
this.doNotShowFeedbackAgain();
95+
break;
96+
}
97+
default: {
98+
sendTelemetryEvent(FEEDBACK, undefined, { action: 'dismissed' });
99+
break;
100+
}
101+
}
102+
// Stop everything for this session.
103+
this.dispose();
104+
});
105+
}
106+
@captureTelemetry(FEEDBACK, { action: 'accepted' })
107+
private displaySurvey() {
108+
this.userResponded.value = true;
109+
110+
let openCommand: string | undefined;
111+
if (os.platform() === 'win32') {
112+
openCommand = 'explorer';
113+
} else if (os.platform() === 'darwin') {
114+
openCommand = '/usr/bin/open';
115+
} else {
116+
openCommand = '/usr/bin/xdg-open';
117+
}
118+
if (!openCommand) {
119+
console.error(`Unable to determine platform to capture user feedback in Python extension ${os.platform()}`);
120+
console.error(`Survey link is: ${FEEDBACK_URL}`);
121+
}
122+
child_process.spawn(openCommand, [FEEDBACK_URL]);
123+
}
124+
@captureTelemetry(FEEDBACK, { action: 'doNotShowAgain' })
125+
private doNotShowFeedbackAgain() {
126+
this.showFeedbackPrompt.value = false;
127+
}
128+
}

src/client/feedback/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
export * from './feedbackService';

src/client/formatters/autoPep8Formatter.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
import * as vscode from 'vscode';
44
import { PythonSettings } from '../common/configSettings';
55
import { Product } from '../common/installer';
6-
import { sendTelemetryWhenDone } from '../common/telemetry';
7-
import { FORMAT } from '../common/telemetry/constants';
8-
import { StopWatch } from '../common/telemetry/stopWatch';
6+
import { sendTelemetryWhenDone } from '../telemetry';
7+
import { FORMAT } from '../telemetry/constants';
8+
import { StopWatch } from '../telemetry/stopWatch';
99
import { BaseFormatter } from './baseFormatter';
1010

1111
export class AutoPep8Formatter extends BaseFormatter {

0 commit comments

Comments
 (0)
0