Skip to content

feat: add privacy-preserving click analytics and a /privacy page - #29

Open
viganogabriele wants to merge 2 commits into
mainfrom
feat/click-analytics
Open

feat: add privacy-preserving click analytics and a /privacy page#29
viganogabriele wants to merge 2 commits into
mainfrom
feat/click-analytics

Conversation

@viganogabriele

Copy link
Copy Markdown

Summary

  • Adds aggregate-only click analytics per URL, stored across 3 new tables (click_analytics_buckets, click_country_analytics, daily_unique_click_dedup) — never raw IP, User-Agent, or per-click timestamps.
  • Country is read only from trusted edge/CDN headers (Cloudflare cf-ipcountry, Vercel x-vercel-ip-country, or a configurable GEO_COUNTRY_HEADER) — no third-party geolocation lookups.
  • Unique-visitor estimates use a same-day HMAC-SHA256 hash (ANALYTICS_HASH_SECRET) that rotates every UTC day, so it can't track a visitor across days or links; the hash rows are purged after ~24-48h (opportunistic purge on ~2% of clicks, since serverless has no long-lived cron).
  • New endpoint GET /urls/:shortCode/analytics; an "Analytics" button/dialog on each URL row shows total clicks, today's unique estimate, top countries, a recent-trend sparkline, and hour/day/month bar charts (hand-rolled, no charting dependency added).
  • New /privacy page disclosing exactly what is and isn't collected, linked from the analytics dialog.
  • Click recording happens in next/server's after() so it never delays or breaks the redirect; failures are logged generically (no IP/UA ever logged).

Scope decisions

  • Simplified vs. vigab.cc: upstream tracks analytics per-scope (whole link / primary code / each alias) because that repo has an alias feature. This repo has no alias concept, so this port uses a single scope per URL — simpler schema and queries. If/when an alias feature is added later, extending this schema with a scope column is a small, additive migration; not worth the complexity now.
  • Privacy page placeholders: the "data controller" contact (info@polinetwork.org, marked with a TODO(PoliNetwork)) and the "Hosting" section (deliberately generic — I don't know PoliNetwork's actual infrastructure providers, unlike vigab.cc's Vercel/Cloudflare-specific version) need PoliNetwork to confirm/fill in before this page is relied on as a complete legal disclosure. Flagging this explicitly rather than fabricating specifics.
  • ANALYTICS_HASH_SECRET is a required new env var (min 16 chars) — vigab.cc falls back to its NEXTAUTH_SECRET (an auth secret this repo doesn't have), so a dedicated required secret is the cleaner fit here. Documented in .env.example.

Dependency

Builds on #27 (vitest + pg-mem test infra), same as #28 (tagging) — both add tests that need that harness. If #27 hasn't merged yet, this branch includes its commit; rebase onto main once #27 merges.

Test plan

  • pnpm test — 25 tests pass (analytics bucketing/dedup/purge logic, geo header parsing)
  • pnpm check (biome) passes
  • pnpm build succeeds
  • Manual: click a short link a few times, open its Analytics dialog, confirm counts/charts/countries render; visit /privacy

viganogabriele and others added 2 commits August 19, 2026 00:34
Introduces the test harness (vitest, an in-memory Postgres via pg-mem, and
`test`/`test:watch` scripts) with no real DB or secrets required, so future
features can ship with unit tests. Included a UrlService smoke-test suite
covering existing create/read/update/delete/search/sort/click-count behavior
as the first consumer of the harness.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds aggregate-only click analytics per URL: hourly/daily/monthly click
buckets, country-level breakdowns (via trusted edge/CDN headers only, never
third-party IP geolocation), and a same-day unique-visitor estimate derived
from a rotating HMAC hash. No raw IP address, User-Agent, or per-click
timestamp is ever stored — the hash itself is purged within ~24-48h.

New REST endpoint GET /urls/:shortCode/analytics, an "Analytics" dialog with
hand-rolled bar charts (no charting dependency), and a /privacy page
disclosing exactly what is and isn't collected.

Scoped down from vigab.cc's version: that implementation also tracked
per-alias analytics scopes (this repo has no alias/custom-domain concept),
so this port is a single scope per URL — a later alias feature can extend
the schema with a scope column if needed, rather than carrying unused
complexity now.

The privacy page's data-controller contact and hosting section use
PoliNetwork placeholders (marked with TODOs) since only PoliNetwork can
confirm the real contact address and infrastructure providers — do not treat
this page as a complete legal disclosure until those are filled in.

Builds on the vitest/pg-mem test infrastructure branch for its test suite.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@viganogabriele, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 32 minutes

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4dfd76f8-9ddc-4779-9300-f71ec7adfa9b

📥 Commits

Reviewing files that changed from the base of the PR and between 8fb680c and 02ebb17.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (21)
  • .env.example
  • package.json
  • src/app/[shortCode]/page.tsx
  • src/app/api/[...ts-rest]/route.ts
  • src/app/privacy/page.tsx
  • src/components/analytics-chart.tsx
  • src/components/analytics-dialog.tsx
  • src/components/dashboard.tsx
  • src/components/url-record-row.tsx
  • src/env.ts
  • src/hooks/urls.ts
  • src/lib/analytics-schema.ts
  • src/lib/analytics-service.test.ts
  • src/lib/analytics-service.ts
  • src/lib/contract.ts
  • src/lib/db.ts
  • src/lib/geo.test.ts
  • src/lib/geo.ts
  • src/lib/schemas.ts
  • src/lib/url-service.test.ts
  • vitest.config.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to 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