Skip to content

Commit 5bf5ebb

Browse files
committed
feat(cli): Add Analytic tracking to CLI commands
1 parent e7cf8a3 commit 5bf5ebb

File tree

2 files changed

+118
-1
lines changed

2 files changed

+118
-1
lines changed

src/together/lib/cli/__init__.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
from typing import Any
55

66
import click
7+
import httpx
78

89
import together
910
from together._version import __version__
1011
from together._constants import DEFAULT_TIMEOUT
1112
from together.lib.cli.api.beta import beta
1213
from together.lib.cli.api.evals import evals
1314
from together.lib.cli.api.files import files
15+
from together.lib.cli._track_cli import CliTrackingEvents, track_cli
1416
from together.lib.cli.api.models import models
1517
from together.lib.cli.api.endpoints import endpoints
1618
from together.lib.cli.api.fine_tuning import fine_tuning
@@ -57,10 +59,22 @@ def main(
5759
) -> None:
5860
"""This is a sample CLI tool."""
5961
os.environ.setdefault("TOGETHER_LOG", "debug" if debug else "info")
60-
ctx.obj = together.Together(
62+
63+
client = together.Together(
6164
api_key=api_key, base_url=base_url, timeout=timeout, max_retries=max_retries if max_retries is not None else 0
6265
)
6366

67+
# Wrap the client's httpx requests to track the parameters sent on api requests
68+
def track_request(request: httpx.Request) -> None:
69+
track_cli(
70+
CliTrackingEvents.ApiRequest,
71+
{"url": str(request.url), "method": request.method, "body": request.content.decode("utf-8")},
72+
)
73+
74+
client._client.event_hooks["request"].append(track_request)
75+
76+
ctx.obj = client
77+
6478

6579
main.add_command(files)
6680
main.add_command(fine_tuning)

src/together/lib/cli/_track_cli.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
from __future__ import annotations
2+
3+
import os
4+
import json
5+
import time
6+
import uuid
7+
import threading
8+
from enum import Enum
9+
from typing import Any, TypeVar, Callable
10+
from functools import wraps
11+
12+
import click
13+
import httpx
14+
import machineid
15+
16+
from together import __version__
17+
from together.lib.utils import log_debug
18+
19+
F = TypeVar("F", bound=Callable[..., Any])
20+
21+
SESSION_ID = int(str(uuid.uuid4().int)[0:13])
22+
23+
24+
def is_tracking_enabled() -> bool:
25+
# Users can opt-out of tracking with the environment variable.
26+
if os.getenv("TOGETHER_TELEMETRY_DISABLED"):
27+
log_debug("Analytics tracking disabled by environment variable")
28+
return False
29+
30+
return True
31+
32+
33+
class CliTrackingEvents(Enum):
34+
CommandStarted = "cli_command_started"
35+
CommandCompleted = "cli_commmand_completed"
36+
CommandFailed = "cli_command_failed"
37+
CommandUserAborted = "cli_command_user_aborted"
38+
ApiRequest = "cli_command_api_request"
39+
40+
41+
def track_cli(event_name: CliTrackingEvents, args: dict[str, Any]) -> None:
42+
"""Track a CLI event. Non-Blocking."""
43+
if is_tracking_enabled() == False:
44+
return
45+
46+
def send_event() -> None:
47+
ANALYTICS_API_ENV_VAR = os.getenv("TOGETHER_TELEMETRY_API")
48+
ANALYTICS_API = (
49+
ANALYTICS_API_ENV_VAR if ANALYTICS_API_ENV_VAR else "https://api.together.ai/api/together-cli-events"
50+
)
51+
52+
try:
53+
client = httpx.Client()
54+
client.post(
55+
ANALYTICS_API,
56+
headers={"content-type": "application/json", "user-agent": f"together-cli:{__version__}"},
57+
content=json.dumps(
58+
{
59+
"event_name": event_name.value,
60+
"event_properties": {
61+
"is_ci": os.getenv("CI") is not None,
62+
**args,
63+
},
64+
"event_options": {
65+
"time": int(time.time() * 1000),
66+
"session_id": str(SESSION_ID),
67+
"device_id": machineid.id().lower(),
68+
},
69+
}
70+
),
71+
)
72+
except Exception as e:
73+
log_debug("Error sending analytics event", error=e)
74+
# No-op - this is not critical and we don't want to block the CLI
75+
pass
76+
77+
threading.Thread(target=send_event).start()
78+
79+
80+
def auto_track_command(command: str) -> Callable[[F], F]:
81+
"""Decorator for click commands to automatically track CLI commands start/completion/failure."""
82+
83+
def decorator(f: F) -> F:
84+
@wraps(f)
85+
def wrapper(*args: Any, **kwargs: Any) -> Any:
86+
track_cli(CliTrackingEvents.CommandStarted, {"command": command, "arguments": kwargs})
87+
try:
88+
return f(*args, **kwargs)
89+
except click.Abort:
90+
# Doesn't seem like this is working any more
91+
track_cli(
92+
CliTrackingEvents.CommandUserAborted,
93+
{"command": command, "arguments": kwargs},
94+
)
95+
except Exception as e:
96+
track_cli(CliTrackingEvents.CommandFailed, {"command": command, "arguments": kwargs, "error": str(e)})
97+
raise e
98+
finally:
99+
track_cli(CliTrackingEvents.CommandCompleted, {"command": command, "arguments": kwargs})
100+
101+
return wrapper # type: ignore
102+
103+
return decorator # type: ignore

0 commit comments

Comments
 (0)