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
23 changes: 23 additions & 0 deletions src/poly/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
REGIONS,
AgentStudioInterface,
)
from poly import telemetry
from poly.resources import resource_utils
from poly.project import (
PROJECT_CONFIG_FILE,
Expand Down Expand Up @@ -576,6 +577,28 @@ def _run_command(cls, args):
else:
logging.basicConfig(level=logging.WARNING)

# --- Usage telemetry (anonymous, fire-and-forget) ---
_subcommand = getattr(args, "action", None)
_project_ctx: dict = {}
if args.command not in {"init", "docs", "completion"}:
try:
_p = cls.read_project_config(getattr(args, "path", "."))
if _p:
_project_ctx = {
"account_id": getattr(_p, "account_id", None),
"project_id": getattr(_p, "project_id", None),
"region": getattr(_p, "region", None),
}
except Exception:
pass
telemetry.track(
args.command,
subcommand=_subcommand,
dry_run=bool(getattr(args, "dry_run", False)),
**_project_ctx,
)
# ---

if args.command == "init":
cls.init_project(
args.base_path,
Expand Down
142 changes: 142 additions & 0 deletions src/poly/telemetry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"""Usage telemetry for the PolyAI ADK.

Copyright PolyAI Limited

Fires a single background HTTP POST for each CLI command so the product team
can understand how developers use the ADK (command frequency, push volume,
active users, etc.)

Identity
--------
We collect the developer's real name and email address (from git config) plus
their OS username and machine hostname. This lets the team track usage at the
individual contributor level.

Opt-out
-------
Set ``POLY_TELEMETRY_DISABLED=1`` (or any truthy value) to suppress all
telemetry. Useful for CI/CD pipelines or local overrides.

The call runs in a daemon thread with a hard 2-second timeout and is silently
swallowed on any error — it *never* blocks or breaks the CLI.
"""

import os
import platform
import socket
import subprocess
import threading
from importlib.metadata import version as _pkg_version
from typing import Any, Dict, Optional

_ENDPOINT = "https://analytics.us-1.platform.polyai.app/ingest/v1/adk"
_TIMEOUT_SECS = 2
_DISABLED_ENVVAR = "POLY_TELEMETRY_DISABLED"


def _is_disabled() -> bool:
val = os.environ.get(_DISABLED_ENVVAR, "").strip().lower()
return val in {"1", "true", "yes", "on"}


def _git_config(key: str) -> Optional[str]:
"""Read a git config value; returns None on any failure."""
try:
result = subprocess.run(
["git", "config", "--global", key],
capture_output=True,
text=True,
timeout=1,
)
value = result.stdout.strip()
return value if value else None
except Exception:
return None


def _identity() -> Dict[str, Optional[str]]:
"""Collect real developer identity from git config and OS."""
return {
"user_email": _git_config("user.email"),
"user_name": _git_config("user.name"),
"os_username": os.getlogin() if hasattr(os, "getlogin") else None,
"hostname": socket.gethostname(),
}


def _adk_version() -> str:
try:
return _pkg_version("polyai-adk")
except Exception:
return "unknown"


def _post(payload: Dict[str, Any]) -> None:
"""Fire-and-forget POST; swallows all exceptions."""
try:
import json as _json
import urllib.request

data = _json.dumps(payload).encode("utf-8")
req = urllib.request.Request(
_ENDPOINT,
data=data,
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=_TIMEOUT_SECS):
pass
except Exception:
pass


def track(
command: str,
*,
subcommand: Optional[str] = None,
account_id: Optional[str] = None,
project_id: Optional[str] = None,
dry_run: bool = False,
region: Optional[str] = None,
) -> None:
"""Track a CLI command invocation in the background.

Parameters
----------
command:
Top-level command name, e.g. ``"push"``, ``"pull"``, ``"init"``.
subcommand:
Optional sub-action, e.g. ``"list"`` for ``poly branch list``.
account_id:
Agent Studio account ID (read from project.yaml when available).
project_id:
Agent Studio project ID (read from project.yaml when available).
dry_run:
Whether the command was invoked with ``--dry-run``.
region:
Agent Studio region, e.g. ``"eu-west-1"``.
"""
if _is_disabled():
return

payload: Dict[str, Any] = {
"source": "adk",
"command": command,
"adk_version": _adk_version(),
"python_version": platform.python_version(),
"os": platform.system().lower(),
**_identity(),
}
if subcommand:
payload["subcommand"] = subcommand
if account_id:
payload["account_id"] = account_id
if project_id:
payload["project_id"] = project_id
if dry_run:
payload["dry_run"] = True
if region:
payload["region"] = region

t = threading.Thread(target=_post, args=(payload,), daemon=True)
t.start()
Loading