Files
openswarm/backend/apps/analytics/analytics.py
T

145 lines
4.7 KiB
Python

"""Analytics SubApp: PostHog for product analytics + local usage summary from session data."""
import json
import logging
import os
import platform
from collections import Counter
from contextlib import asynccontextmanager
from backend.config.Apps import SubApp
from backend.config.paths import SESSIONS_DIR
from backend.apps.analytics.collector import init as init_collector, shutdown as shutdown_collector, record, identify
logger = logging.getLogger(__name__)
@asynccontextmanager
async def analytics_lifespan():
init_collector()
logger.info("PostHog analytics initialised")
try:
from backend.apps.settings.settings import load_settings
settings = load_settings()
providers = []
if getattr(settings, "anthropic_api_key", None):
providers.append("anthropic")
record("app.opened", {
"os": platform.system(),
"platform": platform.platform(),
"provider_count": len(providers),
"providers": providers,
})
identify({
"providers_configured": providers,
"provider_count": len(providers),
})
except Exception as e:
logger.debug(f"Analytics startup event failed (non-critical): {e}")
yield
shutdown_collector()
logger.info("PostHog analytics shut down")
analytics = SubApp("analytics", analytics_lifespan)
def _load_all_sessions() -> list[dict]:
"""Load all persisted session JSON files."""
results = []
if not os.path.exists(SESSIONS_DIR):
return results
for fname in os.listdir(SESSIONS_DIR):
if fname.endswith(".json"):
try:
with open(os.path.join(SESSIONS_DIR, fname)) as f:
results.append(json.load(f))
except Exception:
pass
return results
@analytics.router.get("/usage-summary")
async def usage_summary():
"""Compute usage stats from persisted sessions for the Settings page."""
from backend.apps.agents.agent_manager import agent_manager
# Combine persisted + active sessions
sessions = _load_all_sessions()
for s in agent_manager.get_all_sessions():
sessions.append(s.model_dump(mode="json"))
total_sessions = len(sessions)
total_cost = sum(s.get("cost_usd", 0) for s in sessions)
total_messages = 0
total_tool_calls = 0
total_duration = 0.0
model_counts: Counter = Counter()
provider_counts: Counter = Counter()
tool_counts: Counter = Counter()
status_counts: Counter = Counter()
for s in sessions:
messages = s.get("messages", [])
user_msgs = [m for m in messages if m.get("role") in ("user", "assistant")]
tool_msgs = [m for m in messages if m.get("role") == "tool_call"]
total_messages += len(user_msgs)
total_tool_calls += len(tool_msgs)
model_counts[s.get("model", "unknown")] += 1
provider_counts[s.get("provider", "anthropic")] += 1
status_counts[s.get("status", "unknown")] += 1
# Duration
created = s.get("created_at")
closed = s.get("closed_at")
if created and closed:
try:
from datetime import datetime
fmt = "%Y-%m-%dT%H:%M:%S"
c_str = created[:19]
cl_str = closed[:19]
dur = (datetime.fromisoformat(cl_str) - datetime.fromisoformat(c_str)).total_seconds()
if dur > 0:
total_duration += dur
except Exception:
pass
# Count individual tools
for m in tool_msgs:
content = m.get("content", {})
if isinstance(content, dict):
tool_name = content.get("tool", "")
if tool_name:
tool_counts[tool_name] += 1
avg_duration = total_duration / total_sessions if total_sessions > 0 else 0
avg_cost = total_cost / total_sessions if total_sessions > 0 else 0
completed = status_counts.get("completed", 0)
completion_rate = completed / total_sessions if total_sessions > 0 else 0
return {
"total_sessions": total_sessions,
"total_cost_usd": round(total_cost, 4),
"total_messages": total_messages,
"total_tool_calls": total_tool_calls,
"avg_duration_seconds": round(avg_duration, 1),
"avg_cost_per_session": round(avg_cost, 4),
"completion_rate": round(completion_rate, 3),
"models_used": dict(model_counts.most_common(10)),
"providers_used": dict(provider_counts.most_common(10)),
"top_tools": dict(tool_counts.most_common(15)),
"status_breakdown": dict(status_counts),
}
@analytics.router.get("/status")
async def analytics_status():
return {"status": "posthog", "enabled": True}