goSurvey: pull survey logic out of goLanguageServer.ts
This is not intended to include any functional changes, just moving
code around.
Change-Id: I8f03eda081c3c19dad357efd256a0be86c62c597
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/315749
Trust: Rebecca Stambler <rstambler@golang.org>
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
diff --git a/src/goLanguageServer.ts b/src/goLanguageServer.ts
index a6107a6..966483b 100644
--- a/src/goLanguageServer.ts
+++ b/src/goLanguageServer.ts
@@ -59,7 +59,7 @@
import { GoWorkspaceSymbolProvider } from './goSymbol';
import { getTool, Tool } from './goTools';
import { GoTypeDefinitionProvider } from './goTypeDefinition';
-import { getFromGlobalState, getFromWorkspaceState, updateGlobalState, updateWorkspaceState } from './stateUtils';
+import { getFromGlobalState, updateGlobalState, updateWorkspaceState } from './stateUtils';
import {
getBinPath,
getCheckForToolsUpdatesConfig,
@@ -73,6 +73,7 @@
import WebRequest = require('web-request');
import { FoldingContext } from 'vscode';
import { ProvideFoldingRangeSignature } from 'vscode-languageclient/lib/common/foldingRange';
+import { daysBetween, getStateConfig, maybePromptForSurvey, timeDay, timeMinute } from './goSurvey';
export interface LanguageServerConfig {
serverName: string;
@@ -116,7 +117,7 @@
// lastUserAction is the time of the last user-triggered change.
// A user-triggered change is a didOpen, didChange, didSave, or didClose event.
-let lastUserAction: Date = new Date();
+export let lastUserAction: Date = new Date();
// startLanguageServerWithFallback starts the language server, if enabled,
// or falls back to the default language providers.
@@ -1256,226 +1257,6 @@
return null;
}
-// SurveyConfig is the set of global properties used to determine if
-// we should prompt a user to take the gopls survey.
-export interface SurveyConfig {
- // prompt is true if the user can be prompted to take the survey.
- // It is false if the user has responded "Never" to the prompt.
- prompt?: boolean;
-
- // promptThisMonth is true if we have used a random number generator
- // to determine if the user should be prompted this month.
- // It is undefined if we have not yet made the determination.
- promptThisMonth?: boolean;
-
- // dateToPromptThisMonth is the date on which we should prompt the user
- // this month.
- dateToPromptThisMonth?: Date;
-
- // dateComputedPromptThisMonth is the date on which the values of
- // promptThisMonth and dateToPromptThisMonth were set.
- dateComputedPromptThisMonth?: Date;
-
- // lastDatePrompted is the most recent date that the user has been prompted.
- lastDatePrompted?: Date;
-
- // lastDateAccepted is the most recent date that the user responded "Yes"
- // to the survey prompt. The user need not have completed the survey.
- lastDateAccepted?: Date;
-}
-
-function maybePromptForSurvey() {
- const now = new Date();
- let cfg = shouldPromptForSurvey(now, getSurveyConfig());
- if (!cfg) {
- return;
- }
- flushSurveyConfig(cfg);
- if (!cfg.dateToPromptThisMonth) {
- return;
- }
- const callback = async () => {
- const currentTime = new Date();
-
- // Make sure the user has been idle for at least a minute.
- if (minutesBetween(lastUserAction, currentTime) < 1) {
- setTimeout(callback, 5 * timeMinute);
- return;
- }
- cfg = await promptForSurvey(cfg, now);
- if (cfg) {
- flushSurveyConfig(cfg);
- }
- };
- const ms = msBetween(now, cfg.dateToPromptThisMonth);
- setTimeout(callback, ms);
-}
-
-export function shouldPromptForSurvey(now: Date, cfg: SurveyConfig): SurveyConfig {
- // If the prompt value is not set, assume we haven't prompted the user
- // and should do so.
- if (cfg.prompt === undefined) {
- cfg.prompt = true;
- }
- if (!cfg.prompt) {
- return;
- }
-
- // Check if the user has taken the survey in the last year.
- // Don't prompt them if they have been.
- if (cfg.lastDateAccepted) {
- if (daysBetween(now, cfg.lastDateAccepted) < 365) {
- return;
- }
- }
-
- // Check if the user has been prompted for the survey in the last 90 days.
- // Don't prompt them if they have been.
- if (cfg.lastDatePrompted) {
- if (daysBetween(now, cfg.lastDatePrompted) < 90) {
- return;
- }
- }
-
- // Check if the extension has been activated this month.
- if (cfg.dateComputedPromptThisMonth) {
- // The extension has been activated this month, so we should have already
- // decided if the user should be prompted.
- if (daysBetween(now, cfg.dateComputedPromptThisMonth) < 30) {
- return cfg;
- }
- }
- // This is the first activation this month (or ever), so decide if we
- // should prompt the user. This is done by generating a random number in
- // the range [0, 1) and checking if it is < probability, which varies
- // depending on whether the default or the nightly is in use.
- // We then randomly pick a day in the rest of the month on which to prompt
- // the user.
- let probability = 0.01; // lower probability for the regular extension
- if (isInPreviewMode()) {
- probability = 0.0275;
- }
- cfg.promptThisMonth = Math.random() < probability;
- if (cfg.promptThisMonth) {
- // end is the last day of the month, day is the random day of the
- // month on which to prompt.
- const end = new Date(now.getFullYear(), now.getMonth() + 1, 0);
- const day = randomIntInRange(now.getUTCDate(), end.getUTCDate());
- cfg.dateToPromptThisMonth = new Date(now.getFullYear(), now.getMonth(), day);
- } else {
- cfg.dateToPromptThisMonth = undefined;
- }
- cfg.dateComputedPromptThisMonth = now;
- return cfg;
-}
-
-async function promptForSurvey(cfg: SurveyConfig, now: Date): Promise<SurveyConfig> {
- const selected = await vscode.window.showInformationMessage(
- `Looks like you are using the Go extension for VS Code.
-Could you help us improve this extension by filling out a 1-2 minute survey about your experience with it?`,
- 'Yes',
- 'Not now',
- 'Never'
- );
-
- // Update the time last asked.
- cfg.lastDatePrompted = now;
-
- switch (selected) {
- case 'Yes':
- {
- cfg.lastDateAccepted = now;
- cfg.prompt = true;
- const goplsEnabled = latestConfig.enabled;
- const usersGoplsVersion = await getLocalGoplsVersion(latestConfig);
- await vscode.env.openExternal(
- vscode.Uri.parse(
- `https://google.qualtrics.com/jfe/form/SV_ekAdHVcVcvKUojX?usingGopls=${goplsEnabled}&gopls=${usersGoplsVersion}&extid=${extensionId}`
- )
- );
- }
- break;
- case 'Not now':
- cfg.prompt = true;
-
- vscode.window.showInformationMessage("No problem! We'll ask you again another time.");
- break;
- case 'Never':
- cfg.prompt = false;
-
- vscode.window.showInformationMessage("No problem! We won't ask again.");
- break;
- default:
- // If the user closes the prompt without making a selection, treat it
- // like a "Not now" response.
- cfg.prompt = true;
-
- break;
- }
- return cfg;
-}
-
-export const goplsSurveyConfig = 'goplsSurveyConfig';
-
-function getSurveyConfig(): SurveyConfig {
- return getStateConfig(goplsSurveyConfig) as SurveyConfig;
-}
-
-export function resetSurveyConfig() {
- flushSurveyConfig(null);
-}
-
-function flushSurveyConfig(cfg: SurveyConfig) {
- if (cfg) {
- updateGlobalState(goplsSurveyConfig, JSON.stringify(cfg));
- } else {
- updateGlobalState(goplsSurveyConfig, null); // reset
- }
-}
-
-function getStateConfig(globalStateKey: string, workspace?: boolean): any {
- let saved: any;
- if (workspace === true) {
- saved = getFromWorkspaceState(globalStateKey);
- } else {
- saved = getFromGlobalState(globalStateKey);
- }
- if (saved === undefined) {
- return {};
- }
- try {
- const cfg = JSON.parse(saved, (key: string, value: any) => {
- // Make sure values that should be dates are correctly converted.
- if (key.toLowerCase().includes('date') || key.toLowerCase().includes('timestamp')) {
- return new Date(value);
- }
- return value;
- });
- return cfg || {};
- } catch (err) {
- console.log(`Error parsing JSON from ${saved}: ${err}`);
- return {};
- }
-}
-
-export async function showSurveyConfig() {
- outputChannel.appendLine('Gopls Survey Configuration');
- outputChannel.appendLine(JSON.stringify(getSurveyConfig(), null, 2));
- outputChannel.show();
-
- const selected = await vscode.window.showInformationMessage('Prompt for survey?', 'Yes', 'Maybe', 'No');
- switch (selected) {
- case 'Yes':
- promptForSurvey(getSurveyConfig(), new Date());
- break;
- case 'Maybe':
- maybePromptForSurvey();
- break;
- default:
- break;
- }
-}
-
// errorKind refers to the different possible kinds of gopls errors.
enum errorKind {
initializationFailure,
@@ -1635,31 +1416,6 @@
}
}
-// randomIntInRange returns a random integer between min and max, inclusive.
-function randomIntInRange(min: number, max: number): number {
- const low = Math.ceil(min);
- const high = Math.floor(max);
- return Math.floor(Math.random() * (high - low + 1)) + low;
-}
-
-export const timeMinute = 1000 * 60;
-const timeHour = timeMinute * 60;
-const timeDay = timeHour * 24;
-
-// daysBetween returns the number of days between a and b.
-function daysBetween(a: Date, b: Date): number {
- return msBetween(a, b) / timeDay;
-}
-
-// minutesBetween returns the number of minutes between a and b.
-function minutesBetween(a: Date, b: Date): number {
- return msBetween(a, b) / timeMinute;
-}
-
-function msBetween(a: Date, b: Date): number {
- return Math.abs(a.getTime() - b.getTime());
-}
-
export function showServerOutputChannel() {
if (!languageServerIsRunning) {
vscode.window.showInformationMessage('gopls is not running');
diff --git a/src/goMain.ts b/src/goMain.ts
index 5e1a3c6..0fcd4e5 100644
--- a/src/goMain.ts
+++ b/src/goMain.ts
@@ -47,11 +47,8 @@
import {
isInPreviewMode,
languageServerIsRunning,
- resetSurveyConfig,
showServerOutputChannel,
- showSurveyConfig,
startLanguageServerWithFallback,
- timeMinute,
watchLanguageServerConfiguration
} from './goLanguageServer';
import { lintCode } from './goLint';
@@ -104,6 +101,7 @@
import semver = require('semver');
import vscode = require('vscode');
import { getFormatTool } from './goFormat';
+import { resetSurveyConfig, showSurveyConfig, timeMinute } from './goSurvey';
export let buildDiagnosticCollection: vscode.DiagnosticCollection;
export let lintDiagnosticCollection: vscode.DiagnosticCollection;
diff --git a/src/goSurvey.ts b/src/goSurvey.ts
new file mode 100644
index 0000000..eb02760
--- /dev/null
+++ b/src/goSurvey.ts
@@ -0,0 +1,258 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+/*---------------------------------------------------------
+ * Copyright 2021 The Go Authors. All rights reserved.
+ * Licensed under the MIT License. See LICENSE in the project root for license information.
+ *--------------------------------------------------------*/
+
+'use strict';
+
+import vscode = require('vscode');
+import { getLocalGoplsVersion, isInPreviewMode, lastUserAction, latestConfig } from './goLanguageServer';
+import { outputChannel } from './goStatus';
+import { extensionId } from './const';
+import { getFromGlobalState, getFromWorkspaceState, updateGlobalState } from './stateUtils';
+
+// SurveyConfig is the set of global properties used to determine if
+// we should prompt a user to take the gopls survey.
+export interface SurveyConfig {
+ // prompt is true if the user can be prompted to take the survey.
+ // It is false if the user has responded "Never" to the prompt.
+ prompt?: boolean;
+
+ // promptThisMonth is true if we have used a random number generator
+ // to determine if the user should be prompted this month.
+ // It is undefined if we have not yet made the determination.
+ promptThisMonth?: boolean;
+
+ // dateToPromptThisMonth is the date on which we should prompt the user
+ // this month.
+ dateToPromptThisMonth?: Date;
+
+ // dateComputedPromptThisMonth is the date on which the values of
+ // promptThisMonth and dateToPromptThisMonth were set.
+ dateComputedPromptThisMonth?: Date;
+
+ // lastDatePrompted is the most recent date that the user has been prompted.
+ lastDatePrompted?: Date;
+
+ // lastDateAccepted is the most recent date that the user responded "Yes"
+ // to the survey prompt. The user need not have completed the survey.
+ lastDateAccepted?: Date;
+}
+
+export function maybePromptForSurvey() {
+ const now = new Date();
+ let cfg = shouldPromptForSurvey(now, getSurveyConfig());
+ if (!cfg) {
+ return;
+ }
+ flushSurveyConfig(cfg);
+ if (!cfg.dateToPromptThisMonth) {
+ return;
+ }
+ const callback = async () => {
+ const currentTime = new Date();
+
+ // Make sure the user has been idle for at least a minute.
+ if (minutesBetween(lastUserAction, currentTime) < 1) {
+ setTimeout(callback, 5 * timeMinute);
+ return;
+ }
+ cfg = await promptForSurvey(cfg, now);
+ if (cfg) {
+ flushSurveyConfig(cfg);
+ }
+ };
+ const ms = msBetween(now, cfg.dateToPromptThisMonth);
+ setTimeout(callback, ms);
+}
+
+export function shouldPromptForSurvey(now: Date, cfg: SurveyConfig): SurveyConfig {
+ // If the prompt value is not set, assume we haven't prompted the user
+ // and should do so.
+ if (cfg.prompt === undefined) {
+ cfg.prompt = true;
+ }
+ if (!cfg.prompt) {
+ return;
+ }
+
+ // Check if the user has taken the survey in the last year.
+ // Don't prompt them if they have been.
+ if (cfg.lastDateAccepted) {
+ if (daysBetween(now, cfg.lastDateAccepted) < 365) {
+ return;
+ }
+ }
+
+ // Check if the user has been prompted for the survey in the last 90 days.
+ // Don't prompt them if they have been.
+ if (cfg.lastDatePrompted) {
+ if (daysBetween(now, cfg.lastDatePrompted) < 90) {
+ return;
+ }
+ }
+
+ // Check if the extension has been activated this month.
+ if (cfg.dateComputedPromptThisMonth) {
+ // The extension has been activated this month, so we should have already
+ // decided if the user should be prompted.
+ if (daysBetween(now, cfg.dateComputedPromptThisMonth) < 30) {
+ return cfg;
+ }
+ }
+ // This is the first activation this month (or ever), so decide if we
+ // should prompt the user. This is done by generating a random number in
+ // the range [0, 1) and checking if it is < probability, which varies
+ // depending on whether the default or the nightly is in use.
+ // We then randomly pick a day in the rest of the month on which to prompt
+ // the user.
+ let probability = 0.01; // lower probability for the regular extension
+ if (isInPreviewMode()) {
+ probability = 0.0275;
+ }
+ cfg.promptThisMonth = Math.random() < probability;
+ if (cfg.promptThisMonth) {
+ // end is the last day of the month, day is the random day of the
+ // month on which to prompt.
+ const end = new Date(now.getFullYear(), now.getMonth() + 1, 0);
+ const day = randomIntInRange(now.getUTCDate(), end.getUTCDate());
+ cfg.dateToPromptThisMonth = new Date(now.getFullYear(), now.getMonth(), day);
+ } else {
+ cfg.dateToPromptThisMonth = undefined;
+ }
+ cfg.dateComputedPromptThisMonth = now;
+ return cfg;
+}
+
+// randomIntInRange returns a random integer between min and max, inclusive.
+function randomIntInRange(min: number, max: number): number {
+ const low = Math.ceil(min);
+ const high = Math.floor(max);
+ return Math.floor(Math.random() * (high - low + 1)) + low;
+}
+
+async function promptForSurvey(cfg: SurveyConfig, now: Date): Promise<SurveyConfig> {
+ const selected = await vscode.window.showInformationMessage(
+ `Looks like you are using the Go extension for VS Code.
+Could you help us improve this extension by filling out a 1-2 minute survey about your experience with it?`,
+ 'Yes',
+ 'Not now',
+ 'Never'
+ );
+
+ // Update the time last asked.
+ cfg.lastDatePrompted = now;
+
+ switch (selected) {
+ case 'Yes':
+ {
+ cfg.lastDateAccepted = now;
+ cfg.prompt = true;
+ const goplsEnabled = latestConfig.enabled;
+ const usersGoplsVersion = await getLocalGoplsVersion(latestConfig);
+ await vscode.env.openExternal(
+ vscode.Uri.parse(
+ `https://google.qualtrics.com/jfe/form/SV_ekAdHVcVcvKUojX?usingGopls=${goplsEnabled}&gopls=${usersGoplsVersion}&extid=${extensionId}`
+ )
+ );
+ }
+ break;
+ case 'Not now':
+ cfg.prompt = true;
+
+ vscode.window.showInformationMessage("No problem! We'll ask you again another time.");
+ break;
+ case 'Never':
+ cfg.prompt = false;
+
+ vscode.window.showInformationMessage("No problem! We won't ask again.");
+ break;
+ default:
+ // If the user closes the prompt without making a selection, treat it
+ // like a "Not now" response.
+ cfg.prompt = true;
+
+ break;
+ }
+ return cfg;
+}
+
+export const goplsSurveyConfig = 'goplsSurveyConfig';
+
+function getSurveyConfig(): SurveyConfig {
+ return getStateConfig(goplsSurveyConfig) as SurveyConfig;
+}
+
+export function resetSurveyConfig() {
+ flushSurveyConfig(null);
+}
+
+function flushSurveyConfig(cfg: SurveyConfig) {
+ if (cfg) {
+ updateGlobalState(goplsSurveyConfig, JSON.stringify(cfg));
+ } else {
+ updateGlobalState(goplsSurveyConfig, null); // reset
+ }
+}
+
+export function getStateConfig(globalStateKey: string, workspace?: boolean): any {
+ let saved: any;
+ if (workspace === true) {
+ saved = getFromWorkspaceState(globalStateKey);
+ } else {
+ saved = getFromGlobalState(globalStateKey);
+ }
+ if (saved === undefined) {
+ return {};
+ }
+ try {
+ const cfg = JSON.parse(saved, (key: string, value: any) => {
+ // Make sure values that should be dates are correctly converted.
+ if (key.toLowerCase().includes('date') || key.toLowerCase().includes('timestamp')) {
+ return new Date(value);
+ }
+ return value;
+ });
+ return cfg || {};
+ } catch (err) {
+ console.log(`Error parsing JSON from ${saved}: ${err}`);
+ return {};
+ }
+}
+
+export async function showSurveyConfig() {
+ outputChannel.appendLine('Gopls Survey Configuration');
+ outputChannel.appendLine(JSON.stringify(getSurveyConfig(), null, 2));
+ outputChannel.show();
+
+ const selected = await vscode.window.showInformationMessage('Prompt for survey?', 'Yes', 'Maybe', 'No');
+ switch (selected) {
+ case 'Yes':
+ promptForSurvey(getSurveyConfig(), new Date());
+ break;
+ case 'Maybe':
+ maybePromptForSurvey();
+ break;
+ default:
+ break;
+ }
+}
+
+export const timeMinute = 1000 * 60;
+const timeHour = timeMinute * 60;
+export const timeDay = timeHour * 24;
+
+// daysBetween returns the number of days between a and b.
+export function daysBetween(a: Date, b: Date): number {
+ return msBetween(a, b) / timeDay;
+}
+
+// minutesBetween returns the number of minutes between a and b.
+function minutesBetween(a: Date, b: Date): number {
+ return msBetween(a, b) / timeMinute;
+}
+
+export function msBetween(a: Date, b: Date): number {
+ return Math.abs(a.getTime() - b.getTime());
+}
diff --git a/test/gopls/survey.test.ts b/test/gopls/survey.test.ts
index f065510..3a32954 100644
--- a/test/gopls/survey.test.ts
+++ b/test/gopls/survey.test.ts
@@ -7,11 +7,12 @@
import sinon = require('sinon');
import vscode = require('vscode');
import goLanguageServer = require('../../src/goLanguageServer');
+import goSurvey = require('../../src/goSurvey');
suite('gopls survey tests', () => {
test('prompt for survey', () => {
// global state -> offer survey
- const testCases: [goLanguageServer.SurveyConfig, boolean][] = [
+ const testCases: [goSurvey.SurveyConfig, boolean][] = [
// User who is activating the extension for the first time.
[{}, true],
// User who has already taken the survey.
@@ -70,7 +71,7 @@
sinon.replace(Math, 'random', () => 0);
const now = new Date('2020-04-29');
- const gotPrompt = goLanguageServer.shouldPromptForSurvey(now, testConfig);
+ const gotPrompt = goSurvey.shouldPromptForSurvey(now, testConfig);
if (wantPrompt) {
assert.ok(gotPrompt, `prompt determination failed for ${i}`);
} else {