Skip to content

Commit bb6f0ea

Browse files
committed
src/goInstallTools,goTelemetry: add TelemetryReporter
TelemetryReporter buffers counter updates and periodically invokes a go program (vscgo) that writes the counter to the disk. installVCSGO in goInstallTools.ts installs vscgo. If vscgo installation fails, TelemetryReporter will keep holding the counter in memory. The buffer is a set keyed by the counter and we expect there is a finite set of counters. That installs the vscgo binary in the extension path. The location was chosen so that when users update the extension, a new version can be installed. VS Code will manage the extension path and delete the directory when the extension is uninstalled or the version is no longer used. The extension operates in four different modes and we need to choose how to build the vscgo. The extension includes the vscgo main package source file in it. 1) golang.go, stable/rc releases: PRODUCTION mode. try to install from the remote source (proxy) so its checksum is verified and build version and telemetry counter file matches the extension version. The source repo needs to be tagged. In case of failure, we attempt to fallback to the build with the source code included in the extension. 2) golang.go-nightly, preview release: PRODUCTION mode. Nightly is released daily automatically. Tagging the repo everyday is not practical. Moreover, the nightly extension does not use semver but encodes the release timestamp, so it is not compatible with go commands. Installing from @master or @latest isn't ideal either since vscgo functionality is tied with the extension version. The telemetry data will be labeled with `devel` version. 3) golang.go, preview release: PRODUCTION mode. Used for local testing during development (e.g. npm run package & code --install-extension). The version will include `-dev`. We want to build from the local source included in the extension since there is no usable tag in the remote origin. The telemetry data will be labeled with `devel` version. 4) golang.go, preview release: DEVELOPMENT mode. Used for local testing using the launch.json configuration. VS Code will use the project source code as the extension path. Build vscgo from the project repo on disk. The telemetry data will be labeled with `devel` version. 5) golang.go, preview release: TEST mode. Currently same as 4. No telemetry data is materialized. Tests that are designed for telemetry testing write test data in temporary text file for inspection during tests. For #3023 Change-Id: Ic408e7b296fdcb9ed33b68293ea82f5e29a81515 Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/549244 Commit-Queue: Hyang-Ah Hana Kim <[email protected]> Reviewed-by: Suzy Mueller <[email protected]> TryBot-Result: kokoro <[email protected]>
1 parent 0f48c2f commit bb6f0ea

File tree

9 files changed

+337
-14
lines changed

9 files changed

+337
-14
lines changed

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
bin/
12
out/
23
dist/
34
node_modules/
45
.vscode-test/
56
.DS_Store
6-
.user-data-dir-test/
7+
.user-data-dir-test/

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"gopls"
3737
],
3838
"scripts": {
39-
"clean": "rm -rf ./dist/* && rm -rf ./out/* && rm *.vsix",
39+
"clean": "rm -rf ./dist/* && rm -rf ./out/* && rm -rf ./bin/* && rm *.vsix",
4040
"package": "npx vsce package",
4141
"vscode:prepublish": "npm run compile",
4242
"bundle": "esbuild src/goMain.ts debugAdapter=src/debugAdapter/goDebug.ts --bundle --outdir=dist --external:vscode --format=cjs --platform=node",

src/goInstallTools.ts

+54-1
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,18 @@ import {
3030
GoVersion,
3131
rmdirRecursive
3232
} from './util';
33-
import { getEnvPath, getCurrentGoRoot, setCurrentGoRoot } from './utils/pathUtils';
33+
import {
34+
getEnvPath,
35+
getCurrentGoRoot,
36+
setCurrentGoRoot,
37+
correctBinname,
38+
executableFileExists
39+
} from './utils/pathUtils';
3440
import util = require('util');
3541
import vscode = require('vscode');
3642
import { RestartReason } from './language/goLanguageServer';
43+
import { telemetryReporter } from './goTelemetry';
44+
import { allToolsInformation } from './goToolsInformation';
3745

3846
const STATUS_BAR_ITEM_NAME = 'Go Tools';
3947

@@ -781,3 +789,48 @@ export async function listOutdatedTools(configuredGoVersion: GoVersion | undefin
781789
);
782790
return oldTools.filter((tool): tool is Tool => !!tool);
783791
}
792+
793+
// installVSCGO is a special program released and installed with the Go extension.
794+
// Unlike other tools, it is installed under the extension path (which is cleared
795+
// when a new version is installed).
796+
export async function installVSCGO(
797+
extensionId: string,
798+
extensionVersion: string,
799+
extensionPath: string,
800+
isPreview: boolean,
801+
forceInstall = false
802+
): Promise<string> {
803+
// golang.go stable, golang.go-nightly stable -> install once per version.
804+
// golang.go dev through launch.json -> install every time.
805+
const progPath = path.join(extensionPath, 'bin', correctBinname('vscgo'));
806+
807+
if (!forceInstall && executableFileExists(progPath)) {
808+
return progPath; // reuse existing executable.
809+
}
810+
telemetryReporter.add('vscgo_install', 1);
811+
const mkdir = util.promisify(fs.mkdir);
812+
await mkdir(path.dirname(progPath), { recursive: true });
813+
const execFile = util.promisify(cp.execFile);
814+
815+
const cwd = path.join(extensionPath, 'vscgo');
816+
const env = toolExecutionEnvironment();
817+
env['GOBIN'] = path.dirname(progPath);
818+
819+
// build from source acquired from the module proxy if this is a non-preview version.
820+
if (extensionId === 'golang.go' && !isPreview && !extensionVersion.includes('-dev.')) {
821+
const importPath = allToolsInformation['vscgo'].importPath;
822+
try {
823+
const args = ['install', `${importPath}@v${extensionVersion}`];
824+
await execFile(getBinPath('go'), args, { cwd, env });
825+
return progPath;
826+
} catch (e) {
827+
telemetryReporter.add('vscgo_install_fail', 1);
828+
console.log(`failed to install ${importPath}@v${extensionVersion};\n${e}`);
829+
console.log('falling back to install the dev version packaged in the extension');
830+
}
831+
}
832+
// build from the source included in vsix or test extension.
833+
const args = ['install', '.'];
834+
await execFile(getBinPath('go'), args, { cwd, env }); // throw error in case of failure.
835+
return progPath;
836+
}

src/goMain.ts

+45-6
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
'use strict';
1010

11-
import { getGoConfig } from './config';
11+
import { extensionInfo, getGoConfig } from './config';
1212
import { browsePackages } from './goBrowsePackage';
1313
import { buildCode } from './goBuild';
1414
import { notifyIfGeneratedFile, removeTestStatus } from './goCheck';
@@ -32,15 +32,21 @@ import * as goGenerateTests from './goGenerateTests';
3232
import { goGetPackage } from './goGetPackage';
3333
import { addImport, addImportToWorkspace } from './goImport';
3434
import { installCurrentPackage } from './goInstall';
35-
import { offerToInstallTools, promptForMissingTool, updateGoVarsFromConfig, suggestUpdates } from './goInstallTools';
35+
import {
36+
offerToInstallTools,
37+
promptForMissingTool,
38+
updateGoVarsFromConfig,
39+
suggestUpdates,
40+
installVSCGO
41+
} from './goInstallTools';
3642
import { RestartReason, showServerOutputChannel, watchLanguageServerConfiguration } from './language/goLanguageServer';
3743
import { lintCode } from './goLint';
3844
import { setLogConfig } from './goLogging';
3945
import { GO_MODE } from './goMode';
40-
import { GO111MODULE, goModInit, isModSupported } from './goModules';
46+
import { GO111MODULE, goModInit } from './goModules';
4147
import { playgroundCommand } from './goPlayground';
4248
import { GoRunTestCodeLensProvider } from './goRunTestCodelens';
43-
import { disposeGoStatusBar, expandGoStatusBar, outputChannel, updateGoStatusBar } from './goStatus';
49+
import { disposeGoStatusBar, expandGoStatusBar, updateGoStatusBar } from './goStatus';
4450

4551
import { vetCode } from './goVet';
4652
import {
@@ -52,7 +58,7 @@ import {
5258
updateGlobalState
5359
} from './stateUtils';
5460
import { cancelRunningTests, showTestOutput } from './testUtils';
55-
import { cleanupTempDir, getBinPath, getToolsGopath, isGoPathSet } from './util';
61+
import { cleanupTempDir, getBinPath, getToolsGopath } from './util';
5662
import { clearCacheForTools } from './utils/pathUtils';
5763
import { WelcomePanel } from './welcome';
5864
import vscode = require('vscode');
@@ -67,6 +73,7 @@ import { GoExtensionContext } from './context';
6773
import * as commands from './commands';
6874
import { toggleVulncheckCommandFactory } from './goVulncheck';
6975
import { GoTaskProvider } from './goTaskProvider';
76+
import { telemetryReporter } from './goTelemetry';
7077

7178
const goCtx: GoExtensionContext = {};
7279

@@ -76,6 +83,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<ExtensionA
7683
return;
7784
}
7885

86+
const start = Date.now();
7987
setGlobalState(ctx.globalState);
8088
setWorkspaceState(ctx.workspaceState);
8189
setEnvironmentVariableCollection(ctx.environmentVariableCollection);
@@ -96,6 +104,18 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<ExtensionA
96104

97105
await updateGoVarsFromConfig(goCtx);
98106

107+
// for testing or development mode, always rebuild vscgo.
108+
const forceInstall = ctx.extensionMode !== vscode.ExtensionMode.Production;
109+
installVSCGO(
110+
ctx.extension.id,
111+
extensionInfo.version || '',
112+
ctx.extensionPath,
113+
extensionInfo.isPreview,
114+
forceInstall
115+
)
116+
.then((path) => telemetryReporter.setTool(path))
117+
.catch((reason) => console.error(reason));
118+
99119
suggestUpdates();
100120
offerToInstallLatestGoVersion();
101121
offerToInstallTools();
@@ -201,16 +221,35 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<ExtensionA
201221

202222
registerCommand('go.vulncheck.toggle', toggleVulncheckCommandFactory);
203223

224+
telemetryReporter.add(activationLatency(Date.now() - start), 1);
225+
204226
return extensionAPI;
205227
}
206228

229+
function activationLatency(duration: number): string {
230+
// TODO: generalize and move to goTelemetry.ts
231+
let bucket = '>=5s';
232+
233+
if (duration < 100) {
234+
bucket = '<100ms';
235+
} else if (duration < 500) {
236+
bucket = '<500ms';
237+
} else if (duration < 1000) {
238+
bucket = '<1s';
239+
} else if (duration < 5000) {
240+
bucket = '<5s';
241+
}
242+
return 'activation_latency:' + bucket;
243+
}
244+
207245
export function deactivate() {
208246
return Promise.all([
209247
goCtx.languageClient?.stop(),
210248
cancelRunningTests(),
211249
killRunningPprof(),
212250
Promise.resolve(cleanupTempDir()),
213-
Promise.resolve(disposeGoStatusBar())
251+
Promise.resolve(disposeGoStatusBar()),
252+
telemetryReporter.dispose()
214253
]);
215254
}
216255

src/goTelemetry.ts

+118
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import { createHash } from 'crypto';
99
import { ExecuteCommandRequest } from 'vscode-languageserver-protocol';
1010
import { daysBetween } from './goSurvey';
1111
import { LanguageClient } from 'vscode-languageclient/node';
12+
import * as cp from 'child_process';
13+
import { getWorkspaceFolderPath } from './util';
14+
import { toolExecutionEnvironment } from './goEnv';
1215

1316
// Name of the prompt telemetry command. This is also used to determine if the gopls instance supports telemetry.
1417
// Exported for testing.
@@ -18,6 +21,121 @@ export const GOPLS_MAYBE_PROMPT_FOR_TELEMETRY = 'gopls.maybe_prompt_for_telemetr
1821
// Exported for testing.
1922
export const TELEMETRY_START_TIME_KEY = 'telemetryStartTime';
2023

24+
enum ReporterState {
25+
NOT_INITIALIZED,
26+
IDLE,
27+
STARTING,
28+
RUNNING
29+
}
30+
31+
// exported for testing.
32+
export class TelemetryReporter implements vscode.Disposable {
33+
private _state = ReporterState.NOT_INITIALIZED;
34+
private _counters: { [key: string]: number } = {};
35+
private _flushTimer: NodeJS.Timeout | undefined;
36+
private _tool = '';
37+
constructor(flushIntervalMs = 60_000, private counterFile: string = '') {
38+
if (flushIntervalMs > 0) {
39+
// periodically call flush.
40+
this._flushTimer = setInterval(this.flush.bind(this), flushIntervalMs);
41+
}
42+
}
43+
44+
public setTool(tool: string) {
45+
// allow only once.
46+
if (tool === '' || this._state !== ReporterState.NOT_INITIALIZED) {
47+
return;
48+
}
49+
this._state = ReporterState.IDLE;
50+
this._tool = tool;
51+
}
52+
53+
public add(key: string, value: number) {
54+
if (value <= 0) {
55+
return;
56+
}
57+
key = key.replace(/[\s\n]/g, '_');
58+
this._counters[key] = (this._counters[key] || 0) + value;
59+
}
60+
61+
// flush is called periodically (by the callback set up in the constructor)
62+
// or when the extension is deactivated (with force=true).
63+
public async flush(force = false) {
64+
// If flush runs with force=true, ignore the state and skip state update.
65+
if (!force && this._state !== ReporterState.IDLE) {
66+
// vscgo is not installed yet or is running. flush next time.
67+
return 0;
68+
}
69+
if (!force) {
70+
this._state = ReporterState.STARTING;
71+
}
72+
try {
73+
await this.writeGoTelemetry();
74+
} catch (e) {
75+
console.log(`failed to flush telemetry data: ${e}`);
76+
} finally {
77+
if (!force) {
78+
this._state = ReporterState.IDLE;
79+
}
80+
}
81+
}
82+
83+
private writeGoTelemetry() {
84+
const data = Object.entries(this._counters);
85+
if (data.length === 0) {
86+
return;
87+
}
88+
this._counters = {};
89+
90+
let stderr = '';
91+
return new Promise<number | null>((resolve, reject) => {
92+
const env = toolExecutionEnvironment();
93+
if (this.counterFile !== '') {
94+
env['TELEMETRY_COUNTER_FILE'] = this.counterFile;
95+
}
96+
const p = cp.spawn(this._tool, ['inc_counters'], {
97+
cwd: getWorkspaceFolderPath(),
98+
env
99+
});
100+
101+
p.stderr.on('data', (data) => {
102+
stderr += data;
103+
});
104+
105+
// 'close' fires after exit or error when the subprocess closes all stdio.
106+
p.on('close', (exitCode, signal) => {
107+
if (exitCode > 0) {
108+
reject(`exited with code=${exitCode} signal=${signal} stderr=${stderr}`);
109+
} else {
110+
resolve(exitCode);
111+
}
112+
});
113+
// Stream key/value to the vscgo process.
114+
data.forEach(([key, value]) => {
115+
p.stdin.write(`${key} ${value}\n`);
116+
});
117+
p.stdin.end();
118+
});
119+
}
120+
121+
public async dispose() {
122+
if (this._flushTimer) {
123+
clearInterval(this._flushTimer);
124+
}
125+
this._flushTimer = undefined;
126+
await this.flush(true); // flush any remaining data in buffer.
127+
}
128+
}
129+
130+
// global telemetryReporter instance.
131+
export const telemetryReporter = new TelemetryReporter();
132+
133+
// TODO(hyangah): consolidate the list of all the telemetries and bucketting functions.
134+
135+
export function addTelemetryEvent(name: string, count: number) {
136+
telemetryReporter.add(name, count);
137+
}
138+
21139
// Go extension delegates most of the telemetry logic to gopls.
22140
// TelemetryService provides API to interact with gopls's telemetry.
23141
export class TelemetryService {

src/goToolsInformation.ts

+13-4
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,10 @@ export const allToolsInformation: { [key: string]: Tool } = {
103103
description: 'Language Server from Google',
104104
usePrereleaseInPreviewMode: true,
105105
minimumGoVersion: semver.coerce('1.18'),
106-
latestVersion: semver.parse('v0.14.1'),
107-
latestVersionTimestamp: moment('2023-10-26', 'YYYY-MM-DD'),
108-
latestPrereleaseVersion: semver.parse('v0.14.1'),
109-
latestPrereleaseVersionTimestamp: moment('2023-10-26', 'YYYY-MM-DD')
106+
latestVersion: semver.parse('v0.14.2'),
107+
latestVersionTimestamp: moment('2023-11-14', 'YYYY-MM-DD'),
108+
latestPrereleaseVersion: semver.parse('v0.14.2'),
109+
latestPrereleaseVersionTimestamp: moment('2023-11-14', 'YYYY-MM-DD')
110110
},
111111
'dlv': {
112112
name: 'dlv',
@@ -118,5 +118,14 @@ export const allToolsInformation: { [key: string]: Tool } = {
118118
latestVersion: semver.parse('v1.8.3'),
119119
latestVersionTimestamp: moment('2022-04-26', 'YYYY-MM-DD'),
120120
minimumGoVersion: semver.coerce('1.18')
121+
},
122+
'vscgo': {
123+
name: 'vscgo',
124+
importPath: 'github.com/golang/vscode-go/vscgo',
125+
modulePath: 'github.com/golang/vscode-go/vscgo',
126+
replacedByGopls: false,
127+
isImportant: true,
128+
description: 'VS Code Go helper program',
129+
minimumGoVersion: semver.coerce('1.18')
121130
}
122131
};

0 commit comments

Comments
 (0)