feat(cli): add PostHog telemetry and source tracking to Python & Node CLIs (#4699)

This commit is contained in:
Saket Aryan
2026-04-04 20:47:38 +05:30
committed by GitHub
parent 0daa5d7d03
commit ee67602c58
15 changed files with 529 additions and 8 deletions
+16 -2
View File
@@ -3,6 +3,8 @@
*/
import type { PlatformConfig } from "../config.js";
import { isAgentMode } from "../state.js";
import { CLI_VERSION } from "../version.js";
import {
APIError,
type AddOptions,
@@ -24,6 +26,9 @@ export class PlatformBackend implements Backend {
this.headers = {
Authorization: `Token ${config.apiKey}`,
"Content-Type": "application/json",
"X-Mem0-Source": "cli",
"X-Mem0-Client-Language": "node",
"X-Mem0-Client-Version": CLI_VERSION,
};
}
@@ -38,9 +43,14 @@ export class PlatformBackend implements Backend {
url += `?${qs}`;
}
const headers = {
...this.headers,
"X-Mem0-Caller-Type": isAgentMode() ? "agent" : "user",
};
const fetchOpts: RequestInit = {
method,
headers: this.headers,
headers,
signal: AbortSignal.timeout(30_000),
};
if (opts?.json) {
@@ -286,11 +296,15 @@ export class PlatformBackend implements Backend {
return result;
}
async ping(): Promise<Record<string, unknown>> {
return (await this._request("GET", "/v1/ping/")) as Record<string, unknown>;
}
async status(
opts: { userId?: string; agentId?: string } = {},
): Promise<Record<string, unknown>> {
try {
await this._request("GET", "/v1/ping/");
await this.ping();
return { connected: true, backend: "platform", base_url: this.baseUrl };
} catch (e) {
return {
+20 -2
View File
@@ -41,10 +41,16 @@ async function emailLogin(
const url = baseUrl.replace(/\/+$/, "");
let codeValue = code;
const sourceHeaders = {
"Content-Type": "application/json",
"X-Mem0-Source": "cli",
"X-Mem0-Client-Language": "node",
};
if (!codeValue) {
const resp = await fetch(`${url}/api/v1/auth/email_code/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
headers: sourceHeaders,
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(30_000),
});
@@ -85,7 +91,7 @@ async function emailLogin(
const verifyResp = await fetch(`${url}/api/v1/auth/email_code/verify/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
headers: sourceHeaders,
body: JSON.stringify({ email, code: codeValue.trim() }),
signal: AbortSignal.timeout(30_000),
});
@@ -215,6 +221,16 @@ async function validatePlatform(config: Mem0Config): Promise<void> {
});
if (status.connected) {
printSuccess("Connected to mem0 Platform!");
// Cache user_email from ping response for telemetry distinct_id
try {
const pingData = (await backend.ping()) as Record<string, unknown>;
const userEmail = pingData?.user_email as string | undefined;
if (userEmail) {
config.platform.userEmail = userEmail;
}
} catch {
/* ignore — telemetry ID will fall back to API key hash */
}
} else {
printError(
`Could not connect: ${status.error ?? "Unknown error"}`,
@@ -307,6 +323,7 @@ export async function runInit(
config.platform.apiKey = apiKeyVal;
config.platform.baseUrl = baseUrl;
config.platform.userEmail = email;
config.defaults.userId =
opts.userId || process.env.USER || process.env.USERNAME || "mem0-cli";
@@ -385,6 +402,7 @@ export async function runInit(
config.platform.apiKey = apiKeyVal;
config.platform.baseUrl = baseUrl;
config.platform.userEmail = email;
config.defaults.userId =
opts.userId || process.env.USER || process.env.USERNAME || "mem0-cli";
+6
View File
@@ -20,6 +20,7 @@ export const CONFIG_VERSION = 1;
export interface PlatformConfig {
apiKey: string;
baseUrl: string;
userEmail: string;
}
export interface DefaultsConfig {
@@ -49,6 +50,7 @@ export function createDefaultConfig(): Mem0Config {
platform: {
apiKey: "",
baseUrl: DEFAULT_BASE_URL,
userEmail: "",
},
};
}
@@ -70,6 +72,7 @@ export function loadConfig(): Mem0Config {
const plat = data.platform ?? {};
config.platform.apiKey = plat.api_key ?? "";
config.platform.baseUrl = plat.base_url ?? DEFAULT_BASE_URL;
config.platform.userEmail = plat.user_email ?? "";
const defaults = data.defaults ?? {};
config.defaults.userId = defaults.user_id ?? "";
@@ -114,6 +117,7 @@ export function saveConfig(config: Mem0Config): void {
platform: {
api_key: config.platform.apiKey,
base_url: config.platform.baseUrl,
user_email: config.platform.userEmail,
},
};
@@ -131,6 +135,7 @@ export function redactKey(key: string): string {
const KEY_MAP: Record<string, [keyof Mem0Config, string]> = {
"platform.api_key": ["platform", "apiKey"],
"platform.base_url": ["platform", "baseUrl"],
"platform.user_email": ["platform", "userEmail"],
"defaults.user_id": ["defaults", "userId"],
"defaults.agent_id": ["defaults", "agentId"],
"defaults.app_id": ["defaults", "appId"],
@@ -139,6 +144,7 @@ const KEY_MAP: Record<string, [keyof Mem0Config, string]> = {
// Short-form aliases
api_key: ["platform", "apiKey"],
base_url: ["platform", "baseUrl"],
user_email: ["platform", "userEmail"],
user_id: ["defaults", "userId"],
agent_id: ["defaults", "agentId"],
app_id: ["defaults", "appId"],
+21
View File
@@ -14,6 +14,7 @@ import type { Mem0Config } from "./config.js";
import { loadConfig } from "./config.js";
import { richFormatHelp } from "./help.js";
import { setAgentMode } from "./state.js";
import { captureEvent } from "./telemetry.js";
import { CLI_VERSION } from "./version.js";
const program = new Command();
@@ -123,6 +124,26 @@ program
.addHelpCommand(false)
.configureHelp({ formatHelp: richFormatHelp });
// ── Telemetry hook ───────────────────────────────────────────────────────
program.hook("preAction", (_thisCommand, actionCommand) => {
try {
const commandName = actionCommand.name();
const parentName = actionCommand.parent?.name();
const fullCommand =
parentName && parentName !== "mem0"
? `${parentName}.${commandName}`
: commandName;
const isAgent = !!(program.opts().json || program.opts().agent);
captureEvent(`cli.${fullCommand}`, {
command: fullCommand,
is_agent: isAgent,
});
} catch {
/* silently swallow */
}
});
// ── Init ──────────────────────────────────────────────────────────────────
program
+101
View File
@@ -0,0 +1,101 @@
/**
* CLI telemetry — anonymous usage tracking via PostHog.
*
* Sends fire-and-forget events by spawning a detached child process
* (telemetry-sender.cjs). The parent CLI process exits immediately;
* the child handles email resolution, caching, and the HTTP POST.
*
* Disable with: MEM0_TELEMETRY=false
*/
import { spawn } from "node:child_process";
import { createHash } from "node:crypto";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { CONFIG_FILE, loadConfig } from "./config.js";
import { CLI_VERSION } from "./version.js";
const POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX";
const POSTHOG_HOST = "https://us.i.posthog.com/i/v0/e/";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const SENDER_SCRIPT = path.join(__dirname, "..", "telemetry-sender.cjs");
function isTelemetryEnabled(): boolean {
try {
return process.env.MEM0_TELEMETRY !== "false";
} catch {
return true;
}
}
/**
* Return a stable anonymous identifier for the current user.
*
* Priority: cached user_email (from /v1/ping/) > MD5(api_key) > fallback.
* Matches the SDK pattern in mem0-ts/src/client/mem0.ts.
*/
function getDistinctId(): string {
try {
const config = loadConfig();
if (config.platform.userEmail) {
return config.platform.userEmail;
}
if (config.platform.apiKey) {
return createHash("md5").update(config.platform.apiKey).digest("hex");
}
} catch {
/* ignore */
}
return "anonymous-cli";
}
/**
* Fire a PostHog event (non-blocking, returns void, never throws).
* Spawns telemetry-sender.cjs as a detached subprocess.
*/
export function captureEvent(
eventName: string,
properties: Record<string, unknown> = {},
): void {
if (!isTelemetryEnabled()) return;
try {
const config = loadConfig();
const distinctId = getDistinctId();
const payload = {
api_key: POSTHOG_API_KEY,
distinct_id: distinctId,
event: eventName,
properties: {
source: "CLI",
language: "node",
cli_version: CLI_VERSION,
node_version: process.version,
os: process.platform,
...properties,
$process_person_profile: false,
$lib: "posthog-node",
},
};
const context = {
payload,
posthogHost: POSTHOG_HOST,
needsEmail: !distinctId || !distinctId.includes("@"),
mem0ApiKey: config.platform.apiKey || "",
mem0BaseUrl: config.platform.baseUrl || "https://api.mem0.ai",
configPath: CONFIG_FILE,
};
const child = spawn(
process.execPath,
[SENDER_SCRIPT, JSON.stringify(context)],
{ detached: true, stdio: "ignore" },
);
child.unref();
} catch {
/* silently swallow */
}
}