feat(analytics): add PSA activity logging and enhanced PSA metrics endpoint
- Log `note_posted` and `time_entry_posted` activities to `psa_activity_logs` after each successful PSA push in `psa_documentation_service.py`; errors are caught and logged without blocking the main push flow - Add `PsaFunnel`, `PsaDailyTrend`, and `EnhancedPsaMetrics` Pydantic schemas - Add `GET /analytics/flowpilot/psa-metrics?period=30d` endpoint (team_admin, rate-limited 15/min) returning time entry totals, push funnel (sessions → linked → doc pushed → time entry logged), and daily trend Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@ from app.models.user import User
|
||||
from app.models.tree import Tree
|
||||
from app.models.ai_session import AISession
|
||||
from app.models.flow_proposal import FlowProposal
|
||||
from app.models.psa_activity_log import PsaActivityLog
|
||||
from app.models.psa_post_log import PsaPostLog
|
||||
from app.models.category import TreeCategory
|
||||
from app.schemas.flowpilot_analytics import (
|
||||
@@ -32,6 +33,9 @@ from app.schemas.flowpilot_analytics import (
|
||||
CoverageResponse,
|
||||
FlowQualityRow,
|
||||
FlowQualityResponse,
|
||||
EnhancedPsaMetrics,
|
||||
PsaFunnel,
|
||||
PsaDailyTrend,
|
||||
)
|
||||
from app.services.knowledge_gap_service import get_knowledge_gaps, KnowledgeGapReport
|
||||
|
||||
@@ -613,3 +617,121 @@ async def get_flow_quality(
|
||||
top_performers=top_performers,
|
||||
needs_attention=needs_attention,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/psa-metrics", response_model=EnhancedPsaMetrics)
|
||||
@limiter.limit("15/minute")
|
||||
async def get_enhanced_psa_metrics(
|
||||
request: Request,
|
||||
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
_: None = Depends(require_team_admin),
|
||||
period: str = Query("30d", pattern="^(7d|30d|90d)$"),
|
||||
):
|
||||
"""Get enhanced PSA integration metrics including time entry stats, push funnel, and daily trend."""
|
||||
if not current_user.account_id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="No account")
|
||||
|
||||
account_id = current_user.account_id
|
||||
period_start = _get_period_start(period)
|
||||
|
||||
# ── Time entry totals from psa_activity_logs ──
|
||||
time_entry_result = await db.execute(
|
||||
select(
|
||||
func.count(PsaActivityLog.id).label("entry_count"),
|
||||
func.sum(PsaActivityLog.hours_logged).label("total_hours"),
|
||||
)
|
||||
.where(
|
||||
PsaActivityLog.account_id == account_id,
|
||||
PsaActivityLog.activity_type == "time_entry_posted",
|
||||
PsaActivityLog.created_at >= period_start,
|
||||
)
|
||||
)
|
||||
te_row = time_entry_result.one()
|
||||
total_time_entries = int(te_row.entry_count or 0)
|
||||
total_hours_raw = te_row.total_hours or 0
|
||||
total_hours_logged = round(float(total_hours_raw), 2)
|
||||
avg_hours = round(total_hours_logged / total_time_entries, 2) if total_time_entries > 0 else 0.0
|
||||
|
||||
# ── Push funnel ──
|
||||
# Total sessions in period
|
||||
total_sessions_result = await db.execute(
|
||||
select(func.count(AISession.id)).where(
|
||||
AISession.account_id == account_id,
|
||||
AISession.created_at >= period_start,
|
||||
)
|
||||
)
|
||||
total_sessions = int(total_sessions_result.scalar() or 0)
|
||||
|
||||
# Sessions linked to a ticket
|
||||
linked_result = await db.execute(
|
||||
select(func.count(AISession.id)).where(
|
||||
AISession.account_id == account_id,
|
||||
AISession.created_at >= period_start,
|
||||
AISession.psa_ticket_id.isnot(None),
|
||||
)
|
||||
)
|
||||
linked_to_ticket = int(linked_result.scalar() or 0)
|
||||
|
||||
# Sessions with a successful doc push (via PsaPostLog)
|
||||
doc_pushed_result = await db.execute(
|
||||
select(func.count(PsaPostLog.id.distinct()))
|
||||
.join(AISession, PsaPostLog.ai_session_id == AISession.id)
|
||||
.where(
|
||||
AISession.account_id == account_id,
|
||||
PsaPostLog.ai_session_id.isnot(None),
|
||||
PsaPostLog.status == "success",
|
||||
PsaPostLog.posted_at >= period_start,
|
||||
)
|
||||
)
|
||||
doc_pushed = int(doc_pushed_result.scalar() or 0)
|
||||
|
||||
# Sessions with a time entry logged (via psa_activity_logs)
|
||||
time_entry_sessions_result = await db.execute(
|
||||
select(func.count(PsaActivityLog.session_id.distinct())).where(
|
||||
PsaActivityLog.account_id == account_id,
|
||||
PsaActivityLog.activity_type == "time_entry_posted",
|
||||
PsaActivityLog.created_at >= period_start,
|
||||
PsaActivityLog.session_id.isnot(None),
|
||||
)
|
||||
)
|
||||
time_entry_logged = int(time_entry_sessions_result.scalar() or 0)
|
||||
|
||||
push_funnel = PsaFunnel(
|
||||
total_sessions=total_sessions,
|
||||
linked_to_ticket=linked_to_ticket,
|
||||
doc_pushed=doc_pushed,
|
||||
time_entry_logged=time_entry_logged,
|
||||
)
|
||||
|
||||
# ── Daily trend (time entries grouped by date) ──
|
||||
daily_result = await db.execute(
|
||||
select(
|
||||
cast(PsaActivityLog.created_at, Date).label("day"),
|
||||
func.count(PsaActivityLog.id).label("entries"),
|
||||
func.sum(PsaActivityLog.hours_logged).label("hours"),
|
||||
)
|
||||
.where(
|
||||
PsaActivityLog.account_id == account_id,
|
||||
PsaActivityLog.activity_type == "time_entry_posted",
|
||||
PsaActivityLog.created_at >= period_start,
|
||||
)
|
||||
.group_by(cast(PsaActivityLog.created_at, Date))
|
||||
.order_by(cast(PsaActivityLog.created_at, Date))
|
||||
)
|
||||
daily_trend = [
|
||||
PsaDailyTrend(
|
||||
date=str(r.day),
|
||||
entries=int(r.entries or 0),
|
||||
hours=round(float(r.hours or 0), 2),
|
||||
)
|
||||
for r in daily_result.all()
|
||||
]
|
||||
|
||||
return EnhancedPsaMetrics(
|
||||
total_time_entries=total_time_entries,
|
||||
total_hours_logged=total_hours_logged,
|
||||
avg_hours_per_session=avg_hours,
|
||||
push_funnel=push_funnel,
|
||||
daily_trend=daily_trend,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user