Skip to content

Commit c1e4d5d

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

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+208
-9
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: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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/together/gateway/pub/v1/httpRequest"
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_source": "cli",
60+
"event_type": event_name.value,
61+
"event_properties": {
62+
"is_ci": os.getenv("CI") is not None,
63+
**args,
64+
},
65+
"context": {
66+
"session_id": str(SESSION_ID),
67+
"runtime": {
68+
"name": "together-cli",
69+
"version": __version__,
70+
},
71+
},
72+
"identity": {
73+
"device_id": machineid.id().lower(),
74+
},
75+
"event_options": {
76+
"time": int(time.time() * 1000),
77+
},
78+
}
79+
),
80+
)
81+
except Exception as e:
82+
log_debug("Error sending analytics event", error=e)
83+
# No-op - this is not critical and we don't want to block the CLI
84+
pass
85+
86+
threading.Thread(target=send_event).start()
87+
88+
89+
def auto_track_command(command: str) -> Callable[[F], F]:
90+
"""Decorator for click commands to automatically track CLI commands start/completion/failure."""
91+
92+
def decorator(f: F) -> F:
93+
@wraps(f)
94+
def wrapper(*args: Any, **kwargs: Any) -> Any:
95+
track_cli(CliTrackingEvents.CommandStarted, {"command": command, "arguments": kwargs})
96+
try:
97+
return f(*args, **kwargs)
98+
except click.Abort:
99+
# Doesn't seem like this is working any more
100+
track_cli(
101+
CliTrackingEvents.CommandUserAborted,
102+
{"command": command, "arguments": kwargs},
103+
)
104+
except Exception as e:
105+
track_cli(CliTrackingEvents.CommandFailed, {"command": command, "arguments": kwargs, "error": str(e)})
106+
raise e
107+
finally:
108+
track_cli(CliTrackingEvents.CommandCompleted, {"command": command, "arguments": kwargs})
109+
110+
return wrapper # type: ignore
111+
112+
return decorator # type: ignore

src/together/lib/cli/api/beta/clusters/create.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from rich import print
99

1010
from together import Together
11+
from together.lib.cli._track_cli import auto_track_command
1112
from together.lib.cli.api._utils import handle_api_errors
1213
from together.types.beta.cluster_create_params import SharedVolume, ClusterCreateParams
1314

@@ -61,6 +62,7 @@
6162
)
6263
@click.pass_context
6364
@handle_api_errors("Clusters")
65+
@auto_track_command("clusters create")
6466
def create(
6567
ctx: click.Context,
6668
name: str | None = None,

src/together/lib/cli/api/beta/clusters/delete.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import click
44

55
from together import Together
6+
from together.lib.cli._track_cli import auto_track_command
67
from together.lib.cli.api._utils import handle_api_errors
78

89

@@ -15,6 +16,7 @@
1516
)
1617
@click.pass_context
1718
@handle_api_errors("Clusters")
19+
@auto_track_command("clusters delete")
1820
def delete(ctx: click.Context, cluster_id: str, json: bool) -> None:
1921
"""Delete a cluster by ID"""
2022
client: Together = ctx.obj

src/together/lib/cli/api/beta/clusters/get_credentials.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import click
1111

12+
from together.lib.cli._track_cli import auto_track_command
1213
from together import Together, TogetherError
1314
from together.lib.cli.api._utils import handle_api_errors
1415

@@ -39,6 +40,7 @@
3940
)
4041
@click.pass_context
4142
@handle_api_errors("Clusters")
43+
@auto_track_command("clusters get-credentials")
4244
def get_credentials(
4345
ctx: click.Context,
4446
cluster_id: str,

src/together/lib/cli/api/beta/clusters/list.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import click
44

55
from together import Together
6+
from together.lib.cli._track_cli import auto_track_command
67

78

89
@click.command()
@@ -12,6 +13,7 @@
1213
help="Output in JSON format",
1314
)
1415
@click.pass_context
16+
@auto_track_command("clusters list")
1517
def list(ctx: click.Context, json: bool) -> None:
1618
"""List clusters"""
1719
client: Together = ctx.obj

src/together/lib/cli/api/beta/clusters/list_regions.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from together import Together
88
from together.lib.cli.api._utils import handle_api_errors
9+
from together.lib.cli._track_cli import auto_track_command
910

1011

1112
@click.command()
@@ -16,6 +17,7 @@
1617
)
1718
@click.pass_context
1819
@handle_api_errors("Clusters")
20+
@auto_track_command("clusters list-regions")
1921
def list_regions(ctx: click.Context, json: bool) -> None:
2022
"""List regions"""
2123
client: Together = ctx.obj

src/together/lib/cli/api/beta/clusters/retrieve.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from together import Together
77
from together.lib.cli.api._utils import handle_api_errors
8+
from together.lib.cli._track_cli import auto_track_command
89

910

1011
@click.command()
@@ -16,6 +17,7 @@
1617
)
1718
@click.pass_context
1819
@handle_api_errors("Clusters")
20+
@auto_track_command("clusters retrieve")
1921
def retrieve(ctx: click.Context, cluster_id: str, json: bool) -> None:
2022
"""Retrieve a cluster by ID"""
2123
client: Together = ctx.obj

src/together/lib/cli/api/beta/clusters/storage/create.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from together import Together
66
from together.lib.cli.api._utils import handle_api_errors
7+
from together.lib.cli._track_cli import auto_track_command
78

89

910
@click.command()
@@ -31,6 +32,7 @@
3132
help="Output in JSON format",
3233
)
3334
@click.pass_context
35+
@auto_track_command("clusters storage create")
3436
@handle_api_errors("Clusters Storage")
3537
def create(ctx: click.Context, region: str, size_tib: int, volume_name: str, json: bool) -> None:
3638
"""Create a storage volume"""

src/together/lib/cli/api/beta/clusters/storage/delete.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from together import Together
66
from together.lib.cli.api._utils import handle_api_errors
7+
from together.lib.cli._track_cli import auto_track_command
78

89

910
@click.command()
@@ -18,6 +19,7 @@
1819
)
1920
@click.pass_context
2021
@handle_api_errors("Clusters Storage")
22+
@auto_track_command("clusters storage delete")
2123
def delete(ctx: click.Context, volume_id: str, json: bool) -> None:
2224
"""Delete a storage volume"""
2325
client: Together = ctx.obj

0 commit comments

Comments
 (0)