Skip to content

Commit 9330b08

Browse files
hyangahgopherbot
authored andcommitted
extension/src/goTelemetry: do our JSON enc/dec for telemetry start time
We noticed the vscode Memento API doesn't behave as we expected. That made our recent attempt to increase the telemetry prompt rate stopped by this bug. Instead of relying on the Memento API's JSON stringify for Date object encoding, do the enc/dec work on ourside and let Memento work with string types. The existing changes cover the json encoding, decoding cases. Now goMain.ts activate() returns ExtensionTestAPI in testing mode. In telemetry testing, we want to check if the value recorded with vscode's real Memento API can be still usable. The real implementation is accessible only by capturing the ExtensionContext passed to the activate() invocation. Allow our test to access the extension's globalState using the new ExtensionTestAPI. Fixes #3312 Change-Id: I4540f83201f315624b077d113d6bfe2b3d608719 Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/576775 Reviewed-by: Robert Findley <[email protected]> Auto-Submit: Hyang-Ah Hana Kim <[email protected]> kokoro-CI: kokoro <[email protected]> Commit-Queue: Hyang-Ah Hana Kim <[email protected]>
1 parent f8173bc commit 9330b08

File tree

4 files changed

+70
-17
lines changed

4 files changed

+70
-17
lines changed

extension/src/goMain.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,24 @@ import { telemetryReporter } from './goTelemetry';
7676

7777
const goCtx: GoExtensionContext = {};
7878

79-
export async function activate(ctx: vscode.ExtensionContext): Promise<ExtensionAPI | undefined> {
79+
// Allow tests to access the extension context utilities.
80+
interface ExtensionTestAPI {
81+
globalState: vscode.Memento;
82+
}
83+
84+
export async function activate(ctx: vscode.ExtensionContext): Promise<ExtensionAPI | ExtensionTestAPI | undefined> {
8085
if (process.env['VSCODE_GO_IN_TEST'] === '1') {
81-
// Make sure this does not run when running in test.
82-
return;
86+
// TODO: VSCODE_GO_IN_TEST was introduced long before we learned about
87+
// ctx.extensionMode, and used in multiple places.
88+
// Investigate if use of VSCODE_GO_IN_TEST can be removed
89+
// in favor of ctx.extensionMode and clean up.
90+
if (ctx.extensionMode === vscode.ExtensionMode.Test) {
91+
return { globalState: ctx.globalState };
92+
}
93+
// We shouldn't expose the memento in production mode even when VSCODE_GO_IN_TEST
94+
// environment variable is set.
95+
return; // Skip the remaining activation work.
8396
}
84-
8597
const start = Date.now();
8698
setGlobalState(ctx.globalState);
8799
setWorkspaceState(ctx.workspaceState);

extension/src/goTelemetry.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,25 @@ export const GOPLS_MAYBE_PROMPT_FOR_TELEMETRY = 'gopls.maybe_prompt_for_telemetr
2121
// Exported for testing.
2222
export const TELEMETRY_START_TIME_KEY = 'telemetryStartTime';
2323

24+
// Run our encode/decode function for the Date object, to be defensive
25+
// from vscode Memento API behavior change.
26+
// Exported for testing.
27+
export function recordTelemetryStartTime(storage: vscode.Memento, date: Date) {
28+
storage.update(TELEMETRY_START_TIME_KEY, date.toJSON());
29+
}
30+
31+
function readTelemetryStartTime(storage: vscode.Memento): Date | null {
32+
const value = storage.get<string | number | Date>(TELEMETRY_START_TIME_KEY);
33+
if (!value) {
34+
return null;
35+
}
36+
const telemetryStartTime = new Date(value);
37+
if (telemetryStartTime.toString() === 'Invalid Date') {
38+
return null;
39+
}
40+
return telemetryStartTime;
41+
}
42+
2443
enum ReporterState {
2544
NOT_INITIALIZED,
2645
IDLE,
@@ -153,9 +172,9 @@ export class TelemetryService {
153172
this.active = true;
154173
// record the first time we see the gopls with telemetry support.
155174
// The timestamp will be used to avoid prompting too early.
156-
const telemetryStartTime = globalState.get<Date>(TELEMETRY_START_TIME_KEY);
175+
const telemetryStartTime = readTelemetryStartTime(globalState);
157176
if (!telemetryStartTime) {
158-
globalState.update(TELEMETRY_START_TIME_KEY, new Date());
177+
recordTelemetryStartTime(globalState, new Date());
159178
}
160179
}
161180

@@ -172,9 +191,11 @@ export class TelemetryService {
172191
if (!isVSCodeTelemetryEnabled) return;
173192

174193
// Allow at least 7days for gopls to collect some data.
175-
const now = new Date();
176-
const telemetryStartTime = this.globalState.get<Date>(TELEMETRY_START_TIME_KEY, now);
177-
if (daysBetween(telemetryStartTime, now) < 7) {
194+
const telemetryStartTime = readTelemetryStartTime(this.globalState);
195+
if (!telemetryStartTime) {
196+
return;
197+
}
198+
if (daysBetween(telemetryStartTime, new Date()) < 7) {
178199
return;
179200
}
180201

extension/test/gopls/extension.test.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import * as vscode from 'vscode';
1010
import { getGoConfig } from '../../src/config';
1111
import sinon = require('sinon');
1212
import { getGoVersion, GoVersion } from '../../src/util';
13-
import { GOPLS_MAYBE_PROMPT_FOR_TELEMETRY, TELEMETRY_START_TIME_KEY, TelemetryService } from '../../src/goTelemetry';
13+
import { GOPLS_MAYBE_PROMPT_FOR_TELEMETRY, recordTelemetryStartTime, TelemetryService } from '../../src/goTelemetry';
1414
import { MockMemento } from '../mocks/MockMemento';
1515
import { Env } from './goplsTestEnv.utils';
1616

@@ -205,8 +205,7 @@ suite('Go Extension Tests With Gopls', function () {
205205
const workspaceDir = path.resolve(testdataDir, 'gogetdocTestData');
206206
await env.startGopls(path.join(workspaceDir, 'test.go'), undefined, workspaceDir);
207207
const memento = new MockMemento();
208-
memento.update(TELEMETRY_START_TIME_KEY, new Date('2000-01-01'));
209-
208+
recordTelemetryStartTime(memento, new Date('2000-01-01'));
210209
const sut = new TelemetryService(env.languageClient, memento, [GOPLS_MAYBE_PROMPT_FOR_TELEMETRY]);
211210
try {
212211
await Promise.all([

extension/test/gopls/telemetry.test.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import {
1010
GOPLS_MAYBE_PROMPT_FOR_TELEMETRY,
1111
TELEMETRY_START_TIME_KEY,
1212
TelemetryReporter,
13-
TelemetryService
13+
TelemetryService,
14+
recordTelemetryStartTime
1415
} from '../../src/goTelemetry';
1516
import { MockMemento } from '../mocks/MockMemento';
1617
import { maybeInstallVSCGO } from '../../src/goInstallTools';
@@ -21,9 +22,12 @@ import os = require('os');
2122
import { rmdirRecursive } from '../../src/util';
2223
import { extensionId } from '../../src/const';
2324
import { executableFileExists, fileExists } from '../../src/utils/pathUtils';
24-
import { ExtensionMode } from 'vscode';
25+
import { ExtensionMode, Memento, extensions } from 'vscode';
26+
27+
describe('# prompt for telemetry', async () => {
28+
const extension = extensions.getExtension(extensionId);
29+
assert(extension);
2530

26-
describe('# prompt for telemetry', () => {
2731
it(
2832
'do not prompt if language client is not used',
2933
testTelemetryPrompt(
@@ -131,6 +135,22 @@ describe('# prompt for telemetry', () => {
131135
false
132136
)
133137
);
138+
// testExtensionAPI.globalState is a real memento instance passed by ExtensionHost.
139+
// This instance is active throughout the integration test.
140+
// When you add more test cases that interact with the globalState,
141+
// be aware that multiple test cases may access and mutate it asynchronously.
142+
const testExtensionAPI = await extension.activate();
143+
it('check we can salvage the value in the real memento', async () => {
144+
// write Date with Memento.update - old way. Now we always use string for TELEMETRY_START_TIME_KEY value.
145+
testExtensionAPI.globalState.update(TELEMETRY_START_TIME_KEY, new Date(Date.now() - 7 * 24 * 60 * 60 * 1000));
146+
await testTelemetryPrompt(
147+
{
148+
samplingInterval: 1000,
149+
mementoInstance: testExtensionAPI.globalState
150+
},
151+
true
152+
)();
153+
});
134154
});
135155

136156
interface testCase {
@@ -141,6 +161,7 @@ interface testCase {
141161
vsTelemetryDisabled?: boolean; // assume the user disabled vscode general telemetry.
142162
samplingInterval: number; // N where N out of 1000 are sampled.
143163
hashMachineID?: number; // stub the machine id hash computation function.
164+
mementoInstance?: Memento; // if set, use this instead of mock memento.
144165
}
145166

146167
function testTelemetryPrompt(tc: testCase, wantPrompt: boolean) {
@@ -153,9 +174,9 @@ function testTelemetryPrompt(tc: testCase, wantPrompt: boolean) {
153174
const spy = sinon.spy(languageClient, 'sendRequest');
154175
const lc = tc.noLangClient ? undefined : languageClient;
155176

156-
const memento = new MockMemento();
177+
const memento = tc.mementoInstance ?? new MockMemento();
157178
if (tc.firstDate) {
158-
memento.update(TELEMETRY_START_TIME_KEY, tc.firstDate);
179+
recordTelemetryStartTime(memento, tc.firstDate);
159180
}
160181
const commands = tc.goplsWithoutTelemetry ? [] : [GOPLS_MAYBE_PROMPT_FOR_TELEMETRY];
161182

0 commit comments

Comments
 (0)