added analytics

This commit is contained in:
belviskhoremk
2026-02-23 17:24:41 +00:00
parent 07c4c55072
commit 2ed998058e
5 changed files with 543 additions and 105 deletions

364
app/routers/analytics.py Normal file
View File

@@ -0,0 +1,364 @@
"""
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,
)