Skip to content

Commit 57d924e

Browse files
committed
improve analytics
1 parent 961584a commit 57d924e

3 files changed

Lines changed: 110 additions & 7 deletions

File tree

finbot/apps/cc/routes/analytics.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
get_share_link_stats,
3636
)
3737
from finbot.core.analytics.queries import (
38+
get_api_calls_count,
39+
get_api_latency_percentiles,
3840
get_auth_funnel,
3941
get_bot_pageviews_count,
4042
get_browser_breakdown,
@@ -51,6 +53,7 @@
5153
get_referer_breakdown,
5254
get_response_time_percentiles,
5355
get_session_type_breakdown,
56+
get_top_api_endpoints,
5457
get_top_pages,
5558
get_total_pageviews,
5659
get_unique_visitors,
@@ -92,6 +95,10 @@ async def analytics_dashboard(request: Request):
9295
"funnel": get_auth_funnel(db, days=7),
9396
"latency": latency,
9497
"sessions": get_session_type_breakdown(db, days=7),
98+
"api_calls_7d": get_api_calls_count(db, days=7),
99+
"api_calls_30d": get_api_calls_count(db, days=30),
100+
"api_latency": get_api_latency_percentiles(db, days=7),
101+
"top_api_endpoints": get_top_api_endpoints(db, days=7, limit=10),
95102
}
96103
finally:
97104
db.close()

finbot/apps/cc/templates/pages/analytics.html

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,41 @@ <h3 class="text-text-1 text-xs font-semibold uppercase tracking-wider">Latency O
196196
</div>
197197
</div>
198198

199+
<!-- API Traffic -->
200+
<div class="bg-cc-surface border border-cc-border rounded-lg p-5 mb-6">
201+
<div class="flex items-center justify-between mb-4">
202+
<h3 class="text-text-1 text-xs font-semibold uppercase tracking-wider">API Traffic</h3>
203+
<div class="flex gap-4 text-xs">
204+
<span class="text-text-3">7d: <span class="text-text-1 font-semibold tabular-nums">{{ api_calls_7d }}</span> calls</span>
205+
<span class="text-text-3">30d: <span class="text-text-1 font-semibold tabular-nums">{{ api_calls_30d }}</span> calls</span>
206+
<span class="text-text-3">P50: <span class="text-text-1 font-semibold tabular-nums">{{ api_latency.p50 }}ms</span></span>
207+
<span class="text-text-3">P95: <span class="text-text-1 font-semibold tabular-nums">{{ api_latency.p95 }}ms</span></span>
208+
</div>
209+
</div>
210+
{% if top_api_endpoints %}
211+
<div class="space-y-1">
212+
<div class="flex items-center justify-between text-[10px] text-text-3 uppercase tracking-wider px-2 pb-1">
213+
<span>Endpoint</span>
214+
<div class="flex gap-6">
215+
<span class="w-16 text-right">Calls</span>
216+
<span class="w-16 text-right">Avg</span>
217+
</div>
218+
</div>
219+
{% for ep in top_api_endpoints %}
220+
<a href="/cc/analytics/pages?path={{ ep.path | urlencode }}" class="flex items-center justify-between text-xs py-1 px-2 -mx-2 rounded hover:bg-white/[0.03] transition-colors group">
221+
<span class="text-text-2 truncate mr-4 group-hover:text-cyan transition-colors">{{ ep.path }}</span>
222+
<div class="flex gap-6 shrink-0">
223+
<span class="text-text-1 font-semibold tabular-nums w-16 text-right">{{ ep.calls }}</span>
224+
<span class="text-text-3 tabular-nums w-16 text-right">{{ ep.avg_ms }}ms</span>
225+
</div>
226+
</a>
227+
{% endfor %}
228+
</div>
229+
{% else %}
230+
<p class="text-text-3 text-xs">No API traffic yet</p>
231+
{% endif %}
232+
</div>
233+
199234
<div class="mt-6 text-center">
200235
<p class="text-text-3 text-xs">Total pageviews recorded: {{ total_pageviews }}</p>
201236
</div>

finbot/core/analytics/queries.py

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
from .models import PageView
1414

1515
_HUMAN = or_(PageView.device_type != "bot", PageView.device_type.is_(None))
16+
_PAGE_ONLY = PageView.path.not_like("%/api/%")
17+
_API_ONLY = PageView.path.like("%/api/%")
1618

1719

1820
def _since(days: int | None) -> datetime | None:
@@ -37,7 +39,7 @@ def get_pageviews_count(db: Session, days: int = 7) -> int:
3739
since = datetime.now(UTC) - timedelta(days=days)
3840
return (
3941
db.query(func.count(PageView.id))
40-
.filter(PageView.timestamp >= since, _HUMAN)
42+
.filter(PageView.timestamp >= since, _HUMAN, _PAGE_ONLY)
4143
.scalar() or 0
4244
)
4345

@@ -55,7 +57,7 @@ def get_unique_visitors(db: Session, days: int = 7) -> int:
5557
since = datetime.now(UTC) - timedelta(days=days)
5658
return (
5759
db.query(func.count(distinct(PageView.session_id)))
58-
.filter(PageView.timestamp >= since, PageView.session_id.isnot(None), _HUMAN)
60+
.filter(PageView.timestamp >= since, PageView.session_id.isnot(None), _HUMAN, _PAGE_ONLY)
5961
.scalar()
6062
or 0
6163
)
@@ -65,7 +67,7 @@ def get_top_pages(db: Session, days: int = 7, limit: int = 10) -> list[dict]:
6567
since = datetime.now(UTC) - timedelta(days=days)
6668
rows = (
6769
db.query(PageView.path, func.count(PageView.id).label("views"))
68-
.filter(PageView.timestamp >= since, _HUMAN)
70+
.filter(PageView.timestamp >= since, _HUMAN, _PAGE_ONLY)
6971
.group_by(PageView.path)
7072
.order_by(func.count(PageView.id).desc())
7173
.limit(limit)
@@ -78,7 +80,7 @@ def get_browser_breakdown(db: Session, days: int = 7, limit: int = 10) -> list[d
7880
since = datetime.now(UTC) - timedelta(days=days)
7981
rows = (
8082
db.query(PageView.browser, func.count(PageView.id).label("count"))
81-
.filter(PageView.timestamp >= since, PageView.browser.isnot(None))
83+
.filter(PageView.timestamp >= since, PageView.browser.isnot(None), _PAGE_ONLY)
8284
.group_by(PageView.browser)
8385
.order_by(func.count(PageView.id).desc())
8486
.limit(limit)
@@ -91,7 +93,7 @@ def get_device_breakdown(db: Session, days: int = 7, limit: int = 10) -> list[di
9193
since = datetime.now(UTC) - timedelta(days=days)
9294
rows = (
9395
db.query(PageView.device_type, func.count(PageView.id).label("count"))
94-
.filter(PageView.timestamp >= since, PageView.device_type.isnot(None))
96+
.filter(PageView.timestamp >= since, PageView.device_type.isnot(None), _PAGE_ONLY)
9597
.group_by(PageView.device_type)
9698
.order_by(func.count(PageView.id).desc())
9799
.limit(limit)
@@ -108,6 +110,7 @@ def get_referer_breakdown(db: Session, days: int = 7, limit: int = 10) -> list[d
108110
PageView.timestamp >= since,
109111
PageView.referer_domain.isnot(None),
110112
PageView.referer_domain != "",
113+
_PAGE_ONLY,
111114
)
112115
.group_by(PageView.referer_domain)
113116
.order_by(func.count(PageView.id).desc())
@@ -122,7 +125,7 @@ def get_daily_pageviews(db: Session, days: int | None = 30) -> list[dict]:
122125
func.date(PageView.timestamp).label("day"),
123126
func.count(PageView.id).label("views"),
124127
func.count(distinct(PageView.session_id)).label("visitors"),
125-
)
128+
).filter(_HUMAN, _PAGE_ONLY)
126129
if days:
127130
q = q.filter(PageView.timestamp >= datetime.now(UTC) - timedelta(days=days))
128131
rows = (
@@ -199,6 +202,8 @@ def get_response_time_percentiles(
199202
q = q.filter(PageView.timestamp >= since)
200203
if path:
201204
q = q.filter(PageView.path == path)
205+
else:
206+
q = q.filter(_PAGE_ONLY)
202207
values = [r[0] for r in q.all()]
203208
if not values:
204209
return {"avg": 0, "p50": 0, "p95": 0, "p99": 0}
@@ -227,6 +232,8 @@ def get_daily_latency(
227232
q = q.filter(PageView.timestamp >= since)
228233
if path:
229234
q = q.filter(PageView.path == path)
235+
else:
236+
q = q.filter(_PAGE_ONLY)
230237

231238
results = []
232239
for day, rows in groupby(q.all(), key=attrgetter("day")):
@@ -353,4 +360,58 @@ def get_page_referer_breakdown(
353360

354361

355362
def get_total_pageviews(db: Session) -> int:
356-
return db.query(func.count(PageView.id)).filter(_HUMAN).scalar() or 0
363+
return db.query(func.count(PageView.id)).filter(_HUMAN, _PAGE_ONLY).scalar() or 0
364+
365+
366+
# ---------------------------------------------------------------------------
367+
# API traffic queries
368+
# ---------------------------------------------------------------------------
369+
370+
def get_api_calls_count(db: Session, days: int = 7) -> int:
371+
since = datetime.now(UTC) - timedelta(days=days)
372+
return (
373+
db.query(func.count(PageView.id))
374+
.filter(PageView.timestamp >= since, _HUMAN, _API_ONLY)
375+
.scalar() or 0
376+
)
377+
378+
379+
def get_top_api_endpoints(db: Session, days: int = 7, limit: int = 10) -> list[dict]:
380+
since = datetime.now(UTC) - timedelta(days=days)
381+
rows = (
382+
db.query(
383+
PageView.path,
384+
func.count(PageView.id).label("calls"),
385+
func.avg(PageView.response_time_ms).label("avg_ms"),
386+
)
387+
.filter(PageView.timestamp >= since, _HUMAN, _API_ONLY)
388+
.group_by(PageView.path)
389+
.order_by(func.count(PageView.id).desc())
390+
.limit(limit)
391+
.all()
392+
)
393+
return [
394+
{"path": r.path, "calls": r.calls, "avg_ms": round(r.avg_ms or 0, 1)}
395+
for r in rows
396+
]
397+
398+
399+
def get_api_latency_percentiles(db: Session, days: int = 7) -> dict:
400+
"""Return {avg, p50, p95, p99} response times for API endpoints."""
401+
since = _since(days)
402+
q = (
403+
db.query(PageView.response_time_ms)
404+
.filter(PageView.response_time_ms.isnot(None), _API_ONLY)
405+
.order_by(PageView.response_time_ms)
406+
)
407+
if since:
408+
q = q.filter(PageView.timestamp >= since)
409+
values = [r[0] for r in q.all()]
410+
if not values:
411+
return {"avg": 0, "p50": 0, "p95": 0, "p99": 0}
412+
return {
413+
"avg": round(sum(values) / len(values), 1),
414+
"p50": round(_percentile(values, 50), 1),
415+
"p95": round(_percentile(values, 95), 1),
416+
"p99": round(_percentile(values, 99), 1),
417+
}

0 commit comments

Comments
 (0)