feat: add analytics dashboard with pageview tracking and query metrics - #21

Merged
kodingkin merged 4 commits into
mainfrom
feat/analytics
Aug 12, 2026
Merged

feat: add analytics dashboard with pageview tracking and query metrics#21
kodingkin merged 4 commits into
mainfrom
feat/analytics

Conversation

@kodingkin

@kodingkinkodingkin commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Summary

Add a token-protected analytics dashboard plus pageview/search event tracking across the stack.

  • Backend: shared asyncpg pool (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.
  • Frontend: /analytics dashboard with an unlock gate, summary stat cards, top lists, and recent-visits / recent-searches tables; a fire-and-forget PageTracker beacon mounted in the root layout; a server-side proxy (/api/analytics) that keeps the backend token out of the browser.
  • Ingestion: idempotent CREATE TABLE IF NOT EXISTS for page_views and search_events with visited_at DESC / searched_at DESC indexes.
  • Tests: backend test_analytics.py + frontend AnalyticsPage.test.tsx / PageTracker.test.tsx.

Backend

  • app/db.py (new) — lazy shared asyncpg pool (get_pool(), min 1 / max 5), refactored out of app/search.py.
  • app/analytics.py (new) — analytics domain helpers:
    • hash_ip() — salted SHA-256 of the client IP (not reversible).
    • client_ip() — honors X-Forwarded-For (set by the Next.js proxy).
    • record_page_view() / record_search() — inserts into page_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 the x-analytics-token header mismatches ANALYTICS_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, limit capped at 200, 502 on query errors.
    • POST /api/search now records a search_events row per query (best-effort, non-blocking).
  • app/models.py — adds PageView, SearchEvent, TopItem, AnalyticsSummary Pydantic 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 in sessionStorage (npmatch.analytics.token); states gate → 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 against process.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) — keepalive POST beacon mounted in app/layout.tsx; swallows errors so analytics never breaks a page.
  • lib/analytics.ts (new) — fetchAnalytics(token) = Promise.all over the three proxy kinds (cache: "no-store").
  • app/api/search/route.ts — now forwards x-forwarded-for/x-real-ip and user-agent so backend search events capture the real client IP instead of the proxy IP.
  • types/index.ts — adds TopItem, AnalyticsSummary, PageView, SearchEvent, AnalyticsData.
  • Tests: components/test/AnalyticsPage.test.tsx, components/test/PageTracker.test.tsx.

Ingestion

  • ingestion/src/upsert.tsensurePgTable() now also creates page_views and search_events (idempotent) plus visited_at DESC / searched_at DESC indexes.

Config / misc

  • backend/.env.example — documents ANALYTICS_TOKEN.
  • .gitignore — ignores *.log.
  • frontend/CLAUDE.md — Next.js 16 + Turbopack notes and stale-node_modules troubleshooting.

Blocker resolved (2026-08-13)

The /analytics 502 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 switching DATABASE_CONNECTION_STRING to the session pooler (port 5432); a config-guard test now fails loudly if anyone reintroduces port 6543. See doc/analytics-dashboard-blocker.md (kept local).

Also in this change set:

  • Backend 502 routes now log full tracebacks (logger.exception + raise ... from None).
  • Regression tests: config guard (test_config.py) + 502 failure-path tests for summary/visits/searches.
  • FE resilience: distinct 401 vs 5xx error messages, token persisted only after a successful fetch (cleared on 401), per-section degradation instead of Promise.all blanking 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/40
  • npm test (frontend Jest) passes — 35/35
  • make lint + npm run lint clean
  • Manual: /analytics unlocks with the token and renders (blocker verified end-to-end 2026-08-13)

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.
@vercel

vercelBot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
npmatchReadyReadyPreviewAug 12, 2026 10:42pm
npmatch-backendReadyReadyPreviewAug 12, 2026 10:42pm

_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.
@kodingkin
kodingkin merged commit ef7a60b into mainAug 12, 2026
5 checks passed
@kodingkin
kodingkin deleted the feat/analytics branch August 12, 2026 22:47
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@kodingkin
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat: add analytics dashboard with pageview tracking and query metrics - #21

Merged
kodingkin merged 4 commits into
mainfrom
feat/analytics
Aug 12, 2026
Merged

feat: add analytics dashboard with pageview tracking and query metrics#21
kodingkin merged 4 commits into
mainfrom
feat/analytics

Conversation

@kodingkin

@kodingkinkodingkin commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Summary

Add a token-protected analytics dashboard plus pageview/search event tracking across the stack.

  • Backend: shared asyncpg pool (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.
  • Frontend: /analytics dashboard with an unlock gate, summary stat cards, top lists, and recent-visits / recent-searches tables; a fire-and-forget PageTracker beacon mounted in the root layout; a server-side proxy (/api/analytics) that keeps the backend token out of the browser.
  • Ingestion: idempotent CREATE TABLE IF NOT EXISTS for page_views and search_events with visited_at DESC / searched_at DESC indexes.
  • Tests: backend test_analytics.py + frontend AnalyticsPage.test.tsx / PageTracker.test.tsx.

Backend

  • app/db.py (new) — lazy shared asyncpg pool (get_pool(), min 1 / max 5), refactored out of app/search.py.
  • app/analytics.py (new) — analytics domain helpers:
    • hash_ip() — salted SHA-256 of the client IP (not reversible).
    • client_ip() — honors X-Forwarded-For (set by the Next.js proxy).
    • record_page_view() / record_search() — inserts into page_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 the x-analytics-token header mismatches ANALYTICS_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, limit capped at 200, 502 on query errors.
    • POST /api/search now records a search_events row per query (best-effort, non-blocking).
  • app/models.py — adds PageView, SearchEvent, TopItem, AnalyticsSummary Pydantic 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 in sessionStorage (npmatch.analytics.token); states gate → 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 against process.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) — keepalive POST beacon mounted in app/layout.tsx; swallows errors so analytics never breaks a page.
  • lib/analytics.ts (new) — fetchAnalytics(token) = Promise.all over the three proxy kinds (cache: "no-store").
  • app/api/search/route.ts — now forwards x-forwarded-for/x-real-ip and user-agent so backend search events capture the real client IP instead of the proxy IP.
  • types/index.ts — adds TopItem, AnalyticsSummary, PageView, SearchEvent, AnalyticsData.
  • Tests: components/test/AnalyticsPage.test.tsx, components/test/PageTracker.test.tsx.

Ingestion

  • ingestion/src/upsert.tsensurePgTable() now also creates page_views and search_events (idempotent) plus visited_at DESC / searched_at DESC indexes.

Config / misc

  • backend/.env.example — documents ANALYTICS_TOKEN.
  • .gitignore — ignores *.log.
  • frontend/CLAUDE.md — Next.js 16 + Turbopack notes and stale-node_modules troubleshooting.

Blocker resolved (2026-08-13)

The /analytics 502 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 switching DATABASE_CONNECTION_STRING to the session pooler (port 5432); a config-guard test now fails loudly if anyone reintroduces port 6543. See doc/analytics-dashboard-blocker.md (kept local).

Also in this change set:

  • Backend 502 routes now log full tracebacks (logger.exception + raise ... from None).
  • Regression tests: config guard (test_config.py) + 502 failure-path tests for summary/visits/searches.
  • FE resilience: distinct 401 vs 5xx error messages, token persisted only after a successful fetch (cleared on 401), per-section degradation instead of Promise.all blanking 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/40
  • npm test (frontend Jest) passes — 35/35
  • make lint + npm run lint clean
  • Manual: /analytics unlocks with the token and renders (blocker verified end-to-end 2026-08-13)

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.
@vercel

vercelBot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
npmatchReadyReadyPreviewAug 12, 2026 10:42pm
npmatch-backendReadyReadyPreviewAug 12, 2026 10:42pm

_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.
@kodingkin
kodingkin merged commit ef7a60b into mainAug 12, 2026
5 checks passed
@kodingkin
kodingkin deleted the feat/analytics branch August 12, 2026 22:47
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@kodingkin
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: add analytics dashboard with pageview tracking and query metrics - #21

Merged
kodingkin merged 4 commits into
mainfrom
feat/analytics
Aug 12, 2026
Merged

feat: add analytics dashboard with pageview tracking and query metrics#21
kodingkin merged 4 commits into
mainfrom
feat/analytics

Conversation

@kodingkin

@kodingkinkodingkin commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Summary

Add a token-protected analytics dashboard plus pageview/search event tracking across the stack.

  • Backend: shared asyncpg pool (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.
  • Frontend: /analytics dashboard with an unlock gate, summary stat cards, top lists, and recent-visits / recent-searches tables; a fire-and-forget PageTracker beacon mounted in the root layout; a server-side proxy (/api/analytics) that keeps the backend token out of the browser.
  • Ingestion: idempotent CREATE TABLE IF NOT EXISTS for page_views and search_events with visited_at DESC / searched_at DESC indexes.
  • Tests: backend test_analytics.py + frontend AnalyticsPage.test.tsx / PageTracker.test.tsx.

Backend

  • app/db.py (new) — lazy shared asyncpg pool (get_pool(), min 1 / max 5), refactored out of app/search.py.
  • app/analytics.py (new) — analytics domain helpers:
    • hash_ip() — salted SHA-256 of the client IP (not reversible).
    • client_ip() — honors X-Forwarded-For (set by the Next.js proxy).
    • record_page_view() / record_search() — inserts into page_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 the x-analytics-token header mismatches ANALYTICS_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, limit capped at 200, 502 on query errors.
    • POST /api/search now records a search_events row per query (best-effort, non-blocking).
  • app/models.py — adds PageView, SearchEvent, TopItem, AnalyticsSummary Pydantic 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 in sessionStorage (npmatch.analytics.token); states gate → 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 against process.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) — keepalive POST beacon mounted in app/layout.tsx; swallows errors so analytics never breaks a page.
  • lib/analytics.ts (new) — fetchAnalytics(token) = Promise.all over the three proxy kinds (cache: "no-store").
  • app/api/search/route.ts — now forwards x-forwarded-for/x-real-ip and user-agent so backend search events capture the real client IP instead of the proxy IP.
  • types/index.ts — adds TopItem, AnalyticsSummary, PageView, SearchEvent, AnalyticsData.
  • Tests: components/test/AnalyticsPage.test.tsx, components/test/PageTracker.test.tsx.

Ingestion

  • ingestion/src/upsert.tsensurePgTable() now also creates page_views and search_events (idempotent) plus visited_at DESC / searched_at DESC indexes.

Config / misc

  • backend/.env.example — documents ANALYTICS_TOKEN.
  • .gitignore — ignores *.log.
  • frontend/CLAUDE.md — Next.js 16 + Turbopack notes and stale-node_modules troubleshooting.

Blocker resolved (2026-08-13)

The /analytics 502 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 switching DATABASE_CONNECTION_STRING to the session pooler (port 5432); a config-guard test now fails loudly if anyone reintroduces port 6543. See doc/analytics-dashboard-blocker.md (kept local).

Also in this change set:

  • Backend 502 routes now log full tracebacks (logger.exception + raise ... from None).
  • Regression tests: config guard (test_config.py) + 502 failure-path tests for summary/visits/searches.
  • FE resilience: distinct 401 vs 5xx error messages, token persisted only after a successful fetch (cleared on 401), per-section degradation instead of Promise.all blanking 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/40
  • npm test (frontend Jest) passes — 35/35
  • make lint + npm run lint clean
  • Manual: /analytics unlocks with the token and renders (blocker verified end-to-end 2026-08-13)

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.
@vercel

vercelBot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
npmatchReadyReadyPreviewAug 12, 2026 10:42pm
npmatch-backendReadyReadyPreviewAug 12, 2026 10:42pm

_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.
@kodingkin
kodingkin merged commit ef7a60b into mainAug 12, 2026
5 checks passed
@kodingkin
kodingkin deleted the feat/analytics branch August 12, 2026 22:47
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@kodingkin
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: add analytics dashboard with pageview tracking and query metrics - #21

Merged
kodingkin merged 4 commits into
mainfrom
feat/analytics
Aug 12, 2026
Merged

feat: add analytics dashboard with pageview tracking and query metrics#21
kodingkin merged 4 commits into
mainfrom
feat/analytics

Conversation

@kodingkin

@kodingkinkodingkin commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Summary

Add a token-protected analytics dashboard plus pageview/search event tracking across the stack.

  • Backend: shared asyncpg pool (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.
  • Frontend: /analytics dashboard with an unlock gate, summary stat cards, top lists, and recent-visits / recent-searches tables; a fire-and-forget PageTracker beacon mounted in the root layout; a server-side proxy (/api/analytics) that keeps the backend token out of the browser.
  • Ingestion: idempotent CREATE TABLE IF NOT EXISTS for page_views and search_events with visited_at DESC / searched_at DESC indexes.
  • Tests: backend test_analytics.py + frontend AnalyticsPage.test.tsx / PageTracker.test.tsx.

Backend

  • app/db.py (new) — lazy shared asyncpg pool (get_pool(), min 1 / max 5), refactored out of app/search.py.
  • app/analytics.py (new) — analytics domain helpers:
    • hash_ip() — salted SHA-256 of the client IP (not reversible).
    • client_ip() — honors X-Forwarded-For (set by the Next.js proxy).
    • record_page_view() / record_search() — inserts into page_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 the x-analytics-token header mismatches ANALYTICS_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, limit capped at 200, 502 on query errors.
    • POST /api/search now records a search_events row per query (best-effort, non-blocking).
  • app/models.py — adds PageView, SearchEvent, TopItem, AnalyticsSummary Pydantic 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 in sessionStorage (npmatch.analytics.token); states gate → 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 against process.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) — keepalive POST beacon mounted in app/layout.tsx; swallows errors so analytics never breaks a page.
  • lib/analytics.ts (new) — fetchAnalytics(token) = Promise.all over the three proxy kinds (cache: "no-store").
  • app/api/search/route.ts — now forwards x-forwarded-for/x-real-ip and user-agent so backend search events capture the real client IP instead of the proxy IP.
  • types/index.ts — adds TopItem, AnalyticsSummary, PageView, SearchEvent, AnalyticsData.
  • Tests: components/test/AnalyticsPage.test.tsx, components/test/PageTracker.test.tsx.

Ingestion

  • ingestion/src/upsert.tsensurePgTable() now also creates page_views and search_events (idempotent) plus visited_at DESC / searched_at DESC indexes.

Config / misc

  • backend/.env.example — documents ANALYTICS_TOKEN.
  • .gitignore — ignores *.log.
  • frontend/CLAUDE.md — Next.js 16 + Turbopack notes and stale-node_modules troubleshooting.

Blocker resolved (2026-08-13)

The /analytics 502 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 switching DATABASE_CONNECTION_STRING to the session pooler (port 5432); a config-guard test now fails loudly if anyone reintroduces port 6543. See doc/analytics-dashboard-blocker.md (kept local).

Also in this change set:

  • Backend 502 routes now log full tracebacks (logger.exception + raise ... from None).
  • Regression tests: config guard (test_config.py) + 502 failure-path tests for summary/visits/searches.
  • FE resilience: distinct 401 vs 5xx error messages, token persisted only after a successful fetch (cleared on 401), per-section degradation instead of Promise.all blanking 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/40
  • npm test (frontend Jest) passes — 35/35
  • make lint + npm run lint clean
  • Manual: /analytics unlocks with the token and renders (blocker verified end-to-end 2026-08-13)

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.
@vercel

vercelBot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
npmatchReadyReadyPreviewAug 12, 2026 10:42pm
npmatch-backendReadyReadyPreviewAug 12, 2026 10:42pm

_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.
@kodingkin
kodingkin merged commit ef7a60b into mainAug 12, 2026
5 checks passed
@kodingkin
kodingkin deleted the feat/analytics branch August 12, 2026 22:47
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@kodingkin
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat: add analytics dashboard with pageview tracking and query metrics - #21

Merged
kodingkin merged 4 commits into
mainfrom
feat/analytics
Aug 12, 2026
Merged

feat: add analytics dashboard with pageview tracking and query metrics#21
kodingkin merged 4 commits into
mainfrom
feat/analytics

Conversation

@kodingkin

@kodingkinkodingkin commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Summary

Add a token-protected analytics dashboard plus pageview/search event tracking across the stack.

  • Backend: shared asyncpg pool (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.
  • Frontend: /analytics dashboard with an unlock gate, summary stat cards, top lists, and recent-visits / recent-searches tables; a fire-and-forget PageTracker beacon mounted in the root layout; a server-side proxy (/api/analytics) that keeps the backend token out of the browser.
  • Ingestion: idempotent CREATE TABLE IF NOT EXISTS for page_views and search_events with visited_at DESC / searched_at DESC indexes.
  • Tests: backend test_analytics.py + frontend AnalyticsPage.test.tsx / PageTracker.test.tsx.

Backend

  • app/db.py (new) — lazy shared asyncpg pool (get_pool(), min 1 / max 5), refactored out of app/search.py.
  • app/analytics.py (new) — analytics domain helpers:
    • hash_ip() — salted SHA-256 of the client IP (not reversible).
    • client_ip() — honors X-Forwarded-For (set by the Next.js proxy).
    • record_page_view() / record_search() — inserts into page_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 the x-analytics-token header mismatches ANALYTICS_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, limit capped at 200, 502 on query errors.
    • POST /api/search now records a search_events row per query (best-effort, non-blocking).
  • app/models.py — adds PageView, SearchEvent, TopItem, AnalyticsSummary Pydantic 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 in sessionStorage (npmatch.analytics.token); states gate → 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 against process.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) — keepalive POST beacon mounted in app/layout.tsx; swallows errors so analytics never breaks a page.
  • lib/analytics.ts (new) — fetchAnalytics(token) = Promise.all over the three proxy kinds (cache: "no-store").
  • app/api/search/route.ts — now forwards x-forwarded-for/x-real-ip and user-agent so backend search events capture the real client IP instead of the proxy IP.
  • types/index.ts — adds TopItem, AnalyticsSummary, PageView, SearchEvent, AnalyticsData.
  • Tests: components/test/AnalyticsPage.test.tsx, components/test/PageTracker.test.tsx.

Ingestion

  • ingestion/src/upsert.tsensurePgTable() now also creates page_views and search_events (idempotent) plus visited_at DESC / searched_at DESC indexes.

Config / misc

  • backend/.env.example — documents ANALYTICS_TOKEN.
  • .gitignore — ignores *.log.
  • frontend/CLAUDE.md — Next.js 16 + Turbopack notes and stale-node_modules troubleshooting.

Blocker resolved (2026-08-13)

The /analytics 502 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 switching DATABASE_CONNECTION_STRING to the session pooler (port 5432); a config-guard test now fails loudly if anyone reintroduces port 6543. See doc/analytics-dashboard-blocker.md (kept local).

Also in this change set:

  • Backend 502 routes now log full tracebacks (logger.exception + raise ... from None).
  • Regression tests: config guard (test_config.py) + 502 failure-path tests for summary/visits/searches.
  • FE resilience: distinct 401 vs 5xx error messages, token persisted only after a successful fetch (cleared on 401), per-section degradation instead of Promise.all blanking 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/40
  • npm test (frontend Jest) passes — 35/35
  • make lint + npm run lint clean
  • Manual: /analytics unlocks with the token and renders (blocker verified end-to-end 2026-08-13)

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.
@vercel

vercelBot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
npmatchReadyReadyPreviewAug 12, 2026 10:42pm
npmatch-backendReadyReadyPreviewAug 12, 2026 10:42pm

_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.
@kodingkin
kodingkin merged commit ef7a60b into mainAug 12, 2026
5 checks passed
@kodingkin
kodingkin deleted the feat/analytics branch August 12, 2026 22:47
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@kodingkin
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: add analytics dashboard with pageview tracking and query metrics - #21

Merged
kodingkin merged 4 commits into
mainfrom
feat/analytics
Aug 12, 2026
Merged

feat: add analytics dashboard with pageview tracking and query metrics#21
kodingkin merged 4 commits into
mainfrom
feat/analytics

Conversation

@kodingkin

@kodingkinkodingkin commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Summary

Add a token-protected analytics dashboard plus pageview/search event tracking across the stack.

  • Backend: shared asyncpg pool (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.
  • Frontend: /analytics dashboard with an unlock gate, summary stat cards, top lists, and recent-visits / recent-searches tables; a fire-and-forget PageTracker beacon mounted in the root layout; a server-side proxy (/api/analytics) that keeps the backend token out of the browser.
  • Ingestion: idempotent CREATE TABLE IF NOT EXISTS for page_views and search_events with visited_at DESC / searched_at DESC indexes.
  • Tests: backend test_analytics.py + frontend AnalyticsPage.test.tsx / PageTracker.test.tsx.

Backend

  • app/db.py (new) — lazy shared asyncpg pool (get_pool(), min 1 / max 5), refactored out of app/search.py.
  • app/analytics.py (new) — analytics domain helpers:
    • hash_ip() — salted SHA-256 of the client IP (not reversible).
    • client_ip() — honors X-Forwarded-For (set by the Next.js proxy).
    • record_page_view() / record_search() — inserts into page_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 the x-analytics-token header mismatches ANALYTICS_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, limit capped at 200, 502 on query errors.
    • POST /api/search now records a search_events row per query (best-effort, non-blocking).
  • app/models.py — adds PageView, SearchEvent, TopItem, AnalyticsSummary Pydantic 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 in sessionStorage (npmatch.analytics.token); states gate → 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 against process.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) — keepalive POST beacon mounted in app/layout.tsx; swallows errors so analytics never breaks a page.
  • lib/analytics.ts (new) — fetchAnalytics(token) = Promise.all over the three proxy kinds (cache: "no-store").
  • app/api/search/route.ts — now forwards x-forwarded-for/x-real-ip and user-agent so backend search events capture the real client IP instead of the proxy IP.
  • types/index.ts — adds TopItem, AnalyticsSummary, PageView, SearchEvent, AnalyticsData.
  • Tests: components/test/AnalyticsPage.test.tsx, components/test/PageTracker.test.tsx.

Ingestion

  • ingestion/src/upsert.tsensurePgTable() now also creates page_views and search_events (idempotent) plus visited_at DESC / searched_at DESC indexes.

Config / misc

  • backend/.env.example — documents ANALYTICS_TOKEN.
  • .gitignore — ignores *.log.
  • frontend/CLAUDE.md — Next.js 16 + Turbopack notes and stale-node_modules troubleshooting.

Blocker resolved (2026-08-13)

The /analytics 502 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 switching DATABASE_CONNECTION_STRING to the session pooler (port 5432); a config-guard test now fails loudly if anyone reintroduces port 6543. See doc/analytics-dashboard-blocker.md (kept local).

Also in this change set:

  • Backend 502 routes now log full tracebacks (logger.exception + raise ... from None).
  • Regression tests: config guard (test_config.py) + 502 failure-path tests for summary/visits/searches.
  • FE resilience: distinct 401 vs 5xx error messages, token persisted only after a successful fetch (cleared on 401), per-section degradation instead of Promise.all blanking 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/40
  • npm test (frontend Jest) passes — 35/35
  • make lint + npm run lint clean
  • Manual: /analytics unlocks with the token and renders (blocker verified end-to-end 2026-08-13)

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.
@vercel

vercelBot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
npmatchReadyReadyPreviewAug 12, 2026 10:42pm
npmatch-backendReadyReadyPreviewAug 12, 2026 10:42pm

_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.
@kodingkin
kodingkin merged commit ef7a60b into mainAug 12, 2026
5 checks passed
@kodingkin
kodingkin deleted the feat/analytics branch August 12, 2026 22:47
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@kodingkin
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: add analytics dashboard with pageview tracking and query metrics - #21

Merged
kodingkin merged 4 commits into
mainfrom
feat/analytics
Aug 12, 2026
Merged

feat: add analytics dashboard with pageview tracking and query metrics#21
kodingkin merged 4 commits into
mainfrom
feat/analytics

Conversation

@kodingkin

@kodingkinkodingkin commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Summary

Add a token-protected analytics dashboard plus pageview/search event tracking across the stack.

  • Backend: shared asyncpg pool (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.
  • Frontend: /analytics dashboard with an unlock gate, summary stat cards, top lists, and recent-visits / recent-searches tables; a fire-and-forget PageTracker beacon mounted in the root layout; a server-side proxy (/api/analytics) that keeps the backend token out of the browser.
  • Ingestion: idempotent CREATE TABLE IF NOT EXISTS for page_views and search_events with visited_at DESC / searched_at DESC indexes.
  • Tests: backend test_analytics.py + frontend AnalyticsPage.test.tsx / PageTracker.test.tsx.

Backend

  • app/db.py (new) — lazy shared asyncpg pool (get_pool(), min 1 / max 5), refactored out of app/search.py.
  • app/analytics.py (new) — analytics domain helpers:
    • hash_ip() — salted SHA-256 of the client IP (not reversible).
    • client_ip() — honors X-Forwarded-For (set by the Next.js proxy).
    • record_page_view() / record_search() — inserts into page_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 the x-analytics-token header mismatches ANALYTICS_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, limit capped at 200, 502 on query errors.
    • POST /api/search now records a search_events row per query (best-effort, non-blocking).
  • app/models.py — adds PageView, SearchEvent, TopItem, AnalyticsSummary Pydantic 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 in sessionStorage (npmatch.analytics.token); states gate → 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 against process.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) — keepalive POST beacon mounted in app/layout.tsx; swallows errors so analytics never breaks a page.
  • lib/analytics.ts (new) — fetchAnalytics(token) = Promise.all over the three proxy kinds (cache: "no-store").
  • app/api/search/route.ts — now forwards x-forwarded-for/x-real-ip and user-agent so backend search events capture the real client IP instead of the proxy IP.
  • types/index.ts — adds TopItem, AnalyticsSummary, PageView, SearchEvent, AnalyticsData.
  • Tests: components/test/AnalyticsPage.test.tsx, components/test/PageTracker.test.tsx.

Ingestion

  • ingestion/src/upsert.tsensurePgTable() now also creates page_views and search_events (idempotent) plus visited_at DESC / searched_at DESC indexes.

Config / misc

  • backend/.env.example — documents ANALYTICS_TOKEN.
  • .gitignore — ignores *.log.
  • frontend/CLAUDE.md — Next.js 16 + Turbopack notes and stale-node_modules troubleshooting.

Blocker resolved (2026-08-13)

The /analytics 502 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 switching DATABASE_CONNECTION_STRING to the session pooler (port 5432); a config-guard test now fails loudly if anyone reintroduces port 6543. See doc/analytics-dashboard-blocker.md (kept local).

Also in this change set:

  • Backend 502 routes now log full tracebacks (logger.exception + raise ... from None).
  • Regression tests: config guard (test_config.py) + 502 failure-path tests for summary/visits/searches.
  • FE resilience: distinct 401 vs 5xx error messages, token persisted only after a successful fetch (cleared on 401), per-section degradation instead of Promise.all blanking 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/40
  • npm test (frontend Jest) passes — 35/35
  • make lint + npm run lint clean
  • Manual: /analytics unlocks with the token and renders (blocker verified end-to-end 2026-08-13)

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.
@vercel

vercelBot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
npmatchReadyReadyPreviewAug 12, 2026 10:42pm
npmatch-backendReadyReadyPreviewAug 12, 2026 10:42pm

_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.
@kodingkin
kodingkin merged commit ef7a60b into mainAug 12, 2026
5 checks passed
@kodingkin
kodingkin deleted the feat/analytics branch August 12, 2026 22:47
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@kodingkin
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat: add analytics dashboard with pageview tracking and query metrics - #21

Merged
kodingkin merged 4 commits into
mainfrom
feat/analytics
Aug 12, 2026
Merged

feat: add analytics dashboard with pageview tracking and query metrics#21
kodingkin merged 4 commits into
mainfrom
feat/analytics

Conversation

@kodingkin

@kodingkinkodingkin commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Summary

Add a token-protected analytics dashboard plus pageview/search event tracking across the stack.

  • Backend: shared asyncpg pool (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.
  • Frontend: /analytics dashboard with an unlock gate, summary stat cards, top lists, and recent-visits / recent-searches tables; a fire-and-forget PageTracker beacon mounted in the root layout; a server-side proxy (/api/analytics) that keeps the backend token out of the browser.
  • Ingestion: idempotent CREATE TABLE IF NOT EXISTS for page_views and search_events with visited_at DESC / searched_at DESC indexes.
  • Tests: backend test_analytics.py + frontend AnalyticsPage.test.tsx / PageTracker.test.tsx.

Backend

  • app/db.py (new) — lazy shared asyncpg pool (get_pool(), min 1 / max 5), refactored out of app/search.py.
  • app/analytics.py (new) — analytics domain helpers:
    • hash_ip() — salted SHA-256 of the client IP (not reversible).
    • client_ip() — honors X-Forwarded-For (set by the Next.js proxy).
    • record_page_view() / record_search() — inserts into page_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 the x-analytics-token header mismatches ANALYTICS_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, limit capped at 200, 502 on query errors.
    • POST /api/search now records a search_events row per query (best-effort, non-blocking).
  • app/models.py — adds PageView, SearchEvent, TopItem, AnalyticsSummary Pydantic 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 in sessionStorage (npmatch.analytics.token); states gate → 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 against process.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) — keepalive POST beacon mounted in app/layout.tsx; swallows errors so analytics never breaks a page.
  • lib/analytics.ts (new) — fetchAnalytics(token) = Promise.all over the three proxy kinds (cache: "no-store").
  • app/api/search/route.ts — now forwards x-forwarded-for/x-real-ip and user-agent so backend search events capture the real client IP instead of the proxy IP.
  • types/index.ts — adds TopItem, AnalyticsSummary, PageView, SearchEvent, AnalyticsData.
  • Tests: components/test/AnalyticsPage.test.tsx, components/test/PageTracker.test.tsx.

Ingestion

  • ingestion/src/upsert.tsensurePgTable() now also creates page_views and search_events (idempotent) plus visited_at DESC / searched_at DESC indexes.

Config / misc

  • backend/.env.example — documents ANALYTICS_TOKEN.
  • .gitignore — ignores *.log.
  • frontend/CLAUDE.md — Next.js 16 + Turbopack notes and stale-node_modules troubleshooting.

Blocker resolved (2026-08-13)

The /analytics 502 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 switching DATABASE_CONNECTION_STRING to the session pooler (port 5432); a config-guard test now fails loudly if anyone reintroduces port 6543. See doc/analytics-dashboard-blocker.md (kept local).

Also in this change set:

  • Backend 502 routes now log full tracebacks (logger.exception + raise ... from None).
  • Regression tests: config guard (test_config.py) + 502 failure-path tests for summary/visits/searches.
  • FE resilience: distinct 401 vs 5xx error messages, token persisted only after a successful fetch (cleared on 401), per-section degradation instead of Promise.all blanking 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/40
  • npm test (frontend Jest) passes — 35/35
  • make lint + npm run lint clean
  • Manual: /analytics unlocks with the token and renders (blocker verified end-to-end 2026-08-13)

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.
@vercel

vercelBot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
npmatchReadyReadyPreviewAug 12, 2026 10:42pm
npmatch-backendReadyReadyPreviewAug 12, 2026 10:42pm

_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.
@kodingkin
kodingkin merged commit ef7a60b into mainAug 12, 2026
5 checks passed
@kodingkin
kodingkin deleted the feat/analytics branch August 12, 2026 22:47
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@kodingkin