|
| 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