555e282cc4
pi-agent-plugin checks / lint (push) Has been cancelled
pi-agent-plugin checks / test (20) (push) Has been cancelled
pi-agent-plugin checks / test (22) (push) Has been cancelled
pi-agent-plugin checks / build (push) Has been cancelled
TypeScript SDK CI / check_changes (push) Has been cancelled
TypeScript SDK CI / changelog_check (push) Has been cancelled
ci / changelog_check (push) Has been cancelled
ci / check_changes (push) Has been cancelled
ci / build_mem0 (3.10) (push) Has been cancelled
ci / build_mem0 (3.11) (push) Has been cancelled
ci / build_mem0 (3.12) (push) Has been cancelled
CLI Node CI / lint (push) Has been cancelled
CLI Node CI / test (20) (push) Has been cancelled
CLI Node CI / test (22) (push) Has been cancelled
CLI Node CI / build (push) Has been cancelled
CLI Python CI / lint (push) Has been cancelled
CLI Python CI / test (3.10) (push) Has been cancelled
CLI Python CI / test (3.11) (push) Has been cancelled
CLI Python CI / test (3.12) (push) Has been cancelled
CLI Python CI / build (push) Has been cancelled
openclaw checks / lint (push) Has been cancelled
openclaw checks / test (20) (push) Has been cancelled
openclaw checks / test (22) (push) Has been cancelled
openclaw checks / build (push) Has been cancelled
opencode-plugin checks / build (push) Has been cancelled
TypeScript SDK CI / build_ts_sdk (20) (push) Has been cancelled
TypeScript SDK CI / build_ts_sdk (22) (push) Has been cancelled
TypeScript SDK CI / integration_ts_sdk (20) (push) Has been cancelled
TypeScript SDK CI / integration_ts_sdk (22) (push) Has been cancelled
156 lines
5.1 KiB
Python
156 lines
5.1 KiB
Python
"""CLI telemetry — anonymous usage tracking via PostHog.
|
|
|
|
Sends fire-and-forget events to PostHog by spawning a detached subprocess
|
|
(telemetry_sender.py). The parent CLI process exits immediately; the
|
|
subprocess handles email resolution, caching, and the HTTP POST.
|
|
|
|
Disable with: MEM0_TELEMETRY=false
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import platform
|
|
import subprocess
|
|
import sys
|
|
import uuid
|
|
from typing import Any
|
|
|
|
POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX"
|
|
POSTHOG_HOST = "https://us.i.posthog.com/i/v0/e/"
|
|
|
|
|
|
def _is_telemetry_enabled() -> bool:
|
|
val = os.environ.get("MEM0_TELEMETRY", "true").lower()
|
|
return val not in ("false", "0", "no")
|
|
|
|
|
|
def _get_or_create_anonymous_id() -> str:
|
|
"""Return a persistent per-machine anonymous ID, generating one if needed.
|
|
|
|
Stored in ~/.mem0/config.json under `telemetry.anonymous_id` so that
|
|
repeat runs on the same machine share one PostHog identity instead of
|
|
collapsing into a single shared fallback string.
|
|
"""
|
|
from mem0_cli.config import load_config, save_config
|
|
|
|
config = load_config()
|
|
if config.telemetry.anonymous_id:
|
|
return config.telemetry.anonymous_id
|
|
|
|
new_id = f"cli-anon-{uuid.uuid4().hex}"
|
|
config.telemetry.anonymous_id = new_id
|
|
with contextlib.suppress(Exception):
|
|
save_config(config)
|
|
return new_id
|
|
|
|
|
|
def _get_distinct_id() -> str:
|
|
"""Return a stable anonymous identifier for the current user.
|
|
|
|
Priority: cached user_email (from /v1/ping/) > MD5(api_key) >
|
|
persistent per-machine anonymous ID.
|
|
"""
|
|
try:
|
|
from mem0_cli.config import load_config
|
|
|
|
config = load_config()
|
|
if config.platform.user_email:
|
|
return config.platform.user_email
|
|
if config.platform.api_key:
|
|
return hashlib.md5(config.platform.api_key.encode()).hexdigest()
|
|
except Exception:
|
|
pass
|
|
try:
|
|
return _get_or_create_anonymous_id()
|
|
except Exception:
|
|
return f"cli-anon-{uuid.uuid4().hex}"
|
|
|
|
|
|
def capture_event(
|
|
event_name: str,
|
|
properties: dict[str, Any] | None = None,
|
|
pre_resolved_email: str | None = None,
|
|
) -> None:
|
|
"""Fire a PostHog event via a detached subprocess (non-blocking).
|
|
|
|
When *pre_resolved_email* is provided (e.g. from an upfront ping
|
|
validation), it is used directly as the PostHog distinct ID and the
|
|
subprocess skips its own ``/v1/ping/`` call.
|
|
"""
|
|
if not _is_telemetry_enabled():
|
|
return
|
|
|
|
try:
|
|
from mem0_cli import __version__
|
|
from mem0_cli.config import CONFIG_FILE, load_config, save_config
|
|
|
|
config = load_config()
|
|
distinct_id = pre_resolved_email or _get_distinct_id()
|
|
|
|
# Detect anonymous → identified transition. If a stored anonymous_id
|
|
# exists and we just resolved to a real identity, fire a one-shot
|
|
# $identify event so PostHog stitches the pre-signup history onto
|
|
# the authenticated profile. Clear the stored id so we don't re-alias.
|
|
anon_id_to_alias: str | None = None
|
|
if (
|
|
distinct_id
|
|
and not distinct_id.startswith("cli-anon-")
|
|
and config.telemetry.anonymous_id
|
|
):
|
|
anon_id_to_alias = config.telemetry.anonymous_id
|
|
config.telemetry.anonymous_id = ""
|
|
with contextlib.suppress(Exception):
|
|
save_config(config)
|
|
|
|
# M4: every cli.* event carries agent_mode based on the config flag
|
|
# (unclaimed Agent Mode key). This is the growth-doc property used to
|
|
# join init → add → search funnels in PostHog.
|
|
payload = {
|
|
"api_key": POSTHOG_API_KEY,
|
|
"distinct_id": distinct_id,
|
|
"event": event_name,
|
|
"properties": {
|
|
"source": "CLI",
|
|
"language": "python",
|
|
"cli_version": __version__,
|
|
"agent_mode": bool(config.platform.agent_mode),
|
|
"python_version": sys.version,
|
|
"os": sys.platform,
|
|
"os_version": platform.version(),
|
|
"$process_person_profile": False,
|
|
"$lib": "posthog-python",
|
|
**(properties or {}),
|
|
},
|
|
}
|
|
|
|
context = {
|
|
"payload": payload,
|
|
"posthog_host": POSTHOG_HOST,
|
|
"needs_email": not distinct_id or "@" not in distinct_id,
|
|
"mem0_api_key": config.platform.api_key or "",
|
|
"mem0_base_url": config.platform.base_url or "https://api.mem0.ai",
|
|
"config_path": str(CONFIG_FILE),
|
|
"anon_distinct_id_to_alias": anon_id_to_alias,
|
|
}
|
|
|
|
child = subprocess.Popen(
|
|
[sys.executable, "-m", "mem0_cli.telemetry_sender"],
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
start_new_session=True,
|
|
close_fds=True,
|
|
text=True,
|
|
)
|
|
if child.stdin:
|
|
with contextlib.suppress(Exception):
|
|
child.stdin.write(json.dumps(context))
|
|
with contextlib.suppress(Exception):
|
|
child.stdin.close()
|
|
except Exception:
|
|
pass
|