Analyse: Vorzeitraum, firstDay, Tageswerte und Durchschnitte in der Statistik; activatedAt in der Ansicht

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-04 21:34:43 +00:00
1 parent c1dada3189
commit 37bec17d73
5 files changed
+202 -29

No files matched your search

+107 -2
View File
@@ -132,12 +132,15 @@ 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', 'pages', 'refs', 'series', 'totals']);
assert.deepEqual(Object.keys(j).sort(), ['banner', 'contacts', 'countries', 'devices', 'firstDay', 'from', 'pages', 'previous', 'range', 'refs', 'series', 'to', 'totals']);
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 });
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.deepEqual(j.refs[0], { key: 'g.de', count: 3 });
assert.deepEqual(j.countries, [{ key: 'DE', count: 3 }, { key: 'AT', count: 1 }]);
@@ -161,6 +164,108 @@ test('Stats: gestern noch nicht verdichtet wird live gezählt, nie doppelt', asy
a.close();
});
test('Stats: Vorzeitraum = die range Kalendertage davor, complete nach firstDay', async () => {
const { db, ev } = mk();
// Vorzeitraum zu range 7 am 10.10.: 27.09.–03.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();
assert.equal(j.firstDay, '2026-09-30');
assert.equal(j.previous.complete, false);
// 26.09. liegt vor dem Vorzeitraum und darf nicht mitzählen
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();
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);
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.contacts, [{ key: 'tel', count: 1 }]);
assert.equal(j.totals.pv, 0); // aktueller Zeitraum leer
assert.equal(j.totals.avgSec, null);
a.close();
});
test('Stats: firstDay aus daily und Rohdaten, leer ohne Daten', async () => {
const { db, ev } = mk();
const a = await admin(db);
const first = async () => (await (await a.get(`/a/admin/stats/${KEY}?range=7`)).json());
let j = await first();
assert.equal(j.firstDay, '');
assert.equal(j.previous.complete, false);
// nur Rohdaten (heute)
ev({ ts: NOW - H, type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
assert.equal((await first()).firstDay, '2026-10-10');
// 40 Tage alt: nach dem Nachtlauf nur noch in daily (Rohdaten > 30 Tage gelöscht)
ev({ ts: NOW - 40 * D, type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
assert.equal((await first()).firstDay, '2026-08-31');
runNightly(db, { now: NOW });
assert.equal(db.prepare('SELECT COUNT(*) c FROM events_raw WHERE ts < ?').get(NOW - 31 * D).c, 0);
j = await first();
assert.equal(j.firstDay, '2026-08-31');
assert.equal(j.previous.complete, true);
a.close();
});
test('Stats: avgSec/avgScroll gerundet, null ohne en; Tageswert avgSec', async () => {
const { db, ev } = mk();
const t = NOW - D; // 9.10.
ev({ ts: t, type: 'en', path: '/', vp: 'd', visitor: 'v1', sec: 30, scroll: 50 });
ev({ ts: t + 1, type: 'en', path: '/a', vp: 'd', visitor: 'v2', sec: 91, scroll: 100 });
const a = await admin(db);
const j = await (await a.get(`/a/admin/stats/${KEY}?range=7`)).json();
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);
// 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);
assert.equal(leer.previous.totals.avgSec, null);
assert.equal(leer.previous.totals.avgScroll, null);
a.close();
});
test('Stats: Vorzeitraum über die Zeitumstellung lückenlos und ohne Überlappung', async () => {
const NOV = Date.UTC(2026, 10, 10, 10); // range 30, Zeitumstellung am 25.10. liegt im Zeitraum
const { db } = mk();
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');
const all = [...j.series, ...j.previous.series].map((s) => s.day);
assert.equal(all.length, 60);
assert.equal(new Set(all).size, 60);
a.close();
});
test('Stats: Vorzeitraum aus daily und live, nie doppelt', async () => {
const { db, ev } = mk();
ev({ ts: Date.UTC(2026, 9, 1, 10), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
ev({ ts: Date.UTC(2026, 9, 1, 11), type: 'ct', path: '/', visitor: 'v1', kind: 'mail' });
const a = await admin(db);
const get = async () => (await (await a.get(`/a/admin/stats/${KEY}?range=7`)).json()).previous;
const live = await get(); // noch nicht verdichtet: live aus den Rohdaten
assert.equal(live.totals.pv, 1);
runNightly(db, { now: NOW }); // jetzt aus daily
const verdichtet = await get();
assert.equal(verdichtet.totals.pv, 1);
assert.deepEqual(verdichtet, live);
runNightly(db, { now: NOW + H });
assert.deepEqual(await get(), live);
a.close();
});
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' });