Arag / app /api /analytics_export.py
AuthorBot
Add enterprise growth APIs, billing, admin UI, and test suite.
12930b5
Raw
History Blame Contribute Delete
6.82 kB
"""Author RAG — Analytics CSV/JSON Export API.
Gives authors a way to download their raw conversation data
for use in external analytics tools (Excel, Notion, Google Sheets).
Routes:
GET /api/admin/{slug}/analytics/export/conversations → CSV of all sessions
GET /api/admin/{slug}/analytics/export/events → CSV of raw analytics events
GET /api/admin/{slug}/analytics/summary → JSON summary dashboard data
"""
import csv
import io
from datetime import datetime, timedelta, timezone
from fastapi import APIRouter, Depends, Query, HTTPException
from fastapi.responses import StreamingResponse
from sqlalchemy import select, func, desc
from sqlalchemy.ext.asyncio import AsyncSession
from app.dependencies import get_current_author, get_db
from app.models.analytics import AnalyticsEvent
from app.models.chat_session import ChatSession, ChatMessage
router = APIRouter()
@router.get("/{slug}/analytics/summary")
async def analytics_summary(
slug: str,
days: int = Query(30, ge=1, le=365),
db: AsyncSession = Depends(get_db),
author=Depends(get_current_author),
):
"""JSON analytics summary — powers the admin dashboard charts."""
if author.id != slug:
raise HTTPException(403, "Forbidden")
since = datetime.now(timezone.utc) - timedelta(days=days)
# Total sessions
sessions_result = await db.execute(
select(func.count(ChatSession.session_id))
.where(ChatSession.author_id == slug, ChatSession.created_at >= since)
)
total_sessions = sessions_result.scalar() or 0
# Total messages
messages_result = await db.execute(
select(func.count(ChatMessage.id))
.join(ChatSession, ChatMessage.session_id == ChatSession.session_id)
.where(ChatSession.author_id == slug, ChatSession.created_at >= since)
)
total_messages = messages_result.scalar() or 0
# Intent distribution
intent_result = await db.execute(
select(AnalyticsEvent.event_type, func.count(AnalyticsEvent.id))
.where(
AnalyticsEvent.author_id == slug,
AnalyticsEvent.created_at >= since,
AnalyticsEvent.event_type.like("intent_%"),
)
.group_by(AnalyticsEvent.event_type)
)
intent_counts = {row[0].replace("intent_", ""): row[1] for row in intent_result}
# Daily sessions for sparkline chart (last 14 days)
daily_result = await db.execute(
select(
func.date(ChatSession.created_at).label("day"),
func.count(ChatSession.session_id).label("count"),
)
.where(
ChatSession.author_id == slug,
ChatSession.created_at >= datetime.now(timezone.utc) - timedelta(days=14),
)
.group_by("day")
.order_by("day")
)
daily_sessions = [{"date": str(r.day), "sessions": r.count} for r in daily_result]
# Analytics events
events_result = await db.execute(
select(AnalyticsEvent)
.where(AnalyticsEvent.author_id == slug, AnalyticsEvent.created_at >= since)
.order_by(desc(AnalyticsEvent.created_at))
.limit(5)
)
recent_events = [
{"type": e.event_type, "at": e.created_at.isoformat() if e.created_at else None}
for e in events_result.scalars()
]
return {
"period_days": days,
"total_sessions": total_sessions,
"total_messages": total_messages,
"avg_messages_per_session": round(total_messages / total_sessions, 1) if total_sessions else 0,
"intent_distribution": intent_counts,
"daily_sessions": daily_sessions,
"recent_events": recent_events,
}
@router.get("/{slug}/analytics/export/conversations")
async def export_conversations_csv(
slug: str,
days: int = Query(90, ge=1, le=365),
db: AsyncSession = Depends(get_db),
author=Depends(get_current_author),
):
"""Stream conversation history as a CSV file download."""
if author.id != slug:
raise HTTPException(403, "Forbidden")
since = datetime.now(timezone.utc) - timedelta(days=days)
sessions_result = await db.execute(
select(ChatSession)
.where(ChatSession.author_id == slug, ChatSession.created_at >= since)
.order_by(desc(ChatSession.created_at))
)
sessions = sessions_result.scalars().all()
output = io.StringIO()
writer = csv.writer(output)
writer.writerow(["session_id", "started_at", "role", "message", "tokens_used", "intent"])
for session in sessions:
msgs_result = await db.execute(
select(ChatMessage)
.where(ChatMessage.session_id == session.session_id)
.order_by(ChatMessage.created_at)
)
for msg in msgs_result.scalars():
writer.writerow([
session.session_id,
session.created_at.isoformat() if session.created_at else "",
msg.role,
msg.content[:500], # Truncate very long messages
getattr(msg, "tokens_used", ""),
getattr(msg, "intent", ""),
])
output.seek(0)
filename = f"conversations_{slug}_{datetime.now().strftime('%Y%m%d')}.csv"
return StreamingResponse(
iter([output.getvalue()]),
media_type="text/csv",
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
)
@router.get("/{slug}/analytics/export/events")
async def export_events_csv(
slug: str,
days: int = Query(90, ge=1, le=365),
db: AsyncSession = Depends(get_db),
author=Depends(get_current_author),
):
"""Stream raw analytics events as CSV."""
if author.id != slug:
raise HTTPException(403, "Forbidden")
since = datetime.now(timezone.utc) - timedelta(days=days)
result = await db.execute(
select(AnalyticsEvent)
.where(AnalyticsEvent.author_id == slug, AnalyticsEvent.created_at >= since)
.order_by(desc(AnalyticsEvent.created_at))
)
events = result.scalars().all()
output = io.StringIO()
writer = csv.writer(output)
writer.writerow(["event_type", "session_id", "visitor_id", "book_id", "country", "created_at", "metadata"])
for e in events:
writer.writerow([
e.event_type,
getattr(e, "session_id", ""),
getattr(e, "visitor_id", ""),
getattr(e, "book_id", ""),
getattr(e, "country", ""),
e.created_at.isoformat() if e.created_at else "",
getattr(e, "metadata_json", ""),
])
output.seek(0)
filename = f"events_{slug}_{datetime.now().strftime('%Y%m%d')}.csv"
return StreamingResponse(
iter([output.getvalue()]),
media_type="text/csv",
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
)