From 6cf1f8fe879d85958b7059ed0f95f8767a977a6f Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sat, 21 Mar 2026 09:50:09 -0700 Subject: [PATCH 1/5] feat(ide): add telemetry tracking to VSCode extension Migrate Mixpanel-based telemetry from v2 to track extension usage events while respecting VSCode's telemetry settings. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/ide/vscode/package.json | 2 + .../vscode/src/extension/machine-id-utils.ts | 73 +++++++++++++++++++ packages/ide/vscode/src/extension/main.ts | 2 + .../vscode/src/extension/vscode-telemetry.ts | 62 ++++++++++++++++ pnpm-lock.yaml | 30 +++++--- 5 files changed, 160 insertions(+), 9 deletions(-) create mode 100644 packages/ide/vscode/src/extension/machine-id-utils.ts create mode 100644 packages/ide/vscode/src/extension/vscode-telemetry.ts diff --git a/packages/ide/vscode/package.json b/packages/ide/vscode/package.json index b877a5575..1234e5899 100644 --- a/packages/ide/vscode/package.json +++ b/packages/ide/vscode/package.json @@ -33,6 +33,8 @@ "dependencies": { "@zenstackhq/language": "workspace:*", "langium": "catalog:", + "mixpanel": "^0.18.0", + "uuid": "^11.1.0", "vscode-languageclient": "^9.0.1", "vscode-languageserver": "^9.0.1" }, diff --git a/packages/ide/vscode/src/extension/machine-id-utils.ts b/packages/ide/vscode/src/extension/machine-id-utils.ts new file mode 100644 index 000000000..b0aecfc8c --- /dev/null +++ b/packages/ide/vscode/src/extension/machine-id-utils.ts @@ -0,0 +1,73 @@ +// modified from https://github.com/automation-stack/node-machine-id + +import { execSync } from 'child_process'; +import { createHash } from 'crypto'; +import { v4 as uuid } from 'uuid'; + +const { platform } = process; +const win32RegBinPath = { + native: '%windir%\\System32', + mixed: '%windir%\\sysnative\\cmd.exe /c %windir%\\System32', +}; +const guid = { + darwin: 'ioreg -rd1 -c IOPlatformExpertDevice', + win32: + `${win32RegBinPath[isWindowsProcessMixedOrNativeArchitecture()]}\\REG.exe ` + + 'QUERY HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography ' + + '/v MachineGuid', + linux: '( cat /var/lib/dbus/machine-id /etc/machine-id 2> /dev/null || hostname 2> /dev/null) | head -n 1 || :', + freebsd: 'kenv -q smbios.system.uuid || sysctl -n kern.hostuuid', +}; + +function isWindowsProcessMixedOrNativeArchitecture() { + if (process.arch === 'ia32' && process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432')) { + return 'mixed'; + } + return 'native'; +} + +function hash(guid: string): string { + return createHash('sha256').update(guid).digest('hex'); +} + +function expose(result: string): string { + switch (platform) { + case 'darwin': + return result + .split('IOPlatformUUID')[1]! + .split('\n')[0]! + .replace(/=|\s+|"/gi, '') + .toLowerCase(); + case 'win32': + return result + .toString() + .split('REG_SZ')[1]! + .replace(/\r+|\n+|\s+/gi, '') + .toLowerCase(); + case 'linux': + return result + .toString() + .replace(/\r+|\n+|\s+/gi, '') + .toLowerCase(); + case 'freebsd': + return result + .toString() + .replace(/\r+|\n+|\s+/gi, '') + .toLowerCase(); + default: + throw new Error(`Unsupported platform: ${process.platform}`); + } +} + +export function getMachineId() { + if (!(platform in guid)) { + return uuid(); + } + try { + const value = execSync(guid[platform as keyof typeof guid]); + const id = expose(value.toString()); + return hash(id); + } catch { + return uuid(); + } +} diff --git a/packages/ide/vscode/src/extension/main.ts b/packages/ide/vscode/src/extension/main.ts index a7fab381d..367f85611 100644 --- a/packages/ide/vscode/src/extension/main.ts +++ b/packages/ide/vscode/src/extension/main.ts @@ -2,12 +2,14 @@ import * as path from 'node:path'; import type * as vscode from 'vscode'; import type { LanguageClientOptions, ServerOptions } from 'vscode-languageclient/node.js'; import { LanguageClient, TransportKind } from 'vscode-languageclient/node.js'; +import telemetry from './vscode-telemetry'; let client: LanguageClient; // This function is called when the extension is activated. export function activate(context: vscode.ExtensionContext): void { client = startLanguageClient(context); + telemetry.track('extension:activate'); } // This function is called when the extension is deactivated. diff --git a/packages/ide/vscode/src/extension/vscode-telemetry.ts b/packages/ide/vscode/src/extension/vscode-telemetry.ts new file mode 100644 index 000000000..186c77efe --- /dev/null +++ b/packages/ide/vscode/src/extension/vscode-telemetry.ts @@ -0,0 +1,62 @@ +import { init } from 'mixpanel'; +import type { Mixpanel } from 'mixpanel'; +import * as os from 'os'; +import * as vscode from 'vscode'; +import { getMachineId } from './machine-id-utils'; +import { v5 as uuidv5 } from 'uuid'; +import { version as extensionVersion } from '../../package.json'; + +export const VSCODE_TELEMETRY_TRACKING_TOKEN = ''; + +export type TelemetryEvents = 'extension:activate' | 'extension:zmodel-preview' | 'extension:zmodel-save'; + +export class VSCodeTelemetry { + private readonly mixpanel: Mixpanel | undefined; + private readonly deviceId = this.getDeviceId(); + private readonly _os_type = os.type(); + private readonly _os_release = os.release(); + private readonly _os_arch = os.arch(); + private readonly _os_version = os.version(); + private readonly _os_platform = os.platform(); + private readonly vscodeAppName = vscode.env.appName; + private readonly vscodeVersion = vscode.version; + private readonly vscodeAppHost = vscode.env.appHost; + + constructor() { + if (vscode.env.isTelemetryEnabled) { + this.mixpanel = init(VSCODE_TELEMETRY_TRACKING_TOKEN, { + geolocate: true, + }); + } + } + + private getDeviceId() { + const hostId = getMachineId(); + // namespace UUID for generating UUIDv5 from DNS 'zenstack.dev' + return uuidv5(hostId, '133cac15-3efb-50fa-b5fc-4b90e441e563'); + } + + track(event: TelemetryEvents, properties: Record = {}) { + if (this.mixpanel) { + const payload = { + distinct_id: this.deviceId, + time: new Date(), + $os: this._os_type, + osType: this._os_type, + osRelease: this._os_release, + osPlatform: this._os_platform, + osArch: this._os_arch, + osVersion: this._os_version, + nodeVersion: process.version, + vscodeAppName: this.vscodeAppName, + vscodeVersion: this.vscodeVersion, + vscodeAppHost: this.vscodeAppHost, + extensionVersion, + ...properties, + }; + this.mixpanel.track(event, payload); + } + } +} + +export default new VSCodeTelemetry(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2a4c2023a..8b5d3f521 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -465,6 +465,12 @@ importers: langium: specifier: 'catalog:' version: 3.5.0 + mixpanel: + specifier: ^0.18.0 + version: 0.18.1 + uuid: + specifier: ^11.1.0 + version: 11.1.0 vscode-languageclient: specifier: ^9.0.1 version: 9.0.1 @@ -8616,6 +8622,10 @@ packages: resolution: {integrity: sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==} hasBin: true + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} @@ -9142,7 +9152,7 @@ snapshots: '@babel/types': 7.28.5 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.4.1 + debug: 4.4.3 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -9420,7 +9430,7 @@ snapshots: '@babel/parser': 7.28.5 '@babel/template': 7.27.2 '@babel/types': 7.28.5 - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -11580,7 +11590,7 @@ snapshots: '@typescript-eslint/types': 8.46.2 '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.46.2 - debug: 4.4.1 + debug: 4.4.3 eslint: 9.29.0(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: @@ -11626,7 +11636,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 8.34.1(typescript@5.9.3) '@typescript-eslint/utils': 8.34.1(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3) - debug: 4.4.1 + debug: 4.4.3 eslint: 9.29.0(jiti@2.6.1) ts-api-utils: 2.1.0(typescript@5.9.3) typescript: 5.9.3 @@ -11638,7 +11648,7 @@ snapshots: '@typescript-eslint/types': 8.46.2 '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3) '@typescript-eslint/utils': 8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3) - debug: 4.4.1 + debug: 4.4.3 eslint: 9.29.0(jiti@2.6.1) ts-api-utils: 2.1.0(typescript@5.9.3) typescript: 5.9.3 @@ -11655,7 +11665,7 @@ snapshots: '@typescript-eslint/tsconfig-utils': 8.34.1(typescript@5.9.3) '@typescript-eslint/types': 8.34.1 '@typescript-eslint/visitor-keys': 8.34.1 - debug: 4.4.1 + debug: 4.4.3 fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.8 @@ -11671,7 +11681,7 @@ snapshots: '@typescript-eslint/tsconfig-utils': 8.46.2(typescript@5.9.3) '@typescript-eslint/types': 8.46.2 '@typescript-eslint/visitor-keys': 8.46.2 - debug: 4.4.1 + debug: 4.4.3 fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.8 @@ -12143,7 +12153,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -14017,7 +14027,7 @@ snapshots: https-proxy-agent@5.0.0: dependencies: agent-base: 6.0.2 - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -17075,6 +17085,8 @@ snapshots: uuid@11.0.5: {} + uuid@11.1.0: {} + validate-npm-package-license@3.0.4: dependencies: spdx-correct: 3.2.0 From 42cb3ad59cad9c3c13ac824242284d5212323469 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sat, 21 Mar 2026 09:57:41 -0700 Subject: [PATCH 2/5] feat(ide): add post-build script to inject telemetry token Add a post-build step that replaces the telemetry token placeholder in the bundled output using environment variables from .env files. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/ide/vscode/package.json | 5 +++-- packages/ide/vscode/scripts/post-build.ts | 16 ++++++++++++++++ pnpm-lock.yaml | 3 +++ 3 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 packages/ide/vscode/scripts/post-build.ts diff --git a/packages/ide/vscode/package.json b/packages/ide/vscode/package.json index 1234e5899..80e8c9989 100644 --- a/packages/ide/vscode/package.json +++ b/packages/ide/vscode/package.json @@ -10,7 +10,7 @@ "url": "https://github.com/zenstackhq/zenstack" }, "scripts": { - "build": "tsc --noEmit && tsup", + "build": "tsc --noEmit && tsup && tsx scripts/post-build.ts", "watch": "tsup --watch", "lint": "eslint src --ext ts", "vscode:publish": "pnpm build && vsce publish --no-dependencies --follow-symlinks", @@ -41,7 +41,8 @@ "devDependencies": { "@types/vscode": "^1.90.0", "@zenstackhq/eslint-config": "workspace:*", - "@zenstackhq/typescript-config": "workspace:*" + "@zenstackhq/typescript-config": "workspace:*", + "dotenv": "^17.2.3" }, "files": [ "dist", diff --git a/packages/ide/vscode/scripts/post-build.ts b/packages/ide/vscode/scripts/post-build.ts new file mode 100644 index 000000000..21128dcfd --- /dev/null +++ b/packages/ide/vscode/scripts/post-build.ts @@ -0,0 +1,16 @@ +import dotenv from 'dotenv'; +import fs from 'node:fs'; + +dotenv.config({ path: './.env.local' }); +dotenv.config({ path: './.env' }); + +const telemetryToken = process.env.VSCODE_TELEMETRY_TRACKING_TOKEN; +if (!telemetryToken) { + console.error('Error: VSCODE_TELEMETRY_TRACKING_TOKEN environment variable is not set'); + process.exit(1); +} +const file = 'dist/extension.js'; +let content = fs.readFileSync(file, 'utf-8'); +content = content.replace('', telemetryToken); +fs.writeFileSync(file, content, 'utf-8'); +console.log('Telemetry token injected into dist/extension.js'); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b5d3f521..6d84ca177 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -487,6 +487,9 @@ importers: '@zenstackhq/typescript-config': specifier: workspace:* version: link:../../config/typescript-config + dotenv: + specifier: ^17.2.3 + version: 17.2.3 packages/language: dependencies: From 843a08d811e4660b63e8dcd3e766ee9e75dae907 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sat, 21 Mar 2026 15:06:28 -0700 Subject: [PATCH 3/5] add vscode telemetry token env var --- .github/workflows/build-test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 69d9056e7..37a1b3f91 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -8,6 +8,7 @@ on: env: TELEMETRY_TRACKING_TOKEN: ${{ secrets.TELEMETRY_TRACKING_TOKEN }} + VSCODE_TELEMETRY_TRACKING_TOKEN: ${{ secrets.TELEMETRY_TRACKING_TOKEN }} DO_NOT_TRACK: '1' permissions: From 1ca2d157de9ba7b24e7d2a80a5b09a9b4985e2ec Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sat, 21 Mar 2026 15:06:45 -0700 Subject: [PATCH 4/5] fix env var --- .github/workflows/build-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 37a1b3f91..33dd30733 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -8,7 +8,7 @@ on: env: TELEMETRY_TRACKING_TOKEN: ${{ secrets.TELEMETRY_TRACKING_TOKEN }} - VSCODE_TELEMETRY_TRACKING_TOKEN: ${{ secrets.TELEMETRY_TRACKING_TOKEN }} + VSCODE_TELEMETRY_TRACKING_TOKEN: ${{ secrets.VSCODE_TELEMETRY_TRACKING_TOKEN }} DO_NOT_TRACK: '1' permissions: From d08bab83fac89c7c6e044143664644dc4b188cdd Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sat, 21 Mar 2026 15:16:33 -0700 Subject: [PATCH 5/5] chore(ide): disable no-prototype-builtins eslint rule Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/ide/vscode/eslint.config.mjs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/ide/vscode/eslint.config.mjs b/packages/ide/vscode/eslint.config.mjs index 5698b9910..7c11ab373 100644 --- a/packages/ide/vscode/eslint.config.mjs +++ b/packages/ide/vscode/eslint.config.mjs @@ -1,4 +1,11 @@ import config from '@zenstackhq/eslint-config/base.js'; /** @type {import("eslint").Linter.Config} */ -export default config; +export default [ + ...config, + { + rules: { + 'no-prototype-builtins': 'off', + }, + }, +];