goplsSurvey: remove lumpiness in survey days
Users are occasionally asked to take the gopls survey. The logic
is a little complicated, but in the existing code, if the user is
prompted, it will be for a random day in the rest of the current
month. In the new code all days of the year are pretty nearly equally
probable. The new code chooses a random day in the next 28 days.
There's nothing special about 28, but it's large enough to smooth out
day-of-the-week effects, and small enough that a chosen user
will be prompted for a survey within a month.
Fixes vscode-go/#2545
Change-Id: Ifaccaa5e8e4e8b1a1050768461ed38d4e144561e
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/456041
TryBot-Result: kokoro <noreply+kokoro@google.com>
Run-TryBot: Peter Weinberger <pjw@google.com>
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
diff --git a/src/goSurvey.ts b/src/goSurvey.ts
index a06f1d4..2e9654e 100644
--- a/src/goSurvey.ts
+++ b/src/goSurvey.ts
@@ -7,20 +7,21 @@
'use strict';
import vscode = require('vscode');
-import { getLocalGoplsVersion } from './language/goLanguageServer';
-import { outputChannel } from './goStatus';
+import { CommandFactory } from './commands';
+import { getGoConfig } from './config';
import { extensionId } from './const';
-import { getFromGlobalState, getFromWorkspaceState, updateGlobalState } from './stateUtils';
+import { GoExtensionContext } from './context';
import {
developerSurveyConfig,
getDeveloperSurveyConfig,
maybePromptForDeveloperSurvey,
promptForDeveloperSurvey
} from './goDeveloperSurvey';
-import { getGoConfig } from './config';
+import { outputChannel } from './goStatus';
+import { getLocalGoplsVersion } from './language/goLanguageServer';
+import { getFromGlobalState, getFromWorkspaceState, updateGlobalState } from './stateUtils';
import { getGoVersion } from './util';
-import { GoExtensionContext } from './context';
-import { CommandFactory } from './commands';
+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.
@@ -35,7 +36,7 @@
promptThisMonth?: boolean;
// dateToPromptThisMonth is the date on which we should prompt the user
- // this month.
+ // this month. (It is no longer necessarily in the current month.)
dateToPromptThisMonth?: Date;
// dateComputedPromptThisMonth is the date on which the values of
@@ -113,25 +114,20 @@
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) {
+ 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 rest of the month on which to prompt
- // the user.
+ // 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) {
- // 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);
+ cfg.dateToPromptThisMonth = promptNext4Weeks(now);
} else {
cfg.dateToPromptThisMonth = undefined;
}
@@ -140,13 +136,6 @@
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 promptForGoplsSurvey(
goCtx: GoExtensionContext,
cfg: GoplsSurveyConfig = {},
diff --git a/src/utils/randomDayutils.ts b/src/utils/randomDayutils.ts
new file mode 100644
index 0000000..a1ac83f
--- /dev/null
+++ b/src/utils/randomDayutils.ts
@@ -0,0 +1,22 @@
+/*---------------------------------------------------------
+ * Copyright 2022 The Go Authors. All rights reserved.
+ * Licensed under the MIT License. See LICENSE in the project root for license information.
+ *--------------------------------------------------------*/
+
+// find a random day in the next 4 weeks, starting tomorrow
+// (If this code is changed, run the test in calendartest.ts
+// by hand.)
+export function promptNext4Weeks(now: Date): Date {
+ const day = 24 * 3600 * 1000; // milliseconds in a day
+ // choose a random day in the next 4 weeks, starting tomorrow
+ const delta = randomIntInRange(1, 28);
+ const x = now.getTime() + day * delta;
+ return new Date(x);
+}
+
+// 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;
+}
diff --git a/test/unit/calendartest.ts b/test/unit/calendartest.ts
new file mode 100644
index 0000000..abaea69
--- /dev/null
+++ b/test/unit/calendartest.ts
@@ -0,0 +1,66 @@
+/*---------------------------------------------------------
+ * Copyright 2022 The Go Authors. All rights reserved.
+ * Licensed under the MIT License. See LICENSE in the project root for license information.
+ *--------------------------------------------------------*/
+
+import assert from 'assert';
+import { promptNext4Weeks } from '../../src/utils/randomDayutils';
+
+// Set this to run the test. There's no point in running it over
+// and over, but it should be run if promptNext4Weeks is changed.
+const runNextDaysTest = false;
+
+// Test that a year of generated dates looks uniform. This test relies on
+// statistical tests, so in principle, it could fail. The parameters for the
+// statistics are chosen so there should be no more than a 1 in 1,000,000 chance.
+// (Further, if it passes once and the code it correct,
+// it then becomes a test of the random number generator, which is pointless.)
+// (this test takes about 40 msec on a 2018 Macbook Pro)
+suite('random day tests', () => {
+ test('next days', () => {
+ if (!runNextDaysTest) {
+ return;
+ }
+ const newYear = new Date('2024-01-01');
+ const day = 24 * 3600 * 1000;
+ const seen4 = new Array<number>(366);
+ for (let i = 0; i < 366; i++) {
+ seen4[i] = 0;
+ }
+ for (let i = 0; i < 366; i++) {
+ for (let j = 0; j < 100; j++) {
+ const today = new Date(newYear.getTime() + day * i);
+ const next = promptNext4Weeks(today);
+ assert((next.getTime() - today.getTime()) % day === 0);
+ const days = (next.getTime() - today.getTime()) / day;
+ assert(days >= 1 && days <= 28, 'days: ' + days);
+ const doy = Math.floor((next.getTime() - new Date(next.getFullYear(), 0, 0).getTime()) / day);
+ seen4[doy - 1]++;
+ }
+ }
+ assert.ok(isUniform(seen4));
+ // console.log(`elapsed ${new Date().getTime() - now.getTime()} ms}`);
+ });
+});
+
+// decide if the contnts of x look like a uniform distribution, This assumes x.length > 50 or so,
+// and approximates the chi-squared distribution with a normal distribution. (see the Wikipedia article)
+// The approximation is that sqrt(2*chi2) ~ N(sqrt(2*df-1) is good enough for our purposes.
+// The change of getting a 4.8 sigma deviation is about 1 in 1,000,000.
+function isUniform(x: number[], bound = 4.8): boolean {
+ const k = x.length;
+ const df = k - 1;
+ const sum = x.reduce((sum, current) => sum + current, 0);
+ const exp = sum / k;
+ const chi2 = x.reduce((sum, current) => sum + ((current - exp) * (current - exp)) / exp, 0);
+ const sd = Math.sqrt(2 * chi2) - Math.sqrt(2 * df - 1);
+ // sd would be the standard deviation in units of 1. 1
+ let ret = sd < bound && sd > -bound;
+ // and make sure the individual values aren't crazy (5 std devs of normal has prob 3e-7)
+ const expsd = 5 * Math.sqrt(exp);
+ x.map((v) => {
+ if (v < exp - expsd || v > exp + expsd) ret = false;
+ });
+ // console.log(`sd: ${sd} bound: ${bound} expsd: ${expsd} exp: ${exp} chi2: ${chi2} df: ${df}`);
+ return ret;
+}