From 0cc3dc808ec18ecfe090060e2005cee48229038e Mon Sep 17 00:00:00 2001 From: Arlind Ukshini Date: Mon, 27 Apr 2026 10:29:46 +0200 Subject: [PATCH] events: add events table and prepared statements --- src/db.js | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/db.js b/src/db.js index 3e34174..1fda85c 100644 --- a/src/db.js +++ b/src/db.js @@ -56,6 +56,22 @@ db.exec(` CREATE INDEX IF NOT EXISTS idx_bifrost_joins_email ON bifrost_joins(email); CREATE INDEX IF NOT EXISTS idx_bifrost_joins_clicked_at ON bifrost_joins(clicked_at); + + CREATE TABLE IF NOT EXISTS events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + event_type TEXT NOT NULL, + email TEXT NOT NULL, + occurred_at INTEGER NOT NULL, + session_id TEXT, + device_type TEXT, + os TEXT, + browser TEXT, + user_agent TEXT, + meta TEXT + ); + + CREATE INDEX IF NOT EXISTS idx_events_email ON events(email); + CREATE INDEX IF NOT EXISTS idx_events_type_time ON events(event_type, occurred_at); `); // ─── Migrations ────────────────────────────────────────────── @@ -173,6 +189,49 @@ export const q = { countJoins: db.prepare(`SELECT COUNT(*) AS n FROM bifrost_joins`), countUniqueJoiners: db.prepare(`SELECT COUNT(DISTINCT email) AS n FROM bifrost_joins`), + // events — engagement tracking. One row per landmark event + // (login, timeline_view) with device fields parsed from the UA. + // Read-only from the app side; written once per event, never updated. + recordEvent: db.prepare( + `INSERT INTO events + (event_type, email, occurred_at, session_id, device_type, os, browser, user_agent, meta) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)` + ), + listEvents: db.prepare( + `SELECT id, event_type, email, occurred_at, session_id, device_type, os, browser, user_agent, meta + FROM events ORDER BY occurred_at DESC LIMIT ?` + ), + listEventsByType: db.prepare( + `SELECT id, event_type, email, occurred_at, session_id, device_type, os, browser, user_agent, meta + FROM events WHERE event_type = ? ORDER BY occurred_at DESC LIMIT ?` + ), + listEventsForEmail: db.prepare( + `SELECT id, event_type, occurred_at, session_id, device_type, os, browser, meta + FROM events WHERE email = ? ORDER BY occurred_at DESC` + ), + // Per-user summary: pivot login + timeline_view counts onto one row. + summariseEvents: db.prepare( + `SELECT email, + SUM(CASE WHEN event_type = 'login' THEN 1 ELSE 0 END) AS logins, + SUM(CASE WHEN event_type = 'timeline_view' THEN 1 ELSE 0 END) AS timeline_views, + MAX(occurred_at) AS last_seen + FROM events + GROUP BY email + ORDER BY last_seen DESC` + ), + countEventsByType: db.prepare( + `SELECT event_type, + COUNT(*) AS total, + COUNT(DISTINCT email) AS unique_users + FROM events GROUP BY event_type ORDER BY event_type` + ), + deviceBreakdown: db.prepare( + `SELECT device_type, COUNT(*) AS n + FROM events + WHERE device_type IS NOT NULL + GROUP BY device_type ORDER BY n DESC` + ), + // cleanup cleanup: { sessions: db.prepare('DELETE FROM sessions WHERE expires_at < ?'),