70 lines
2.9 KiB
JavaScript
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,
|
|
};
|
|
}
|