Skip to content

Commit ca860c4

Browse files
fadeevhernan-clich
andauthored
feat: usage analytics (#59)
Co-authored-by: Hernan Clich <hernan.g.clich@gmail.com>
1 parent 33c968b commit ca860c4

File tree

6 files changed

+150
-4
lines changed

6 files changed

+150
-4
lines changed

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"lint": "npm run lint:js",
1515
"zetachain": "npx tsx src/index.ts",
1616
"zetachain:js": "yarn build && node dist/index.js",
17-
"docs": "cat README.md > docs/index.md && npx tsx src/index.ts docs >> docs/index.md"
17+
"docs": "cat README.md > docs/index.md && npx tsx src/index.ts docs --no-analytics >> docs/index.md"
1818
},
1919
"repository": {
2020
"type": "git",
@@ -40,7 +40,9 @@
4040
"marked": "^15.0.6",
4141
"marked-terminal": "^7.2.1",
4242
"node-fetch": "^3.3.2",
43-
"simple-git": "^3.27.0"
43+
"posthog-node": "^5.8.1",
44+
"simple-git": "^3.27.0",
45+
"uuid": "^11.1.0"
4446
},
4547
"devDependencies": {
4648
"@types/clear": "^0.1.4",

src/analytics.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { Command } from "commander";
2+
import dns from "dns";
3+
import fs from "fs";
4+
import { EventMessage, PostHog } from "posthog-node";
5+
import { v4 as uuid } from "uuid";
6+
7+
import { getFullCommandPath } from "./commands/docs";
8+
import {
9+
POSTHOG_API_KEY,
10+
POSTHOG_ENDPOINT,
11+
ZETACHAIN_CONFIG_FILE,
12+
ZETACHAIN_DIR,
13+
} from "./constants";
14+
15+
type Config = {
16+
uuid?: string;
17+
};
18+
19+
const getOrCreateUserUUID = (): string => {
20+
try {
21+
fs.mkdirSync(ZETACHAIN_DIR, { recursive: true });
22+
} catch {
23+
// Silently continue - analytics will work without persistent config
24+
}
25+
26+
let data: Config = {};
27+
let needsWrite = false;
28+
29+
if (fs.existsSync(ZETACHAIN_CONFIG_FILE)) {
30+
try {
31+
const raw = fs.readFileSync(ZETACHAIN_CONFIG_FILE, "utf8");
32+
if (raw.trim()) data = JSON.parse(raw) as Config;
33+
} catch (err) {
34+
// Silently recreate config if corrupted
35+
data = {};
36+
needsWrite = true;
37+
}
38+
} else {
39+
needsWrite = true;
40+
}
41+
42+
if (!data || typeof data !== "object") {
43+
data = {};
44+
needsWrite = true;
45+
}
46+
47+
if (!data.uuid || typeof data.uuid !== "string" || data.uuid.length === 0) {
48+
data.uuid = uuid();
49+
needsWrite = true;
50+
}
51+
52+
if (needsWrite) {
53+
try {
54+
fs.writeFileSync(
55+
ZETACHAIN_CONFIG_FILE,
56+
JSON.stringify(data, null, 2),
57+
"utf8",
58+
);
59+
} catch {
60+
// Silently continue - will generate new UUID next time
61+
}
62+
}
63+
64+
return data.uuid as string;
65+
};
66+
67+
const canResolveHost = async (
68+
hostname: string,
69+
timeout = 300,
70+
): Promise<boolean> => {
71+
try {
72+
const resolve = dns.promises.lookup(hostname);
73+
const timeoutPromise = new Promise<never>((_, reject) =>
74+
setTimeout(() => reject(new Error("dns-timeout")), timeout),
75+
);
76+
await Promise.race([resolve, timeoutPromise]);
77+
return true;
78+
} catch (_err) {
79+
return false;
80+
}
81+
};
82+
83+
export const setupAnalytics = (program: Command) => {
84+
program.hook("preAction", async (_thisCommand, actionCommand) => {
85+
const opts = program.opts();
86+
if (opts && opts.analytics === false) return;
87+
88+
// Skip analytics if the PostHog host cannot be resolved (offline).
89+
const host = new URL(POSTHOG_ENDPOINT).hostname;
90+
const canResolve = await canResolveHost(host);
91+
if (!canResolve) return;
92+
93+
let analytics: PostHog | null = null;
94+
try {
95+
analytics = new PostHog(POSTHOG_API_KEY, {
96+
host: POSTHOG_ENDPOINT,
97+
});
98+
analytics.on("error", () => {});
99+
100+
const event: EventMessage = {
101+
distinctId: getOrCreateUserUUID(),
102+
event: "ZetaChain CLI command executed",
103+
properties: {
104+
command: getFullCommandPath(actionCommand),
105+
},
106+
};
107+
analytics.capture(event);
108+
109+
await analytics.shutdown();
110+
} catch {
111+
// Skip analytics errors (e.g. offline / network failures), to prevent CLI disruption
112+
}
113+
});
114+
};

src/commands/docs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Command } from "commander";
22

3-
const getFullCommandPath = (cmd: Command): string => {
3+
export const getFullCommandPath = (cmd: Command): string => {
44
if (!cmd.parent) return cmd.name();
55
return `${getFullCommandPath(cmd.parent)} ${cmd.name()}`;
66
};

src/constants.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import os from "os";
2+
import path from "path";
3+
4+
export const POSTHOG_API_KEY =
5+
"phc_y7X3IXJu9751SOeEsisUIo3pGDgo1jxJ6Fk7odUMWJ4";
6+
export const POSTHOG_ENDPOINT = "https://us.i.posthog.com";
7+
8+
export const ZETACHAIN_DIR = path.join(os.homedir(), ".zetachain");
9+
export const ZETACHAIN_CONFIG_FILE = path.join(ZETACHAIN_DIR, "config.json");

src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from "@zetachain/toolkit/commands";
1414
import { Command } from "commander";
1515

16+
import { setupAnalytics } from "./analytics";
1617
import { docsCommand } from "./commands/docs";
1718
import { newCommand } from "./commands/new";
1819
import config from "./config.json";
@@ -23,7 +24,8 @@ program
2324
.name("zetachain")
2425
.description("CLI tool for ZetaChain development.")
2526
.helpCommand(false)
26-
.version(config.version);
27+
.version(config.version)
28+
.option("--no-analytics", "Disable analytics collection");
2729

2830
program.addCommand(newCommand);
2931
program.addCommand(accountsCommand);
@@ -38,4 +40,6 @@ program.addCommand(bitcoinCommand);
3840
program.addCommand(localnetCommand);
3941
program.addCommand(docsCommand);
4042

43+
setupAnalytics(program);
44+
4145
program.parse(process.argv);

yarn.lock

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1421,6 +1421,11 @@
14211421
resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.0.tgz#8dff61038cb5884789d8b323d9869e5363b976f7"
14221422
integrity sha512-vsJDAkYR6qCPu+ioGScGiMYR7LvZYIXh/dlQeviqoTWNCVfKTLYD/LkNWH4Mxsv2a5vpIRc77FN5DnmK1eBggQ==
14231423

1424+
"@posthog/core@1.0.2":
1425+
version "1.0.2"
1426+
resolved "https://registry.npmjs.org/@posthog/core/-/core-1.0.2.tgz#8e6125d271348f646f51269c0b7b9bbf6549f984"
1427+
integrity sha512-hWk3rUtJl2crQK0WNmwg13n82hnTwB99BT99/XI5gZSvIlYZ1TPmMZE8H2dhJJ98J/rm9vYJ/UXNzw3RV5HTpQ==
1428+
14241429
"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
14251430
version "1.1.2"
14261431
resolved "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
@@ -6176,6 +6181,13 @@ possible-typed-array-names@^1.0.0:
61766181
resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz#93e3582bc0e5426586d9d07b79ee40fc841de4ae"
61776182
integrity sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==
61786183

6184+
posthog-node@^5.8.1:
6185+
version "5.8.1"
6186+
resolved "https://registry.npmjs.org/posthog-node/-/posthog-node-5.8.1.tgz#463b1f8c5de40f95433838fa0583c5294d93dba6"
6187+
integrity sha512-YJYlYnlpItVjHqM9IhvZx8TzK8gnx2nU+0uhiog4RN47NnV0Z0K1AdC4ul+O8VuvS/jHqKCQvL8iAONRA37+0A==
6188+
dependencies:
6189+
"@posthog/core" "1.0.2"
6190+
61796191
prelude-ls@^1.2.1:
61806192
version "1.2.1"
61816193
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
@@ -7348,6 +7360,11 @@ uuid@^10.0.0:
73487360
resolved "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294"
73497361
integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==
73507362

7363+
uuid@^11.1.0:
7364+
version "11.1.0"
7365+
resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.0.tgz#9549028be1753bb934fc96e2bca09bb4105ae912"
7366+
integrity sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==
7367+
73517368
uuid@^8.3.2:
73527369
version "8.3.2"
73537370
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"

0 commit comments

Comments
 (0)