Skip to content

Commit 3988749

Browse files
committed
extension/src: rename developer survey config with state
vscode-go extension stores developer survey state (including the user reaction and prompt time) in vscode memento. vscode-go will use the previously stored state to determine whether we should prompt. This CL does not change any developer survey prompting behavior, only handles refactoring. For #2891 Change-Id: Icac7d288c41b44a5492a33703956d8504e811b53 Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/695395 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Robert Findley <[email protected]>
1 parent a3bbaa9 commit 3988749

File tree

7 files changed

+340
-279
lines changed

7 files changed

+340
-279
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*---------------------------------------------------------
2+
* Copyright 2025 The Go Authors. All rights reserved.
3+
* Licensed under the MIT License. See LICENSE in the project root for license information.
4+
*--------------------------------------------------------*/
5+
6+
/**
7+
* DeveloperSurveyConfig holds the configuration for the Go Developer survey.
8+
*/
9+
export interface DeveloperSurveyConfig {
10+
/** The start date for the survey promotion. The survey will not be prompted before this date. */
11+
Start: Date;
12+
/** The end date for the survey promotion. The survey will not be prompted after this date. */
13+
End: Date;
14+
/** The URL for the survey. */
15+
URL: string;
16+
}
17+
18+
export const latestSurveyConfig: DeveloperSurveyConfig = {
19+
Start: new Date('Sep 9 2024 00:00:00 GMT'),
20+
End: new Date('Sep 23 2024 00:00:00 GMT'),
21+
URL: 'https://google.qualtrics.com/jfe/form/SV_ei0CDV2K9qQIsp8?s=p'
22+
};
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
/*---------------------------------------------------------
3+
* Copyright 2021 The Go Authors. All rights reserved.
4+
* Licensed under the MIT License. See LICENSE in the project root for license information.
5+
*--------------------------------------------------------*/
6+
7+
'use strict';
8+
9+
import vscode = require('vscode');
10+
import { getGoConfig } from '../config';
11+
import { daysBetween, storeSurveyState, getStateConfig, minutesBetween, timeMinute } from '../goSurvey';
12+
import { GoExtensionContext } from '../context';
13+
import { DeveloperSurveyConfig, latestSurveyConfig } from './config';
14+
15+
/**
16+
* DEVELOPER_SURVEY_KEY is the key for the go developer survey state stored in
17+
* VSCode memento. It should not be changed to maintain backward compatibility
18+
* with previous extension versions.
19+
*/
20+
export const DEVELOPER_SURVEY_KEY = 'developerSurveyConfig';
21+
22+
/**
23+
* DeveloperSurveyState is the set of global properties used to determine if
24+
* we should prompt a user to take the go developer survey.
25+
* This interface is stored in VS Code's memento. The field names should not
26+
* be changed as they are key to parsing the stored data from previous releases.
27+
*/
28+
export interface DeveloperSurveyState {
29+
// prompt is true if the user can be prompted to take the survey.
30+
// It is false if the user was not selected to be prompted (e.g. part of the
31+
// 90% of users that are not prompted) or if we prompted and the user has
32+
// responded "Never" to the prompt. This state is kept per survey;
33+
// rejecting a survey means we will not prompt again for that specific
34+
// survey, but we will still prompt for the next one.
35+
prompt?: boolean;
36+
37+
// datePromptComputed is the date on which the value of the prompt field
38+
// was set. It is usually the same as lastDatePrompted, but not necessarily.
39+
datePromptComputed?: Date;
40+
41+
// lastDatePrompted is the most recent date that the user has been prompted.
42+
lastDatePrompted?: Date;
43+
44+
// lastDateAccepted is the most recent date that the user responded "Yes"
45+
// to the survey prompt. The user need not have completed the survey.
46+
lastDateAccepted?: Date;
47+
}
48+
49+
export function maybePromptForDeveloperSurvey(goCtx: GoExtensionContext) {
50+
// First, check the value of the 'go.survey.prompt' setting to see
51+
// if the user has opted out of all survey prompts.
52+
const goConfig = getGoConfig();
53+
if (goConfig.get('survey.prompt') === false) {
54+
return;
55+
}
56+
57+
const now = new Date();
58+
const config = getLatestDeveloperSurvey();
59+
const state = shouldPromptForSurvey(now, getDeveloperSurveyState(), config);
60+
if (!state) {
61+
return;
62+
}
63+
64+
const prompt = async (state: DeveloperSurveyState) => {
65+
const currentTime = new Date();
66+
const { lastUserAction = new Date() } = goCtx;
67+
// Make sure the user has been idle for at least a minute.
68+
if (minutesBetween(lastUserAction, currentTime) < 1) {
69+
setTimeout(prompt, 5 * timeMinute);
70+
return;
71+
}
72+
state = await promptForDeveloperSurvey(now, state, config);
73+
if (state) {
74+
storeSurveyState(DEVELOPER_SURVEY_KEY, state);
75+
}
76+
};
77+
prompt(state);
78+
}
79+
80+
/**
81+
* shouldPromptForSurvey decides if we should prompt the given user to take the
82+
* survey. It returns the DeveloperSurveyState if we should prompt, and
83+
* undefined if we should not prompt.
84+
*/
85+
export function shouldPromptForSurvey(
86+
now: Date,
87+
state: DeveloperSurveyState,
88+
config: DeveloperSurveyConfig
89+
): DeveloperSurveyState | undefined {
90+
// Don't prompt if the survey hasn't started or is over.
91+
if (!inDateRange(config, now)) {
92+
return;
93+
}
94+
95+
// TODO(rstambler): Merge checks for surveys into a setting.
96+
if (!state.datePromptComputed || !inDateRange(config, state.datePromptComputed)) {
97+
// state is missing or stale: reinitialize.
98+
state = {};
99+
100+
// This is the first activation for this survey period, so decide if we
101+
// should prompt the user. This is done by generating a random number in
102+
// the range [0, 1) and checking if it is < probability.
103+
state.datePromptComputed = now;
104+
105+
const promptProbability = 0.1;
106+
state.prompt = Math.random() < promptProbability;
107+
108+
// The state have changed, store it to memento.
109+
storeSurveyState(DEVELOPER_SURVEY_KEY, state);
110+
}
111+
112+
if (!state.prompt) {
113+
return;
114+
}
115+
116+
// Check if the user has taken the survey in the current survey period.
117+
// Don't prompt them if they have.
118+
if (state.lastDateAccepted && inDateRange(config, state.lastDateAccepted)) {
119+
return;
120+
}
121+
122+
// Check if the user has been prompted for the survey in the last 5 days.
123+
// Don't prompt them if they have been.
124+
if (state.lastDatePrompted) {
125+
const daysSinceLastPrompt = daysBetween(now, state.lastDatePrompted);
126+
// Don't prompt twice on the same day, even if it's the last day of the
127+
// survey.
128+
if (daysSinceLastPrompt < 1) {
129+
return;
130+
}
131+
// If the survey will end in 5 days, prompt on the next day.
132+
// Otherwise, wait for 5 days.
133+
if (daysBetween(now, config.End) > 5) {
134+
return;
135+
}
136+
}
137+
138+
return state;
139+
}
140+
141+
export async function promptForDeveloperSurvey(
142+
now: Date,
143+
state: DeveloperSurveyState,
144+
config: DeveloperSurveyConfig
145+
): Promise<DeveloperSurveyState> {
146+
const selected = await vscode.window.showInformationMessage(
147+
`Help shape Go’s future! Would you like to help ensure that Go is meeting your needs
148+
by participating in this 10-minute Go Developer Survey (${config.End.getMonth.toString()} ${config.End.getFullYear.toString()}) before ${config.End.toDateString()}?`,
149+
'Yes',
150+
'Remind me later',
151+
'Never'
152+
);
153+
154+
// Update the time last asked.
155+
state.lastDatePrompted = now;
156+
state.datePromptComputed = now;
157+
158+
switch (selected) {
159+
case 'Yes':
160+
{
161+
state.lastDateAccepted = now;
162+
state.prompt = true;
163+
await vscode.env.openExternal(vscode.Uri.parse(config.URL));
164+
}
165+
break;
166+
case 'Remind me later':
167+
state.prompt = true;
168+
169+
vscode.window.showInformationMessage("No problem! We'll ask you again another time.");
170+
break;
171+
case 'Never': {
172+
state.prompt = false;
173+
174+
const selected = await vscode.window.showInformationMessage(
175+
`No problem! We won't ask again.
176+
If you'd like to opt-out of all survey prompts, you can set 'go.survey.prompt' to false.`,
177+
'Open Settings'
178+
);
179+
switch (selected) {
180+
case 'Open Settings':
181+
vscode.commands.executeCommand('workbench.action.openSettings', 'go.survey.prompt');
182+
break;
183+
default:
184+
break;
185+
}
186+
break;
187+
}
188+
default:
189+
// If the user closes the prompt without making a selection, treat it
190+
// like a "Not now" response.
191+
state.prompt = true;
192+
193+
break;
194+
}
195+
return state;
196+
}
197+
198+
export function getDeveloperSurveyState(): DeveloperSurveyState {
199+
return getStateConfig(DEVELOPER_SURVEY_KEY) as DeveloperSurveyState;
200+
}
201+
202+
// Assumes that end > start.
203+
export function inDateRange(cfg: DeveloperSurveyConfig, date: Date): boolean {
204+
// date is before the start time.
205+
if (date.getTime() - cfg.Start.getTime() < 0) {
206+
return false;
207+
}
208+
// end is before the date.
209+
if (cfg.End.getTime() - date.getTime() < 0) {
210+
return false;
211+
}
212+
return true;
213+
}
214+
215+
export function getLatestDeveloperSurvey(): DeveloperSurveyConfig {
216+
// TODO(golang/vscode-go#2891): fetch latest developer survey config from
217+
// module and compare with hard coded developr survey.
218+
return latestSurveyConfig;
219+
}

0 commit comments

Comments
 (0)