1+ from __future__ import annotations
2+
3+ import os
4+ import re
5+ import sys
6+ import json
7+ import time
8+ import uuid
9+ import platform
10+ import threading
11+ from enum import Enum
12+ from typing import Any , TypeVar , Callable , cast
13+ from pathlib import Path
14+ from functools import wraps
15+
16+ import httpx
17+ import machineid
18+ from detect_agent import determine_agent
19+
20+ from together import __version__
21+ from together .lib .utils import log_debug
22+
23+ F = TypeVar ("F" , bound = Callable [..., Any ])
24+
25+ SESSION_ID = int (str (uuid .uuid4 ().int )[0 :13 ])
26+
27+ _ENV_TELEMETRY_OFF = frozenset ({"1" , "true" , "yes" })
28+ _ERROR_MESSAGE_MAX_LEN = 500
29+ _CONFIG_DIR_NAME = "together"
30+ _CONFIG_FILE_NAME = "cli.json"
31+
32+
33+ def telemetry_config_path () -> Path :
34+ if sys .platform == "win32" :
35+ appdata = os .environ .get ("APPDATA" )
36+ if appdata :
37+ return Path (appdata ) / "Together" / _CONFIG_FILE_NAME
38+ xdg = os .environ .get ("XDG_CONFIG_HOME" )
39+ if xdg :
40+ return Path (xdg ) / _CONFIG_DIR_NAME / _CONFIG_FILE_NAME
41+ return Path .home () / ".config" / _CONFIG_DIR_NAME / _CONFIG_FILE_NAME
42+
43+
44+ def load_telemetry_config () -> dict [str , Any ]:
45+ path = telemetry_config_path ()
46+ if not path .is_file ():
47+ return {}
48+ try :
49+ raw = path .read_text (encoding = "utf-8" )
50+ data = json .loads (raw )
51+ if not isinstance (data , dict ):
52+ return {}
53+ return cast (dict [str , Any ], data )
54+ except (OSError , json .JSONDecodeError ):
55+ return {}
56+
57+
58+ def save_telemetry_config (data : dict [str , Any ]) -> None :
59+ path = telemetry_config_path ()
60+ path .parent .mkdir (parents = True , exist_ok = True )
61+ tmp = path .with_name (path .name + ".tmp" )
62+ tmp .write_text (json .dumps (data , indent = 2 , sort_keys = True ) + "\n " , encoding = "utf-8" )
63+ tmp .replace (path )
64+ if sys .platform != "win32" :
65+ try :
66+ path .chmod (0o600 )
67+ except OSError :
68+ pass
69+
70+
71+ def is_tracking_enabled () -> bool :
72+ if _env_telemetry_disabled ():
73+ log_debug ("Analytics tracking disabled by environment variable" )
74+ return False
75+ if _config_telemetry_disabled ():
76+ log_debug ("Analytics tracking disabled by config file" )
77+ return False
78+ return True
79+
80+ class CliTrackingEvents (Enum ):
81+ CommandStarted = "cli_command_started"
82+ CommandCompleted = "cli_command_completed"
83+ CommandFailed = "cli_command_failed"
84+ CommandUserAborted = "cli_command_user_aborted"
85+ ApiRequest = "cli_command_api_request"
86+
87+
88+ def track_cli (event_name : CliTrackingEvents , args : dict [str , Any ]) -> None :
89+ """Track a CLI event. Non-Blocking."""
90+ if not is_tracking_enabled ():
91+ return
92+
93+ def send_event () -> None :
94+ analytics_api_env = os .getenv ("TOGETHER_TELEMETRY_API" )
95+ analytics_api = (
96+ analytics_api_env
97+ if analytics_api_env
98+ else "https://api.together.ai/together/gateway/pub/v1/httpRequest"
99+ )
100+
101+ try :
102+ agent_info = determine_agent ()
103+ agent_name = ""
104+ if agent_info ["agent" ]:
105+ agent_name = agent_info ["agent" ]["name" ]
106+
107+ log_debug ("Analytics event sending" , event_name = event_name .value , args = args )
108+
109+ payload = {
110+ "event_source" : "cli" ,
111+ "event_type" : event_name .value ,
112+ "event_properties" : {
113+ "is_ci" : os .getenv ("CI" ) is not None ,
114+ "is_agent" : agent_info ["is_agent" ],
115+ "agent_name" : agent_name ,
116+ "os" : platform .system (),
117+ "arch" : platform .machine () or "" ,
118+ ** args ,
119+ },
120+ "context" : {
121+ "session_id" : str (SESSION_ID ),
122+ "runtime" : {
123+ "name" : "together-cli" ,
124+ "version" : __version__ ,
125+ },
126+ },
127+ "identity" : {
128+ "device_id" : machineid .id ().lower (),
129+ },
130+ "event_options" : {
131+ "time" : int (time .time () * 1000 ),
132+ },
133+ }
134+ body = json .dumps (payload )
135+ with httpx .Client () as client :
136+ client .post (
137+ analytics_api ,
138+ headers = {
139+ "content-type" : "application/json" ,
140+ "user-agent" : f"together-cli:{ __version__ } " ,
141+ },
142+ content = body ,
143+ )
144+ except Exception as e :
145+ log_debug ("Error sending analytics event" , error = e )
146+
147+ threading .Thread (target = send_event ).start ()
148+
149+
150+ def auto_track_command (command : str ) -> Callable [[F ], F ]:
151+ """Decorator for click commands to automatically track CLI commands start/completion/failure."""
152+
153+ def decorator (f : F ) -> F :
154+ @wraps (f )
155+ def wrapper (* args : Any , ** kwargs : Any ) -> Any :
156+ track_cli (CliTrackingEvents .CommandStarted , {"command" : command , "arguments" : kwargs })
157+ try :
158+ result = f (* args , ** kwargs )
159+ except KeyboardInterrupt as e :
160+ track_cli (
161+ CliTrackingEvents .CommandUserAborted ,
162+ {"command" : command , "arguments" : kwargs },
163+ )
164+ raise e
165+
166+ except Exception as e :
167+ track_cli (
168+ CliTrackingEvents .CommandFailed ,
169+ {
170+ "command" : command ,
171+ "arguments" : kwargs ,
172+ "error" : _sanitize_cli_error_message (str (e )),
173+ },
174+ )
175+ raise e
176+
177+ track_cli (CliTrackingEvents .CommandCompleted , {"command" : command , "arguments" : kwargs })
178+ return result
179+
180+ return wrapper # type: ignore
181+
182+ return decorator # type: ignore
183+
184+
185+ def _sanitize_cli_error_message (msg : str ) -> str :
186+ s = msg .strip ()
187+ if len (s ) > _ERROR_MESSAGE_MAX_LEN :
188+ s = s [: _ERROR_MESSAGE_MAX_LEN ] + "…"
189+ s = re .sub (r"(?i)(bearer\s+)[A-Za-z0-9._\-/+]{20,}" , r"\1<redacted>" , s )
190+ s = re .sub (
191+ r"(?i)(api[_-]?key\s*[\"':=]\s*|api[_-]?key\s+)([A-Za-z0-9._\-]{20,})" ,
192+ r"\1<redacted>" ,
193+ s ,
194+ )
195+ s = re .sub (r"(?i)(Authorization:\s*)([^\s]+)" , r"\1<redacted>" , s )
196+ return s
197+
198+ def _env_telemetry_disabled () -> bool :
199+ v = os .getenv ("TOGETHER_TELEMETRY_DISABLED" , "" ).strip ().lower ()
200+ return v in _ENV_TELEMETRY_OFF
201+
202+
203+ def _config_telemetry_disabled () -> bool :
204+ return load_telemetry_config ().get ("telemetry_enabled" ) is False
0 commit comments