Analyse: Client prüft Einwilligung vor dem Senden, Fassung der Banner-Wahl, fetch ohne Cookies
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c2cbc4156f
commit
8d64a18d35
5 files changed
+354
-131
No files matched your search
Vendored
+7
-1
@@ -6,6 +6,11 @@
|
||||
|
||||
const CONSENT_KEY = 'wk-consent';
|
||||
const CONSENT_MAX_AGE = 183 * 86400000;
|
||||
const BANNER_VERSION = '2026-10-02';
|
||||
const BANNER_TEXT = 'Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung. Mit Deiner Zustimmung messen wir zusätzlich, von welcher Website Du kommst, wie lange Du bleibst, wie weit Du scrollst und wo Du klickst (Heatmap), mit einer täglich wechselnden Kennung. Die Messung betreibt für uns die WEBklar GbR in Deutschland, ohne Cookies und ohne Werbung. Nach „Ablehnen“ zählen wir nur noch, dass Du abgelehnt hast. Deine Zustimmung kannst Du jederzeit über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen. Details: ';
|
||||
const BANNER_LINK_LABEL = 'Datenschutz';
|
||||
const BANNER_ARIA = 'Einwilligung zur Reichweitenmessung';
|
||||
const SETTINGS_LABEL = 'Datenschutz-Einstellungen';
|
||||
|
||||
function vpClass(width) {
|
||||
const w = Number(width);
|
||||
@@ -92,9 +97,10 @@ function readConsent(storage, now) {
|
||||
if (!raw) return null;
|
||||
const c = JSON.parse(raw);
|
||||
if (!c || c.v !== 1 || (c.a !== 'accept' && c.a !== 'reject') || typeof c.t !== 'number' || !isFinite(c.t)) return null;
|
||||
if (c.bv !== BANNER_VERSION) return null; // andere/fehlende Fassung des Banner-Textes: neu fragen
|
||||
const age = now - c.t;
|
||||
if (age < 0 || age >= CONSENT_MAX_AGE) return null;
|
||||
return { v: 1, a: c.a, t: c.t };
|
||||
return { v: 1, a: c.a, t: c.t, bv: c.bv };
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
|
||||
|
||||
Vendored
+80
-33
@@ -6,6 +6,11 @@
|
||||
|
||||
const CONSENT_KEY = 'wk-consent';
|
||||
const CONSENT_MAX_AGE = 183 * 86400000;
|
||||
const BANNER_VERSION = '2026-10-02';
|
||||
const BANNER_TEXT = 'Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung. Mit Deiner Zustimmung messen wir zusätzlich, von welcher Website Du kommst, wie lange Du bleibst, wie weit Du scrollst und wo Du klickst (Heatmap), mit einer täglich wechselnden Kennung. Die Messung betreibt für uns die WEBklar GbR in Deutschland, ohne Cookies und ohne Werbung. Nach „Ablehnen“ zählen wir nur noch, dass Du abgelehnt hast. Deine Zustimmung kannst Du jederzeit über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen. Details: ';
|
||||
const BANNER_LINK_LABEL = 'Datenschutz';
|
||||
const BANNER_ARIA = 'Einwilligung zur Reichweitenmessung';
|
||||
const SETTINGS_LABEL = 'Datenschutz-Einstellungen';
|
||||
|
||||
function vpClass(width) {
|
||||
const w = Number(width);
|
||||
@@ -92,9 +97,10 @@ function readConsent(storage, now) {
|
||||
if (!raw) return null;
|
||||
const c = JSON.parse(raw);
|
||||
if (!c || c.v !== 1 || (c.a !== 'accept' && c.a !== 'reject') || typeof c.t !== 'number' || !isFinite(c.t)) return null;
|
||||
if (c.bv !== BANNER_VERSION) return null; // andere/fehlende Fassung des Banner-Textes: neu fragen
|
||||
const age = now - c.t;
|
||||
if (age < 0 || age >= CONSENT_MAX_AGE) return null;
|
||||
return { v: 1, a: c.a, t: c.t };
|
||||
return { v: 1, a: c.a, t: c.t, bv: c.bv };
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
|
||||
@@ -113,7 +119,8 @@ function escapeHtml(s) {
|
||||
}
|
||||
|
||||
// WEBklar Analyse: Einwilligungs-Banner + Ereignisse. Laeuft auf oeffentlichen Kunden-Websites.
|
||||
// Keine Cookies, keine Kennung im Browser. Nur pv ohne Einwilligung; ck/ct/en nur nach Annehmen.
|
||||
// Keine Cookies, keine Kennung im Browser. Ohne Wahl nur pv {t,site,path}; ref/vp/cx/ck/ct/en nur nach Annehmen;
|
||||
// nach Ablehnen oder bei GPC/DNT geht nichts mehr raus.
|
||||
|
||||
function main() {
|
||||
const DOC = document;
|
||||
@@ -149,7 +156,6 @@ function main() {
|
||||
function send(obj) {
|
||||
try {
|
||||
const body = JSON.stringify(obj);
|
||||
if (navigator.sendBeacon && navigator.sendBeacon(endpoint, body)) return;
|
||||
fetch(endpoint, { method: 'POST', body, keepalive: true, credentials: 'omit', headers: { 'Content-Type': 'text/plain' } }).catch(function () {});
|
||||
} catch (e) { /* egal */ }
|
||||
}
|
||||
@@ -157,37 +163,71 @@ function main() {
|
||||
const vp = () => vpClass(WIN.innerWidth);
|
||||
const ownHost = () => WIN.location.hostname;
|
||||
|
||||
// ---- Seitenaufruf (pv): immer, auch ohne Einwilligung ----
|
||||
// ---- Widerspruch (GPC/DNT) und Wahl: vor jedem Senden und vor jedem Hook ----
|
||||
let storage = null;
|
||||
try { storage = WIN.localStorage; } catch (e) { /* gesperrt */ }
|
||||
const dnt = (v) => v === '1' || v === 'yes';
|
||||
const optOut = navigator.globalPrivacyControl === true || dnt(navigator.doNotTrack) || dnt(WIN.doNotTrack) || dnt(navigator.msDoNotTrack);
|
||||
if (optOut) return; // wie Ablehnen: nichts senden, kein Banner, keine Hooks
|
||||
const stored = readConsent(storage, Date.now());
|
||||
let memChoice = null;
|
||||
let tracking = false;
|
||||
// Wahl bei jedem Aufruf frisch lesen (Speicher, sonst nur fuer diese Seite gemerkt)
|
||||
function choiceNow() {
|
||||
const c = readConsent(storage, Date.now());
|
||||
return c ? c.a : (memChoice ? memChoice.a : null);
|
||||
}
|
||||
const counting = () => choiceNow() !== 'reject';
|
||||
|
||||
// ---- Seitenaufruf (pv): ohne Wahl nur Seitenadresse, ref/vp nur nach Annehmen ----
|
||||
let lastPath = null;
|
||||
let pvStart = Date.now();
|
||||
let maxScroll = 0;
|
||||
let enSent = false;
|
||||
let pvSent = false;
|
||||
let cxSent = false;
|
||||
function sendPv(first) {
|
||||
if (!counting()) return;
|
||||
lastPath = cleanPath(WIN.location.pathname);
|
||||
pvStart = Date.now(); maxScroll = 0; enSent = false;
|
||||
send({ t: 'pv', site, path: lastPath, ref: first ? refHost(DOC.referrer, ownHost()) : '', vp: vp() });
|
||||
const ev = { t: 'pv', site, path: lastPath };
|
||||
if (choiceNow() === 'accept') {
|
||||
if (first) { ev.ref = refHost(DOC.referrer, ownHost()); cxSent = true; }
|
||||
ev.vp = vp();
|
||||
}
|
||||
pvSent = true;
|
||||
send(ev);
|
||||
}
|
||||
let navTimer = null;
|
||||
function scheduleNav() {
|
||||
if (navTimer) return;
|
||||
if (navTimer || !counting()) return;
|
||||
navTimer = setTimeout(safe(function () {
|
||||
navTimer = null;
|
||||
if (cleanPath(WIN.location.pathname) !== lastPath) sendPv(false);
|
||||
if (counting() && cleanPath(WIN.location.pathname) !== lastPath) sendPv(false);
|
||||
}), 300);
|
||||
}
|
||||
sendPv(true);
|
||||
WIN.addEventListener('pageshow', safe(function (e) { if (e && e.persisted) sendPv(false); }));
|
||||
try {
|
||||
const orig = WIN.history.pushState;
|
||||
WIN.history.pushState = function () { const r = orig.apply(this, arguments); try { scheduleNav(); } catch (e) { /* egal */ } return r; };
|
||||
WIN.addEventListener('popstate', safe(scheduleNav));
|
||||
} catch (e) { /* egal */ }
|
||||
let hooksOn = false;
|
||||
function ensureHooks() {
|
||||
if (hooksOn) return;
|
||||
hooksOn = true;
|
||||
WIN.addEventListener('pageshow', safe(function (e) { if (e && e.persisted) sendPv(false); }));
|
||||
try {
|
||||
const orig = WIN.history.pushState;
|
||||
WIN.history.pushState = function () { const r = orig.apply(this, arguments); try { scheduleNav(); } catch (e) { /* egal */ } return r; };
|
||||
WIN.addEventListener('popstate', safe(scheduleNav));
|
||||
} catch (e) { /* egal */ }
|
||||
}
|
||||
// Nach "Annehmen": Herkunft und Fenstergroesse einmal nachreichen, oder (noch kein pv) pv mit Daten senden
|
||||
function onAccept() {
|
||||
ensureHooks();
|
||||
if (!pvSent) { sendPv(true); return; }
|
||||
if (cxSent) return;
|
||||
cxSent = true;
|
||||
send({ t: 'cx', site, path: lastPath, ref: refHost(DOC.referrer, ownHost()), vp: vp() });
|
||||
}
|
||||
if (choiceNow() !== 'reject') { sendPv(true); ensureHooks(); }
|
||||
|
||||
// ---- Tracking (nur nach Annehmen erreichbar) ----
|
||||
let storage = null;
|
||||
try { storage = WIN.localStorage; } catch (e) { /* gesperrt */ }
|
||||
let memChoice = null;
|
||||
let tracking = false;
|
||||
const hosts = [];
|
||||
const scrollPct = () => {
|
||||
const h = DOC.documentElement.scrollHeight || 1;
|
||||
@@ -239,8 +279,7 @@ function main() {
|
||||
|
||||
// Einwilligung unmittelbar vor jedem Senden pruefen (z.B. in einem anderen Tab widerrufen)
|
||||
function allowed() {
|
||||
const c = readConsent(storage, Date.now());
|
||||
const ok = c ? c.a === 'accept' : !!(memChoice && memChoice.a === 'accept');
|
||||
const ok = choiceNow() === 'accept';
|
||||
if (!ok && tracking) stopTracking();
|
||||
return ok;
|
||||
}
|
||||
@@ -264,19 +303,14 @@ function main() {
|
||||
}
|
||||
|
||||
// ---- Einwilligung ----
|
||||
const dnt = (v) => v === '1' || v === 'yes';
|
||||
const gpc = navigator.globalPrivacyControl === true || dnt(navigator.doNotTrack) || dnt(WIN.doNotTrack) || dnt(navigator.msDoNotTrack);
|
||||
if (gpc) return; // wie Ablehnen: kein Banner, kein bn, nie Tracking
|
||||
|
||||
const stored = readConsent(storage, Date.now());
|
||||
let bannerHost = null;
|
||||
let linkHostEl = null;
|
||||
|
||||
function choose(a) {
|
||||
memChoice = null;
|
||||
try { storage.setItem(CONSENT_KEY, JSON.stringify({ v: 1, a, t: Date.now() })); } catch (e) { memChoice = { a }; /* ohne Speicher: gilt nur fuer diese Seite */ }
|
||||
try { storage.setItem(CONSENT_KEY, JSON.stringify({ v: 1, a, t: Date.now(), bv: BANNER_VERSION })); } catch (e) { memChoice = { a }; /* ohne Speicher: gilt nur fuer diese Seite */ }
|
||||
send({ t: 'bn', site, a });
|
||||
if (a === 'accept') startTracking(); else stopTracking();
|
||||
if (a === 'accept') { startTracking(); onAccept(); } else stopTracking();
|
||||
closeBanner();
|
||||
}
|
||||
|
||||
@@ -297,6 +331,13 @@ function main() {
|
||||
'button:focus-visible{outline:3px solid #1a73e8;outline-offset:2px}' +
|
||||
'@media (max-width:767px){.bar{display:block;margin:8px;padding:16px;border:1px solid #d0d0d0;border-radius:12px}.r{flex-direction:column;margin-top:12px}button{width:100%}}';
|
||||
|
||||
let prevPad = '';
|
||||
function applyPad() {
|
||||
if (!bannerHost) return;
|
||||
const h = Math.ceil(bannerHost.getBoundingClientRect().height || bannerHost.offsetHeight || 0);
|
||||
DOC.body.style.paddingBottom = h + 'px';
|
||||
}
|
||||
const onResize = safe(applyPad);
|
||||
function openBanner(focus) {
|
||||
if (bannerHost) return;
|
||||
const prev = DOC.activeElement;
|
||||
@@ -306,9 +347,9 @@ function main() {
|
||||
hosts.push(bannerHost);
|
||||
const root = bannerHost.attachShadow({ mode: 'open' });
|
||||
root.appendChild(el('style', {}, CSS));
|
||||
const bar = el('div', { class: 'bar', role: 'dialog', 'aria-label': 'Cookie-Einstellungen' });
|
||||
const p = el('p', { class: 't' }, 'Wir möchten mit deiner Zustimmung anonym auswerten, wie unsere Website genutzt wird (keine Cookies, keine Weitergabe). Details: ');
|
||||
p.appendChild(el('a', { href: '/datenschutz-analyse.html' }, 'Datenschutz'));
|
||||
const bar = el('div', { class: 'bar', role: 'dialog', 'aria-label': BANNER_ARIA });
|
||||
const p = el('p', { class: 't' }, BANNER_TEXT);
|
||||
p.appendChild(el('a', { href: '/datenschutz-analyse.html' }, BANNER_LINK_LABEL));
|
||||
const row = el('div', { class: 'r' });
|
||||
const no = el('button', { type: 'button' }, 'Ablehnen');
|
||||
const yes = el('button', { type: 'button' }, 'Annehmen');
|
||||
@@ -318,6 +359,10 @@ function main() {
|
||||
bar.appendChild(p); bar.appendChild(row);
|
||||
root.appendChild(bar);
|
||||
DOC.body.appendChild(bannerHost);
|
||||
// Platz unten schaffen, damit Footer-Links (Impressum, Datenschutz) erreichbar bleiben
|
||||
prevPad = DOC.body.style.paddingBottom;
|
||||
applyPad();
|
||||
WIN.addEventListener('resize', onResize);
|
||||
if (linkHostEl) linkHostEl.style.display = 'none';
|
||||
if (focus === true) no.focus();
|
||||
}
|
||||
@@ -327,6 +372,8 @@ function main() {
|
||||
if (i !== -1) hosts.splice(i, 1);
|
||||
bannerHost.remove();
|
||||
bannerHost = null;
|
||||
WIN.removeEventListener('resize', onResize);
|
||||
DOC.body.style.paddingBottom = prevPad;
|
||||
if (linkHostEl) linkHostEl.style.display = '';
|
||||
}
|
||||
|
||||
@@ -336,8 +383,8 @@ function main() {
|
||||
st.position = 'fixed'; st.right = '12px'; st.bottom = '12px'; st.zIndex = '2147483646';
|
||||
hosts.push(linkHostEl);
|
||||
const root = linkHostEl.attachShadow({ mode: 'open' });
|
||||
root.appendChild(el('style', {}, ':host{all:initial}button{font:12px/1 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;padding:6px 10px;border:1px solid #bbb;border-radius:6px;background:#fff;color:#444;cursor:pointer}button:focus-visible{outline:3px solid #1a73e8}'));
|
||||
const b = el('button', { type: 'button' }, 'Cookie-Einstellungen');
|
||||
root.appendChild(el('style', {}, ':host{all:initial}button{font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;font-size:13px;line-height:1;min-width:24px;min-height:32px;padding:8px 12px;border:1px solid #bbb;border-radius:6px;background:#fff;color:#444;cursor:pointer}button:focus-visible{outline:3px solid #1a73e8}'));
|
||||
const b = el('button', { type: 'button' }, SETTINGS_LABEL);
|
||||
b.addEventListener('click', safe(function () { openBanner(true); }));
|
||||
root.appendChild(b);
|
||||
DOC.body.appendChild(linkHostEl);
|
||||
@@ -353,7 +400,7 @@ function main() {
|
||||
WIN.addEventListener('storage', safe(function (e) {
|
||||
if (e.key !== null && e.key !== CONSENT_KEY) return;
|
||||
const c = readConsent(storage, Date.now());
|
||||
if (c && c.a === 'accept') startTracking(); else stopTracking();
|
||||
if (choiceNow() === 'accept') { startTracking(); onAccept(); } else stopTracking();
|
||||
if (c) closeBanner();
|
||||
}));
|
||||
|
||||
|
||||
Reference in new issue
Block a user