feat: add analytics dashboard with pageview tracking and query metrics - #21
Merged
Conversation
Add token-protected /analytics dashboard, pageview tracking endpoint (120/min rate limit), and backend analytics helpers with salted IP hashing. Refactors backend DB pool into shared db.py. Adds timestamps on upsert.
The latest updates on your projects. Learn more about Vercel for GitHub.
|
_require_analytics_token returns 503 when ANALYTICS_TOKEN env var is not configured, and 401 when the token header is missing/wrong. The three token-required tests did not set ANALYTICS_TOKEN via monkeypatch, so they got 503 instead of the expected 401.
- Backend: use Supabase session pooler (5432) — transaction pooler (6543) breaks asyncpg named prepared statements (DuplicatePreparedStatementError). Log full tracebacks on 502 via logger.exception + raise ... from None. - Tests: config guard asserting DATABASE_CONNECTION_STRING uses the session pooler port 5432; 502 failure-path tests for summary/visits/searches verifying detail string and traceback logging. - Frontend: distinct error messages per failure class (401 vs 5xx), persist analytics token only after a successful fetch (clear on 401), and degrade per-section instead of Promise.all blanking the dashboard.
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Add a token-protected analytics dashboard plus pageview/search event tracking across the stack.
app/db.py), analytics helpers with salted IP hashing (app/analytics.py), token-gated read endpoints (/api/analytics/summary|visits|searches), a rate-limited pageview ingest endpoint (POST /api/track/pageview, 120/min), and best-effort search-event recording./analyticsdashboard with an unlock gate, summary stat cards, top lists, and recent-visits / recent-searches tables; a fire-and-forgetPageTrackerbeacon mounted in the root layout; a server-side proxy (/api/analytics) that keeps the backend token out of the browser.CREATE TABLE IF NOT EXISTSforpage_viewsandsearch_eventswithvisited_at DESC/searched_at DESCindexes.test_analytics.py+ frontendAnalyticsPage.test.tsx/PageTracker.test.tsx.Backend
app/db.py(new) — lazy sharedasyncpgpool (get_pool(), min 1 / max 5), refactored out ofapp/search.py.app/analytics.py(new) — analytics domain helpers:hash_ip()— salted SHA-256 of the client IP (not reversible).client_ip()— honorsX-Forwarded-For(set by the Next.js proxy).record_page_view()/record_search()— inserts intopage_views/search_events.get_analytics_summary(),list_page_views(),list_searches().app/main.py:_require_analytics_token()— 503 when the token is not configured, 401 when thex-analytics-tokenheader mismatchesANALYTICS_TOKEN.POST /api/track/pageview—@limiter.limit("120/minute"); recording failures are logged and never fail the client (fire-and-forget).GET /api/analytics/summary|visits|searches— token-gated,limitcapped at 200, 502 on query errors.POST /api/searchnow records asearch_eventsrow per query (best-effort, non-blocking).app/models.py— addsPageView,SearchEvent,TopItem,AnalyticsSummaryPydantic models.app/test/test_analytics.py(new) — covers hashing, token gate, and the analytics handlers.Frontend
app/analytics/page.tsx(new) — client dashboard. Token stored insessionStorage(npmatch.analytics.token); statesgate → loading → error → done; renders stat cards, top queries/frameworks/referrers, recent visitors, and recent searches.app/api/analytics/route.ts(new) — server-side proxy for?kind=summary|visits|searches&limit=N; enforces the token againstprocess.env.ANALYTICS_TOKEN, forwards upstream, returns the upstream status on failure.app/api/track/pageview/route.ts(new) — forwards the pageview beacon with the real client IP + user agent.components/PageTracker.tsx(new) —keepalivePOST beacon mounted inapp/layout.tsx; swallows errors so analytics never breaks a page.lib/analytics.ts(new) —fetchAnalytics(token)=Promise.allover the three proxy kinds (cache: "no-store").app/api/search/route.ts— now forwardsx-forwarded-for/x-real-ipanduser-agentso backend search events capture the real client IP instead of the proxy IP.types/index.ts— addsTopItem,AnalyticsSummary,PageView,SearchEvent,AnalyticsData.components/test/AnalyticsPage.test.tsx,components/test/PageTracker.test.tsx.Ingestion
ingestion/src/upsert.ts—ensurePgTable()now also createspage_viewsandsearch_events(idempotent) plusvisited_at DESC/searched_at DESCindexes.Config / misc
backend/.env.example— documentsANALYTICS_TOKEN..gitignore— ignores*.log.frontend/CLAUDE.md— Next.js 16 + Turbopack notes and stale-node_modulestroubleshooting.Blocker resolved (2026-08-13)
The
/analytics502 blocker is RESOLVED. Root cause: the backend DB used Supabase's transaction pooler (:6543), which breaks asyncpg's named prepared statements (DuplicatePreparedStatementError) → intermittent 502s. Fixed by switchingDATABASE_CONNECTION_STRINGto the session pooler (port 5432); a config-guard test now fails loudly if anyone reintroduces port 6543. Seedoc/analytics-dashboard-blocker.md(kept local).Also in this change set:
logger.exception+raise ... from None).test_config.py) + 502 failure-path tests for summary/visits/searches.Promise.allblanking the dashboard.Verified live: 40× summary + 20× visits + 20× searches under 8-way concurrency → 0 failures; no-token → 401; search streams on cloud Qdrant.
Checklist
make test(backend pytest) passes — 40/40npm test(frontend Jest) passes — 35/35make lint+npm run lintclean/analyticsunlocks with the token and renders (blocker verified end-to-end 2026-08-13)