from fastapi import APIRouter, Header, HTTPException, Query from typing import Optional from datetime import datetime, timedelta import logging import os from ..storage.analytics_store import AnalyticsStore from ..utils.access_control import require_api_permission router = APIRouter() logger = logging.getLogger(__name__) # Initialize analytics store, but don't crash the app if Supabase is not available. # In environments like Hugging Face Spaces where Supabase isn't configured, # we disable analytics gracefully instead of raising at import time. try: # Only attempt to initialize Supabase analytics when credentials are present if os.getenv("SUPABASE_URL") and os.getenv("SUPABASE_SERVICE_KEY"): analytics_store: Optional[AnalyticsStore] = AnalyticsStore() else: analytics_store = None logger.debug( "AnalyticsStore: Supabase credentials not configured. " "Analytics endpoints will return 503." ) except RuntimeError as exc: analytics_store = None # Only log at warning level if credentials are configured (actual error) # Otherwise log at debug level (expected when Supabase is not configured) if os.getenv("SUPABASE_URL") and os.getenv("SUPABASE_SERVICE_KEY"): logger.warning( "AnalyticsStore initialization failed (%s). Analytics endpoints will return 503.", str(exc).split('\n')[0], # Only first line ) else: logger.debug( "AnalyticsStore not configured (%s). Analytics endpoints will return 503.", str(exc).split('\n')[0], ) @router.get("/overview") async def analytics_overview( x_tenant_id: str = Header(None), days: int = Query(30, description="Number of days to look back"), x_user_role: str = Header("viewer") ): """ Returns an overview of analytics for the dashboard. Includes total queries, tool usage, red-flag count, and active users. """ if not x_tenant_id: raise HTTPException(status_code=400, detail="Missing tenant ID") require_api_permission(x_user_role, "view_analytics") # Return empty data if analytics is not configured (instead of 503) if analytics_store is None: return { "tenant_id": x_tenant_id, "overview": { "total_queries": 0, "tool_usage": {}, "redflag_count": 0, "active_users": 0, "last_query": None, "rag_quality": { "total_searches": 0, "avg_hits_per_search": 0, "avg_score": 0.0, "avg_top_score": 0.0, "avg_latency_ms": 0.0 } } } since_timestamp = int((datetime.now() - timedelta(days=days)).timestamp()) if days else None tool_usage = analytics_store.get_tool_usage_stats(x_tenant_id, since_timestamp) activity = analytics_store.get_activity_summary(x_tenant_id, since_timestamp) rag_quality = analytics_store.get_rag_quality_metrics(x_tenant_id, since_timestamp) return { "tenant_id": x_tenant_id, "overview": { "total_queries": activity["total_queries"], "tool_usage": tool_usage, "redflag_count": activity["redflag_count"], "active_users": activity["active_users"], "last_query": activity["last_query"], "rag_quality": rag_quality } } @router.get("/tool-usage") async def analytics_tool_usage( x_tenant_id: str = Header(None), days: int = Query(30, description="Number of days to look back"), x_user_role: str = Header("viewer") ): """ Returns how often each tool (RAG, Web, Admin, LLM) was used with detailed stats. Includes counts, latency, tokens, and success/error rates. """ if not x_tenant_id: raise HTTPException(status_code=400, detail="Missing tenant ID") require_api_permission(x_user_role, "view_analytics") # Return empty data if analytics is not configured (instead of 503) if analytics_store is None: return { "tenant_id": x_tenant_id, "tool_usage": {}, "period_days": days } since_timestamp = int((datetime.now() - timedelta(days=days)).timestamp()) if days else None tool_usage = analytics_store.get_tool_usage_stats(x_tenant_id, since_timestamp) return { "tenant_id": x_tenant_id, "tool_usage": tool_usage, "period_days": days } @router.get("/redflags") async def analytics_redflags( x_tenant_id: str = Header(None), limit: int = Query(50, description="Maximum number of violations to return"), days: int = Query(30, description="Number of days to look back"), x_user_role: str = Header("viewer") ): """ Returns red-flag violations for this tenant. Includes rule details, severity, confidence, and timestamps. """ if not x_tenant_id: raise HTTPException(status_code=400, detail="Missing tenant ID") require_api_permission(x_user_role, "view_analytics") # Return empty data if analytics is not configured (instead of 503) if analytics_store is None: return { "tenant_id": x_tenant_id, "redflags": [], "count": 0 } since_timestamp = int((datetime.now() - timedelta(days=days)).timestamp()) if days else None redflags = analytics_store.get_redflag_violations(x_tenant_id, limit, since_timestamp) # Convert timestamps to ISO format for violation in redflags: if "timestamp" in violation: violation["timestamp_iso"] = datetime.fromtimestamp(violation["timestamp"]).isoformat() return { "tenant_id": x_tenant_id, "redflags": redflags, "count": len(redflags) } @router.get("/activity") async def analytics_activity( x_tenant_id: str = Header(None), days: int = Query(30, description="Number of days to look back"), x_user_role: str = Header("viewer") ): """ Returns general tenant activity statistics. Includes total queries, active users, last query timestamp, and individual activity records for heatmap visualization. """ if not x_tenant_id: raise HTTPException(status_code=400, detail="Missing tenant ID") require_api_permission(x_user_role, "view_analytics") # Return empty data if analytics is not configured (instead of 503) if analytics_store is None: return { "tenant_id": x_tenant_id, "activity": { "total_queries": 0, "active_users": 0, "redflag_count": 0, "last_query": None }, "activities": [], "period_days": days } since_timestamp = int((datetime.now() - timedelta(days=days)).timestamp()) if days else None activity = analytics_store.get_activity_summary(x_tenant_id, since_timestamp) # Also fetch individual activity records for heatmap visualization activities = analytics_store.get_activity_records(x_tenant_id, since_timestamp) return { "tenant_id": x_tenant_id, "activity": activity, "activities": activities, # Individual records with timestamps for heatmap "period_days": days } @router.get("/rag-quality") async def analytics_rag_quality( x_tenant_id: str = Header(None), days: int = Query(30, description="Number of days to look back"), x_user_role: str = Header("viewer") ): """ Returns RAG quality metrics including recall/precision indicators. Includes average hits, scores, and latency. """ if not x_tenant_id: raise HTTPException(status_code=400, detail="Missing tenant ID") require_api_permission(x_user_role, "view_analytics") # Return empty data if analytics is not configured (instead of 503) if analytics_store is None: return { "tenant_id": x_tenant_id, "rag_quality": { "total_searches": 0, "avg_hits_per_search": 0, "avg_score": 0.0, "avg_top_score": 0.0, "avg_latency_ms": 0.0 }, "period_days": days } since_timestamp = int((datetime.now() - timedelta(days=days)).timestamp()) if days else None rag_quality = analytics_store.get_rag_quality_metrics(x_tenant_id, since_timestamp) return { "tenant_id": x_tenant_id, "rag_quality": rag_quality, "period_days": days }