Admin step A: privacy-respecting first-party event logging
- events table (kind, article_id, visitor_hash, day) with a UNIQUE key that dedups to one row per visitor-day — caps volume and makes counts mean distinct visitor-days. NO ip/ua/referrer/url. Groupings derived from article_id at query time, never stored. - POST /api/events (public): whitelisted kinds (visit/open/share_ub/copy_source/ native_share/source_click); visitor token hashed server-side (never raw). - Frontend analytics.js: random localStorage visitor token; track() via sendBeacon; visit once/day; open on article click; share_ub/copy_source/native_share from the share menu; /a landing pages fire source_click. 127 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -205,6 +205,24 @@ CREATE TABLE IF NOT EXISTS article_summaries (
|
||||
model TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Privacy-respecting, first-party analytics. NO IP / user-agent / referrer / raw
|
||||
-- URL. visitor_hash is a hash of a random localStorage token (never email/IP).
|
||||
-- The UNIQUE key dedups to one row per (kind, article, visitor, day) — that both
|
||||
-- caps volume and makes counts mean "distinct visitor-days". Groupings are derived
|
||||
-- from article_id at query time, never stored here.
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
kind TEXT NOT NULL,
|
||||
article_id INTEGER NOT NULL DEFAULT 0,
|
||||
visitor_hash TEXT NOT NULL DEFAULT '',
|
||||
day TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE (kind, article_id, visitor_hash, day)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_events_day ON events(day);
|
||||
CREATE INDEX IF NOT EXISTS idx_events_kind ON events(kind);
|
||||
CREATE INDEX IF NOT EXISTS idx_events_article ON events(article_id);
|
||||
"""
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user