blob: 5d09301eee1f9fd10ad743764c99a32640f7cd2d [file] [log] [blame]
/* eslint-disable node/no-unpublished-import */
/*---------------------------------------------------------
* Copyright 2023 The Go Authors. All rights reserved.
* Licensed under the MIT License. See LICENSE in the project root for license information.
*--------------------------------------------------------*/
import * as sinon from 'sinon';
import { describe, it } from 'mocha';
import {
GOPLS_MAYBE_PROMPT_FOR_TELEMETRY,
TELEMETRY_START_TIME_KEY,
TelemetryReporter,
TelemetryService,
recordTelemetryStartTime
} from '../../src/goTelemetry';
import { MockMemento } from '../mocks/MockMemento';
import { maybeInstallVSCGO } from '../../src/goInstallTools';
import assert from 'assert';
import path from 'path';
import * as fs from 'fs-extra';
import os = require('os');
import { rmdirRecursive } from '../../src/util';
import { extensionId } from '../../src/const';
import { executableFileExists, fileExists } from '../../src/utils/pathUtils';
import { ExtensionMode, Memento, extensions } from 'vscode';
describe('# prompt for telemetry', async () => {
const extension = extensions.getExtension(extensionId);
assert(extension);
it(
'do not prompt if language client is not used',
testTelemetryPrompt(
{
noLangClient: true,
previewExtension: true,
samplingInterval: 1000
},
false
)
); // no crash when there is no language client.
it(
'do not prompt if gopls does not support telemetry',
testTelemetryPrompt(
{
goplsWithoutTelemetry: true,
previewExtension: true,
samplingInterval: 1000
},
false
)
);
it(
'prompt when telemetry started a while ago',
testTelemetryPrompt(
{
firstDate: new Date('2022-01-01'),
samplingInterval: 1000
},
true
)
);
it(
'do not prompt if telemetry started two days ago',
testTelemetryPrompt(
{
firstDate: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000), // two days ago!
samplingInterval: 1000
},
false
)
);
it(
'do not prompt if gopls with telemetry never ran',
testTelemetryPrompt(
{
firstDate: undefined, // gopls with telemetry not seen before.
samplingInterval: 1000
},
false
)
);
it(
'do not prompt if not sampled',
testTelemetryPrompt(
{
firstDate: new Date('2022-01-01'),
samplingInterval: 0
},
false
)
);
it(
'prompt only if sampled (machineID = 0, samplingInterval = 1)',
testTelemetryPrompt(
{
firstDate: new Date('2022-01-01'),
samplingInterval: 1,
hashMachineID: 0
},
true
)
);
it(
'prompt only if sampled (machineID = 1, samplingInterval = 1)',
testTelemetryPrompt(
{
firstDate: new Date('2022-01-01'),
samplingInterval: 1,
hashMachineID: 1
},
false
)
);
it(
'prompt all preview extension users',
testTelemetryPrompt(
{
firstDate: new Date('2022-01-01'),
previewExtension: true,
samplingInterval: 0
},
true
)
);
it(
'do not prompt if vscode telemetry is disabled',
testTelemetryPrompt(
{
firstDate: new Date('2022-01-01'),
vsTelemetryDisabled: true,
previewExtension: true,
samplingInterval: 1000
},
false
)
);
// testExtensionAPI.globalState is a real memento instance passed by ExtensionHost.
// This instance is active throughout the integration test.
// When you add more test cases that interact with the globalState,
// be aware that multiple test cases may access and mutate it asynchronously.
const testExtensionAPI = await extension.activate();
it('check we can salvage the value in the real memento', async () => {
// write Date with Memento.update - old way. Now we always use string for TELEMETRY_START_TIME_KEY value.
testExtensionAPI.globalState.update(TELEMETRY_START_TIME_KEY, new Date(Date.now() - 7 * 24 * 60 * 60 * 1000));
await testTelemetryPrompt(
{
samplingInterval: 1000,
mementoInstance: testExtensionAPI.globalState
},
true
)();
});
});
interface testCase {
noLangClient?: boolean; // gopls is not running.
goplsWithoutTelemetry?: boolean; // gopls is too old.
firstDate?: Date; // first date the extension observed gopls with telemetry feature.
previewExtension?: boolean; // assume we are in dev/nightly extension.
vsTelemetryDisabled?: boolean; // assume the user disabled vscode general telemetry.
samplingInterval: number; // N where N out of 1000 are sampled.
hashMachineID?: number; // stub the machine id hash computation function.
mementoInstance?: Memento; // if set, use this instead of mock memento.
}
function testTelemetryPrompt(tc: testCase, wantPrompt: boolean) {
return async () => {
const languageClient = {
sendRequest: () => {
return Promise.resolve();
}
};
const spy = sinon.spy(languageClient, 'sendRequest');
const lc = tc.noLangClient ? undefined : languageClient;
const memento = tc.mementoInstance ?? new MockMemento();
if (tc.firstDate) {
recordTelemetryStartTime(memento, tc.firstDate);
}
const commands = tc.goplsWithoutTelemetry ? [] : [GOPLS_MAYBE_PROMPT_FOR_TELEMETRY];
const sut = new TelemetryService(lc, memento, commands);
if (tc.hashMachineID !== undefined) {
sinon.stub(sut, 'hashMachineID').returns(tc.hashMachineID);
}
await sut.promptForTelemetry(!!tc.previewExtension, !tc.vsTelemetryDisabled, tc.samplingInterval);
if (wantPrompt) {
sinon.assert.calledOnce(spy);
} else {
sinon.assert.neverCalledWith(spy);
}
};
}
describe('# telemetry reporter using vscgo', async function () {
this.timeout(10000); // go install can be slow.
// installVSCGO expects
// {extensionPath}/vscgo: vscgo source code for testing.
// {extensionPath}/bin: where compiled vscgo will be stored.
// During testing, extensionDevelopmentPath is the root of the extension.
// __dirname = out/test/gopls.
const extensionDevelopmentPath = path.resolve(__dirname, '../../..');
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'telemetryReporter'));
const counterfile = path.join(tmpDir, 'counterfile.txt');
const sut = new TelemetryReporter(0, counterfile);
let vscgo: string;
suiteSetup(async () => {
try {
vscgo = await maybeInstallVSCGO(
ExtensionMode.Test,
extensionId,
'',
extensionDevelopmentPath,
true /*isPreview*/
);
} catch (e) {
assert.fail(`failed to install vscgo needed for testing: ${e}`);
}
});
suiteTeardown(() => {
rmdirRecursive(tmpDir);
if (executableFileExists(vscgo)) {
fs.unlink(vscgo);
}
});
teardown(() => {
if (fileExists(counterfile)) {
fs.unlink(counterfile);
}
});
it('add succeeds before telemetryReporter.setTool runs', () => {
sut.add('hello', 1);
sut.add('world', 2);
});
it('flush is noop before setTool', async () => {
await sut.flush();
assert(!fileExists(counterfile), 'counterfile exists');
});
it('flush writes accumulated counters after setTool', async () => {
sut.setTool(vscgo);
await sut.flush();
const readAll = fs.readFileSync(counterfile).toString();
assert(readAll.includes('hello 1\n') && readAll.includes('world 2\n'), readAll);
});
it('dispose triggers flush', async () => {
sut.add('bye', 3);
await sut.dispose();
const readAll = fs.readFileSync(counterfile).toString();
assert(readAll.includes('bye 3\n'), readAll);
});
});