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
+74
-33
@@ -1,6 +1,7 @@
|
||||
// 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.
|
||||
import { cleanPath, vpClass, refHost, cssPath, contactKind, linkHost, readConsent, contrastOk, CONSENT_KEY } from './core.js';
|
||||
// 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.
|
||||
import { cleanPath, vpClass, refHost, cssPath, contactKind, linkHost, readConsent, contrastOk, CONSENT_KEY, BANNER_VERSION, BANNER_TEXT, BANNER_LINK_LABEL, BANNER_ARIA, SETTINGS_LABEL } from './core.js';
|
||||
|
||||
function main() {
|
||||
const DOC = document;
|
||||
@@ -36,7 +37,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 */ }
|
||||
}
|
||||
@@ -44,37 +44,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;
|
||||
@@ -126,8 +160,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;
|
||||
}
|
||||
@@ -151,19 +184,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();
|
||||
}
|
||||
|
||||
@@ -184,6 +212,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;
|
||||
@@ -193,9 +228,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');
|
||||
@@ -205,6 +240,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();
|
||||
}
|
||||
@@ -214,6 +253,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 = '';
|
||||
}
|
||||
|
||||
@@ -223,8 +264,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);
|
||||
@@ -240,7 +281,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