Files
wehub-resource-sync 555e282cc4
ci / changelog_check (push) Waiting to run
ci / check_changes (push) Waiting to run
ci / build_mem0 (3.10) (push) Blocked by required conditions
ci / build_mem0 (3.11) (push) Blocked by required conditions
ci / build_mem0 (3.12) (push) Blocked by required conditions
CLI Node CI / lint (push) Waiting to run
CLI Node CI / test (20) (push) Waiting to run
CLI Node CI / test (22) (push) Waiting to run
CLI Node CI / build (push) Waiting to run
CLI Python CI / lint (push) Waiting to run
CLI Python CI / test (3.10) (push) Waiting to run
CLI Python CI / test (3.11) (push) Waiting to run
CLI Python CI / test (3.12) (push) Waiting to run
CLI Python CI / build (push) Waiting to run
openclaw checks / lint (push) Waiting to run
openclaw checks / test (20) (push) Waiting to run
openclaw checks / test (22) (push) Waiting to run
openclaw checks / build (push) Waiting to run
opencode-plugin checks / build (push) Waiting to run
pi-agent-plugin checks / lint (push) Waiting to run
pi-agent-plugin checks / test (20) (push) Waiting to run
pi-agent-plugin checks / test (22) (push) Waiting to run
pi-agent-plugin checks / build (push) Waiting to run
TypeScript SDK CI / check_changes (push) Waiting to run
TypeScript SDK CI / changelog_check (push) Blocked by required conditions
TypeScript SDK CI / build_ts_sdk (20) (push) Blocked by required conditions
TypeScript SDK CI / build_ts_sdk (22) (push) Blocked by required conditions
TypeScript SDK CI / integration_ts_sdk (20) (push) Blocked by required conditions
TypeScript SDK CI / integration_ts_sdk (22) (push) Blocked by required conditions
chore: import upstream snapshot with attribution
2026-07-13 13:03:45 +08:00

174 lines
5.6 KiB
Python

#!/usr/bin/env python3
"""Lightweight fire-and-forget telemetry for the mem0 plugin.
Sends anonymous usage events to PostHog using the same project key and
endpoint as the mem0 Python SDK and CLI. No posthog library dependency —
uses stdlib urllib directly (same pattern as cli/python telemetry_sender.py).
CLI usage (called from hooks as a background subprocess):
python3 telemetry.py <event_type> [--memory_count=N] [--categories_count=N]
[--error_detected] [--file_paths_detected]
[--source=<src>] [--tool=<name>]
Opt-out: set MEM0_TELEMETRY=false (or 0/no/off) to disable all telemetry.
Never sends: user content, memory content, API keys, raw user/project IDs.
Only sends: event type, platform, plugin version, anonymized hashes, counts.
"""
from __future__ import annotations
import hashlib
import json
import os
import platform
import sys
import urllib.error
import urllib.request
# Each editor surface ships its own manifest with its own version line
# (Antigravity is on 0.1.x while Claude/Cursor/Codex are on 0.2.x), so we read
# the manifest matching the detected platform rather than a single shared one.
_PLATFORM_MANIFESTS = {
"antigravity": ("..", "plugin.json"),
"claude-code": ("..", ".claude-plugin", "plugin.json"),
"cursor": ("..", ".cursor-plugin", "plugin.json"),
"codex": ("..", ".codex-plugin", "plugin.json"),
}
_DEFAULT_MANIFEST = ("..", ".claude-plugin", "plugin.json")
def _load_plugin_version(platform_name: str = "") -> str:
parts = _PLATFORM_MANIFESTS.get(platform_name, _DEFAULT_MANIFEST)
try:
plugin_json = os.path.join(os.path.dirname(__file__), *parts)
with open(plugin_json) as f:
return json.load(f).get("version", "unknown")
except (OSError, json.JSONDecodeError, KeyError):
return "unknown"
POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX"
POSTHOG_HOST = "https://us.i.posthog.com/i/v0/e/"
REQUEST_TIMEOUT = 2
SAMPLE_RATE = 1.0
def _sha256(value: str) -> str:
return hashlib.sha256(value.encode("utf-8")).hexdigest()
def _distinct_id() -> str:
"""Stable anonymous ID: SHA-256 of API key if available, else SHA-256 of username."""
api_key = os.environ.get("MEM0_API_KEY") or os.environ.get("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY") or ""
if api_key:
return hashlib.sha256(api_key.encode()).hexdigest()[:32]
user_id = os.environ.get("MEM0_RESOLVED_USER_ID") or os.environ.get("USER") or "unknown"
return _sha256(user_id)
def detect_platform() -> str:
explicit = os.environ.get("MEM0_PLATFORM")
if explicit:
return explicit
if os.environ.get("ANTIGRAVITY_PLUGIN_ROOT"):
return "antigravity"
if os.environ.get("PLUGIN_ROOT"):
return "codex"
if os.environ.get("CLAUDECODE") or os.environ.get("CLAUDE_PLUGIN_ROOT"):
return "claude-code"
if os.environ.get("CURSOR_PLUGIN_ROOT"):
return "cursor"
if os.environ.get("WINDSURF_PLUGIN_ROOT"):
return "windsurf"
return "plugin"
def is_enabled() -> bool:
return os.environ.get("MEM0_TELEMETRY", "true").lower() not in ("false", "0", "no", "off")
def build_posthog_payload(event_name: str, properties: dict | None = None) -> dict:
project_id = os.environ.get("MEM0_PROJECT_ID") or "unknown"
plat = detect_platform()
return {
"api_key": POSTHOG_API_KEY,
"distinct_id": _distinct_id(),
"event": event_name,
"properties": {
**(properties or {}),
"source": "plugin",
"platform": plat,
"plugin_version": _load_plugin_version(plat),
"project_hash": _sha256(project_id),
"os": sys.platform,
"os_version": platform.version(),
"sample_rate": SAMPLE_RATE,
"$process_person_profile": False,
"$lib": "posthog-python",
},
}
def send(payload: dict) -> None:
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(
POSTHOG_HOST,
data=data,
headers={"Content-Type": "application/json"},
)
try:
with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT):
pass
except Exception:
pass
def emit(event_type: str, properties: dict | None = None) -> None:
if not is_enabled():
return
send(build_posthog_payload(f"plugin.{event_type}", properties))
def main() -> int:
if not is_enabled():
return 0
if len(sys.argv) < 2:
return 1
event_type = sys.argv[1]
properties: dict = {}
for arg in sys.argv[2:]:
if arg.startswith("--memory_count="):
try:
properties["memory_count"] = int(arg.split("=", 1)[1])
except ValueError:
pass
elif arg.startswith("--categories_count="):
try:
properties["categories_count"] = int(arg.split("=", 1)[1])
except ValueError:
pass
elif arg == "--error_detected":
properties["error_detected"] = True
elif arg == "--file_paths_detected":
properties["file_paths_detected"] = True
elif arg.startswith("--source="):
properties["source_detail"] = arg.split("=", 1)[1]
elif arg.startswith("--tool="):
properties["tool"] = arg.split("=", 1)[1]
elif arg.startswith("--files_count="):
try:
properties["files_count"] = int(arg.split("=", 1)[1])
except ValueError:
pass
emit(event_type, properties)
return 0
if __name__ == "__main__":
sys.exit(main())