Add dev-side pageview analytics via proxy.ts + Postgres, with an Admin Analytics tab - #208
Merged
Conversation
…n Analytics tab We wanted lightweight, self-hosted pageview analytics without embedding a third-party script or cookie on the live site — GA was ruled out for exactly that reason. Pageviews are now captured server-side by a Next.js request interceptor and written to a new PageView table; the data is surfaced only in a new admin-only Analytics tab (pageviews-over-time chart, unique visitors, top pages, top referrers). visitor_hash is a daily-rotating, non-reversible hash of IP + user agent — no raw IP is stored and no cookie is set. The ingest endpoint is unauthenticated (the interceptor has no admin session) but gated by a shared secret header plus a per-IP rate limit; the admin summary endpoint follows the existing get_current_user pattern used by every other admin router. Built as proxy.ts rather than middleware.ts: Next.js 16 renamed the convention and made Node.js the only, non-configurable runtime for it, which the original plan hadn't accounted for. Verified: 588/588 backend tests pass (15 new), ruff clean, tsc clean, vitest passes, and `next build` succeeds with /admin/analytics in the static route table and Proxy (Middleware) correctly registered. Fixes #206
The new pageview ingest endpoint needs a secret shared between the frontend's proxy.ts and the backend, following the same pattern already used for JWT_SECRET: apply-secrets.sh now generates/reuses it in the senate-secrets OpenShift Secret, and template.yaml injects it into both the backend and frontend Deployments via secretKeyRef.
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 free
to 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.
We wanted lightweight, self-hosted pageview analytics for the site — similar in spirit to Google Analytics, but without embedding a third-party script or cookie on the live pages users see. GA was ruled out because it ships visitor data off our infra and requires cookie consent, which is disproportionate for a UNC-affiliated public site. Instead, pageviews are captured server-side and surfaced only in a new admin-only tab — nothing changes about what a visitor's browser loads.
Changes:
proxy.tsrequest interceptor that fires a non-blocking POST per real pageview (prefetches filtered out), because that's the only way to observe traffic without adding a client-side scriptPageViewtable plusPOST /api/analytics/pageviewfor ingest, protected by a shared secret header and a per-IP rate limit, since the proxy has no admin session to authenticate withGET /api/admin/analytics/summary(daily pageviews, unique visitors, top pages, top referrers), gated by the existingget_current_useradmin auth like every other admin routevisitor_hashis a daily-rotating, non-reversible hash of IP + user agent — no raw IP is stored and no cookie is set, since unique-visitor counting shouldn't require eitherproxy.tsrather thanmiddleware.ts— Next.js 16 renamed the convention and made Node.js the only, non-configurable runtime for it, which the original issue plan hadn't accounted forANALYTICS_INGEST_SECRETintodeploy/cloudapps/scripts/apply-secrets.shandtemplate.yaml(samesecretKeyRefpattern asJWT_SECRET), because adding the env var to the app alone doesn't reach the running OpenShift podsDeployment — action needed after merge: run
./deploy/cloudapps/scripts/apply-environment.sh senateonce (requiresocaccess) so the new secret gets generated and injected into both Deployments. Safe to merge without doing this right away — if the secret isn't present yet, the backend ingest route returns 503 and the frontend proxy just skips sending events, so nothing breaks in the meantime.Verified: 588/588 backend tests pass (15 new), ruff clean, tsc clean, vitest passes,
next buildsucceeds.Closes #206