116 lines
4.3 KiB
JavaScript
116 lines
4.3 KiB
JavaScript
#!/usr/bin/env node
|
|
// ─────────────────────────────────────────────────────────────
|
|
// bin/events.js — read the engagement-event log.
|
|
//
|
|
// Records every landmark event (login, timeline_view) with the
|
|
// user's email, device fields parsed from the UA, and the session
|
|
// ID at time-of-event.
|
|
//
|
|
// Usage:
|
|
// node bin/events.js list [--type <event>] [--limit <n>]
|
|
// node bin/events.js summary # per-user counts
|
|
// node bin/events.js for <email> # full history for one user
|
|
// node bin/events.js stats # totals + device breakdown
|
|
// ─────────────────────────────────────────────────────────────
|
|
import { q } from '../src/db.js';
|
|
|
|
const args = process.argv.slice(2);
|
|
const cmd = args[0];
|
|
const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
|
|
|
|
function help() {
|
|
console.log('Usage:');
|
|
console.log(' events list [--type <event>] [--limit <n>]');
|
|
console.log(' events summary # per-user counts');
|
|
console.log(' events for <email> # event history for a user');
|
|
console.log(' events stats # totals + device breakdown');
|
|
process.exit(1);
|
|
}
|
|
|
|
function iso(t) { return new Date(t).toISOString(); }
|
|
function shortSid(s) { return s ? `[${s.slice(0, 8)}…]` : '[—]'; }
|
|
|
|
function parseFlag(name) {
|
|
const i = args.indexOf(name);
|
|
return i >= 0 ? args[i + 1] : null;
|
|
}
|
|
|
|
function parseMeta(s) {
|
|
if (!s) return null;
|
|
try { return JSON.parse(s); } catch { return s; }
|
|
}
|
|
|
|
function metaCompact(m) {
|
|
if (!m) return '';
|
|
if (typeof m !== 'object') return String(m);
|
|
return Object.entries(m).map(([k, v]) => `${k}=${v}`).join(' ');
|
|
}
|
|
|
|
switch (cmd) {
|
|
case 'list': {
|
|
const type = parseFlag('--type');
|
|
const limit = Number(parseFlag('--limit') || 200);
|
|
const rows = type
|
|
? q.listEventsByType.all(type, limit)
|
|
: q.listEvents.all(limit);
|
|
if (rows.length === 0) { console.log('(no events yet)'); break; }
|
|
for (const r of rows) {
|
|
const dev = [r.device_type, r.os, r.browser].filter(Boolean).join('/') || '?';
|
|
const meta = metaCompact(parseMeta(r.meta));
|
|
console.log(
|
|
` ${iso(r.occurred_at)} ${r.event_type.padEnd(14)} ${r.email.padEnd(28)} ${dev.padEnd(24)} ${shortSid(r.session_id)} ${meta}`
|
|
);
|
|
}
|
|
console.log(`\n${rows.length} event${rows.length === 1 ? '' : 's'} shown.`);
|
|
break;
|
|
}
|
|
|
|
case 'summary': {
|
|
const rows = q.summariseEvents.all();
|
|
if (rows.length === 0) { console.log('(no events yet)'); break; }
|
|
console.log(' LOGINS TIMELINE LAST SEEN EMAIL');
|
|
for (const r of rows) {
|
|
const lg = String(r.logins).padStart(6);
|
|
const tv = String(r.timeline_views).padStart(8);
|
|
console.log(` ${lg} ${tv} ${iso(r.last_seen)} ${r.email}`);
|
|
}
|
|
console.log(`\n${rows.length} unique user${rows.length === 1 ? '' : 's'}.`);
|
|
break;
|
|
}
|
|
|
|
case 'for': {
|
|
const arg = args[1];
|
|
if (!arg || !EMAIL_RE.test(arg)) help();
|
|
const email = arg.trim().toLowerCase();
|
|
const rows = q.listEventsForEmail.all(email);
|
|
if (rows.length === 0) { console.log(`(no events for ${email})`); break; }
|
|
console.log(`Events for ${email}:`);
|
|
for (const r of rows) {
|
|
const dev = [r.device_type, r.os, r.browser].filter(Boolean).join('/') || '?';
|
|
const meta = metaCompact(parseMeta(r.meta));
|
|
console.log(` ${iso(r.occurred_at)} ${r.event_type.padEnd(14)} ${dev.padEnd(24)} ${shortSid(r.session_id)} ${meta}`);
|
|
}
|
|
console.log(`\n${rows.length} event${rows.length === 1 ? '' : 's'}.`);
|
|
break;
|
|
}
|
|
|
|
case 'stats': {
|
|
const byType = q.countEventsByType.all();
|
|
if (byType.length === 0) { console.log('(no events yet)'); break; }
|
|
console.log(' EVENT TYPE TOTAL UNIQUE USERS');
|
|
for (const r of byType) {
|
|
console.log(` ${r.event_type.padEnd(14)} ${String(r.total).padStart(5)} ${String(r.unique_users).padStart(12)}`);
|
|
}
|
|
const dev = q.deviceBreakdown.all();
|
|
if (dev.length > 0) {
|
|
console.log('\n DEVICE TYPE COUNT');
|
|
for (const r of dev) {
|
|
console.log(` ${r.device_type.padEnd(14)} ${String(r.n).padStart(5)}`);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
default:
|
|
help();
|
|
}
|