From 30412cb0ea6efccf53837b595f9c4c0bbf20cf7a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 13:32:21 +0000 Subject: [PATCH] Analyse: Verweildauer bei internem Seitenwechsel sofort senden Co-Authored-By: Claude Sonnet 5.5 --- analytics/client/dist/w.js | 19 +++++++++++++- analytics/client/w.js | 19 +++++++++++++- analytics/test/client.test.js | 48 ++++++++++++++++++++++++++++++++++- 3 files changed, 83 insertions(+), 3 deletions(-) diff --git a/analytics/client/dist/w.js b/analytics/client/dist/w.js index 8215576..fcdce99 100644 --- a/analytics/client/dist/w.js +++ b/analytics/client/dist/w.js @@ -214,11 +214,26 @@ function main() { } }); const onScroll = safe(function () { maxScroll = Math.max(maxScroll, scrollPct()); }); - const onLeave = safe(function () { + function sendEn() { if (enSent || !allowed()) return; enSent = true; send({ t: 'en', site, path: lastPath || cleanPath(WIN.location.pathname), vp: vp(), sec: Math.max(0, Math.min(3600, Math.round((Date.now() - pvStart) / 1000))), scroll: Math.max(maxScroll, scrollPct()) }); + } + const onLeave = safe(sendEn); + // Interner Seitenwechsel per Link: en sofort senden, der Beacon beim pagehide geht in Chromium oft verloren. + // Bubbling-Listener am Dokument: laeuft nach den Handlern der Seite, defaultPrevented ist dann bekannt. + const onLinkClick = safe(function (e) { + if (e.defaultPrevented || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey || (typeof e.button === 'number' && e.button !== 0)) return; + let a = e.target; + while (a && String(a.tagName).toUpperCase() !== 'A') a = a.parentNode; + if (!a || hosts.indexOf(e.target) !== -1) return; + const tg = String(a.getAttribute('target') || '').toLowerCase(); + if ((tg && tg !== '_self') || a.getAttribute('download') !== null) return; + const u = new URL(String(a.href || a.getAttribute('href') || ''), WIN.location.href); + if (u.protocol !== 'http:' && u.protocol !== 'https:') return; + if (u.origin !== WIN.location.origin || u.pathname === WIN.location.pathname) return; + sendEn(); }); const onVis = safe(function () { if (DOC.visibilityState === 'hidden') onLeave(); }); @@ -234,6 +249,7 @@ function main() { if (tracking) return; tracking = true; DOC.addEventListener('click', onClick, { capture: true, passive: true }); + DOC.addEventListener('click', onLinkClick, { passive: true }); DOC.addEventListener('visibilitychange', onVis); WIN.addEventListener('pagehide', onLeave); WIN.addEventListener('scroll', onScroll, { passive: true }); @@ -241,6 +257,7 @@ function main() { function stopTracking() { tracking = false; DOC.removeEventListener('click', onClick, { capture: true }); + DOC.removeEventListener('click', onLinkClick); DOC.removeEventListener('visibilitychange', onVis); WIN.removeEventListener('pagehide', onLeave); WIN.removeEventListener('scroll', onScroll); diff --git a/analytics/client/w.js b/analytics/client/w.js index 07a1d66..8ffd22f 100644 --- a/analytics/client/w.js +++ b/analytics/client/w.js @@ -101,11 +101,26 @@ function main() { } }); const onScroll = safe(function () { maxScroll = Math.max(maxScroll, scrollPct()); }); - const onLeave = safe(function () { + function sendEn() { if (enSent || !allowed()) return; enSent = true; send({ t: 'en', site, path: lastPath || cleanPath(WIN.location.pathname), vp: vp(), sec: Math.max(0, Math.min(3600, Math.round((Date.now() - pvStart) / 1000))), scroll: Math.max(maxScroll, scrollPct()) }); + } + const onLeave = safe(sendEn); + // Interner Seitenwechsel per Link: en sofort senden, der Beacon beim pagehide geht in Chromium oft verloren. + // Bubbling-Listener am Dokument: laeuft nach den Handlern der Seite, defaultPrevented ist dann bekannt. + const onLinkClick = safe(function (e) { + if (e.defaultPrevented || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey || (typeof e.button === 'number' && e.button !== 0)) return; + let a = e.target; + while (a && String(a.tagName).toUpperCase() !== 'A') a = a.parentNode; + if (!a || hosts.indexOf(e.target) !== -1) return; + const tg = String(a.getAttribute('target') || '').toLowerCase(); + if ((tg && tg !== '_self') || a.getAttribute('download') !== null) return; + const u = new URL(String(a.href || a.getAttribute('href') || ''), WIN.location.href); + if (u.protocol !== 'http:' && u.protocol !== 'https:') return; + if (u.origin !== WIN.location.origin || u.pathname === WIN.location.pathname) return; + sendEn(); }); const onVis = safe(function () { if (DOC.visibilityState === 'hidden') onLeave(); }); @@ -121,6 +136,7 @@ function main() { if (tracking) return; tracking = true; DOC.addEventListener('click', onClick, { capture: true, passive: true }); + DOC.addEventListener('click', onLinkClick, { passive: true }); DOC.addEventListener('visibilitychange', onVis); WIN.addEventListener('pagehide', onLeave); WIN.addEventListener('scroll', onScroll, { passive: true }); @@ -128,6 +144,7 @@ function main() { function stopTracking() { tracking = false; DOC.removeEventListener('click', onClick, { capture: true }); + DOC.removeEventListener('click', onLinkClick); DOC.removeEventListener('visibilitychange', onVis); WIN.removeEventListener('pagehide', onLeave); WIN.removeEventListener('scroll', onScroll); diff --git a/analytics/test/client.test.js b/analytics/test/client.test.js index 01e4373..bb12ade 100644 --- a/analytics/test/client.test.js +++ b/analytics/test/client.test.js @@ -185,7 +185,7 @@ function harness({ gpc, dnt, stored, search = '', src = 'https://project.webklar const history = { pushState() {}, replaceState(s, t, u) { this.replaced = u; } }; const win = { innerWidth: 1200, innerHeight: 800, scrollX: 0, scrollY: 300, document, history, - location: { hostname: 'mein.de', pathname: '/start', search, href: 'https://mein.de/start' + search, hash: '' }, + location: { hostname: 'mein.de', origin: 'https://mein.de', pathname: '/start', search, href: 'https://mein.de/start' + search, hash: '' }, addEventListener(t, f) { (winL[t] ||= []).push(f); }, removeEventListener(t, f) { winL[t] = (winL[t] || []).filter((x) => x !== f); }, }; @@ -431,3 +431,49 @@ test('w.js: [data-webklar-consent] spaeter hinzugefuegt funktioniert per Delegat assert.ok(hostEl(h)); assert.ok(btn(hostEl(h).shadow, 'Ablehnen').focused); }); + +// ---------- Verweildauer bei internem Seitenwechsel ---------- +const ACC = { v: 1, a: 'accept', t: 1_700_000_000_000 - 1000 }; +const lnk = (href, attrs = {}) => { const a = makeEl('a'); a.setAttribute('href', href); for (const k in attrs) a.setAttribute(k, attrs[k]); a.parentNode = makeEl('body'); return a; }; +const enCount = (h) => h.sent.filter((s) => s.d.t === 'en').length; + +test('w.js: interner Link-Klick sendet en sofort, pagehide danach nicht doppelt', () => { + const h = harness({ stored: ACC }); + h.ck(lnk('/kontakt')); + assert.equal(enCount(h), 1); + assert.deepEqual(h.sent.map((s) => s.d.t), ['pv', 'ck', 'en']); + const en = h.sent.at(-1).d; + assert.equal(en.path, '/start'); assert.equal(en.scroll, 55); + h.leave(); + assert.equal(enCount(h), 1); + h.ck(lnk('https://mein.de/andere?x=1#a')); + assert.equal(enCount(h), 1); +}); + +test('w.js: kein fruehes en bei externem Link, Anker, gleicher Seite, Modifier, Ziel/Download, defaultPrevented', () => { + const h = harness({ stored: ACC }); + h.ck(lnk('https://anders.de/x')); + h.ck(lnk('#oben')); + h.ck(lnk('/start#unten')); + h.ck(lnk('/kontakt'), { ctrlKey: true }); + h.ck(lnk('/kontakt'), { metaKey: true }); + h.ck(lnk('/kontakt'), { shiftKey: true }); + h.ck(lnk('/kontakt'), { button: 1 }); + h.ck(lnk('/kontakt', { target: '_blank' })); + h.ck(lnk('/datei.pdf', { download: '' })); + h.ck(lnk('/kontakt'), { defaultPrevented: true }); + h.ck(lnk('mailto:a@b.de')); + assert.equal(enCount(h), 0); + h.leave(); + assert.equal(enCount(h), 1); +}); + +test('w.js: ohne Einwilligung oder nach Widerruf kein fruehes en', () => { + const h = harness(); + h.ck(lnk('/kontakt')); + assert.equal(enCount(h), 0); + const g = harness({ stored: ACC }); + g.store['wk-consent'] = JSON.stringify({ v: 1, a: 'reject', t: 1_700_000_000_000 }); + g.ck(lnk('/kontakt')); + assert.equal(enCount(g), 0); +});