From a73e0be9523ba530961ce23c667e7dcabca712a0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 16:00:54 +0000 Subject: [PATCH] =?UTF-8?q?Analyse:=20Messdienst:=20Heatmap-Pfad=20wie=20g?= =?UTF-8?q?espeicherter=20Pfad,=20Tests=20f=C3=BCr=20Kodierung=20und=20Stu?= =?UTF-8?q?nde?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- analytics/src/heat.js | 4 ++-- analytics/src/ingest.js | 2 +- analytics/test/heat.test.js | 8 ++++++++ analytics/test/ingest.test.js | 24 ++++++++++++++++++++++++ 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/analytics/src/heat.js b/analytics/src/heat.js index 648e6e8..4bf8d55 100644 --- a/analytics/src/heat.js +++ b/analytics/src/heat.js @@ -1,7 +1,7 @@ // GET /a/heat?token= (öffentlicher Listener): Klickpunkte für das Heatmap-Overlay. // Token (R3): ., e = Ablauf in Unix-Sekunden. import crypto from 'node:crypto'; -import { hostOf } from './ingest.js'; +import { hostOf, cleanPath } from './ingest.js'; const SITE_KEY = /^[a-z0-9]{16,40}$/; const MAX_AHEAD = 30 * 60; // Sekunden @@ -79,7 +79,7 @@ export function createHeat({ db, config = {}, now }) { } cors(res, req, domainsOf(site)); const since = clock() - CLICK_DAYS * 86400 * 1000; - const points = getClicks.all(t.siteKey, t.path, t.vp, since, POINTS).map((r) => ({ x: r.x, y: r.y })); + const points = getClicks.all(t.siteKey, cleanPath(t.path) || t.path, t.vp, since, POINTS).map((r) => ({ x: r.x, y: r.y })); return end(200, { points }); }; } diff --git a/analytics/src/ingest.js b/analytics/src/ingest.js index fee894d..c98fee9 100644 --- a/analytics/src/ingest.js +++ b/analytics/src/ingest.js @@ -64,7 +64,7 @@ function looksLikeId(seg) { return /\d{6,}/.test(d) || UUID.test(d) || d.includes('@'); } -function cleanPath(p) { +export function cleanPath(p) { if (typeof p !== 'string') return null; const s = p.split(/[?#]/)[0]; if (!s.startsWith('/') || s.length > 300 || /[\u0000-\u001f\u007f]/.test(s)) return null; diff --git a/analytics/test/heat.test.js b/analytics/test/heat.test.js index de0d650..c3ed3e9 100644 --- a/analytics/test/heat.test.js +++ b/analytics/test/heat.test.js @@ -150,3 +150,11 @@ test('Heatmap: gleiche Stunde (ts-Gleichstand), Limit schneidet über die niedri assert.ok(!j.points.some((p) => p.x === 0.99)); s.close(); }); + +test('Heatmap: Token-Pfad mit Kennung findet die als :id gespeicherten Klicks', async () => { + const s = await setup(); + s.db.prepare('INSERT INTO clicks (site, ts, path, vp, x, y) VALUES (?,?,?,?,?,?)').run(KEY, NOW - 1000, '/kunde/:id', 'd', 0.4, 0.6); + const j = await (await s.get(tok({ path: '/kunde/12345678' }))).json(); + assert.deepEqual(j, { points: [{ x: 0.4, y: 0.6 }] }); + s.close(); +}); diff --git a/analytics/test/ingest.test.js b/analytics/test/ingest.test.js index e79c595..12f8dbb 100644 --- a/analytics/test/ingest.test.js +++ b/analytics/test/ingest.test.js @@ -425,3 +425,27 @@ test('T14: kennungsartige Pfadsegmente werden zu :id, alle Typen', async () => { assert.equal(t.db.prepare('SELECT path FROM clicks').get().path, '/kunde/:id'); t.close(); }); + +test('T14: ungültige Prozent-Kodierung fällt auf den Rohwert zurück', async () => { + const t = await setup(); + await t.post(pv({ path: '/a/%E0%A4%A/123456' })); + await t.post(pv({ path: '/a/%E0%A4%A/x' })); + await t.post(pv({ path: '/a/%E0%A4%A40/x' })); + assert.deepEqual(t.rows().map((r) => r.path), ['/a/%E0%A4%A/:id', '/a/%E0%A4%A/x', '/a/%E0%A4%A40/x']); + t.close(); +}); + +test('T3: jede gespeicherte Zeile jedes Typs liegt auf der vollen Stunde (ungerade Uhrzeit)', async () => { + const t = await setup({ now: () => Date.UTC(2026, 9, 1, 10, 37, 12, 345) }); + await t.post({ t: 'bn', site: KEY, a: 'shown' }); + await t.post(pv()); + await t.post({ t: 'cx', site: KEY, path: '/', vp: 'd' }); + await t.post({ t: 'en', site: KEY, path: '/', vp: 'd', sec: 5, scroll: 10 }); + await t.post({ t: 'ct', site: KEY, path: '/', kind: 'tel' }); + await t.post({ t: 'ck', site: KEY, path: '/', vp: 'd', x: 0.5, y: 0.5 }); + const r = t.rows(); + assert.deepEqual([...new Set(r.map((x) => x.type))].sort(), ['bn', 'ct', 'cx', 'en', 'pv']); + for (const x of r) assert.equal(x.ts % 3600000, 0); + for (const x of t.db.prepare('SELECT ts FROM clicks').all()) assert.equal(x.ts % 3600000, 0); + t.close(); +});