| """Per-tenant analytics event recording + aggregation.""" |
|
|
| from __future__ import annotations |
|
|
| from datetime import UTC, datetime, timedelta |
| from typing import Any |
|
|
| from sqlalchemy import func, select |
| from sqlalchemy.ext.asyncio import AsyncSession |
|
|
| from app.models import Event |
|
|
|
|
| def record( |
| db: AsyncSession, |
| tenant_id: int | None, |
| type_: str, |
| *, |
| session_id: str | None = None, |
| meta: dict[str, Any] | None = None, |
| ) -> None: |
| """Queue an analytics event (persisted with the turn's commit).""" |
| db.add(Event(tenant_id=tenant_id, type=type_, session_id=session_id, meta=meta or {})) |
|
|
|
|
| async def summary(db: AsyncSession, tenant_id: int, days: int = 30) -> dict[str, Any]: |
| since = datetime.now(UTC) - timedelta(days=days) |
| rows = ( |
| await db.execute( |
| select(Event.type, func.count()) |
| .where(Event.tenant_id == tenant_id, Event.created_at >= since) |
| .group_by(Event.type) |
| ) |
| ).all() |
| counts = {t: int(n) for t, n in rows} |
| sessions = ( |
| await db.execute( |
| select(func.count(func.distinct(Event.session_id))).where( |
| Event.tenant_id == tenant_id, Event.created_at >= since |
| ) |
| ) |
| ).scalar() or 0 |
| return { |
| "days": days, |
| "conversations": int(sessions), |
| "messages": counts.get("turn", 0), |
| "product_views": counts.get("products_shown", 0), |
| "cart_links": counts.get("cart_link", 0), |
| "order_lookups": counts.get("order_lookup", 0), |
| "escalations": counts.get("escalation", 0), |
| "stock_watches": counts.get("stock_watch", 0), |
| "by_type": counts, |
| } |
|
|