Analyse: Zeitraum = abgeschlossene Tage, "Heute bisher" getrennt, Feinschliff
Final-Review-Fixwelle der Cockpit-Übersicht:
- F1 Collector: Der Zeitraum sind die range Kalendertage bis gestern (Berliner Tag),
der Vorzeitraum die range Tage davor; heute steht getrennt in today { day, pv, visitors,
contacts }. Kein Einbruch mehr am rechten Rand von Diagramm und Mini-Verläufen,
kein falsches "x % weniger" tagsüber. Portal: weekdayStats bekommt die ganze Reihe
(completedDays entfällt), "Vergleich ab" = firstDay + 2 x range, Kopfzeile
"Analyse aktiv · Heute bisher: N Aufrufe · Stand: HH:MM Uhr" (alter Collector ohne today
bleibt ohne "Heute bisher").
- F2 "direkt kontaktiert" zählt nur Telefon, E-Mail und WhatsApp (auch im Vorzeitraum),
Links nach außen nicht.
- F3 Mini-Verlauf "Ø Zeit pro Seite": Tage ohne Messung sind Lücken statt 0-Tälern.
- F4 Fehlerfarbe beim Ausschalten rot, --an-faint auf 55 %, Achsenschrift per
Container-Abfragen auf 11-12 px (y-Rand wächst mit der Stellenzahl, x-Beschriftungen mit
Mindestabstand), Ranglisten-Anteile an allen Seitenaufrufen, Tippflächen bis 880 px
mindestens 40 px hoch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
eadb768d1a
commit
0db59d38e9
11 files changed
+376
-135
No files matched your search
@@ -97,10 +97,15 @@ function firstDayOf(db, site) {
|
||||
return fromDaily < fromRaw ? fromDaily : fromRaw;
|
||||
}
|
||||
|
||||
// Der Zeitraum sind die `range` abgeschlossenen Kalendertage, die gestern enden (wie GA4 "Letzte 28 Tage"): Der
|
||||
// angebrochene heutige Tag würde gegen einen vollen Vorzeitraum als Einbruch erscheinen. Heute steht getrennt in `today`.
|
||||
export function computeStats(db, site, range, now = Date.now()) {
|
||||
const days = dayList(dayOf(now), range);
|
||||
const todayDay = dayOf(now);
|
||||
const days = dayList(shiftDay(todayDay, -1), range);
|
||||
const cur = collect(db, site, days, now);
|
||||
const { banner, totals, series } = shape(cur);
|
||||
// Heute bis jetzt: live aus den Rohdaten (der Tag ist noch nicht verdichtet)
|
||||
const t = shape(collect(db, site, [todayDay], now)).series[0];
|
||||
// Vorzeitraum: die `range` Kalendertage direkt vor dem aktuellen Zeitraum (kalendarisch, nie now - range * 24 h)
|
||||
const prevDays = dayList(shiftDay(days[0], -1), range);
|
||||
const prevCollected = collect(db, site, prevDays, now);
|
||||
@@ -113,6 +118,7 @@ export function computeStats(db, site, range, now = Date.now()) {
|
||||
firstDay,
|
||||
totals,
|
||||
series,
|
||||
today: { day: todayDay, pv: t.pv, visitors: t.visitors, contacts: t.contacts },
|
||||
pages: top(cur.dims.pv_path || new Map()),
|
||||
refs: top(cur.dims.pv_ref || new Map()),
|
||||
countries: top(cur.dims.pv_country || new Map()),
|
||||
|
||||
@@ -115,12 +115,13 @@ async function admin(db, now = () => NOW) {
|
||||
return { get, close: () => server.close() };
|
||||
}
|
||||
|
||||
test('Stats-Endpunkt: Form, Serie lückenlos, heute live + Vergangenheit aus daily, consentRate', async () => {
|
||||
test('Stats-Endpunkt: abgeschlossene Tage bis gestern, heute getrennt in today, Vergangenheit aus daily, consentRate', async () => {
|
||||
const { db, ev } = mk();
|
||||
const y = NOW - D;
|
||||
for (let i = 0; i < 3; i++) ev({ ts: y + i, type: 'pv', path: '/a', vp: 'd', ref: 'g.de', country: 'DE', device: 'd' });
|
||||
ev({ ts: y + 10, type: 'en', path: '/a', vp: 'd', visitor: 'v1', sec: 5, scroll: 5 });
|
||||
ev({ ts: y + 11, type: 'bn', kind: 'accept' });
|
||||
ev({ ts: y + 12, type: 'ct', path: '/a', visitor: 'v1', kind: 'tel' });
|
||||
runNightly(db, { now: NOW }); // gestern -> daily
|
||||
ev({ ts: NOW - H, type: 'pv', path: '/b', vp: 'm', country: 'AT', device: 'm' }); // heute live
|
||||
ev({ ts: NOW - H, type: 'ct', path: '/b', visitor: 'v9', kind: 'mail' });
|
||||
@@ -132,60 +133,101 @@ test('Stats-Endpunkt: Form, Serie lückenlos, heute live + Vergangenheit aus dai
|
||||
const r = await a.get(`/a/admin/stats/${KEY}?range=7`);
|
||||
assert.equal(r.status, 200);
|
||||
const j = await r.json();
|
||||
assert.deepEqual(Object.keys(j).sort(), ['banner', 'contacts', 'countries', 'devices', 'firstDay', 'from', 'pages', 'previous', 'range', 'refs', 'series', 'to', 'totals']);
|
||||
assert.deepEqual(Object.keys(j).sort(), ['banner', 'contacts', 'countries', 'devices', 'firstDay', 'from', 'pages', 'previous', 'range', 'refs', 'series', 'to', 'today', 'totals']);
|
||||
// Der Zeitraum sind die 7 abgeschlossenen Tage bis gestern (3.–9.10.); heute (10.10.) steht nur in `today`
|
||||
assert.equal(j.series.length, 7);
|
||||
assert.equal(j.series[0].day, '2026-10-04');
|
||||
assert.equal(j.series[6].day, '2026-10-10');
|
||||
assert.deepEqual(j.series.map((s) => s.pv), [0, 0, 0, 0, 0, 3, 1]);
|
||||
assert.deepEqual(j.totals, { pv: 4, visitors: 2, contacts: 1, consentRate: 2 / 4, engaged: 1, avgSec: 5, avgScroll: 5 });
|
||||
assert.deepEqual(j.series[5], { day: '2026-10-09', pv: 3, visitors: 1, contacts: 0, avgSec: 5 });
|
||||
assert.deepEqual(j.series[6], { day: '2026-10-10', pv: 1, visitors: 1, contacts: 1, avgSec: null });
|
||||
assert.equal(j.from, '2026-10-04'); assert.equal(j.to, '2026-10-10'); assert.equal(j.range, 7);
|
||||
assert.deepEqual(j.pages, [{ key: '/a', count: 3 }, { key: '/b', count: 1 }]);
|
||||
assert.equal(j.series[0].day, '2026-10-03');
|
||||
assert.equal(j.series[6].day, '2026-10-09');
|
||||
assert.deepEqual(j.series.map((s) => s.pv), [0, 0, 0, 0, 0, 0, 3]);
|
||||
assert.deepEqual(j.totals, { pv: 3, visitors: 1, contacts: 1, consentRate: 1, engaged: 1, avgSec: 5, avgScroll: 5 });
|
||||
assert.deepEqual(j.series[6], { day: '2026-10-09', pv: 3, visitors: 1, contacts: 1, avgSec: 5 });
|
||||
assert.deepEqual(j.today, { day: '2026-10-10', pv: 1, visitors: 1, contacts: 1 });
|
||||
assert.equal(j.from, '2026-10-03'); assert.equal(j.to, '2026-10-09'); assert.equal(j.range, 7);
|
||||
assert.deepEqual(j.pages, [{ key: '/a', count: 3 }]);
|
||||
assert.deepEqual(j.refs[0], { key: 'g.de', count: 3 });
|
||||
assert.deepEqual(j.countries, [{ key: 'DE', count: 3 }, { key: 'AT', count: 1 }]);
|
||||
assert.deepEqual(j.devices, [{ key: 'd', count: 3 }, { key: 'm', count: 1 }]);
|
||||
assert.deepEqual(j.contacts, [{ key: 'mail', count: 1 }]);
|
||||
assert.deepEqual(j.banner, { shown: 1, accept: 2, reject: 2 });
|
||||
assert.deepEqual(j.countries, [{ key: 'DE', count: 3 }]);
|
||||
assert.deepEqual(j.devices, [{ key: 'd', count: 3 }]);
|
||||
assert.deepEqual(j.contacts, [{ key: 'tel', count: 1 }]);
|
||||
assert.deepEqual(j.banner, { shown: 0, accept: 1, reject: 0 });
|
||||
// range 90: 90 Tage
|
||||
const j90 = await (await a.get(`/a/admin/stats/${KEY}?range=90`)).json();
|
||||
assert.equal(j90.series.length, 90);
|
||||
assert.equal(j90.totals.pv, 4);
|
||||
assert.equal(j90.series[89].day, '2026-10-09');
|
||||
assert.equal(j90.totals.pv, 3);
|
||||
assert.deepEqual(j90.today, j.today);
|
||||
a.close();
|
||||
});
|
||||
|
||||
test('Stats: today = heutiger Berliner Tag bis jetzt, auch ohne Ereignisse; Mitternacht in Berlin trennt gestern und heute', async () => {
|
||||
const { db, ev } = mk();
|
||||
const a = await admin(db);
|
||||
assert.deepEqual((await (await a.get(`/a/admin/stats/${KEY}?range=7`)).json()).today, { day: '2026-10-10', pv: 0, visitors: 0, contacts: 0 });
|
||||
a.close();
|
||||
// 11.10. 00:10 Berlin (= 10.10. 22:10 UTC, Sommerzeit): 23:00 Berlin am 10.10. gehört zu gestern, 00:05 Berlin am 11.10. zu heute
|
||||
const LATE = Date.UTC(2026, 9, 10, 22, 10);
|
||||
ev({ ts: Date.UTC(2026, 9, 10, 21, 0), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
|
||||
ev({ ts: Date.UTC(2026, 9, 10, 22, 5), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
|
||||
ev({ ts: Date.UTC(2026, 9, 10, 22, 6), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
|
||||
ev({ ts: Date.UTC(2026, 9, 10, 22, 7), type: 'ct', path: '/', visitor: 'v1', kind: 'wa' });
|
||||
ev({ ts: Date.UTC(2026, 9, 10, 23, 0), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' }); // noch in der Zukunft: zählt nicht
|
||||
const b = await admin(db, () => LATE);
|
||||
const j = await (await b.get(`/a/admin/stats/${KEY}?range=7`)).json();
|
||||
assert.equal(j.to, '2026-10-10');
|
||||
assert.equal(j.series[6].day, '2026-10-10'); assert.equal(j.series[6].pv, 1);
|
||||
assert.equal(j.totals.pv, 1);
|
||||
assert.deepEqual(j.today, { day: '2026-10-11', pv: 2, visitors: 1, contacts: 1 });
|
||||
assert.equal(j.contacts.length, 0); // die Kontakt-Aktion von 00:07 ist heute
|
||||
b.close();
|
||||
});
|
||||
|
||||
test('Stats: gestern noch nicht verdichtet wird live gezählt, nie doppelt', async () => {
|
||||
const { db, ev } = mk();
|
||||
ev({ ts: NOW - D, type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
|
||||
ev({ ts: NOW - H, type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' }); // heute: nur in today
|
||||
const a = await admin(db);
|
||||
assert.equal((await (await a.get(`/a/admin/stats/${KEY}?range=7`)).json()).totals.pv, 1);
|
||||
const stats = async () => (await (await a.get(`/a/admin/stats/${KEY}?range=7`)).json());
|
||||
assert.equal((await stats()).totals.pv, 1);
|
||||
runNightly(db, { now: NOW });
|
||||
assert.equal((await (await a.get(`/a/admin/stats/${KEY}?range=7`)).json()).totals.pv, 1);
|
||||
const j = await stats();
|
||||
assert.equal(j.totals.pv, 1);
|
||||
assert.equal(j.today.pv, 1);
|
||||
a.close();
|
||||
});
|
||||
|
||||
test('Stats: Vorzeitraum = die range Kalendertage davor, complete nach firstDay', async () => {
|
||||
test('Stats: Vorzeitraum = die range Kalendertage vor dem aktuellen Zeitraum, complete nach firstDay', async () => {
|
||||
const { db, ev } = mk();
|
||||
// Vorzeitraum zu range 7 am 10.10.: 27.09.–03.10.
|
||||
// aktueller Zeitraum zu range 7 am 10.10.: 03.–09.10.; Vorzeitraum: 26.09.–02.10.
|
||||
ev({ ts: Date.UTC(2026, 8, 30, 10), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
|
||||
ev({ ts: Date.UTC(2026, 9, 1, 10), type: 'en', path: '/', vp: 'd', visitor: 'v1', sec: 20, scroll: 40 });
|
||||
ev({ ts: Date.UTC(2026, 9, 2, 10), type: 'ct', path: '/', visitor: 'v2', kind: 'tel' });
|
||||
const a = await admin(db);
|
||||
let j = await (await a.get(`/a/admin/stats/${KEY}?range=7`)).json();
|
||||
const get = async () => (await (await a.get(`/a/admin/stats/${KEY}?range=7`)).json());
|
||||
let j = await get();
|
||||
assert.equal(j.firstDay, '2026-09-30');
|
||||
assert.equal(j.previous.complete, false);
|
||||
// 26.09. liegt vor dem Vorzeitraum und darf nicht mitzählen
|
||||
// 27.09. liegt noch einen Tag nach dem Beginn des Vorzeitraums (26.09.): weiter unvollständig
|
||||
ev({ ts: Date.UTC(2026, 8, 27, 10), type: 'pv', path: '/x', vp: 'd', country: 'DE', device: 'd' });
|
||||
j = await get();
|
||||
assert.equal(j.firstDay, '2026-09-27');
|
||||
assert.equal(j.previous.complete, false);
|
||||
// genau am ersten Tag des Vorzeitraums: vollständig
|
||||
ev({ ts: Date.UTC(2026, 8, 26, 10), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
|
||||
j = await (await a.get(`/a/admin/stats/${KEY}?range=7`)).json();
|
||||
j = await get();
|
||||
assert.equal(j.firstDay, '2026-09-26');
|
||||
assert.equal(j.previous.complete, true);
|
||||
assert.equal(j.previous.from, '2026-09-27'); assert.equal(j.previous.to, '2026-10-03');
|
||||
assert.equal(j.previous.series.length, 7); assert.equal(j.previous.series[3].day, '2026-09-30');
|
||||
assert.equal(j.previous.totals.pv, 1);
|
||||
// 25.09. liegt vor dem Vorzeitraum und darf nicht mitzählen
|
||||
ev({ ts: Date.UTC(2026, 8, 25, 10), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
|
||||
j = await get();
|
||||
assert.equal(j.firstDay, '2026-09-25');
|
||||
assert.equal(j.previous.complete, true);
|
||||
assert.equal(j.previous.from, '2026-09-26'); assert.equal(j.previous.to, '2026-10-02');
|
||||
assert.equal(j.previous.series.length, 7); assert.equal(j.previous.series[4].day, '2026-09-30');
|
||||
assert.equal(j.previous.totals.pv, 3); // 26.09., 27.09., 30.09.
|
||||
assert.equal(j.previous.totals.engaged, 1);
|
||||
assert.equal(j.previous.totals.avgSec, 20);
|
||||
assert.equal(j.previous.totals.contacts, 1);
|
||||
assert.deepEqual(j.previous.series[4], { day: '2026-10-01', pv: 0, visitors: 1, contacts: 0, avgSec: 20 });
|
||||
assert.deepEqual(j.previous.series[5], { day: '2026-10-01', pv: 0, visitors: 1, contacts: 0, avgSec: 20 });
|
||||
assert.deepEqual(j.previous.series[6], { day: '2026-10-02', pv: 0, visitors: 1, contacts: 1, avgSec: null });
|
||||
assert.deepEqual(j.previous.contacts, [{ key: 'tel', count: 1 }]);
|
||||
assert.equal(j.totals.pv, 0); // aktueller Zeitraum leer
|
||||
assert.equal(j.totals.avgSec, null);
|
||||
@@ -223,8 +265,8 @@ test('Stats: avgSec/avgScroll gerundet, null ohne en; Tageswert avgSec', async (
|
||||
assert.equal(j.totals.engaged, 2);
|
||||
assert.equal(j.totals.avgSec, 61); // 60,5 gerundet
|
||||
assert.equal(j.totals.avgScroll, 75);
|
||||
assert.equal(j.series[5].avgSec, 61);
|
||||
assert.equal(j.series[4].avgSec, null);
|
||||
assert.equal(j.series[6].avgSec, 61); // 9.10. ist der letzte abgeschlossene Tag
|
||||
assert.equal(j.series[5].avgSec, null);
|
||||
// ohne en: beide null, engaged 0
|
||||
const leer = await (await a.get(`/a/admin/stats/${KEY}?range=30`)).json();
|
||||
assert.equal(leer.previous.totals.engaged, 0);
|
||||
@@ -238,20 +280,23 @@ test('Stats: Vorzeitraum über die Zeitumstellung lückenlos und ohne Überlappu
|
||||
// schon der 12.10. (00:30 Berlin) und ein naives Fenster würde sich überlappen; mittags fiele das nicht auf.
|
||||
const NOV = Date.UTC(2026, 10, 10, 22, 30);
|
||||
const { db, ev } = mk();
|
||||
ev({ ts: Date.UTC(2026, 9, 11, 10), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' }); // 11.10. mittags: Vorzeitraum
|
||||
ev({ ts: Date.UTC(2026, 9, 12, 10), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' }); // 12.10. mittags: aktueller Zeitraum
|
||||
ev({ ts: Date.UTC(2026, 9, 10, 10), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' }); // 10.10. mittags: Vorzeitraum
|
||||
ev({ ts: Date.UTC(2026, 9, 11, 10), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' }); // 11.10. mittags: aktueller Zeitraum
|
||||
ev({ ts: Date.UTC(2026, 10, 10, 12), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' }); // 10.11. mittags: heute
|
||||
const a = await admin(db, () => NOV);
|
||||
const j = await (await a.get(`/a/admin/stats/${KEY}?range=30`)).json();
|
||||
assert.equal(j.series[0].day, '2026-10-12');
|
||||
assert.equal(j.series[29].day, '2026-11-10');
|
||||
assert.equal(j.previous.series[29].day, '2026-10-11');
|
||||
assert.equal(j.previous.series[0].day, '2026-09-12');
|
||||
assert.equal(j.previous.from, '2026-09-12'); assert.equal(j.previous.to, '2026-10-11');
|
||||
assert.equal(j.series[0].day, '2026-10-11');
|
||||
assert.equal(j.series[29].day, '2026-11-09');
|
||||
assert.equal(j.previous.series[29].day, '2026-10-10');
|
||||
assert.equal(j.previous.series[0].day, '2026-09-11');
|
||||
assert.equal(j.from, '2026-10-11'); assert.equal(j.to, '2026-11-09');
|
||||
assert.equal(j.previous.from, '2026-09-11'); assert.equal(j.previous.to, '2026-10-10');
|
||||
const all = [...j.series, ...j.previous.series].map((s) => s.day);
|
||||
assert.equal(all.length, 60);
|
||||
assert.equal(new Set(all).size, 60);
|
||||
assert.equal(j.previous.totals.pv, 1); assert.equal(j.previous.series[29].pv, 1);
|
||||
assert.equal(j.totals.pv, 1); assert.equal(j.series[0].pv, 1);
|
||||
assert.equal(j.today.day, '2026-11-10'); assert.equal(j.today.pv, 1);
|
||||
a.close();
|
||||
});
|
||||
|
||||
@@ -274,7 +319,7 @@ test('Stats: Vorzeitraum aus daily und live, nie doppelt', async () => {
|
||||
|
||||
test('Stats: Top 20 absteigend, consentRate 0 ohne Entscheidungen, Fehlerfälle', async () => {
|
||||
const { db, ev } = mk();
|
||||
for (let p = 0; p < 25; p++) for (let i = 0; i <= p; i++) ev({ ts: NOW - 1000, type: 'pv', path: `/p${p}`, vp: 'd', country: 'DE', device: 'd' });
|
||||
for (let p = 0; p < 25; p++) for (let i = 0; i <= p; i++) ev({ ts: NOW - D - 1000, type: 'pv', path: `/p${p}`, vp: 'd', country: 'DE', device: 'd' });
|
||||
const a = await admin(db);
|
||||
const j = await (await a.get(`/a/admin/stats/${KEY}?range=30`)).json();
|
||||
assert.equal(j.pages.length, 20);
|
||||
|
||||
Reference in new issue
Block a user