Files
Webklar-Kundenbereich/analytics/src/stats.js
T

70 lines
2.9 KiB
JavaScript

// Statistik für das Portal: Vergangenheit aus `daily`, noch nicht verdichtete Tage live aus den Rohdaten.
import { dayOf, aggregateRows } from './jobs.js';
const DAY_MS = 86400 * 1000;
const TOP = 20;
// Liste der Kalendertage (YYYY-MM-DD), oldest first, endend am Berlin-Tag von `now`.
function dayList(now, range) {
const [y, m, d] = dayOf(now).split('-').map(Number);
const days = [];
for (let i = range - 1; i >= 0; i--) days.push(new Date(Date.UTC(y, m - 1, d - i)).toISOString().slice(0, 10));
return days;
}
const top = (map) => [...map].map(([key, count]) => ({ key, count })).filter((e) => e.count > 0)
.sort((a, b) => b.count - a.count || (a.key < b.key ? -1 : 1)).slice(0, TOP);
export function computeStats(db, site, range, now = Date.now()) {
const days = dayList(now, range);
const first = days[0];
const inRange = new Set(days);
const pvByDay = new Map(days.map((d) => [d, 0]));
const dims = {};
const bump = (dim, key, n) => {
if (!dims[dim]) dims[dim] = new Map();
dims[dim].set(key, (dims[dim].get(key) || 0) + n);
};
const apply = (day, dim, key, n) => {
if (dim === 'pv_total') pvByDay.set(day, pvByDay.get(day) + n);
bump(dim, key, n);
};
const done = new Set(db.prepare('SELECT day FROM agg_state WHERE site = ? AND day >= ?').all(site, first).map((r) => r.day));
for (const r of db.prepare('SELECT day, dim, key, count FROM daily WHERE site = ? AND day >= ?').all(site, first)) {
if (inRange.has(r.day)) apply(r.day, r.dim, r.key, r.count);
}
// Tage ohne agg_state (heute, ggf. gestern vor 02:30) live aus den Rohdaten
const live = new Map();
const from = now - (range + 1) * DAY_MS;
for (const r of db.prepare('SELECT ts, type, path, vp, ref, country, device, visitor, sec, scroll, kind FROM events_raw WHERE site = ? AND ts >= ? AND ts <= ? ORDER BY ts')
.all(site, from, now)) {
const d = dayOf(r.ts);
if (!inRange.has(d) || done.has(d)) continue;
if (!live.has(d)) live.set(d, []);
live.get(d).push(r);
}
for (const [day, rows] of live) for (const [dim, key, n] of aggregateRows(rows)) apply(day, dim, key, n);
const sum = (dim) => [...(dims[dim] || new Map()).values()].reduce((a, b) => a + b, 0);
const banner = { shown: sum('bn_shown'), accept: sum('bn_accept'), reject: sum('bn_reject') };
const decided = banner.accept + banner.reject;
const contacts = top(dims.ct_kind || new Map());
return {
totals: {
pv: sum('pv_total'),
// Summe der Tages-Besucher (kein tagesübergreifender Abgleich möglich: Salz wechselt täglich)
visitors: sum('visitors'),
contacts: sum('ct_kind'),
consentRate: decided ? banner.accept / decided : 0,
},
series: days.map((day) => ({ day, pv: pvByDay.get(day) })),
pages: top(dims.pv_path || new Map()),
refs: top(dims.pv_ref || new Map()),
countries: top(dims.pv_country || new Map()),
devices: top(dims.pv_device || new Map()),
contacts,
banner,
};
}