Skip to content

feat(stats): record page views first-party - #3181

Draft
Chia1104 wants to merge 1 commit into
developfrom
feat/page-view-stats
Draft

Chia1104 wants to merge 1 commit into
developfrom
feat/page-view-stats

Conversation

@Chia1104

Copy link
Copy Markdown
Owner

Why

The site should later show article view counts and public usage stats. Analytics vendors cannot supply exact all-time numbers on this plan: Vercel Hobby's query API covers 31 days, and Cloudflare Web Analytics covers about 6 months, sampled. So views are recorded first-party. This PR only collects; nothing reads or displays the data until enough has accumulated.

What changes

Storage

  • chia_page_view (migration 20260927164701_page_view) holds one row per counted view:
    • path, without the locale prefix;
    • feed_id, set when the path is a post or note page, and SET NULL when the feed is deleted;
    • locale;
    • visitor, a daily-salted hash;
    • referrer_host, external referrers only;
    • country, from cf-ipcountry.
  • It is indexed on created_at, (feed_id, created_at) and (path, created_at).
  • @chia/db/repos/stats insertPageView resolves /posts/<slug> or /notes/<slug> to the feed id inside the same insert, via a subselect.

Service (packages/services/stats)

  • stats.view is public, behind callerGuard() and a new stats budget: 60/min for anonymous and guest callers, 120/min for sessions.
  • Visitor: SHA-256 of client IP and user agent, with a random salt kept in KV for one UTC day, truncated to 22 chars. No cookie; a visitor cannot be linked across days.
  • Views that are dropped:
    • repeats of the same page by the same visitor within 30 minutes;
    • crawlers (isbot, a new catalog dependency) and requests without a user agent;
    • API-key callers and the operator (Root).

www

  • PageViewTracker in the [locale] layout sends { path, locale, referrer } on every page. path comes from next-intl's usePathname, so it has no prefix, and referrer is sent only on the landing page. Readers with Do Not Track or Global Privacy Control are skipped. Failures are ignored.
  • Everything happens client-side, so no cached page changes.

packages/services/AGENTS.md and apps/AGENTS.md record the privacy rules.

Deploy order

  1. Run migration 20260927164701_page_view.
  2. Deploy service.
  3. Deploy www.

A www build that reaches an older service gets an unknown-procedure error, which the tracker ignores.

Test plan

  • type:check for @chia/db, @chia/services, service, www and dash; oxlint on changed code
  • Tests: @chia/services (143, 10 new in stats/__tests__/view.service.test.ts), service (120), www (70). The new tests cover:
    • a post view linked to its feed;
    • note and non-feed paths;
    • internal and external referrers;
    • the 30-minute repeat;
    • visitors split by address;
    • crawler, missing user agent, operator and API-key skips;
    • guest and session counting.
  • Throwaway pgvector Postgres with every migration applied, calling insertPageView directly:
    • a post resolves to its feed id;
    • a type mismatch, an unknown slug and a non-feed path store NULL;
    • deleting the feed sets feed_id to NULL and keeps the row.
  • Browser (Playwright) against local www:
    • the landing page sends /contact, en and the external referrer;
    • client navigation sends /projects without a referrer;
    • Do Not Track sends nothing.
  • End to end through a running service. The local stack was down, so verify after deploy that rows appear.

🤖 Generated with Claude Code

Article view counts and public usage stats need exact, all-time numbers
that no analytics vendor gives on this plan, so views are recorded in
our own database. Nothing reads them yet.

- `chia_page_view` stores one row per counted view: path without the
  locale prefix, the post or note it resolves to, locale, a visitor hash,
  external referrer host and Cloudflare's country.
- `stats.view` is public. The visitor is a SHA-256 of address and user
  agent with a salt kept one UTC day in KV, so it never links days and
  no cookie is set. Repeats of a page within 30 minutes, crawlers
  (`isbot`), API-key callers and the operator are dropped. A post or
  note path resolves to its feed inside the insert.
- www's `PageViewTracker` reports each page from the browser, and skips
  readers sending Do Not Track or Global Privacy Control.

Migration 20260927164701_page_view must run before the service deploys.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
chia1104 Ready Ready Preview Sep 27, 2026 4:55pm UTC

@coderabbitai

coderabbitai Bot commented Sep 27, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Next included review available in 12 minutes.

Check out review usage here.

View limit details

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.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 245fcf50-a1a5-46e2-84cd-035919e1ebf0

📥 Commits

Reviewing files that changed from the base of the PR and between bef762c and f8f9f3c.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (19)
  • apps/AGENTS.md
  • apps/www/src/app/[locale]/layout.tsx
  • apps/www/src/components/commons/page-view-tracker.tsx
  • packages/db/.drizzle/migrations/20260927164701_page_view/migration.sql
  • packages/db/.drizzle/migrations/20260927164701_page_view/snapshot.json
  • packages/db/package.json
  • packages/db/src/libs/stats/index.ts
  • packages/db/src/schemas/schema.ts
  • packages/db/src/schemas/stats.schema.ts
  • packages/services/AGENTS.md
  • packages/services/package.json
  • packages/services/router.contract.ts
  • packages/services/router.ts
  • packages/services/shared/rate-limits.ts
  • packages/services/stats/__tests__/view.service.test.ts
  • packages/services/stats/stats.contract.ts
  • packages/services/stats/stats.route.ts
  • packages/services/stats/view.service.ts
  • pnpm-workspace.yaml

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.

This branch was successfully deployed

1 active deployment
Preview — f8f9f3c1 Deployed Sep 27, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant