From 6ec49c8544c58c2463a97d055ede07daad73a306 Mon Sep 17 00:00:00 2001 From: Derek Rein Date: Thu, 12 Mar 2026 15:46:13 +0700 Subject: [PATCH] feat(cli-sdk): add anonymous telemetry to all CLI tools Introduces a lightweight, fire-and-forget telemetry module in cli-sdk that sends usage events to the Pulse analytics endpoint. Integrated into all 4 CLIs (walletconnect, staking, pay, companion-wallet). - Tracks command lifecycle (invoked/succeeded/failed), connections, transactions, and bridge completions - Opt-out via WALLETCONNECT_TELEMETRY=0 env var or config set telemetry false - Error messages sanitized (wallet addresses stripped, truncated to 256 chars) - Flush with 2-second timeout cap, never blocks process exit - Region-aware endpoint selection (.com vs .org) - 15 new tests for telemetry module Co-Authored-By: Claude Opus 4.6 --- .changeset/telemetry-pulse.md | 8 + packages/cli-sdk/src/cli.ts | 173 ++++++++++++---------- packages/cli-sdk/src/config.ts | 1 + packages/cli-sdk/src/index.ts | 2 + packages/cli-sdk/src/telemetry.ts | 103 +++++++++++++ packages/cli-sdk/test/telemetry.spec.ts | 188 ++++++++++++++++++++++++ packages/companion-wallet/src/cli.ts | 34 +++-- packages/pay-cli/src/cli.ts | 20 ++- packages/staking-cli/src/cli.ts | 119 ++++++++------- 9 files changed, 509 insertions(+), 139 deletions(-) create mode 100644 .changeset/telemetry-pulse.md create mode 100644 packages/cli-sdk/src/telemetry.ts create mode 100644 packages/cli-sdk/test/telemetry.spec.ts diff --git a/.changeset/telemetry-pulse.md b/.changeset/telemetry-pulse.md new file mode 100644 index 0000000..dcfdd56 --- /dev/null +++ b/.changeset/telemetry-pulse.md @@ -0,0 +1,8 @@ +--- +"@walletconnect/cli-sdk": minor +"@walletconnect/staking-cli": minor +"@walletconnect/pay-cli": minor +"@walletconnect/companion-wallet": minor +--- + +Add anonymous telemetry to all CLI tools via Pulse analytics endpoint diff --git a/packages/cli-sdk/src/cli.ts b/packages/cli-sdk/src/cli.ts index 4a2b8f5..8667677 100644 --- a/packages/cli-sdk/src/cli.ts +++ b/packages/cli-sdk/src/cli.ts @@ -1,7 +1,7 @@ import { WalletConnectCLI } from "./client.js"; import { resolveProjectId, setConfigValue, getConfigValue } from "./config.js"; +import { createTelemetry, trackCommand } from "./telemetry.js"; import { trySwidgeBeforeSend, swidgeViaWalletConnect, rpcUrl, waitForReceipt } from "./swidge.js"; -import type { TxReceipt } from "./swidge.js"; // Prevent unhandled WC relay errors from crashing the process with minified dumps process.on("unhandledRejection", (err) => { @@ -12,6 +12,12 @@ process.on("unhandledRejection", (err) => { declare const __VERSION__: string; +const telemetry = createTelemetry({ + binary: "walletconnect", + version: typeof __VERSION__ !== "undefined" ? __VERSION__ : "0.0.0-dev", + projectId: resolveProjectId(), +}); + const METADATA = { name: "WalletConnect Agent SDK", description: "WalletConnect CLI", @@ -49,9 +55,11 @@ Options: Config keys: project-id WalletConnect Cloud project ID + telemetry Enable/disable anonymous telemetry (true/false) Environment: - WALLETCONNECT_PROJECT_ID Overrides config project-id when set`); + WALLETCONNECT_PROJECT_ID Overrides config project-id when set + WALLETCONNECT_TELEMETRY Set to 0 to disable telemetry`); } function getProjectId(): string { @@ -93,6 +101,7 @@ async function cmdConnect(browser: boolean, chains?: string[]): Promise { try { console.log("Scan this QR code with your wallet app:\n"); const result = await sdk.connect(); + telemetry.track("connection_established", { command: "connect" }); console.log("\nConnected!"); for (const account of result.accounts) { const { chain, address } = parseAccount(account); @@ -293,6 +302,7 @@ async function cmdSendTransaction(jsonInput: string, browser: boolean): Promise< } } + telemetry.track("transaction_sent", { command: "send-transaction", chainId }); process.stdout.write(JSON.stringify({ transactionHash: txHash, reverted })); if (reverted) process.exit(1); } @@ -348,6 +358,7 @@ async function cmdSwidge(browser: boolean, args: string[]): Promise { amount, }); + telemetry.track("bridge_completed", { command: "swidge", fromChain, toChain }); process.stdout.write(JSON.stringify(bridgeResult)); } finally { await sdk.destroy(); @@ -389,92 +400,106 @@ async function main(): Promise { } const command = filtered[0]; - switch (command) { - case "connect": - await cmdConnect(browser, chains.length > 0 ? chains : ["evm"]); - break; - case "whoami": - await cmdWhoami(json); - break; - case "sign": { - const message = filtered[1]; - if (!message) { - console.error("Usage: walletconnect sign "); - process.exit(1); - } - await cmdSign(message, browser); - break; - } - case "sign-typed-data": { - const typedData = filtered[1]; - if (!typedData) { - console.error("Usage: walletconnect sign-typed-data "); - process.exit(1); + const dispatch = async (): Promise => { + switch (command) { + case "connect": + await cmdConnect(browser, chains.length > 0 ? chains : ["evm"]); + break; + case "whoami": + await cmdWhoami(json); + break; + case "sign": { + const message = filtered[1]; + if (!message) { + console.error("Usage: walletconnect sign "); + process.exit(1); + } + await cmdSign(message, browser); + break; } - await cmdSignTypedData(typedData, browser); - break; - } - case "send-transaction": { - const txJson = filtered[1]; - if (!txJson) { - console.error("Usage: walletconnect send-transaction ''"); - process.exit(1); + case "sign-typed-data": { + const typedData = filtered[1]; + if (!typedData) { + console.error("Usage: walletconnect sign-typed-data "); + process.exit(1); + } + await cmdSignTypedData(typedData, browser); + break; } - await cmdSendTransaction(txJson, browser); - break; - } - case "swidge": - await cmdSwidge(browser, filtered.slice(1)); - break; - case "disconnect": - await cmdDisconnect(); - break; - case "config": { - const action = filtered[1]; - const key = filtered[2]; - if (action === "set") { - const value = filtered[3]; - if (key === "project-id" && value) { - setConfigValue("projectId", value); - console.log(`Saved project-id to ~/.walletconnect-cli/config.json`); - } else { - console.error("Usage: walletconnect config set project-id "); + case "send-transaction": { + const txJson = filtered[1]; + if (!txJson) { + console.error("Usage: walletconnect send-transaction ''"); process.exit(1); } - } else if (action === "get") { - if (key === "project-id") { - const value = getConfigValue("projectId"); - console.log(value || "(not set)"); + await cmdSendTransaction(txJson, browser); + break; + } + case "swidge": + await cmdSwidge(browser, filtered.slice(1)); + break; + case "disconnect": + await cmdDisconnect(); + break; + case "config": { + const action = filtered[1]; + const key = filtered[2]; + if (action === "set") { + const value = filtered[3]; + if (key === "project-id" && value) { + setConfigValue("projectId", value); + console.log(`Saved project-id to ~/.walletconnect-cli/config.json`); + } else if (key === "telemetry" && value) { + setConfigValue("telemetry", value); + console.log(`Saved telemetry to ~/.walletconnect-cli/config.json`); + } else { + console.error("Usage: walletconnect config set "); + process.exit(1); + } + } else if (action === "get") { + if (key === "project-id") { + const value = getConfigValue("projectId"); + console.log(value || "(not set)"); + } else if (key === "telemetry") { + const value = getConfigValue("telemetry"); + console.log(value || "true"); + } else { + console.error("Usage: walletconnect config get "); + process.exit(1); + } } else { - console.error("Usage: walletconnect config get project-id"); + console.error("Usage: walletconnect config [value]"); process.exit(1); } - } else { - console.error("Usage: walletconnect config [value]"); - process.exit(1); + break; } - break; + case "--version": + case "-v": + console.log(__VERSION__); + break; + case "--help": + case "-h": + case undefined: + usage(); + break; + default: + console.error(`Unknown command: ${command}`); + usage(); + process.exit(1); } - case "--version": - case "-v": - console.log(__VERSION__); - break; - case "--help": - case "-h": - case undefined: - usage(); - break; - default: - console.error(`Unknown command: ${command}`); - usage(); - process.exit(1); + }; + + if (command && !command.startsWith("-")) { + await trackCommand(telemetry, command, dispatch); + } else { + await dispatch(); } } main().then( - () => process.exit(0), + () => telemetry.flush().finally(() => process.exit(0)), (err) => { console.error(err instanceof Error ? err.message : err); - process.exit(1); + telemetry.flush().finally(() => process.exit(1)); }, ); diff --git a/packages/cli-sdk/src/config.ts b/packages/cli-sdk/src/config.ts index 5931fc6..723f09f 100644 --- a/packages/cli-sdk/src/config.ts +++ b/packages/cli-sdk/src/config.ts @@ -7,6 +7,7 @@ const CONFIG_FILE = join(CONFIG_DIR, "config.json"); interface Config { projectId?: string; + telemetry?: string; } function readConfig(): Config { diff --git a/packages/cli-sdk/src/index.ts b/packages/cli-sdk/src/index.ts index 8c8f44f..62fd025 100644 --- a/packages/cli-sdk/src/index.ts +++ b/packages/cli-sdk/src/index.ts @@ -6,6 +6,8 @@ export { createSessionManager } from "./session.js"; export { createTerminalUI } from "./terminal-ui.js"; export { createBrowserUI } from "./browser-ui/server.js"; export { getConfigValue, setConfigValue, resolveProjectId } from "./config.js"; +export { createTelemetry, trackCommand } from "./telemetry.js"; +export type { TelemetryClient, TelemetryOptions } from "./telemetry.js"; // CWP (CLI Wallet Protocol) — provider discovery, execution, and selection export { diff --git a/packages/cli-sdk/src/telemetry.ts b/packages/cli-sdk/src/telemetry.ts new file mode 100644 index 0000000..da951c2 --- /dev/null +++ b/packages/cli-sdk/src/telemetry.ts @@ -0,0 +1,103 @@ +import { randomUUID } from "node:crypto"; +import { getConfigValue } from "./config.js"; + +export interface TelemetryOptions { + binary: string; + version: string; + projectId?: string; +} + +export interface TelemetryClient { + track(event: string, props?: Record): void; + flush(): Promise; +} + +const NOOP_CLIENT: TelemetryClient = { + track() {}, + async flush() {}, +}; + +function resolveEndpoint(): string { + try { + const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; + if (tz === "Asia/Shanghai" || tz === "Asia/Hong_Kong") { + return "https://pulse.walletconnect.org/e"; + } + } catch { + // default to .com + } + return "https://pulse.walletconnect.com/e"; +} + +export function createTelemetry(options: TelemetryOptions): TelemetryClient { + if (process.env.WALLETCONNECT_TELEMETRY === "0") return NOOP_CLIENT; + if (getConfigValue("telemetry") === "false") return NOOP_CLIENT; + if (!options.projectId) return NOOP_CLIENT; + + const projectId = options.projectId; + const endpoint = resolveEndpoint(); + const pending: Promise[] = []; + + return { + track(event: string, props?: Record): void { + const payload = { + eventId: randomUUID(), + timestamp: Date.now(), + props: { + event, + binary: options.binary, + os: process.platform, + nodeVersion: process.version, + ...props, + }, + }; + + const p = fetch(endpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-project-id": projectId, + "x-sdk-type": "walletconnect-cli", + "x-sdk-version": options.version, + }, + body: JSON.stringify(payload), + }).catch(() => {}); + + pending.push(p); + }, + + async flush(): Promise { + const batch = pending.splice(0); + if (batch.length === 0) return; + let timer: ReturnType | undefined; + const timeout = new Promise((resolve) => { timer = setTimeout(resolve, 2000); }); + await Promise.race([Promise.allSettled(batch), timeout]); + if (timer) clearTimeout(timer); + }, + }; +} + +/** Sanitize error message for telemetry — strip addresses, truncate length */ +function sanitizeError(err: unknown): string { + const msg = err instanceof Error ? err.message : String(err); + return msg.replace(/0x[a-fA-F0-9]{40}/g, "0x***").slice(0, 256); +} + +/** + * Wraps a command execution with telemetry lifecycle tracking. + * Tracks command_invoked before, command_succeeded/command_failed after. + */ +export async function trackCommand( + telemetry: TelemetryClient, + command: string, + fn: () => Promise, +): Promise { + telemetry.track("command_invoked", { command }); + try { + await fn(); + telemetry.track("command_succeeded", { command }); + } catch (err) { + telemetry.track("command_failed", { command, error: sanitizeError(err) }); + throw err; + } +} diff --git a/packages/cli-sdk/test/telemetry.spec.ts b/packages/cli-sdk/test/telemetry.spec.ts new file mode 100644 index 0000000..257790e --- /dev/null +++ b/packages/cli-sdk/test/telemetry.spec.ts @@ -0,0 +1,188 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; + +// Mock config before importing telemetry +vi.mock("../src/config.js", () => ({ + getConfigValue: vi.fn(() => undefined), +})); + +import { createTelemetry, trackCommand } from "../src/telemetry.js"; +import { getConfigValue } from "../src/config.js"; + +const mockedGetConfigValue = vi.mocked(getConfigValue); + +describe("createTelemetry", () => { + let fetchSpy: ReturnType; + const originalEnv = process.env.WALLETCONNECT_TELEMETRY; + + beforeEach(() => { + vi.restoreAllMocks(); + mockedGetConfigValue.mockReturnValue(undefined); + delete process.env.WALLETCONNECT_TELEMETRY; + fetchSpy = vi.fn(() => Promise.resolve(new Response())); + vi.stubGlobal("fetch", fetchSpy); + }); + + afterEach(() => { + if (originalEnv !== undefined) { + process.env.WALLETCONNECT_TELEMETRY = originalEnv; + } else { + delete process.env.WALLETCONNECT_TELEMETRY; + } + vi.unstubAllGlobals(); + }); + + it("returns no-op client when WALLETCONNECT_TELEMETRY=0", () => { + process.env.WALLETCONNECT_TELEMETRY = "0"; + const client = createTelemetry({ binary: "test", version: "1.0.0", projectId: "abc" }); + client.track("test_event"); + expect(fetchSpy).not.toHaveBeenCalled(); + }); + + it("returns no-op client when config telemetry is false", () => { + mockedGetConfigValue.mockReturnValue("false"); + const client = createTelemetry({ binary: "test", version: "1.0.0", projectId: "abc" }); + client.track("test_event"); + expect(fetchSpy).not.toHaveBeenCalled(); + }); + + it("returns no-op client when no projectId", () => { + const client = createTelemetry({ binary: "test", version: "1.0.0" }); + client.track("test_event"); + expect(fetchSpy).not.toHaveBeenCalled(); + }); + + it("sends correct payload shape", () => { + const client = createTelemetry({ binary: "walletconnect", version: "1.2.3", projectId: "proj-123" }); + client.track("command_invoked", { command: "connect" }); + + expect(fetchSpy).toHaveBeenCalledTimes(1); + const [url, opts] = fetchSpy.mock.calls[0]; + expect(url).toMatch(/^https:\/\/pulse\.walletconnect\.(com|org)\/e$/); + expect(opts.method).toBe("POST"); + expect(opts.headers["x-project-id"]).toBe("proj-123"); + expect(opts.headers["x-sdk-type"]).toBe("walletconnect-cli"); + expect(opts.headers["x-sdk-version"]).toBe("1.2.3"); + + const body = JSON.parse(opts.body); + expect(body.eventId).toBeDefined(); + expect(body.timestamp).toBeTypeOf("number"); + expect(body.props.event).toBe("command_invoked"); + expect(body.props.binary).toBe("walletconnect"); + expect(body.props.os).toBe(process.platform); + expect(body.props.nodeVersion).toBe(process.version); + expect(body.props.command).toBe("connect"); + }); + + it("uses .com endpoint by default", () => { + const client = createTelemetry({ binary: "test", version: "1.0.0", projectId: "abc" }); + client.track("test_event"); + + const [url] = fetchSpy.mock.calls[0]; + expect(url).toBe("https://pulse.walletconnect.com/e"); + }); + + it("flush resolves even when no events tracked", async () => { + const client = createTelemetry({ binary: "test", version: "1.0.0", projectId: "abc" }); + await expect(client.flush()).resolves.toBeUndefined(); + }); + + it("flush waits for pending events", async () => { + const client = createTelemetry({ binary: "test", version: "1.0.0", projectId: "abc" }); + client.track("event_1"); + client.track("event_2"); + await client.flush(); + expect(fetchSpy).toHaveBeenCalledTimes(2); + }); + + it("flush clears pending array", async () => { + const client = createTelemetry({ binary: "test", version: "1.0.0", projectId: "abc" }); + client.track("event_1"); + await client.flush(); + expect(fetchSpy).toHaveBeenCalledTimes(1); + // Second flush should be a no-op (pending was cleared) + await client.flush(); + // fetch count should not increase + expect(fetchSpy).toHaveBeenCalledTimes(1); + }); + + it("flush does not throw on fetch failures", async () => { + fetchSpy.mockRejectedValue(new Error("network error")); + const client = createTelemetry({ binary: "test", version: "1.0.0", projectId: "abc" }); + client.track("test_event"); + await expect(client.flush()).resolves.toBeUndefined(); + }); + + it("track silently catches fetch errors", () => { + fetchSpy.mockRejectedValue(new Error("network error")); + const client = createTelemetry({ binary: "test", version: "1.0.0", projectId: "abc" }); + // Should not throw + expect(() => client.track("test_event")).not.toThrow(); + }); + + it("no-op flush resolves immediately", async () => { + process.env.WALLETCONNECT_TELEMETRY = "0"; + const client = createTelemetry({ binary: "test", version: "1.0.0", projectId: "abc" }); + await expect(client.flush()).resolves.toBeUndefined(); + }); +}); + +describe("trackCommand", () => { + let fetchSpy: ReturnType; + + beforeEach(() => { + vi.restoreAllMocks(); + mockedGetConfigValue.mockReturnValue(undefined); + delete process.env.WALLETCONNECT_TELEMETRY; + fetchSpy = vi.fn(() => Promise.resolve(new Response())); + vi.stubGlobal("fetch", fetchSpy); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("tracks invoked and succeeded on success", async () => { + const client = createTelemetry({ binary: "test", version: "1.0.0", projectId: "abc" }); + await trackCommand(client, "connect", async () => {}); + + expect(fetchSpy).toHaveBeenCalledTimes(2); + const events = fetchSpy.mock.calls.map((c: unknown[]) => JSON.parse((c[1] as { body: string }).body).props.event); + expect(events).toEqual(["command_invoked", "command_succeeded"]); + }); + + it("tracks invoked and failed on error, then rethrows", async () => { + const client = createTelemetry({ binary: "test", version: "1.0.0", projectId: "abc" }); + + await expect( + trackCommand(client, "sign", async () => { throw new Error("boom"); }), + ).rejects.toThrow("boom"); + + expect(fetchSpy).toHaveBeenCalledTimes(2); + const calls = fetchSpy.mock.calls.map((c: unknown[]) => JSON.parse((c[1] as { body: string }).body).props); + expect(calls[0].event).toBe("command_invoked"); + expect(calls[1].event).toBe("command_failed"); + expect(calls[1].error).toBe("boom"); + }); + + it("sanitizes wallet addresses from error messages", async () => { + const client = createTelemetry({ binary: "test", version: "1.0.0", projectId: "abc" }); + + await expect( + trackCommand(client, "send", async () => { + throw new Error("insufficient funds for 0xABCDEF1234567890abcdef1234567890ABCDEF12"); + }), + ).rejects.toThrow(); + + const calls = fetchSpy.mock.calls.map((c: unknown[]) => JSON.parse((c[1] as { body: string }).body).props); + expect(calls[1].error).toBe("insufficient funds for 0x***"); + expect(calls[1].error).not.toContain("ABCDEF"); + }); + + it("includes command name in all events", async () => { + const client = createTelemetry({ binary: "test", version: "1.0.0", projectId: "abc" }); + await trackCommand(client, "stake", async () => {}); + + const commands = fetchSpy.mock.calls.map((c: unknown[]) => JSON.parse((c[1] as { body: string }).body).props.command); + expect(commands).toEqual(["stake", "stake"]); + }); +}); diff --git a/packages/companion-wallet/src/cli.ts b/packages/companion-wallet/src/cli.ts index d2a483d..1e11b23 100644 --- a/packages/companion-wallet/src/cli.ts +++ b/packages/companion-wallet/src/cli.ts @@ -1,6 +1,7 @@ import { readFileSync } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; +import { resolveProjectId, createTelemetry, trackCommand } from "@walletconnect/cli-sdk"; import { generateAndStore, loadKey, listAddresses } from "./keystore.js"; import { signMessage, signTypedData, signTransaction } from "./signer.js"; import { sendTransaction, getBalances } from "./rpc.js"; @@ -47,6 +48,12 @@ function getVersion(): string { } } +const telemetry = createTelemetry({ + binary: "companion-wallet", + version: getVersion(), + projectId: resolveProjectId(), +}); + let _stdinCache: string | undefined; async function readStdin(): Promise { @@ -237,6 +244,8 @@ async function handleSendTransaction(): Promise { input.chain, ); + telemetry.track("transaction_sent", { command: "send-transaction", chainId: input.chain }); + if (input.sessionId) { recordSessionUsage(input.sessionId, "send-transaction", input); } @@ -605,19 +614,18 @@ async function main(): Promise { }); } - try { - await HANDLERS[operation](); - } catch (err) { + await trackCommand(telemetry, operation, () => HANDLERS[operation]()); +} + +main().then( + () => telemetry.flush().finally(() => process.exit(0)), + (err) => { if (err instanceof SessionError) { respondError(err.message, "SESSION_ERROR"); - process.exit(ExitCode.SESSION_ERROR); + telemetry.flush().finally(() => process.exit(ExitCode.SESSION_ERROR)); + } else { + respondError(err instanceof Error ? err.message : "Internal error", "INTERNAL_ERROR"); + telemetry.flush().finally(() => process.exit(ExitCode.ERROR)); } - respondError( - err instanceof Error ? err.message : "Internal error", - "INTERNAL_ERROR", - ); - process.exit(ExitCode.ERROR); - } -} - -main(); + }, +); diff --git a/packages/pay-cli/src/cli.ts b/packages/pay-cli/src/cli.ts index cc7b059..f8d0f23 100644 --- a/packages/pay-cli/src/cli.ts +++ b/packages/pay-cli/src/cli.ts @@ -1,4 +1,5 @@ import { Cli, z } from "incur"; +import { resolveProjectId, createTelemetry } from "@walletconnect/cli-sdk"; import { createPayClient } from "./api.js"; import { createFrontendPayClient } from "./frontend-client.js"; import { PAY_FRONTEND_STAGING, PAY_FRONTEND_PROD } from "./constants.js"; @@ -11,6 +12,12 @@ declare const __VERSION__: string; const sdkVersion = typeof __VERSION__ !== "undefined" ? __VERSION__ : "0.0.0-dev"; +const telemetry = createTelemetry({ + binary: "walletconnect-pay", + version: sdkVersion, + projectId: resolveProjectId(), +}); + /** * Resolve the PayClient to use. When `--proxy` is set (or no wallet API key * is available), routes through the frontend's TanStack Start server @@ -54,6 +61,7 @@ cli.command("status", { proxy: z.boolean().optional().describe("Proxy through frontend (no API keys needed)"), }), async run(c) { + telemetry.track("command_invoked", { command: "status" }); const client = resolveClient(c.options); const payment = await client.getPayment(c.args.paymentId); @@ -64,6 +72,7 @@ cli.command("status", { console.log(label("Expires", new Date(payment.expiresAt * 1000).toLocaleString())); console.log(); + telemetry.track("command_succeeded", { command: "status" }); return { merchant: payment.merchant.name, amount: payment.amount, @@ -87,6 +96,7 @@ cli.command("create", { proxy: z.boolean().optional().describe("Proxy through frontend (no API keys needed)"), }), async run(c) { + telemetry.track("command_invoked", { command: "create" }); const client = resolveClient(c.options); const result = await client.createPayment({ @@ -101,6 +111,7 @@ cli.command("create", { console.log(label("Expires", new Date(result.expiresAt * 1000).toLocaleString())); console.log(); + telemetry.track("command_succeeded", { command: "create" }); return { paymentId: result.paymentId, gatewayUrl: result.gatewayUrl, @@ -126,6 +137,7 @@ cli.command("checkout", { pobAddress: z.string().optional().describe("Place of birth city/state, e.g. 'New York, NY'"), }), async run(c) { + telemetry.track("command_invoked", { command: "checkout" }); const client = resolveClient(c.options); // 1. Fetch payment details @@ -169,6 +181,7 @@ cli.command("checkout", { console.log(label("Action", `${rpc.method} on ${rpc.chain_id}`)); console.log(" Approve the request in your wallet app..."); const result = await sendTransaction(provider.path, option.account, rpc.chain_id, rpc); + telemetry.track("transaction_sent", { command: "checkout", chainId: rpc.chain_id }); results.push(result); } @@ -223,6 +236,7 @@ cli.command("checkout", { } console.log(); + telemetry.track("command_succeeded", { command: "checkout" }); return { paymentId: c.args.paymentId, status: final.status, @@ -241,6 +255,10 @@ function resolveAction(action: Action): WalletRpcAction { return JSON.parse(json) as WalletRpcAction; } -cli.serve(); +cli.serve(undefined, { + exit: (code: number) => { + telemetry.flush().finally(() => process.exit(code)); + }, +}); export default cli; diff --git a/packages/staking-cli/src/cli.ts b/packages/staking-cli/src/cli.ts index f346d85..cde4755 100644 --- a/packages/staking-cli/src/cli.ts +++ b/packages/staking-cli/src/cli.ts @@ -1,4 +1,4 @@ -import { selectProvider, walletExec } from "@walletconnect/cli-sdk"; +import { selectProvider, walletExec, resolveProjectId, createTelemetry, trackCommand } from "@walletconnect/cli-sdk"; import type { WalletProviderInfo } from "@walletconnect/cli-sdk"; import { CAIP2_CHAIN_ID } from "./constants.js"; import { stake, unstake, claim, status, balance } from "./commands.js"; @@ -6,6 +6,12 @@ import { createCwpSender } from "./wallet.js"; declare const __VERSION__: string; +const telemetry = createTelemetry({ + binary: "walletconnect-staking", + version: typeof __VERSION__ !== "undefined" ? __VERSION__ : "0.0.0-dev", + projectId: resolveProjectId(), +}); + function usage(): void { console.log(`Usage: walletconnect-staking [options] @@ -103,71 +109,82 @@ async function resolveWallet( async function main(): Promise { const { command, positional, address, wallet } = parseArgs(process.argv.slice(2)); - switch (command) { - case "stake": { - const amount = positional[0]; - const weeks = positional[1]; - if (!amount || !weeks) { - console.error("Usage: walletconnect-staking stake "); - process.exit(1); + const dispatch = async (): Promise => { + switch (command) { + case "stake": { + const amount = positional[0]; + const weeks = positional[1]; + if (!amount || !weeks) { + console.error("Usage: walletconnect-staking stake "); + process.exit(1); + } + const weeksNum = parseInt(weeks, 10); + if (isNaN(weeksNum) || weeksNum <= 0) { + console.error("Error: must be a positive integer."); + process.exit(1); + } + const resolved = await resolveWallet(wallet, address); + await stake(createCwpSender(resolved.providerPath), resolved.address, amount, weeksNum); + telemetry.track("transaction_sent", { command, chainId: CAIP2_CHAIN_ID }); + break; } - const weeksNum = parseInt(weeks, 10); - if (isNaN(weeksNum) || weeksNum <= 0) { - console.error("Error: must be a positive integer."); - process.exit(1); + + case "unstake": { + const resolved = await resolveWallet(wallet, address); + await unstake(createCwpSender(resolved.providerPath), resolved.address); + telemetry.track("transaction_sent", { command, chainId: CAIP2_CHAIN_ID }); + break; } - const resolved = await resolveWallet(wallet, address); - await stake(createCwpSender(resolved.providerPath), resolved.address, amount, weeksNum); - break; - } - case "unstake": { - const resolved = await resolveWallet(wallet, address); - await unstake(createCwpSender(resolved.providerPath), resolved.address); - break; - } + case "claim": { + const resolved = await resolveWallet(wallet, address); + await claim(createCwpSender(resolved.providerPath), resolved.address); + telemetry.track("transaction_sent", { command, chainId: CAIP2_CHAIN_ID }); + break; + } - case "claim": { - const resolved = await resolveWallet(wallet, address); - await claim(createCwpSender(resolved.providerPath), resolved.address); - break; - } + case "status": { + if (!address) { + console.error("Usage: walletconnect-staking status --address=0x..."); + process.exit(1); + } + await status(address); + break; + } - case "status": { - if (!address) { - console.error("Usage: walletconnect-staking status --address=0x..."); - process.exit(1); + case "balance": { + if (!address) { + console.error("Usage: walletconnect-staking balance --address=0x..."); + process.exit(1); + } + await balance(address); + break; } - await status(address); - break; - } - case "balance": { - if (!address) { - console.error("Usage: walletconnect-staking balance --address=0x..."); + case "--help": + case "-h": + case undefined: + usage(); + break; + + default: + console.error(`Unknown command: ${command}`); + usage(); process.exit(1); - } - await balance(address); - break; } + }; - case "--help": - case "-h": - case undefined: - usage(); - break; - - default: - console.error(`Unknown command: ${command}`); - usage(); - process.exit(1); + if (command && !command.startsWith("-")) { + await trackCommand(telemetry, command, dispatch); + } else { + await dispatch(); } } main().then( - () => process.exit(0), + () => telemetry.flush().finally(() => process.exit(0)), (err) => { console.error(err instanceof Error ? err.message : err); - process.exit(1); + telemetry.flush().finally(() => process.exit(1)); }, );