Skip to content

Commit 3243d2a

Browse files
committed
extension/src: fetch latest developer survey config from vscode-go module
The fetched configuration will be stored in vscode momento with last udpate time. The vscode-go extension will re-fetch if the cached config is one day ago. Local experiment at #2891 (comment) For #2891 Change-Id: I8e6d2bd64ca8ccbc1f7ab1a2c0e87d5897200dea Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/697195 Reviewed-by: Robert Findley <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Madeline Kalil <[email protected]>
1 parent 4fcd1d8 commit 3243d2a

File tree

4 files changed

+324
-58
lines changed

4 files changed

+324
-58
lines changed

extension/src/developerSurvey/config.ts

Lines changed: 137 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,151 @@
33
* Licensed under the MIT License. See LICENSE in the project root for license information.
44
*--------------------------------------------------------*/
55

6+
import * as cp from 'child_process';
7+
import * as fs from 'fs';
8+
import * as path from 'path';
9+
import * as util from 'util';
10+
import { getStateConfig } from '../goSurvey';
11+
import { getBinPath } from '../util';
12+
import { updateGlobalState } from '../stateUtils';
13+
import { outputChannel } from '../goStatus';
14+
615
/**
716
* DeveloperSurveyConfig holds the configuration for the Go Developer survey.
817
*/
918
export interface DeveloperSurveyConfig {
1019
/** The start date for the survey promotion. The survey will not be prompted before this date. */
11-
Start: Date;
20+
StartDate: Date;
1221
/** The end date for the survey promotion. The survey will not be prompted after this date. */
13-
End: Date;
22+
EndDate: Date;
1423
/** The URL for the survey. */
1524
URL: string;
1625
}
1726

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'
27+
/**
28+
* DEVELOPER_SURVEY_CONFIG_STATE_KEY is the key for the latest go developer
29+
* survey config stored in VSCode memento. It should not be changed to maintain
30+
* backward compatibility with previous extension versions.
31+
*/
32+
export const DEVELOPER_SURVEY_CONFIG_STATE_KEY = 'developerSurveyConfigState';
33+
34+
/**
35+
* DeveloperSurveyConfigState holds the most recently fetched survey
36+
* configuration, along with metadata about when it was fetched and its version.
37+
* This data is stored in the global memento to be used as a cache.
38+
*/
39+
export interface DeveloperSurveyConfigState {
40+
config: DeveloperSurveyConfig;
41+
version: string;
42+
lastDateUpdated: Date;
43+
}
44+
45+
export function getDeveloperSurveyConfigState(): DeveloperSurveyConfigState {
46+
return getStateConfig(DEVELOPER_SURVEY_CONFIG_STATE_KEY) as DeveloperSurveyConfigState;
47+
}
48+
49+
/**
50+
* getLatestDeveloperSurvey fetches the latest Go Developer Survey configuration.
51+
*
52+
* It first checks for a cached version of the survey config and returns it if it's
53+
* less than 24 hours old. Otherwise, it attempts to download the latest survey
54+
* configuration by fetching the specified Go module. If the download fails,
55+
* it falls back to returning the stale cached config if available.
56+
*
57+
* @returns A Promise that resolves to the DeveloperSurveyConfig, or undefined.
58+
*/
59+
export async function getLatestDeveloperSurvey(now: Date): Promise<DeveloperSurveyConfig | undefined> {
60+
const oldState = getDeveloperSurveyConfigState();
61+
if (oldState && oldState.config) {
62+
const SURVEY_CACHE_DURATION_MS = 24 * 60 * 60 * 1000; // 24 hours
63+
64+
if (now.getTime() - oldState.lastDateUpdated.getTime() <= SURVEY_CACHE_DURATION_MS) {
65+
outputChannel.info(`Using cached Go developer survey: ${oldState.version}`);
66+
outputChannel.info(
67+
`Survey active from ${oldState.config.StartDate.toDateString()} to ${oldState.config.EndDate.toDateString()}`
68+
);
69+
return oldState.config;
70+
}
71+
}
72+
73+
// Fetch the latest go developer survey module and flush it to momento.
74+
const res = await fetchRemoteSurveyConfig();
75+
if (!res) {
76+
if (oldState && oldState.config) {
77+
outputChannel.info(`Falling back to cached Go developer survey: ${oldState.version}`);
78+
outputChannel.info(
79+
`Survey active from ${oldState.config.StartDate.toDateString()} to ${oldState.config.EndDate.toDateString()}`
80+
);
81+
return oldState.config;
82+
} else {
83+
return undefined;
84+
}
85+
}
86+
87+
const [content, version] = res;
88+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
89+
const config = JSON.parse(content.toString(), (key: string, value: any) => {
90+
// Manually parse date fields.
91+
if (key === 'StartDate' || key === 'EndDate') {
92+
return new Date(value);
93+
}
94+
return value;
95+
}) as DeveloperSurveyConfig;
96+
97+
const newState: DeveloperSurveyConfigState = {
98+
config: config,
99+
version: version,
100+
lastDateUpdated: now
101+
};
102+
103+
updateGlobalState(DEVELOPER_SURVEY_CONFIG_STATE_KEY, JSON.stringify(newState));
104+
105+
outputChannel.info(`Using fetched Go developer survey: ${newState.version}`);
106+
outputChannel.info(
107+
`Survey active from ${newState.config.StartDate.toDateString()} to ${newState.config.EndDate.toDateString()}`
108+
);
109+
return config;
110+
}
111+
112+
/**
113+
* Fetches the latest survey config file from its Go module.
114+
* @returns A tuple containing the file content and the module version.
115+
*
116+
* This is defined as a const function expression rather than a function
117+
* declaration to allow it to be stubbed in tests. By defining it as a const,
118+
* it becomes a property on the module's exports object, which can be
119+
* replaced by test spies (e.g., using sandbox.stub).
120+
*/
121+
export const fetchRemoteSurveyConfig = async (): Promise<[string, string] | undefined> => {
122+
const SURVEY_MODULE_PATH = 'github.com/golang/vscode-go/survey';
123+
124+
outputChannel.info('Fetching latest go developer survey');
125+
const goRuntimePath = getBinPath('go');
126+
if (!goRuntimePath) {
127+
console.warn('Failed to run "go mod download" as the "go" binary cannot be found');
128+
return;
129+
}
130+
131+
const execFile = util.promisify(cp.execFile);
132+
133+
try {
134+
const { stdout } = await execFile(goRuntimePath, ['mod', 'download', '-json', `${SURVEY_MODULE_PATH}@latest`]);
135+
136+
/**
137+
* Interface for the expected JSON output from `go mod download -json`.
138+
* See https://go.dev/ref/mod#go-mod-download for details.
139+
*/
140+
interface DownloadModuleOutput {
141+
Path: string;
142+
Version: string;
143+
Dir: string;
144+
}
145+
const info = JSON.parse(stdout) as DownloadModuleOutput;
146+
return [fs.readFileSync(path.join(info.Dir, 'config.json')).toString(), info.Version];
147+
} catch (err) {
148+
outputChannel.error(
149+
`Failed to download the go developer survey module and parse "config.json": ${SURVEY_MODULE_PATH}:${err}`
150+
);
151+
return;
152+
}
22153
};

extension/src/developerSurvey/prompt.ts

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,22 @@
1-
/* eslint-disable @typescript-eslint/no-explicit-any */
21
/*---------------------------------------------------------
32
* Copyright 2021 The Go Authors. All rights reserved.
43
* Licensed under the MIT License. See LICENSE in the project root for license information.
54
*--------------------------------------------------------*/
65

76
'use strict';
87

9-
import vscode = require('vscode');
10-
import { getGoConfig } from '../config';
11-
import { daysBetween, storeSurveyState, getStateConfig, minutesBetween, timeMinute } from '../goSurvey';
8+
import * as vscode from 'vscode';
129
import { GoExtensionContext } from '../context';
13-
import { DeveloperSurveyConfig, latestSurveyConfig } from './config';
10+
import { getGoConfig } from '../config';
11+
import { daysBetween, getStateConfig, minutesBetween, storeSurveyState, timeMinute } from '../goSurvey';
12+
import { DeveloperSurveyConfig, getLatestDeveloperSurvey } from './config';
1413

1514
/**
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.
15+
* DEVELOPER_SURVEY_STATE_KEY is the key for the go developer survey state
16+
* stored in VSCode memento. It should not be changed to maintain backward
17+
* compatibility with previous extension versions.
1918
*/
20-
export const DEVELOPER_SURVEY_KEY = 'developerSurveyConfig';
19+
export const DEVELOPER_SURVEY_STATE_KEY = 'developerSurveyConfig';
2120

2221
/**
2322
* DeveloperSurveyState is the set of global properties used to determine if
@@ -46,7 +45,7 @@ export interface DeveloperSurveyState {
4645
lastDateAccepted?: Date;
4746
}
4847

49-
export function maybePromptForDeveloperSurvey(goCtx: GoExtensionContext) {
48+
export async function maybePromptForDeveloperSurvey(goCtx: GoExtensionContext) {
5049
// First, check the value of the 'go.survey.prompt' setting to see
5150
// if the user has opted out of all survey prompts.
5251
const goConfig = getGoConfig();
@@ -55,7 +54,11 @@ export function maybePromptForDeveloperSurvey(goCtx: GoExtensionContext) {
5554
}
5655

5756
const now = new Date();
58-
const config = getLatestDeveloperSurvey();
57+
const config = await getLatestDeveloperSurvey(now);
58+
if (!config) {
59+
return;
60+
}
61+
5962
const state = shouldPromptForSurvey(now, getDeveloperSurveyState(), config);
6063
if (!state) {
6164
return;
@@ -71,7 +74,7 @@ export function maybePromptForDeveloperSurvey(goCtx: GoExtensionContext) {
7174
}
7275
state = await promptForDeveloperSurvey(now, state, config);
7376
if (state) {
74-
storeSurveyState(DEVELOPER_SURVEY_KEY, state);
77+
storeSurveyState(DEVELOPER_SURVEY_STATE_KEY, state);
7578
}
7679
};
7780
prompt(state);
@@ -106,7 +109,7 @@ export function shouldPromptForSurvey(
106109
state.prompt = Math.random() < promptProbability;
107110

108111
// The state have changed, store it to memento.
109-
storeSurveyState(DEVELOPER_SURVEY_KEY, state);
112+
storeSurveyState(DEVELOPER_SURVEY_STATE_KEY, state);
110113
}
111114

112115
if (!state.prompt) {
@@ -130,7 +133,7 @@ export function shouldPromptForSurvey(
130133
}
131134
// If the survey will end in 5 days, prompt on the next day.
132135
// Otherwise, wait for 5 days.
133-
if (daysBetween(now, config.End) > 5) {
136+
if (daysBetween(now, config.EndDate) > 5) {
134137
return;
135138
}
136139
}
@@ -145,7 +148,7 @@ export async function promptForDeveloperSurvey(
145148
): Promise<DeveloperSurveyState> {
146149
const selected = await vscode.window.showInformationMessage(
147150
`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()}?`,
151+
by participating in this 10-minute Go Developer Survey (${config.EndDate.getFullYear().toString()}-${config.EndDate.getMonth().toString()}) before ${config.EndDate.toDateString()}?`,
149152
'Yes',
150153
'Remind me later',
151154
'Never'
@@ -196,24 +199,18 @@ If you'd like to opt-out of all survey prompts, you can set 'go.survey.prompt' t
196199
}
197200

198201
export function getDeveloperSurveyState(): DeveloperSurveyState {
199-
return getStateConfig(DEVELOPER_SURVEY_KEY) as DeveloperSurveyState;
202+
return getStateConfig(DEVELOPER_SURVEY_STATE_KEY) as DeveloperSurveyState;
200203
}
201204

202205
// Assumes that end > start.
203206
export function inDateRange(cfg: DeveloperSurveyConfig, date: Date): boolean {
204207
// date is before the start time.
205-
if (date.getTime() - cfg.Start.getTime() < 0) {
208+
if (date.getTime() - cfg.StartDate.getTime() < 0) {
206209
return false;
207210
}
208211
// end is before the date.
209-
if (cfg.End.getTime() - date.getTime() < 0) {
212+
if (cfg.EndDate.getTime() - date.getTime() < 0) {
210213
return false;
211214
}
212215
return true;
213216
}
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)