forked from goplus/vscode-gop
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgoSurvey.ts
295 lines (264 loc) · 9.31 KB
/
goSurvey.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
/* 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 { CommandFactory } from './commands';
import { getGoConfig } from './config';
import { extensionId } from './const';
import { GoExtensionContext } from './context';
import {
developerSurveyConfig,
getDeveloperSurveyConfig,
maybePromptForDeveloperSurvey,
promptForDeveloperSurvey
} from './goDeveloperSurvey';
import { outputChannel } from './goStatus';
import { getLocalGoplsVersion } from './language/goLanguageServer';
import { getFromGlobalState, getFromWorkspaceState, updateGlobalState } from './stateUtils';
import { getGoVersion } from './util';
import { promptNext4Weeks } from './utils/randomDayutils';
// GoplsSurveyConfig is the set of global properties used to determine if
// we should prompt a user to take the gopls survey.
export interface GoplsSurveyConfig {
// 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. (It is no longer necessarily in the current 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 maybePromptForGoplsSurvey(goCtx: GoExtensionContext) {
// First, check the value of the 'gop.survey.prompt' setting to see
// if the user has opted out of all survey prompts.
const goConfig = getGoConfig();
if (goConfig.get('survey.prompt') === false) {
return;
}
const now = new Date();
let cfg = shouldPromptForSurvey(now, getGoplsSurveyConfig());
if (!cfg) {
return;
}
if (!cfg.dateToPromptThisMonth) {
return;
}
const callback = async () => {
const currentTime = new Date();
const { lastUserAction = new Date() } = goCtx;
// Make sure the user has been idle for at least a minute.
if (minutesBetween(lastUserAction, currentTime) < 1) {
setTimeout(callback, 5 * timeMinute);
return;
}
cfg = await promptForGoplsSurvey(goCtx, cfg, now);
if (cfg) {
flushSurveyConfig(goplsSurveyConfig, cfg);
}
};
const ms = msBetween(now, cfg.dateToPromptThisMonth);
setTimeout(callback, ms);
}
export function shouldPromptForSurvey(now: Date, cfg: GoplsSurveyConfig): GoplsSurveyConfig | undefined {
// 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;
}
flushSurveyConfig(goplsSurveyConfig, cfg);
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) < 28) {
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.
// We then randomly pick a day in the next 4 weeks to prompt the user.
// Probability is set based on the # of responses received, and will be
// decreased if we begin receiving > 200 responses/month.
const probability = 0.06;
cfg.promptThisMonth = Math.random() < probability;
if (cfg.promptThisMonth) {
cfg.dateToPromptThisMonth = promptNext4Weeks(now);
} else {
cfg.dateToPromptThisMonth = undefined;
}
cfg.dateComputedPromptThisMonth = now;
flushSurveyConfig(goplsSurveyConfig, cfg);
return cfg;
}
async function promptForGoplsSurvey(
goCtx: GoExtensionContext,
cfg: GoplsSurveyConfig = {},
now: Date
): Promise<GoplsSurveyConfig> {
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':
{
const { latestConfig } = goCtx;
cfg.lastDateAccepted = now;
cfg.prompt = true;
const goplsEnabled = latestConfig?.enabled;
const usersGoplsVersion = await getLocalGoplsVersion(latestConfig);
const goV = await getGoVersion();
const goVersion = goV ? (goV.isDevel ? 'devel' : goV.format(true)) : 'na';
const surveyURL = `https://go.dev/s/ide-hats-survey/?s=c&usingGopls=${goplsEnabled}&gopls=${usersGoplsVersion?.version}&extid=${extensionId}&go=${goVersion}&os=${process.platform}`;
await vscode.env.openExternal(vscode.Uri.parse(surveyURL));
}
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;
const selected = await vscode.window.showInformationMessage(
`No problem! We won't ask again.
To opt-out of all survey prompts, please disable the 'Go > Survey: Prompt' setting.`,
'Open Settings'
);
switch (selected) {
case 'Open Settings':
vscode.commands.executeCommand('workbench.action.openSettings', 'gop.survey.prompt');
break;
default:
break;
}
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 getGoplsSurveyConfig(): GoplsSurveyConfig {
return getStateConfig(goplsSurveyConfig) as GoplsSurveyConfig;
}
export const resetSurveyConfigs: CommandFactory = () => () => {
flushSurveyConfig(goplsSurveyConfig, null);
flushSurveyConfig(developerSurveyConfig, null);
};
export function flushSurveyConfig(key: string, cfg: any) {
if (cfg) {
updateGlobalState(key, JSON.stringify(cfg));
} else {
updateGlobalState(key, 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 const showSurveyConfig: CommandFactory = (ctx, goCtx) => async () => {
// TODO(rstambler): Add developer survey config.
outputChannel.appendLine('HaTs Survey Configuration');
outputChannel.appendLine(JSON.stringify(getGoplsSurveyConfig(), null, 2));
outputChannel.show();
outputChannel.appendLine('Developer Survey Configuration');
outputChannel.appendLine(JSON.stringify(getDeveloperSurveyConfig(), null, 2));
outputChannel.show();
let selected = await vscode.window.showInformationMessage('Prompt for HaTS survey?', 'Yes', 'Maybe', 'No');
switch (selected) {
case 'Yes':
promptForGoplsSurvey(goCtx, getGoplsSurveyConfig(), new Date());
break;
case 'Maybe':
maybePromptForGoplsSurvey(goCtx);
break;
default:
break;
}
selected = await vscode.window.showInformationMessage('Prompt for Developer survey?', 'Yes', 'Maybe', 'No');
switch (selected) {
case 'Yes':
promptForDeveloperSurvey(getDeveloperSurveyConfig(), new Date());
break;
case 'Maybe':
maybePromptForDeveloperSurvey(goCtx);
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.
export 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());
}