mirror of
https://github.com/Aider-AI/aider.git
synced 2025-05-29 16:54:59 +00:00
250 lines
7 KiB
Python
250 lines
7 KiB
Python
import json
|
|
import platform
|
|
import sys
|
|
import time
|
|
import uuid
|
|
from pathlib import Path
|
|
|
|
from mixpanel import MixpanelException
|
|
from posthog import Posthog
|
|
|
|
from aider import __version__
|
|
from aider.dump import dump # noqa: F401
|
|
from aider.models import model_info_manager
|
|
|
|
PERCENT = 10
|
|
|
|
|
|
def compute_hex_threshold(percent):
|
|
"""Convert percentage to 6-digit hex threshold.
|
|
|
|
Args:
|
|
percent: Percentage threshold (0-100)
|
|
|
|
Returns:
|
|
str: 6-digit hex threshold
|
|
"""
|
|
return format(int(0xFFFFFF * percent / 100), "06x")
|
|
|
|
|
|
def is_uuid_in_percentage(uuid_str, percent):
|
|
"""Check if a UUID string falls within the first X percent of the UUID space.
|
|
|
|
Args:
|
|
uuid_str: UUID string to test
|
|
percent: Percentage threshold (0-100)
|
|
|
|
Returns:
|
|
bool: True if UUID falls within the first X percent
|
|
"""
|
|
if not (0 <= percent <= 100):
|
|
raise ValueError("Percentage must be between 0 and 100")
|
|
|
|
if not uuid_str:
|
|
return False
|
|
|
|
# Convert percentage to hex threshold (1% = "04...", 10% = "1a...", etc)
|
|
# Using first 6 hex digits
|
|
if percent == 0:
|
|
return False
|
|
|
|
threshold = compute_hex_threshold(percent)
|
|
return uuid_str[:6] <= threshold
|
|
|
|
|
|
mixpanel_project_token = "6da9a43058a5d1b9f3353153921fb04d"
|
|
posthog_project_api_key = "phc_99T7muzafUMMZX15H8XePbMSreEUzahHbtWjy3l5Qbv"
|
|
posthog_host = "https://us.i.posthog.com"
|
|
|
|
|
|
class Analytics:
|
|
# providers
|
|
mp = None
|
|
ph = None
|
|
|
|
# saved
|
|
user_id = None
|
|
permanently_disable = None
|
|
asked_opt_in = None
|
|
|
|
# ephemeral
|
|
logfile = None
|
|
|
|
def __init__(self, logfile=None, permanently_disable=False):
|
|
self.logfile = logfile
|
|
self.get_or_create_uuid()
|
|
|
|
if self.permanently_disable or permanently_disable or not self.asked_opt_in:
|
|
self.disable(permanently_disable)
|
|
|
|
def enable(self):
|
|
if not self.user_id:
|
|
self.disable(False)
|
|
return
|
|
|
|
if self.permanently_disable:
|
|
self.disable(True)
|
|
return
|
|
|
|
if not self.asked_opt_in:
|
|
self.disable(False)
|
|
return
|
|
|
|
# self.mp = Mixpanel(mixpanel_project_token)
|
|
self.ph = Posthog(
|
|
project_api_key=posthog_project_api_key,
|
|
host=posthog_host,
|
|
on_error=self.posthog_error,
|
|
enable_exception_autocapture=True,
|
|
super_properties=self.get_system_info(), # Add system info to all events
|
|
)
|
|
|
|
def disable(self, permanently):
|
|
self.mp = None
|
|
self.ph = None
|
|
|
|
if permanently:
|
|
self.asked_opt_in = True
|
|
self.permanently_disable = True
|
|
self.save_data()
|
|
|
|
def need_to_ask(self, args_analytics):
|
|
if args_analytics is False:
|
|
return False
|
|
|
|
could_ask = not self.asked_opt_in and not self.permanently_disable
|
|
if not could_ask:
|
|
return False
|
|
|
|
if args_analytics is True:
|
|
return True
|
|
|
|
assert args_analytics is None, args_analytics
|
|
|
|
if not self.user_id:
|
|
return False
|
|
|
|
return is_uuid_in_percentage(self.user_id, PERCENT)
|
|
|
|
def get_data_file_path(self):
|
|
try:
|
|
data_file = Path.home() / ".aider" / "analytics.json"
|
|
data_file.parent.mkdir(parents=True, exist_ok=True)
|
|
return data_file
|
|
except OSError:
|
|
# If we can't create/access the directory, just disable analytics
|
|
self.disable(permanently=False)
|
|
return None
|
|
|
|
def get_or_create_uuid(self):
|
|
self.load_data()
|
|
if self.user_id:
|
|
return
|
|
|
|
self.user_id = str(uuid.uuid4())
|
|
self.save_data()
|
|
|
|
def load_data(self):
|
|
data_file = self.get_data_file_path()
|
|
if not data_file:
|
|
return
|
|
|
|
if data_file.exists():
|
|
try:
|
|
data = json.loads(data_file.read_text())
|
|
self.permanently_disable = data.get("permanently_disable")
|
|
self.user_id = data.get("uuid")
|
|
self.asked_opt_in = data.get("asked_opt_in", False)
|
|
except (json.decoder.JSONDecodeError, OSError):
|
|
self.disable(permanently=False)
|
|
|
|
def save_data(self):
|
|
data_file = self.get_data_file_path()
|
|
if not data_file:
|
|
return
|
|
|
|
data = dict(
|
|
uuid=self.user_id,
|
|
permanently_disable=self.permanently_disable,
|
|
asked_opt_in=self.asked_opt_in,
|
|
)
|
|
|
|
try:
|
|
data_file.write_text(json.dumps(data, indent=4))
|
|
except OSError:
|
|
# If we can't write the file, just disable analytics
|
|
self.disable(permanently=False)
|
|
|
|
def get_system_info(self):
|
|
return {
|
|
"python_version": sys.version.split()[0],
|
|
"os_platform": platform.system(),
|
|
"os_release": platform.release(),
|
|
"machine": platform.machine(),
|
|
"aider_version": __version__,
|
|
}
|
|
|
|
def _redact_model_name(self, model):
|
|
if not model:
|
|
return None
|
|
|
|
info = model_info_manager.get_model_from_cached_json_db(model.name)
|
|
if info:
|
|
return model.name
|
|
elif "/" in model.name:
|
|
return model.name.split("/")[0] + "/REDACTED"
|
|
return None
|
|
|
|
def posthog_error(self):
|
|
"""disable posthog if we get an error"""
|
|
print("X" * 100)
|
|
# https://github.com/PostHog/posthog-python/blob/9e1bb8c58afaa229da24c4fb576c08bb88a75752/posthog/consumer.py#L86
|
|
# https://github.com/Aider-AI/aider/issues/2532
|
|
self.ph = None
|
|
|
|
def event(self, event_name, main_model=None, **kwargs):
|
|
if not self.mp and not self.ph and not self.logfile:
|
|
return
|
|
|
|
properties = {}
|
|
|
|
if main_model:
|
|
properties["main_model"] = self._redact_model_name(main_model)
|
|
properties["weak_model"] = self._redact_model_name(main_model.weak_model)
|
|
properties["editor_model"] = self._redact_model_name(main_model.editor_model)
|
|
|
|
properties.update(kwargs)
|
|
|
|
# Handle numeric values
|
|
for key, value in properties.items():
|
|
if isinstance(value, (int, float)):
|
|
properties[key] = value
|
|
else:
|
|
properties[key] = str(value)
|
|
|
|
if self.mp:
|
|
try:
|
|
self.mp.track(self.user_id, event_name, dict(properties))
|
|
except MixpanelException:
|
|
self.mp = None # Disable mixpanel on connection errors
|
|
|
|
if self.ph:
|
|
self.ph.capture(self.user_id, event_name, dict(properties))
|
|
|
|
if self.logfile:
|
|
log_entry = {
|
|
"event": event_name,
|
|
"properties": properties,
|
|
"user_id": self.user_id,
|
|
"time": int(time.time()),
|
|
}
|
|
try:
|
|
with open(self.logfile, "a") as f:
|
|
json.dump(log_entry, f)
|
|
f.write("\n")
|
|
except OSError:
|
|
pass # Ignore OS errors when writing to logfile
|
|
|
|
|
|
if __name__ == "__main__":
|
|
dump(compute_hex_threshold(PERCENT))
|