""" Analytics router - provides chatbot performance data for Starter+ users. Available to: Starter, Pro, Enterprise plans only. No LLM cost data is exposed to users. """ from fastapi import APIRouter, HTTPException, Depends from app.database import get_supabase from app.dependencies import get_current_user from app.config import PLAN_LIMITS from typing import List, Optional, Dict from pydantic import BaseModel from datetime import datetime, timedelta import logging logger = logging.getLogger(__name__) router = APIRouter(prefix="/analytics", tags=["Analytics"]) # ─── Response Models ─────────────────────────────────────────────────────────── class DailyConversations(BaseModel): date: str count: int class TopQuery(BaseModel): query: str count: int class ChatbotAnalyticsResponse(BaseModel): chatbot_id: str chatbot_name: str total_conversations: int unique_sessions: int total_messages: int average_messages_per_conversation: float average_rating: Optional[float] total_ratings: int conversations_today: int conversations_this_week: int conversations_this_month: int daily_conversations: List[DailyConversations] top_queries: List[TopQuery] languages_used: Dict[str, int] peak_hour: Optional[int] # 0-23 class OverviewAnalyticsResponse(BaseModel): total_chatbots: int published_chatbots: int total_conversations: int total_messages: int unique_sessions: int conversations_this_month: int average_rating: Optional[float] chatbots: List[ChatbotAnalyticsResponse] plan: str conversations_limit: int conversations_used: int # ─── Helpers ─────────────────────────────────────────────────────────────────── def _get_user_plan(user_id: str) -> str: supabase = get_supabase() result = supabase.table("subscriptions") \ .select("plan") \ .eq("user_id", user_id) \ .eq("status", "active") \ .execute() return result.data[0]["plan"] if result.data else "free" def _check_analytics_access(plan: str): """Ensure user has analytics access (Starter+).""" plan_config = PLAN_LIMITS.get(plan, PLAN_LIMITS["free"]) if not plan_config.get("analytics", False): raise HTTPException( status_code=402, detail="Analytics is available on Starter and Pro plans. Upgrade to access your chatbot analytics." ) # ─── Endpoints ───────────────────────────────────────────────────────────────── @router.get("/overview", response_model=OverviewAnalyticsResponse) async def get_analytics_overview(user=Depends(get_current_user)): """ Get analytics overview across all chatbots for the current user. Requires Starter+ plan. """ plan = _get_user_plan(user.id) _check_analytics_access(plan) supabase = get_supabase() # Get user's company company = supabase.table("companies").select("id").eq("owner_id", user.id).execute() if not company.data: raise HTTPException(status_code=404, detail="Company not found") company_id = company.data[0]["id"] # Get all chatbots chatbots = supabase.table("chatbots").select("*").eq("company_id", company_id).execute() chatbot_list = chatbots.data or [] chatbot_ids = [c["id"] for c in chatbot_list] if not chatbot_ids: plan_config = PLAN_LIMITS.get(plan, PLAN_LIMITS["free"]) return OverviewAnalyticsResponse( total_chatbots=0, published_chatbots=0, total_conversations=0, total_messages=0, unique_sessions=0, conversations_this_month=0, average_rating=None, chatbots=[], plan=plan, conversations_limit=plan_config.get("conversations_limit", 0), conversations_used=0, ) # Gather per-chatbot analytics chatbot_analytics = [] total_convos = 0 total_msgs = 0 total_sessions = 0 month_convos = 0 all_ratings = [] now = datetime.utcnow() month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) week_start = now - timedelta(days=now.weekday()) week_start = week_start.replace(hour=0, minute=0, second=0, microsecond=0) today_start = now.replace(hour=0, minute=0, second=0, microsecond=0) thirty_days_ago = now - timedelta(days=30) for chatbot in chatbot_list: cid = chatbot["id"] # Total conversations convos = supabase.table("conversations").select("id, session_id, language, created_at", count="exact") \ .eq("chatbot_id", cid).execute() conv_count = convos.count or 0 conv_data = convos.data or [] total_convos += conv_count # Unique sessions sessions = set(c.get("session_id") for c in conv_data if c.get("session_id")) unique_sess = len(sessions) total_sessions += unique_sess # Total messages msgs = supabase.table("messages").select("id", count="exact") \ .in_("conversation_id", [c["id"] for c in conv_data] if conv_data else [""]).execute() msg_count = msgs.count or 0 total_msgs += msg_count # Time-based conversation counts today_count = sum(1 for c in conv_data if c.get("created_at") and c["created_at"][:10] == today_start.strftime("%Y-%m-%d")) week_count = sum(1 for c in conv_data if c.get("created_at") and c["created_at"] >= week_start.isoformat()) month_count = sum(1 for c in conv_data if c.get("created_at") and c["created_at"] >= month_start.isoformat()) month_convos += month_count # Daily conversations (last 30 days) daily = {} for c in conv_data: if c.get("created_at") and c["created_at"] >= thirty_days_ago.isoformat(): day = c["created_at"][:10] daily[day] = daily.get(day, 0) + 1 daily_list = [DailyConversations(date=d, count=n) for d, n in sorted(daily.items())] # Languages used lang_counts: Dict[str, int] = {} for c in conv_data: lang = c.get("language", "en") lang_counts[lang] = lang_counts.get(lang, 0) + 1 # Peak hour (approximate from created_at) hour_counts: Dict[int, int] = {} for c in conv_data: if c.get("created_at") and len(c["created_at"]) > 13: try: hour = int(c["created_at"][11:13]) hour_counts[hour] = hour_counts.get(hour, 0) + 1 except (ValueError, IndexError): pass peak = max(hour_counts, key=hour_counts.get) if hour_counts else None # Top queries (from user messages, get first message per conversation) top_queries: List[TopQuery] = [] if conv_data: conv_ids = [c["id"] for c in conv_data[:100]] # limit to recent 100 user_msgs = supabase.table("messages").select("content") \ .in_("conversation_id", conv_ids) \ .eq("role", "user") \ .limit(200).execute() query_counts: Dict[str, int] = {} for m in (user_msgs.data or []): content = (m.get("content") or "")[:100].strip() if content: query_counts[content] = query_counts.get(content, 0) + 1 top_sorted = sorted(query_counts.items(), key=lambda x: -x[1])[:5] top_queries = [TopQuery(query=q, count=n) for q, n in top_sorted] # Rating rating = chatbot.get("average_rating") if rating: all_ratings.append(rating) # Average messages per conversation avg_msgs = round(msg_count / conv_count, 1) if conv_count > 0 else 0.0 chatbot_analytics.append(ChatbotAnalyticsResponse( chatbot_id=cid, chatbot_name=chatbot.get("name", "Untitled"), total_conversations=conv_count, unique_sessions=unique_sess, total_messages=msg_count, average_messages_per_conversation=avg_msgs, average_rating=rating, total_ratings=0, # would need a ratings table for precise count conversations_today=today_count, conversations_this_week=week_count, conversations_this_month=month_count, daily_conversations=daily_list, top_queries=top_queries, languages_used=lang_counts, peak_hour=peak, )) # Overall average rating avg_rating = round(sum(all_ratings) / len(all_ratings), 1) if all_ratings else None plan_config = PLAN_LIMITS.get(plan, PLAN_LIMITS["free"]) return OverviewAnalyticsResponse( total_chatbots=len(chatbot_list), published_chatbots=sum(1 for c in chatbot_list if c.get("is_published")), total_conversations=total_convos, total_messages=total_msgs, unique_sessions=total_sessions, conversations_this_month=month_convos, average_rating=avg_rating, chatbots=chatbot_analytics, plan=plan, conversations_limit=plan_config.get("conversations_limit", 0), conversations_used=month_convos, ) @router.get("/chatbot/{chatbot_id}", response_model=ChatbotAnalyticsResponse) async def get_chatbot_analytics(chatbot_id: str, user=Depends(get_current_user)): """ Get detailed analytics for a specific chatbot. Requires Starter+ plan and ownership of the chatbot. """ plan = _get_user_plan(user.id) _check_analytics_access(plan) supabase = get_supabase() # Verify ownership company = supabase.table("companies").select("id").eq("owner_id", user.id).execute() if not company.data: raise HTTPException(status_code=404, detail="Company not found") chatbot = supabase.table("chatbots").select("*") \ .eq("id", chatbot_id) \ .eq("company_id", company.data[0]["id"]).execute() if not chatbot.data: raise HTTPException(status_code=404, detail="Chatbot not found") cb = chatbot.data[0] now = datetime.utcnow() month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) week_start = now - timedelta(days=now.weekday()) week_start = week_start.replace(hour=0, minute=0, second=0, microsecond=0) today_start = now.replace(hour=0, minute=0, second=0, microsecond=0) thirty_days_ago = now - timedelta(days=30) # Conversations convos = supabase.table("conversations").select("id, session_id, language, created_at", count="exact") \ .eq("chatbot_id", chatbot_id).execute() conv_count = convos.count or 0 conv_data = convos.data or [] sessions = set(c.get("session_id") for c in conv_data if c.get("session_id")) # Messages conv_ids = [c["id"] for c in conv_data] if conv_data else [""] msgs = supabase.table("messages").select("id", count="exact") \ .in_("conversation_id", conv_ids).execute() msg_count = msgs.count or 0 today_count = sum(1 for c in conv_data if c.get("created_at") and c["created_at"][:10] == today_start.strftime("%Y-%m-%d")) week_count = sum(1 for c in conv_data if c.get("created_at") and c["created_at"] >= week_start.isoformat()) month_count = sum(1 for c in conv_data if c.get("created_at") and c["created_at"] >= month_start.isoformat()) # Daily daily = {} for c in conv_data: if c.get("created_at") and c["created_at"] >= thirty_days_ago.isoformat(): day = c["created_at"][:10] daily[day] = daily.get(day, 0) + 1 daily_list = [DailyConversations(date=d, count=n) for d, n in sorted(daily.items())] # Languages lang_counts: Dict[str, int] = {} for c in conv_data: lang = c.get("language", "en") lang_counts[lang] = lang_counts.get(lang, 0) + 1 # Peak hour hour_counts: Dict[int, int] = {} for c in conv_data: if c.get("created_at") and len(c["created_at"]) > 13: try: hour = int(c["created_at"][11:13]) hour_counts[hour] = hour_counts.get(hour, 0) + 1 except (ValueError, IndexError): pass peak = max(hour_counts, key=hour_counts.get) if hour_counts else None # Top queries top_queries: List[TopQuery] = [] if conv_data: recent_ids = [c["id"] for c in conv_data[:100]] user_msgs = supabase.table("messages").select("content") \ .in_("conversation_id", recent_ids) \ .eq("role", "user") \ .limit(200).execute() query_counts: Dict[str, int] = {} for m in (user_msgs.data or []): content = (m.get("content") or "")[:100].strip() if content: query_counts[content] = query_counts.get(content, 0) + 1 top_sorted = sorted(query_counts.items(), key=lambda x: -x[1])[:10] top_queries = [TopQuery(query=q, count=n) for q, n in top_sorted] avg_msgs = round(msg_count / conv_count, 1) if conv_count > 0 else 0.0 return ChatbotAnalyticsResponse( chatbot_id=chatbot_id, chatbot_name=cb.get("name", "Untitled"), total_conversations=conv_count, unique_sessions=len(sessions), total_messages=msg_count, average_messages_per_conversation=avg_msgs, average_rating=cb.get("average_rating"), total_ratings=0, conversations_today=today_count, conversations_this_week=week_count, conversations_this_month=month_count, daily_conversations=daily_list, top_queries=top_queries, languages_used=lang_counts, peak_hour=peak, )