SPB Git

spb/admin-ka

Public
4.6 KB · 117 lines javascript
1// admin-ka — Analytics first-party : visiteurs uniques par site KA.2// Un beacon (1px) sur chaque site envoie GET /collect?site=..&vid=..&p=..3// On stocke les hits (SQLite), on compte les uniques (vid distincts) en live,4// et on fige un rollup quotidien pour l'historique long terme.5import fs from 'node:fs';6import path from 'node:path';7import { fileURLToPath } from 'node:url';8import { DatabaseSync } from 'node:sqlite';910const __dirname = path.dirname(fileURLToPath(import.meta.url));11const DATA_DIR = path.join(__dirname, '..', 'data');12const DB_PATH = path.join(DATA_DIR, 'analytics.sqlite');1314let db = null;15const RETENTION_DAYS = 45;1617// GIF transparent 1x1 renvoyé au beacon18export const PIXEL = Buffer.from('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', 'base64');1920function dayKey(ts) {21  // jour local (Amérique/Montréal ~ heure du serveur)22  const d = new Date(ts);23  return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');24}2526export function initAnalytics() {27  fs.mkdirSync(DATA_DIR, { recursive: true });28  db = new DatabaseSync(DB_PATH);29  db.exec(`30    CREATE TABLE IF NOT EXISTS hits (site TEXT, vid TEXT, ts INTEGER, path TEXT);31    CREATE INDEX IF NOT EXISTS idx_hits_site_ts ON hits(site, ts);32    CREATE TABLE IF NOT EXISTS daily (site TEXT, day TEXT, uniques INTEGER, pv INTEGER, PRIMARY KEY(site, day));33  `);34  rollup();35  setInterval(rollup, 10 * 60 * 1000);       // fige les stats quotidiennes36  setInterval(purge, 6 * 60 * 60 * 1000);    // purge le brut ancien37  purge();38}3940// enregistre un hit (appelé par /collect)41export function recordHit(site, vid, p) {42  if (!db || !site || !vid) return;43  site = String(site).slice(0, 40);44  vid = String(vid).slice(0, 64);45  p = String(p || '/').slice(0, 200);46  try { db.prepare('INSERT INTO hits (site,vid,ts,path) VALUES (?,?,?,?)').run(site, vid, Date.now(), p); } catch {}47}4849function purge() {50  if (!db) return;51  const cutoff = Date.now() - RETENTION_DAYS * 864e5;52  try { db.prepare('DELETE FROM hits WHERE ts < ?').run(cutoff); } catch {}53}5455// fige les uniques/pv par jour dans `daily` (sauvegarde long terme)56function rollup() {57  if (!db) return;58  try {59    const rows = db.prepare(`60      SELECT site, ts, vid FROM hits WHERE ts >= ?`).all(Date.now() - 3 * 864e5);61    const agg = {}; // site|day -> {set, pv}62    for (const r of rows) {63      const k = r.site + '|' + dayKey(r.ts);64      if (!agg[k]) agg[k] = { set: new Set(), pv: 0 };65      agg[k].set.add(r.vid); agg[k].pv++;66    }67    const up = db.prepare('INSERT OR REPLACE INTO daily (site,day,uniques,pv) VALUES (?,?,?,?)');68    for (const k in agg) { const [site, day] = k.split('|'); up.run(site, day, agg[k].set.size, agg[k].pv); }69  } catch {}70}7172function uniquesSince(site, sinceTs) {73  try { return db.prepare('SELECT COUNT(DISTINCT vid) c FROM hits WHERE site=? AND ts>=?').get(site, sinceTs).c; } catch { return 0; }74}75function pvSince(site, sinceTs) {76  try { return db.prepare('SELECT COUNT(*) c FROM hits WHERE site=? AND ts>=?').get(site, sinceTs).c; } catch { return 0; }77}7879export function analyticsSummary(sites) {80  const now = Date.now();81  const startToday = new Date(); startToday.setHours(0, 0, 0, 0);82  const t0 = startToday.getTime();83  const out = [];84  let g = { online: 0, today: 0, d7: 0, pv_today: 0 };85  for (const s of sites) {86    const site = s.app || s;87    const row = {88      site, label: s.label || site, domain: s.domain || '',89      online: uniquesSince(site, now - 5 * 60 * 1000),      // uniques dans les 5 dernières min90      today: uniquesSince(site, t0),91      d7: uniquesSince(site, now - 7 * 864e5),92      d30: uniquesSince(site, now - 30 * 864e5),93      pv_today: pvSince(site, t0),94      pv_total: pvSince(site, now - RETENTION_DAYS * 864e5),95    };96    out.push(row);97    g.online += row.online; g.today += row.today; g.d7 += row.d7; g.pv_today += row.pv_today;98  }99  out.sort((a, b) => b.online - a.online || b.today - a.today);100  return { sites: out, global: g, updated: now };101}102103// série quotidienne (uniques) des N derniers jours pour un site104export function analyticsSeries(site, days = 30) {105  const out = [];106  for (let i = days - 1; i >= 0; i--) {107    const d = new Date(); d.setHours(0, 0, 0, 0); d.setDate(d.getDate() - i);108    const key = dayKey(d.getTime());109    let u = 0;110    try { const r = db.prepare('SELECT uniques FROM daily WHERE site=? AND day=?').get(site, key); if (r) u = r.uniques; } catch {}111    // aujourd'hui : recalcule en direct (le rollup peut être en retard)112    if (i === 0) u = Math.max(u, uniquesSince(site, d.getTime()));113    out.push({ day: key, uniques: u });114  }115  return out;116}117
Files · 61f3df0ec2a211ca8122a06ade57b5d9e375c47d
Menu

Theme