chore: import upstream snapshot with attribution
CI: cua-driver distro-compat matrix / debian:12 (glibc 2.36) (push) Has been cancelled
CI: SPDX Headers / Check SPDX headers (warn-only) (push) Has been cancelled
CD: Docs MCP Server / build (linux/amd64) (push) Has been cancelled
CD: Docs MCP Server / build (linux/arm64) (push) Has been cancelled
CD: Docs MCP Server / merge (push) Has been cancelled
CI: cua-driver distro-compat matrix / Resolve release version (push) Has been cancelled
CI: cua-driver distro-compat matrix / fedora:41 (glibc 2.40) (push) Has been cancelled
CI: cua-driver distro-compat matrix / rockylinux:9 (glibc 2.34) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:22.04 (glibc 2.35) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:24.04 (glibc 2.39) (push) Has been cancelled
CI: cua-driver distro-compat matrix / Distro compat summary (push) Has been cancelled
CI: Rust Linux unit / Rust Linux unit and compile (push) Has been cancelled
CI: Rust Windows unit / Rust Windows unit and compile (push) Has been cancelled
CI: Nix Linux Rust source / Nix / compositor build (push) Has been cancelled
CI: Nix Linux Rust source / Nix / driver package (push) Has been cancelled
CI: Nix Linux Rust source / Nix / Rust unit tests (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:03:19 +08:00
commit 91e75e620b
3227 changed files with 1307078 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
[bumpversion]
current_version = 0.3.1
commit = True
tag = True
tag_name = core-v{new_version}
message = Bump cua-core to v{new_version}
[bumpversion:file:pyproject.toml]
search = version = "{current_version}"
replace = version = "{new_version}"
+29
View File
@@ -0,0 +1,29 @@
<div align="center">
<h1>
<div class="image-wrapper" style="display: inline-block;">
<picture>
<source media="(prefers-color-scheme: dark)" alt="logo" height="150" srcset="https://raw.githubusercontent.com/trycua/cua/main/img/logo_white.svg" style="display: block; margin: auto;">
<source media="(prefers-color-scheme: light)" alt="logo" height="150" srcset="https://raw.githubusercontent.com/trycua/cua/main/img/logo_black.svg" style="display: block; margin: auto;">
<img alt="Shows my svg">
</picture>
</div>
[![Python](https://img.shields.io/badge/Python-333333?logo=python&logoColor=white&labelColor=333333)](#)
[![macOS](https://img.shields.io/badge/macOS-000000?logo=apple&logoColor=F0F0F0)](#)
[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?&logo=discord&logoColor=white)](https://discord.com/invite/mVnXXpdE85)
[![PyPI](https://img.shields.io/pypi/v/cua-core?color=333333)](https://pypi.org/project/cua-core/)
</h1>
</div>
**Cua Core** provides essential shared functionality and utilities used across the Cua ecosystem:
- Privacy-focused telemetry system for transparent usage analytics
- Common helper functions and utilities used by other Cua packages
- Core infrastructure components shared between modules
## Installation
```bash
pip install cua-core
```
+5
View File
@@ -0,0 +1,5 @@
"""Core functionality shared across Cua components."""
__version__ = "0.1.8"
from cua_core.http import CUA_CLIENT_VERSION_HEADER, cua_version_headers
+35
View File
@@ -0,0 +1,35 @@
"""Shared HTTP utilities for CUA SDK requests."""
from functools import lru_cache
CUA_CLIENT_VERSION_HEADER = "X-Cua-Client-Version"
# CUA packages whose versions are included in the header value.
_CUA_PACKAGES = ("cua-agent", "cua-computer", "cua-core")
@lru_cache(maxsize=1)
def _build_version_string() -> str:
"""Return a composite version string like ``agent:0.4.0 computer:0.1.0 core:0.1.8``."""
from importlib.metadata import PackageNotFoundError, version
parts: list[str] = []
for pkg in _CUA_PACKAGES:
try:
short = pkg.removeprefix("cua-")
parts.append(f"{short}:{version(pkg)}")
except PackageNotFoundError:
continue
return " ".join(parts)
def cua_version_headers() -> dict[str, str]:
"""Return headers dict containing the CUA client version header.
Only installed CUA packages are included. If none are found the dict is
empty so it is always safe to unpack with ``**cua_version_headers()``.
"""
value = _build_version_string()
if not value:
return {}
return {CUA_CLIENT_VERSION_HEADER: value}
@@ -0,0 +1,38 @@
"""This module provides the core telemetry functionality for Cua libraries.
It provides a low-overhead way to collect anonymous usage data via PostHog
and operational metrics via OpenTelemetry.
"""
# OpenTelemetry instrumentation for Four Golden Signals
from cua_core.telemetry.otel import (
create_span,
instrument_async,
instrument_sync,
is_otel_enabled,
record_error,
record_operation,
record_tokens,
track_concurrent,
)
from cua_core.telemetry.posthog import (
destroy_telemetry_client,
is_telemetry_enabled,
record_event,
)
__all__ = [
# PostHog (product analytics)
"record_event",
"is_telemetry_enabled",
"destroy_telemetry_client",
# OpenTelemetry (operational metrics)
"is_otel_enabled",
"record_operation",
"record_error",
"record_tokens",
"track_concurrent",
"create_span",
"instrument_async",
"instrument_sync",
]
+537
View File
@@ -0,0 +1,537 @@
"""OpenTelemetry instrumentation for CUA SDK.
Provides metrics and tracing for the Four Golden Signals:
- Latency: Operation duration histograms
- Traffic: Operation counters
- Errors: Error counters
- Saturation: Concurrent operation gauges
"""
from __future__ import annotations
import atexit
import logging
import os
import time
from contextlib import contextmanager
from functools import wraps
from threading import Lock
from typing import Any, Callable, Dict, Generator, Optional, TypeVar, Union
logger = logging.getLogger("core.telemetry.otel")
# Type vars for decorator
F = TypeVar("F", bound=Callable[..., Any])
# Default OTEL endpoint
DEFAULT_OTEL_ENDPOINT = "https://otel.cua.ai"
# Lazy initialization state
_initialized = False
_init_failed = False
_init_lock = Lock()
# OTEL components (lazily initialized)
_meter: Optional[Any] = None
_tracer: Optional[Any] = None
_meter_provider: Optional[Any] = None
_tracer_provider: Optional[Any] = None
# Metrics (lazily initialized)
_operation_duration: Optional[Any] = None # Histogram
_operations_total: Optional[Any] = None # Counter
_errors_total: Optional[Any] = None # Counter
_concurrent_operations: Optional[Any] = None # UpDownCounter
_tokens_total: Optional[Any] = None # Counter
def is_otel_enabled() -> bool:
"""Check if OpenTelemetry is enabled.
Canonical opt-out: ``CUA_TELEMETRY_ENABLED=false``.
``CUA_TELEMETRY_DISABLED`` is deprecated — a warning is emitted on first
use and the value is honoured for backwards compatibility.
"""
import warnings
disabled_val = os.environ.get("CUA_TELEMETRY_DISABLED", "")
if disabled_val:
warnings.warn(
"CUA_TELEMETRY_DISABLED is deprecated. " "Use CUA_TELEMETRY_ENABLED=false instead.",
DeprecationWarning,
stacklevel=2,
)
if disabled_val.lower() in {"1", "true", "yes", "on"}:
return False
return os.environ.get("CUA_TELEMETRY_ENABLED", "true").lower() in {
"1",
"true",
"yes",
"on",
}
def _get_otel_endpoint() -> str:
"""Get the OTLP endpoint URL."""
return os.environ.get("CUA_OTEL_ENDPOINT", DEFAULT_OTEL_ENDPOINT)
def _get_service_name() -> str:
"""Get the service name for OTEL."""
return os.environ.get("CUA_OTEL_SERVICE_NAME", "cua-sdk")
def _initialize_otel() -> bool:
"""Initialize OpenTelemetry components.
Returns True if initialization succeeded, False otherwise.
Thread-safe via lock.
"""
global _initialized, _init_failed, _meter, _tracer, _meter_provider, _tracer_provider
global _operation_duration, _operations_total, _errors_total
global _concurrent_operations, _tokens_total
if _initialized:
return True
if _init_failed:
return False
with _init_lock:
# Double-check after acquiring lock
if _initialized:
return True
if _init_failed:
return False
if not is_otel_enabled():
logger.debug("OpenTelemetry disabled via CUA_TELEMETRY_DISABLED")
return False
try:
# Import OTEL packages lazily
from opentelemetry import metrics, trace
from opentelemetry.exporter.otlp.proto.http.metric_exporter import (
OTLPMetricExporter,
)
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
OTLPSpanExporter,
)
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
# Create resource with service info
resource = Resource.create(
{
"service.name": _get_service_name(),
"service.version": _get_sdk_version(),
}
)
endpoint = _get_otel_endpoint()
# Set up metrics
metric_exporter = OTLPMetricExporter(
endpoint=f"{endpoint}/v1/metrics",
)
metric_reader = PeriodicExportingMetricReader(
metric_exporter,
export_interval_millis=60000, # Export every 60 seconds
)
_meter_provider = MeterProvider(
resource=resource,
metric_readers=[metric_reader],
)
metrics.set_meter_provider(_meter_provider)
_meter = metrics.get_meter("cua-sdk", _get_sdk_version())
# Set up tracing
trace_exporter = OTLPSpanExporter(
endpoint=f"{endpoint}/v1/traces",
)
_tracer_provider = TracerProvider(resource=resource)
_tracer_provider.add_span_processor(BatchSpanProcessor(trace_exporter))
trace.set_tracer_provider(_tracer_provider)
_tracer = trace.get_tracer("cua-sdk", _get_sdk_version())
# Create metrics instruments
_operation_duration = _meter.create_histogram(
name="cua_sdk_operation_duration_seconds",
description="Duration of SDK operations in seconds",
unit="s",
)
_operations_total = _meter.create_counter(
name="cua_sdk_operations_total",
description="Total number of SDK operations",
unit="1",
)
_errors_total = _meter.create_counter(
name="cua_sdk_errors_total",
description="Total number of SDK errors",
unit="1",
)
_concurrent_operations = _meter.create_up_down_counter(
name="cua_sdk_concurrent_operations",
description="Number of concurrent SDK operations",
unit="1",
)
_tokens_total = _meter.create_counter(
name="cua_sdk_tokens_total",
description="Total tokens consumed",
unit="1",
)
# Register shutdown handler
atexit.register(_shutdown_otel)
_initialized = True
logger.info(f"OpenTelemetry initialized with endpoint: {endpoint}")
return True
except ImportError as e:
_init_failed = True
logger.debug(
f"OpenTelemetry packages not installed: {e}. "
"Install with: pip install opentelemetry-api opentelemetry-sdk "
"opentelemetry-exporter-otlp-proto-http"
)
return False
except Exception as e:
_init_failed = True
logger.warning(f"Failed to initialize OpenTelemetry: {e}")
return False
def _shutdown_otel() -> None:
"""Shutdown OpenTelemetry providers gracefully."""
global _meter_provider, _tracer_provider
try:
if _meter_provider is not None:
_meter_provider.shutdown()
if _tracer_provider is not None:
_tracer_provider.shutdown()
logger.debug("OpenTelemetry shutdown complete")
except Exception as e:
logger.debug(f"Error during OpenTelemetry shutdown: {e}")
def _get_sdk_version() -> str:
"""Get the CUA SDK version."""
try:
from cua_core import __version__
return __version__
except ImportError:
return "unknown"
# --- Public API ---
def record_operation(
operation: str,
duration_seconds: float,
status: str = "success",
model: Optional[str] = None,
os_type: Optional[str] = None,
**extra_attributes: Any,
) -> None:
"""Record an operation metric (latency + traffic).
Args:
operation: Operation name (e.g., "agent.run", "computer.action.click")
duration_seconds: Duration of the operation in seconds
status: Operation status ("success" or "error")
model: Model name if applicable
os_type: OS type if applicable
**extra_attributes: Additional attributes to record
"""
if not _initialize_otel():
return
attributes: Dict[str, str] = {
"operation": operation,
"status": status,
}
if model:
attributes["model"] = model
if os_type:
attributes["os_type"] = os_type
for key, value in extra_attributes.items():
if value is not None:
attributes[key] = str(value)
try:
if _operation_duration is not None:
_operation_duration.record(duration_seconds, attributes)
if _operations_total is not None:
_operations_total.add(1, attributes)
except Exception as e:
logger.debug(f"Failed to record operation metric: {e}")
def record_error(
error_type: str,
operation: str,
model: Optional[str] = None,
**extra_attributes: Any,
) -> None:
"""Record an error metric.
Args:
error_type: Type of error (e.g., "api_error", "timeout", "computer_error")
operation: Operation that failed
model: Model name if applicable
**extra_attributes: Additional attributes to record
"""
if not _initialize_otel():
return
attributes: Dict[str, str] = {
"error_type": error_type,
"operation": operation,
}
if model:
attributes["model"] = model
for key, value in extra_attributes.items():
if value is not None:
attributes[key] = str(value)
try:
if _errors_total is not None:
_errors_total.add(1, attributes)
except Exception as e:
logger.debug(f"Failed to record error metric: {e}")
def record_tokens(
prompt_tokens: int = 0,
completion_tokens: int = 0,
model: Optional[str] = None,
) -> None:
"""Record token usage metrics.
Args:
prompt_tokens: Number of prompt tokens
completion_tokens: Number of completion tokens
model: Model name
"""
if not _initialize_otel():
return
try:
if _tokens_total is not None:
if prompt_tokens > 0:
_tokens_total.add(
prompt_tokens,
{"token_type": "prompt", "model": model or "unknown"},
)
if completion_tokens > 0:
_tokens_total.add(
completion_tokens,
{"token_type": "completion", "model": model or "unknown"},
)
except Exception as e:
logger.debug(f"Failed to record token metric: {e}")
@contextmanager
def track_concurrent(operation_type: str) -> Generator[None, None, None]:
"""Context manager to track concurrent operations.
Args:
operation_type: Type of operation (e.g., "sessions", "runs")
Example:
with track_concurrent("agent_sessions"):
# session is active
pass
"""
if not _initialize_otel():
yield
return
attributes = {"operation_type": operation_type}
try:
if _concurrent_operations is not None:
_concurrent_operations.add(1, attributes)
except Exception as e:
logger.debug(f"Failed to increment concurrent counter: {e}")
try:
yield
finally:
try:
if _concurrent_operations is not None:
_concurrent_operations.add(-1, attributes)
except Exception as e:
logger.debug(f"Failed to decrement concurrent counter: {e}")
@contextmanager
def create_span(
name: str,
attributes: Optional[Dict[str, Any]] = None,
) -> Generator[Any, None, None]:
"""Create a trace span context.
Args:
name: Span name
attributes: Span attributes
Yields:
The span object (or None if tracing disabled)
Example:
with create_span("agent.run", {"model": "claude-3"}) as span:
# do work
if span:
span.set_attribute("steps", 5)
"""
if not _initialize_otel() or _tracer is None:
yield None
return
_yielded = False
try:
with _tracer.start_as_current_span(name, attributes=attributes) as span:
_yielded = True
yield span
except Exception as e:
if _yielded:
raise
logger.debug(f"Failed to create span: {e}")
yield None
def instrument_async(
operation: str,
model_attr: Optional[str] = None,
os_type_attr: Optional[str] = None,
) -> Callable[[F], F]:
"""Decorator to instrument an async function.
Records duration, success/error status, and creates a trace span.
Args:
operation: Operation name for metrics
model_attr: Attribute name to extract model from kwargs
os_type_attr: Attribute name to extract os_type from kwargs
Example:
@instrument_async("agent.run", model_attr="model")
async def run(self, prompt: str, model: str = "claude-3"):
...
"""
def decorator(func: F) -> F:
@wraps(func)
async def wrapper(*args: Any, **kwargs: Any) -> Any:
if not is_otel_enabled():
return await func(*args, **kwargs)
model = kwargs.get(model_attr) if model_attr else None
os_type = kwargs.get(os_type_attr) if os_type_attr else None
start_time = time.perf_counter()
status = "success"
error_type = None
with create_span(operation, {"model": model, "os_type": os_type}):
try:
result = await func(*args, **kwargs)
return result
except Exception as e:
status = "error"
error_type = type(e).__name__
raise
finally:
duration = time.perf_counter() - start_time
record_operation(
operation=operation,
duration_seconds=duration,
status=status,
model=model,
os_type=os_type,
)
if error_type:
record_error(
error_type=error_type,
operation=operation,
model=model,
)
return wrapper # type: ignore
return decorator
def instrument_sync(
operation: str,
model_attr: Optional[str] = None,
os_type_attr: Optional[str] = None,
) -> Callable[[F], F]:
"""Decorator to instrument a sync function.
Records duration, success/error status, and creates a trace span.
Args:
operation: Operation name for metrics
model_attr: Attribute name to extract model from kwargs
os_type_attr: Attribute name to extract os_type from kwargs
Example:
@instrument_sync("computer.screenshot")
def screenshot(self):
...
"""
def decorator(func: F) -> F:
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
if not is_otel_enabled():
return func(*args, **kwargs)
model = kwargs.get(model_attr) if model_attr else None
os_type = kwargs.get(os_type_attr) if os_type_attr else None
start_time = time.perf_counter()
status = "success"
error_type = None
with create_span(operation, {"model": model, "os_type": os_type}):
try:
result = func(*args, **kwargs)
return result
except Exception as e:
status = "error"
error_type = type(e).__name__
raise
finally:
duration = time.perf_counter() - start_time
record_operation(
operation=operation,
duration_seconds=duration,
status=status,
model=model,
os_type=os_type,
)
if error_type:
record_error(
error_type=error_type,
operation=operation,
model=model,
)
return wrapper # type: ignore
return decorator
@@ -0,0 +1,250 @@
"""Telemetry client using PostHog for collecting anonymous usage data."""
from __future__ import annotations
import logging
import os
import sys
import uuid
from pathlib import Path
from typing import Any, Dict, List, Optional
import posthog
from cua_core import __version__
logger = logging.getLogger("core.telemetry")
# Public PostHog config for anonymous telemetry
# These values are intentionally public and meant for anonymous telemetry only
# https://posthog.com/docs/product-analytics/troubleshooting#is-it-ok-for-my-api-key-to-be-exposed-and-public
PUBLIC_POSTHOG_API_KEY = "phc_eSkLnbLxsnYFaXksif1ksbrNzYlJShr35miFLDppF14"
PUBLIC_POSTHOG_HOST = "https://eu.i.posthog.com"
class PostHogTelemetryClient:
"""Collects and reports telemetry data via PostHog."""
# Global singleton (class-managed)
_singleton: Optional["PostHogTelemetryClient"] = None
def __init__(self):
"""Initialize PostHog telemetry client."""
self.installation_id = self._get_or_create_installation_id()
self.initialized = False
self.queued_events: List[Dict[str, Any]] = []
# Log telemetry status on startup
if self.is_telemetry_enabled():
logger.info("Telemetry enabled")
# Initialize PostHog client if config is available
self._initialize_posthog()
else:
logger.info("Telemetry disabled")
@classmethod
def is_telemetry_enabled(cls) -> bool:
"""True if telemetry is currently active for this process.
Canonical opt-out: ``CUA_TELEMETRY_ENABLED=false``.
``CUA_TELEMETRY_DISABLED`` is deprecated — a warning is emitted on
first use and the value is honoured for backwards compatibility.
"""
# Deprecated env var: CUA_TELEMETRY_DISABLED
disabled_val = os.environ.get("CUA_TELEMETRY_DISABLED", "")
if disabled_val:
import warnings
warnings.warn(
"CUA_TELEMETRY_DISABLED is deprecated. " "Use CUA_TELEMETRY_ENABLED=false instead.",
DeprecationWarning,
stacklevel=2,
)
if disabled_val.lower() in {"1", "true", "yes", "on"}:
return False
return os.environ.get("CUA_TELEMETRY_ENABLED", "true").lower() in {
"1",
"true",
"yes",
"on",
}
def _get_or_create_installation_id(self) -> str:
"""Get or create a unique installation ID that persists across runs.
Stored in ``~/.config/cua/installation_id`` (XDG-compliant) so that
the ID survives package upgrades and is shared across venvs.
This ID is not tied to any personal information.
"""
try:
config_dir = Path.home() / ".config" / "cua"
config_dir.mkdir(parents=True, exist_ok=True)
id_file = config_dir / "installation_id"
# Try to read existing ID
if id_file.exists():
try:
stored_id = id_file.read_text().strip()
if stored_id: # Make sure it's not empty
logger.debug(f"Using existing installation ID: {stored_id}")
return stored_id
except Exception as e:
logger.debug(f"Error reading installation ID file: {e}")
# Create new ID
new_id = str(uuid.uuid4())
try:
id_file.write_text(new_id)
logger.debug(f"Created new installation ID: {new_id}")
return new_id
except Exception as e:
logger.warning(f"Could not write installation ID: {e}")
except Exception as e:
logger.warning(f"Error accessing core module directory: {e}")
# Last resort: Create a new in-memory ID
logger.warning("Using random installation ID (will not persist across runs)")
return str(uuid.uuid4())
def _initialize_posthog(self) -> bool:
"""Initialize the PostHog client with configuration.
Returns:
bool: True if initialized successfully, False otherwise
"""
if self.initialized:
return True
try:
# Allow overrides from environment for testing/region control
posthog.api_key = PUBLIC_POSTHOG_API_KEY
posthog.host = PUBLIC_POSTHOG_HOST
# Configure the client
posthog.debug = os.environ.get("CUA_TELEMETRY_DEBUG", "").lower() == "on"
# Log telemetry status
logger.info(
f"Initializing PostHog telemetry with installation ID: {self.installation_id}"
)
if posthog.debug:
logger.debug(f"PostHog API Key: {posthog.api_key}")
logger.debug(f"PostHog Host: {posthog.host}")
# Identify this installation
self._identify()
# Process any queued events
for event in self.queued_events:
posthog.capture(
distinct_id=self.installation_id,
event=event["event"],
properties=event["properties"],
)
self.queued_events = []
self.initialized = True
return True
except Exception as e:
logger.warning(f"Failed to initialize PostHog: {e}")
return False
def _identify(self) -> None:
"""Set up user properties for the current installation with PostHog."""
try:
properties = {
"version": __version__,
"is_ci": "CI" in os.environ,
"os": os.name,
"python_version": sys.version.split()[0],
}
logger.debug(
f"Setting up PostHog user properties for: {self.installation_id} with properties: {properties}"
)
# In the Python SDK, we capture an identification event instead of calling identify()
posthog.capture(
distinct_id=self.installation_id, event="$identify", properties={"$set": properties}
)
logger.info(f"Set up PostHog user properties for installation: {self.installation_id}")
except Exception as e:
logger.warning(f"Failed to set up PostHog user properties: {e}")
def record_event(self, event_name: str, properties: Optional[Dict[str, Any]] = None) -> None:
"""Record an event with optional properties.
Args:
event_name: Name of the event
properties: Event properties (must not contain sensitive data)
"""
# Respect runtime telemetry opt-out.
if not self.is_telemetry_enabled():
logger.debug("Telemetry disabled; event not recorded.")
return
event_properties = {"version": __version__, **(properties or {})}
logger.info(f"Recording event: {event_name} with properties: {event_properties}")
if self.initialized:
try:
posthog.capture(
distinct_id=self.installation_id, event=event_name, properties=event_properties
)
logger.info(f"Sent event to PostHog: {event_name}")
# Flush immediately to ensure delivery
posthog.flush()
except Exception as e:
logger.warning(f"Failed to send event to PostHog: {e}")
else:
# Queue the event for later
logger.info(f"PostHog not initialized, queuing event for later: {event_name}")
self.queued_events.append({"event": event_name, "properties": event_properties})
# Try to initialize now if not already
initialize_result = self._initialize_posthog()
logger.info(f"Attempted to initialize PostHog: {initialize_result}")
def flush(self) -> bool:
"""Flush any pending events to PostHog.
Returns:
bool: True if successful, False otherwise
"""
if not self.initialized and not self._initialize_posthog():
return False
try:
posthog.flush()
return True
except Exception as e:
logger.debug(f"Failed to flush PostHog events: {e}")
return False
@classmethod
def get_client(cls) -> "PostHogTelemetryClient":
"""Return the global PostHogTelemetryClient instance, creating it if needed."""
if cls._singleton is None:
cls._singleton = cls()
return cls._singleton
@classmethod
def destroy_client(cls) -> None:
"""Destroy the global PostHogTelemetryClient instance."""
cls._singleton = None
def destroy_telemetry_client() -> None:
"""Destroy the global PostHogTelemetryClient instance (class-managed)."""
PostHogTelemetryClient.destroy_client()
def is_telemetry_enabled() -> bool:
return PostHogTelemetryClient.is_telemetry_enabled()
def record_event(event_name: str, properties: Optional[Dict[str, Any]] | None = None) -> None:
"""Record an arbitrary PostHog event."""
PostHogTelemetryClient.get_client().record_event(event_name, properties or {})
+2
View File
@@ -0,0 +1,2 @@
[virtualenvs]
in-project = true
+47
View File
@@ -0,0 +1,47 @@
[build-system]
requires = ["pdm-backend"]
build-backend = "pdm.backend"
[project]
name = "cua-core"
version = "0.3.1"
description = "Core functionality for Cua including telemetry and shared utilities"
readme = "README.md"
authors = [
{ name = "TryCua", email = "gh@trycua.com" }
]
dependencies = [
"posthog>=3.20.0",
]
requires-python = ">=3.11,<3.14"
[project.optional-dependencies]
# OpenTelemetry for operational metrics (Four Golden Signals)
otel = [
"opentelemetry-api>=1.20.0",
"opentelemetry-sdk>=1.20.0",
"opentelemetry-exporter-otlp-proto-http>=1.20.0",
]
# All telemetry features
telemetry = [
"opentelemetry-api>=1.20.0",
"opentelemetry-sdk>=1.20.0",
"opentelemetry-exporter-otlp-proto-http>=1.20.0",
]
[tool.pdm]
distribution = true
[tool.pdm.build]
includes = ["cua_core/"]
source-includes = ["tests/", "README.md", "LICENSE"]
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
python_files = "test_*.py"
[dependency-groups]
dev = [
"pytest>=8.3.5",
]
+43
View File
@@ -0,0 +1,43 @@
"""Pytest configuration and shared fixtures for core package tests.
This file contains shared fixtures and configuration for all core tests.
Following SRP: This file ONLY handles test setup/teardown.
"""
from unittest.mock import AsyncMock, Mock, patch
import pytest
@pytest.fixture
def mock_httpx_client():
"""Mock httpx.AsyncClient for API calls.
Use this fixture to avoid making real HTTP requests during tests.
"""
with patch("httpx.AsyncClient") as mock_client:
mock_instance = AsyncMock()
mock_client.return_value.__aenter__.return_value = mock_instance
yield mock_instance
@pytest.fixture
def mock_posthog():
"""Mock PostHog client for telemetry tests.
Use this fixture to avoid sending real telemetry during tests.
"""
with patch("posthog.Posthog") as mock_ph:
mock_instance = Mock()
mock_ph.return_value = mock_instance
yield mock_instance
@pytest.fixture
def disable_telemetry(monkeypatch):
"""Disable telemetry for tests that don't need it.
Use this fixture to ensure telemetry is disabled during tests.
"""
monkeypatch.setenv("CUA_TELEMETRY_ENABLED", "false")
yield
+247
View File
@@ -0,0 +1,247 @@
"""Unit tests for core telemetry functionality.
This file tests ONLY telemetry logic, following SRP.
All external dependencies (PostHog, file system) are mocked.
"""
import os
from pathlib import Path
from unittest.mock import MagicMock, Mock, mock_open, patch
import pytest
class TestTelemetryEnabled:
"""Test telemetry enable/disable logic (SRP: Only tests enable/disable)."""
def test_telemetry_enabled_by_default(self, monkeypatch):
"""Test that telemetry is enabled by default."""
# Remove any environment variables that might affect the test
monkeypatch.delenv("CUA_TELEMETRY", raising=False)
monkeypatch.delenv("CUA_TELEMETRY_ENABLED", raising=False)
monkeypatch.delenv("CUA_TELEMETRY_DISABLED", raising=False)
from cua_core.telemetry import is_telemetry_enabled
assert is_telemetry_enabled() is True
def test_telemetry_disabled_with_flag(self, monkeypatch):
"""Test that telemetry can be disabled with CUA_TELEMETRY_ENABLED=false."""
monkeypatch.setenv("CUA_TELEMETRY_ENABLED", "false")
from cua_core.telemetry import is_telemetry_enabled
assert is_telemetry_enabled() is False
@pytest.mark.parametrize("value", ["0", "false", "no", "off"])
def test_telemetry_disabled_with_various_values(self, monkeypatch, value):
"""Test that telemetry respects various disable values."""
monkeypatch.setenv("CUA_TELEMETRY_ENABLED", value)
from cua_core.telemetry import is_telemetry_enabled
assert is_telemetry_enabled() is False
@pytest.mark.parametrize("value", ["1", "true", "yes", "on"])
def test_telemetry_enabled_with_various_values(self, monkeypatch, value):
"""Test that telemetry respects various enable values."""
monkeypatch.delenv("CUA_TELEMETRY_DISABLED", raising=False)
monkeypatch.setenv("CUA_TELEMETRY_ENABLED", value)
from cua_core.telemetry import is_telemetry_enabled
assert is_telemetry_enabled() is True
class TestPostHogTelemetryClient:
"""Test PostHogTelemetryClient class (SRP: Only tests client logic)."""
@patch("cua_core.telemetry.posthog.posthog")
@patch("cua_core.telemetry.posthog.Path")
def test_client_initialization(self, mock_path, mock_posthog, disable_telemetry):
"""Test that client initializes correctly."""
from cua_core.telemetry.posthog import PostHogTelemetryClient
# Mock the storage directory
mock_storage_dir = MagicMock()
mock_storage_dir.exists.return_value = False
mock_path.return_value.parent.parent = MagicMock()
mock_path.return_value.parent.parent.__truediv__.return_value = mock_storage_dir
# Reset singleton
PostHogTelemetryClient.destroy_client()
client = PostHogTelemetryClient()
assert client is not None
assert hasattr(client, "installation_id")
assert hasattr(client, "initialized")
assert hasattr(client, "queued_events")
@patch("cua_core.telemetry.posthog.posthog")
@patch("cua_core.telemetry.posthog.Path")
def test_installation_id_generation(self, mock_path, mock_posthog, disable_telemetry):
"""Test that installation ID is generated if not exists."""
from cua_core.telemetry.posthog import PostHogTelemetryClient
# Mock file system: Path.home() / ".config" / "cua" / "installation_id"
mock_id_file = MagicMock()
mock_id_file.exists.return_value = False
mock_config_dir = MagicMock()
mock_config_dir.__truediv__.return_value = mock_id_file
mock_path.home.return_value.__truediv__.return_value.__truediv__.return_value = (
mock_config_dir
)
# Reset singleton
PostHogTelemetryClient.destroy_client()
client = PostHogTelemetryClient()
# Should have generated a new UUID
assert client.installation_id is not None
assert len(client.installation_id) == 36 # UUID format
@patch("cua_core.telemetry.posthog.posthog")
@patch("cua_core.telemetry.posthog.Path")
def test_installation_id_persistence(self, mock_path, mock_posthog, disable_telemetry):
"""Test that installation ID is read from file if exists."""
from cua_core.telemetry.posthog import PostHogTelemetryClient
existing_id = "test-installation-id-123"
# Mock file system: Path.home() / ".config" / "cua" / "installation_id"
mock_id_file = MagicMock()
mock_id_file.exists.return_value = True
mock_id_file.read_text.return_value.strip.return_value = existing_id
mock_config_dir = MagicMock()
mock_config_dir.__truediv__.return_value = mock_id_file
mock_path.home.return_value.__truediv__.return_value.__truediv__.return_value = (
mock_config_dir
)
# Reset singleton
PostHogTelemetryClient.destroy_client()
client = PostHogTelemetryClient()
assert client.installation_id == existing_id
@patch("cua_core.telemetry.posthog.posthog")
@patch("cua_core.telemetry.posthog.Path")
def test_record_event_when_disabled(self, mock_path, mock_posthog, monkeypatch):
"""Test that events are not recorded when telemetry is disabled."""
from cua_core.telemetry.posthog import PostHogTelemetryClient
# Disable telemetry explicitly using the correct environment variable
monkeypatch.setenv("CUA_TELEMETRY_ENABLED", "false")
# Mock file system
mock_storage_dir = MagicMock()
mock_storage_dir.exists.return_value = False
mock_path.return_value.parent.parent = MagicMock()
mock_path.return_value.parent.parent.__truediv__.return_value = mock_storage_dir
# Reset singleton
PostHogTelemetryClient.destroy_client()
client = PostHogTelemetryClient()
client.record_event("test_event", {"key": "value"})
# PostHog capture should not be called at all when telemetry is disabled
mock_posthog.capture.assert_not_called()
@patch("cua_core.telemetry.posthog.posthog")
@patch("cua_core.telemetry.posthog.Path")
def test_record_event_when_enabled(self, mock_path, mock_posthog, monkeypatch):
"""Test that events are recorded when telemetry is enabled."""
from cua_core.telemetry.posthog import PostHogTelemetryClient
# Enable telemetry
monkeypatch.delenv("CUA_TELEMETRY_DISABLED", raising=False)
monkeypatch.setenv("CUA_TELEMETRY_ENABLED", "true")
# Mock file system
mock_storage_dir = MagicMock()
mock_storage_dir.exists.return_value = False
mock_path.return_value.parent.parent = MagicMock()
mock_path.return_value.parent.parent.__truediv__.return_value = mock_storage_dir
# Reset singleton
PostHogTelemetryClient.destroy_client()
client = PostHogTelemetryClient()
client.initialized = True # Pretend it's initialized
event_name = "test_event"
event_props = {"key": "value"}
client.record_event(event_name, event_props)
# PostHog capture should be called
assert mock_posthog.capture.call_count >= 1
@patch("cua_core.telemetry.posthog.posthog")
@patch("cua_core.telemetry.posthog.Path")
def test_singleton_pattern(self, mock_path, mock_posthog, disable_telemetry):
"""Test that get_client returns the same instance."""
from cua_core.telemetry.posthog import PostHogTelemetryClient
# Mock file system
mock_storage_dir = MagicMock()
mock_storage_dir.exists.return_value = False
mock_path.return_value.parent.parent = MagicMock()
mock_path.return_value.parent.parent.__truediv__.return_value = mock_storage_dir
# Reset singleton
PostHogTelemetryClient.destroy_client()
client1 = PostHogTelemetryClient.get_client()
client2 = PostHogTelemetryClient.get_client()
assert client1 is client2
class TestRecordEvent:
"""Test the public record_event function (SRP: Only tests public API)."""
@patch("cua_core.telemetry.posthog.PostHogTelemetryClient")
def test_record_event_calls_client(self, mock_client_class, disable_telemetry):
"""Test that record_event delegates to the client."""
from cua_core.telemetry import record_event
mock_client_instance = Mock()
mock_client_class.get_client.return_value = mock_client_instance
event_name = "test_event"
event_props = {"key": "value"}
record_event(event_name, event_props)
mock_client_instance.record_event.assert_called_once_with(event_name, event_props)
@patch("cua_core.telemetry.posthog.PostHogTelemetryClient")
def test_record_event_without_properties(self, mock_client_class, disable_telemetry):
"""Test that record_event works without properties."""
from cua_core.telemetry import record_event
mock_client_instance = Mock()
mock_client_class.get_client.return_value = mock_client_instance
event_name = "test_event"
record_event(event_name)
mock_client_instance.record_event.assert_called_once_with(event_name, {})
class TestDestroyTelemetryClient:
"""Test client destruction (SRP: Only tests cleanup)."""
@patch("cua_core.telemetry.posthog.PostHogTelemetryClient")
def test_destroy_client_calls_class_method(self, mock_client_class):
"""Test that destroy_telemetry_client delegates correctly."""
from cua_core.telemetry import destroy_telemetry_client
destroy_telemetry_client()
mock_client_class.destroy_client.assert_called_once()