Files
matchmaking-v2/app/engine/stats/clients.py
raulgupta be921559e7 Real Scout dashboard backend + per-board cursor rework
- get_scout_stats aggregator (app/engine/stats): assembles the REAL Summary metrics —
  funnel + cohort engagement rank + active-window (activity timestamps by hour) from our DB,
  accumulating salary band (avg of each deck's peak, ₹L), match/competition stats from the
  feed, Momentum/QX + Q-Score trend (qscore-service), day streak (user-service). Honest:
  unsourced cards return None so the UI omits/locks them, never faked. posted_date extractor.
- Activity tracking: viewed/saved flags + search_count → funnel (Matches→Viewed→Shortlisted→
  Applied) + engagement percentile. matchesFound = all-time count.
- Per-board search cursors {board: page} (replaces the single cursor): only boards that truly
  paginate (LinkedIn) get one; cursor = LAST page fetched (1st search of a new query → 1).
  Resets on query change OR >24h (boards refresh ~daily). Dropped Naukri incremental/stateKey
  (opaque, exhausting, cross-account dedup state) — dedup is the PER-USER seen-net only.
- tests: stats helpers (posting-age, histogram, active-window, engagement score).
2026-06-21 12:12:28 +05:30

76 lines
3.0 KiB
Python

"""Cross-service reads for the dashboard — QScore (Momentum/QX) + user-service (Day streak).
Direct REST, Clerk-mesh-authed. Both degrade to None on any failure (timeout / 404 / service down) —
a missing KPI is omitted by the UI, never faked, and never crashes the dashboard.
"""
from __future__ import annotations
import logging
import httpx
from app.config import get_settings
logger = logging.getLogger(__name__)
async def fetch_qx(user_uuid: str | None) -> dict | None:
"""QScore for the user → {qx, quotients}. GET /v1/qscore/{uuid}?org_id (qscore-service)."""
if not user_uuid:
return None
s = get_settings()
try:
async with httpx.AsyncClient(timeout=4.0) as c:
r = await c.get(f"{s.QSCORE_BASE_URL}/v1/qscore/{user_uuid}", params={"org_id": s.QSCORE_ORG_ID})
if r.status_code == 404: # no score computed yet — honest absence
return None
r.raise_for_status()
d = r.json()
qx = d.get("q_score")
return {"qx": round(float(qx), 1), "quotients": d.get("quotients") or {}} if qx is not None else None
except (httpx.HTTPError, ValueError) as e:
logger.warning("fetch_qx failed (non-fatal): %s", e)
return None
async def fetch_qx_trend(user_uuid: str | None) -> list[float] | None:
"""Q-Score trend (oldest→newest) ← qscore-service GET /v1/qscore/{uuid}/history. Same source as
the live QX, so the trend's latest point matches the Momentum value (consistent, not stale).
None on failure → the UI keeps the Readiness card locked."""
if not user_uuid:
return None
s = get_settings()
try:
async with httpx.AsyncClient(timeout=4.0) as c:
r = await c.get(
f"{s.QSCORE_BASE_URL}/v1/qscore/{user_uuid}/history",
params={"org_id": s.QSCORE_ORG_ID, "points": 10},
)
if r.status_code == 404:
return None
r.raise_for_status()
pts = [round(float(p["q_score"]), 1) for p in (r.json().get("points") or []) if p.get("q_score") is not None]
return pts if len(pts) >= 2 else None # need ≥2 points for a trend
except (httpx.HTTPError, ValueError, TypeError, KeyError) as e:
logger.warning("fetch_qx_trend failed (non-fatal): %s", e)
return None
async def fetch_streak(clerk_id: str | None) -> int | None:
"""Day streak ← user-service metadata.current_streak. GET /api/state/{clerk_id} (Bearer A2A)."""
if not clerk_id:
return None
s = get_settings()
try:
async with httpx.AsyncClient(timeout=4.0) as c:
r = await c.get(
f"{s.USER_SERVICE_BASE_URL}/api/state/{clerk_id}",
headers={"Authorization": f"Bearer {s.A2A_OUTBOUND_KEY}"},
)
r.raise_for_status()
streak = (r.json().get("metadata") or {}).get("current_streak")
return int(streak) if streak is not None else None
except (httpx.HTTPError, ValueError, TypeError) as e:
logger.warning("fetch_streak failed (non-fatal): %s", e)
return None