import functools import json import os import pathlib import platform import threading import urllib.error import urllib.request from typing import Any, TypedDict from langgraph_cli.constants import ( DEFAULT_CONFIG, DEFAULT_PORT, SUPABASE_PUBLIC_API_KEY, SUPABASE_URL, ) from langgraph_cli.version import __version__ class LogData(TypedDict): os: str os_version: str python_version: str cli_version: str cli_command: str params: dict[str, Any] def get_anonymized_params( kwargs: dict[str, Any], *, cli_command: str ) -> dict[str, bool | str]: params: dict[str, bool | str] = {} if cli_command == "deploy" and ( analytics_source := os.getenv("LANGGRAPH_CLI_ANALYTICS_SOURCE") ): params["source"] = analytics_source # anonymize params with values if config := kwargs.get("config"): if config != pathlib.Path(DEFAULT_CONFIG).resolve(): params["config"] = True if port := kwargs.get("port"): if port != DEFAULT_PORT: params["port"] = True if kwargs.get("docker_compose"): params["docker_compose"] = True if kwargs.get("debugger_port"): params["debugger_port"] = True if kwargs.get("postgres_uri"): params["postgres_uri"] = True # pick up exact values for boolean flags for boolean_param in ["recreate", "pull", "watch", "wait", "verbose"]: if kwargs.get(boolean_param): params[boolean_param] = kwargs[boolean_param] return params def log_data(data: LogData) -> None: headers = { "Content-Type": "application/json", "apikey": SUPABASE_PUBLIC_API_KEY, "User-Agent": "Mozilla/5.0", } supabase_url = SUPABASE_URL req = urllib.request.Request( f"{supabase_url}/rest/v1/logs", data=json.dumps(data).encode("utf-8"), headers=headers, method="POST", ) try: urllib.request.urlopen(req) except urllib.error.URLError: pass def log_command(func): @functools.wraps(func) def decorator(*args, **kwargs): if os.getenv("LANGGRAPH_CLI_NO_ANALYTICS") == "1": return func(*args, **kwargs) data = { "os": platform.system(), "os_version": platform.version(), "python_version": platform.python_version(), "cli_version": __version__, "cli_command": func.__name__, "params": get_anonymized_params(kwargs, cli_command=func.__name__), } background_thread = threading.Thread(target=log_data, args=(data,)) background_thread.start() return func(*args, **kwargs) return decorator