Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/telemetry-pulse.md
Original file line number Diff line number Diff line change
@@ -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
173 changes: 99 additions & 74 deletions packages/cli-sdk/src/cli.ts
Original file line number Diff line number Diff line change
@@ -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) => {
Expand All @@ -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",
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -93,6 +101,7 @@ async function cmdConnect(browser: boolean, chains?: string[]): Promise<void> {
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);
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -348,6 +358,7 @@ async function cmdSwidge(browser: boolean, args: string[]): Promise<void> {
amount,
});

telemetry.track("bridge_completed", { command: "swidge", fromChain, toChain });
process.stdout.write(JSON.stringify(bridgeResult));
} finally {
await sdk.destroy();
Expand Down Expand Up @@ -389,92 +400,106 @@ async function main(): Promise<void> {
}
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 <message>");
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 <json>");
process.exit(1);
const dispatch = async (): Promise<void> => {
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 <message>");
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 '<json>'");
process.exit(1);
case "sign-typed-data": {
const typedData = filtered[1];
if (!typedData) {
console.error("Usage: walletconnect sign-typed-data <json>");
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 <value>");
case "send-transaction": {
const txJson = filtered[1];
if (!txJson) {
console.error("Usage: walletconnect send-transaction '<json>'");
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 <project-id|telemetry> <value>");
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 <project-id|telemetry>");
process.exit(1);
}
} else {
console.error("Usage: walletconnect config get project-id");
console.error("Usage: walletconnect config <set|get> <key> [value]");
process.exit(1);
}
} else {
console.error("Usage: walletconnect config <set|get> <key> [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));
},
);
1 change: 1 addition & 0 deletions packages/cli-sdk/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const CONFIG_FILE = join(CONFIG_DIR, "config.json");

interface Config {
projectId?: string;
telemetry?: string;
}

function readConfig(): Config {
Expand Down
2 changes: 2 additions & 0 deletions packages/cli-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
103 changes: 103 additions & 0 deletions packages/cli-sdk/src/telemetry.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>): void;
flush(): Promise<void>;
}

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<unknown>[] = [];

return {
track(event: string, props?: Record<string, unknown>): 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<void> {
const batch = pending.splice(0);
if (batch.length === 0) return;
let timer: ReturnType<typeof setTimeout> | undefined;
const timeout = new Promise<void>((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<void>,
): Promise<void> {
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;
}
}
Loading
Loading