Skip to content

Commit cb49667

Browse files
authored
feat(ide): add telemetry tracking to VSCode extension (#2505)
1 parent cbebe0c commit cb49667

File tree

8 files changed

+191
-12
lines changed

8 files changed

+191
-12
lines changed

.github/workflows/build-test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ on:
88

99
env:
1010
TELEMETRY_TRACKING_TOKEN: ${{ secrets.TELEMETRY_TRACKING_TOKEN }}
11+
VSCODE_TELEMETRY_TRACKING_TOKEN: ${{ secrets.VSCODE_TELEMETRY_TRACKING_TOKEN }}
1112
DO_NOT_TRACK: '1'
1213

1314
permissions:
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
import config from '@zenstackhq/eslint-config/base.js';
22

33
/** @type {import("eslint").Linter.Config} */
4-
export default config;
4+
export default [
5+
...config,
6+
{
7+
rules: {
8+
'no-prototype-builtins': 'off',
9+
},
10+
},
11+
];

packages/ide/vscode/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"url": "https://github.com/zenstackhq/zenstack"
1111
},
1212
"scripts": {
13-
"build": "tsc --noEmit && tsup",
13+
"build": "tsc --noEmit && tsup && tsx scripts/post-build.ts",
1414
"watch": "tsup --watch",
1515
"lint": "eslint src --ext ts",
1616
"vscode:publish": "pnpm build && vsce publish --no-dependencies --follow-symlinks",
@@ -33,13 +33,16 @@
3333
"dependencies": {
3434
"@zenstackhq/language": "workspace:*",
3535
"langium": "catalog:",
36+
"mixpanel": "^0.18.0",
37+
"uuid": "^11.1.0",
3638
"vscode-languageclient": "^9.0.1",
3739
"vscode-languageserver": "^9.0.1"
3840
},
3941
"devDependencies": {
4042
"@types/vscode": "^1.90.0",
4143
"@zenstackhq/eslint-config": "workspace:*",
42-
"@zenstackhq/typescript-config": "workspace:*"
44+
"@zenstackhq/typescript-config": "workspace:*",
45+
"dotenv": "^17.2.3"
4346
},
4447
"files": [
4548
"dist",
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import dotenv from 'dotenv';
2+
import fs from 'node:fs';
3+
4+
dotenv.config({ path: './.env.local' });
5+
dotenv.config({ path: './.env' });
6+
7+
const telemetryToken = process.env.VSCODE_TELEMETRY_TRACKING_TOKEN;
8+
if (!telemetryToken) {
9+
console.error('Error: VSCODE_TELEMETRY_TRACKING_TOKEN environment variable is not set');
10+
process.exit(1);
11+
}
12+
const file = 'dist/extension.js';
13+
let content = fs.readFileSync(file, 'utf-8');
14+
content = content.replace('<VSCODE_TELEMETRY_TRACKING_TOKEN>', telemetryToken);
15+
fs.writeFileSync(file, content, 'utf-8');
16+
console.log('Telemetry token injected into dist/extension.js');
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// modified from https://github.com/automation-stack/node-machine-id
2+
3+
import { execSync } from 'child_process';
4+
import { createHash } from 'crypto';
5+
import { v4 as uuid } from 'uuid';
6+
7+
const { platform } = process;
8+
const win32RegBinPath = {
9+
native: '%windir%\\System32',
10+
mixed: '%windir%\\sysnative\\cmd.exe /c %windir%\\System32',
11+
};
12+
const guid = {
13+
darwin: 'ioreg -rd1 -c IOPlatformExpertDevice',
14+
win32:
15+
`${win32RegBinPath[isWindowsProcessMixedOrNativeArchitecture()]}\\REG.exe ` +
16+
'QUERY HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography ' +
17+
'/v MachineGuid',
18+
linux: '( cat /var/lib/dbus/machine-id /etc/machine-id 2> /dev/null || hostname 2> /dev/null) | head -n 1 || :',
19+
freebsd: 'kenv -q smbios.system.uuid || sysctl -n kern.hostuuid',
20+
};
21+
22+
function isWindowsProcessMixedOrNativeArchitecture() {
23+
if (process.arch === 'ia32' && process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432')) {
24+
return 'mixed';
25+
}
26+
return 'native';
27+
}
28+
29+
function hash(guid: string): string {
30+
return createHash('sha256').update(guid).digest('hex');
31+
}
32+
33+
function expose(result: string): string {
34+
switch (platform) {
35+
case 'darwin':
36+
return result
37+
.split('IOPlatformUUID')[1]!
38+
.split('\n')[0]!
39+
.replace(/=|\s+|"/gi, '')
40+
.toLowerCase();
41+
case 'win32':
42+
return result
43+
.toString()
44+
.split('REG_SZ')[1]!
45+
.replace(/\r+|\n+|\s+/gi, '')
46+
.toLowerCase();
47+
case 'linux':
48+
return result
49+
.toString()
50+
.replace(/\r+|\n+|\s+/gi, '')
51+
.toLowerCase();
52+
case 'freebsd':
53+
return result
54+
.toString()
55+
.replace(/\r+|\n+|\s+/gi, '')
56+
.toLowerCase();
57+
default:
58+
throw new Error(`Unsupported platform: ${process.platform}`);
59+
}
60+
}
61+
62+
export function getMachineId() {
63+
if (!(platform in guid)) {
64+
return uuid();
65+
}
66+
try {
67+
const value = execSync(guid[platform as keyof typeof guid]);
68+
const id = expose(value.toString());
69+
return hash(id);
70+
} catch {
71+
return uuid();
72+
}
73+
}

packages/ide/vscode/src/extension/main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ import * as path from 'node:path';
22
import type * as vscode from 'vscode';
33
import type { LanguageClientOptions, ServerOptions } from 'vscode-languageclient/node.js';
44
import { LanguageClient, TransportKind } from 'vscode-languageclient/node.js';
5+
import telemetry from './vscode-telemetry';
56

67
let client: LanguageClient;
78

89
// This function is called when the extension is activated.
910
export function activate(context: vscode.ExtensionContext): void {
1011
client = startLanguageClient(context);
12+
telemetry.track('extension:activate');
1113
}
1214

1315
// This function is called when the extension is deactivated.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { init } from 'mixpanel';
2+
import type { Mixpanel } from 'mixpanel';
3+
import * as os from 'os';
4+
import * as vscode from 'vscode';
5+
import { getMachineId } from './machine-id-utils';
6+
import { v5 as uuidv5 } from 'uuid';
7+
import { version as extensionVersion } from '../../package.json';
8+
9+
export const VSCODE_TELEMETRY_TRACKING_TOKEN = '<VSCODE_TELEMETRY_TRACKING_TOKEN>';
10+
11+
export type TelemetryEvents = 'extension:activate' | 'extension:zmodel-preview' | 'extension:zmodel-save';
12+
13+
export class VSCodeTelemetry {
14+
private readonly mixpanel: Mixpanel | undefined;
15+
private readonly deviceId = this.getDeviceId();
16+
private readonly _os_type = os.type();
17+
private readonly _os_release = os.release();
18+
private readonly _os_arch = os.arch();
19+
private readonly _os_version = os.version();
20+
private readonly _os_platform = os.platform();
21+
private readonly vscodeAppName = vscode.env.appName;
22+
private readonly vscodeVersion = vscode.version;
23+
private readonly vscodeAppHost = vscode.env.appHost;
24+
25+
constructor() {
26+
if (vscode.env.isTelemetryEnabled) {
27+
this.mixpanel = init(VSCODE_TELEMETRY_TRACKING_TOKEN, {
28+
geolocate: true,
29+
});
30+
}
31+
}
32+
33+
private getDeviceId() {
34+
const hostId = getMachineId();
35+
// namespace UUID for generating UUIDv5 from DNS 'zenstack.dev'
36+
return uuidv5(hostId, '133cac15-3efb-50fa-b5fc-4b90e441e563');
37+
}
38+
39+
track(event: TelemetryEvents, properties: Record<string, unknown> = {}) {
40+
if (this.mixpanel) {
41+
const payload = {
42+
distinct_id: this.deviceId,
43+
time: new Date(),
44+
$os: this._os_type,
45+
osType: this._os_type,
46+
osRelease: this._os_release,
47+
osPlatform: this._os_platform,
48+
osArch: this._os_arch,
49+
osVersion: this._os_version,
50+
nodeVersion: process.version,
51+
vscodeAppName: this.vscodeAppName,
52+
vscodeVersion: this.vscodeVersion,
53+
vscodeAppHost: this.vscodeAppHost,
54+
extensionVersion,
55+
...properties,
56+
};
57+
this.mixpanel.track(event, payload);
58+
}
59+
}
60+
}
61+
62+
export default new VSCodeTelemetry();

pnpm-lock.yaml

Lines changed: 24 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)