148 lines
11 KiB
Markdown
148 lines
11 KiB
Markdown
# Website-Analyse (Funktion im Kundenportal) – Design
|
||
|
||
Stand: 2026-10-01 · Status: vom Nutzer im Chat freigegeben (Abschnitte 1–3), Spec zur Durchsicht
|
||
Teilprojekt **3** von 3 (1 Projektarten – live 30.09.; 2 Preview-Runner – live 01.10.).
|
||
Regel des Nutzers: bauen → live → Nutzer testet live.
|
||
Recherchen: `/root/reports/analyse-funktion-2026-09-30/` (recht-tracking-deutschland.md,
|
||
technik-analyse-heatmap.md). Keine Rechtsberatung – AVV/Texte vor Livegang anwaltlich prüfen lassen.
|
||
|
||
## Ziel
|
||
|
||
Kunden mit Wartungs-Abo schalten im Portal (Funktionen) eine Website-Analyse ein. Auf ihrer Website
|
||
erscheint ein Cookie-Banner (Annehmen/Ablehnen). Ohne Einwilligung werden nur serverseitig Seitenaufrufe,
|
||
Herkunft, Land und Gerätetyp gezählt; nach „Annehmen“ zusätzlich Besucher, Verweildauer, Scrolltiefe,
|
||
Klick-Heatmap und Kontakt-Klicks. Alles läuft auf dem eigenen Server, ohne Google o. ä., und wird
|
||
**ohne Änderung am Kunden-Repo** beim Veröffentlichen durch den Preview-Runner eingebunden.
|
||
|
||
## Entscheidungen (vom Nutzer)
|
||
|
||
| # | Entscheidung |
|
||
|---|---|
|
||
| W1 | Nur mit **Wartungs-Abo** (`productKey === 'wartung'`, Status in `ACTIVE_STATUSES`). Ohne Abo: Kachel „Im Wartungs-Abo enthalten“ mit Link zum Abo. Hosting-Abo (`hosting`) ausgeschlossen (bestätigt 01.10.). |
|
||
| W2 | **Kunde schaltet selbst ein** (Portal, AVV-Haken); WOMS kann ebenfalls schalten (Ausnahmen, protokolliert). |
|
||
| W3 | Umfang V1 wie unten (Abschnitt 2); nicht: Session-Aufzeichnung, Formularinhalte, Ladezeiten/JS-Fehler, Trichter/UTM. |
|
||
| W4 | Banner: **schlichte Leiste unten**, „Ablehnen“ und „Annehmen“ gleichwertig, Link „Datenschutz“, eine wählbare Akzentfarbe; Handy: kompakte Karte. Footer-Link „Cookie-Einstellungen“. Entscheidung gilt 6 Monate. Deutsch. |
|
||
| W5 | Datenschutz: **automatische Zusatzseite** `/datenschutz-analyse.html` + **WOMS-Ticket** „Datenschutzerklärung von <Projekt> um Hinweis auf Analyse ergänzen“. |
|
||
| W6 | **Eigene Lösung** (Sammeldienst + Skript), kein Umami/Matomo/Google. |
|
||
| W7 | Nur auf **öffentlichen** Websites (`isPublic`), nicht auf login-geschützten Previews. |
|
||
| W8 | Abo endet oder Analyse aus ⇒ Banner/Skript beim nächsten Veröffentlichen entfernt, Daten nach **60 Tagen** gelöscht. |
|
||
| W9 | Aufbewahrung: Rohdaten **30 Tage**, Tageswerte bis Abo-Ende + 60 Tage. |
|
||
|
||
## 1. Bausteine
|
||
|
||
1. **Sammeldienst** `webklar-analytics` – Code im Portal-Repo unter `analytics/` (eigenes `package.json`,
|
||
keine Laufzeit-Abhängigkeiten), Container `node:24-alpine` (eingebautes `node:sqlite`), Code
|
||
schreibgeschützt eingehängt wie der Runner, Daten `/srv/webklar-analytics/` (SQLite). `deploy.sh`
|
||
startet ihn neu, wenn sich `analytics/` geändert hat. Erreichbar über Traefik unter
|
||
`https://project.webklar.com/a/…` (eigener Router mit höherer Priorität auf `PathPrefix(/a/)`).
|
||
Länder über **DB-IP Lite Country** (CSV/MMDB, CC BY 4.0, Namensnennung im Portal), monatlich
|
||
aktualisiert per Skript.
|
||
2. **Einbinden über den Runner** (Teilprojekt 2): Ist die Analyse für das Projekt aktiv **und** die Seite
|
||
öffentlich, setzt der Runner beim Veröffentlichen (static und build) vor `</head>` jeder HTML-Datei ein:
|
||
`<script src="https://project.webklar.com/a/w.js" data-site="<siteKey>" data-color="<hex>" defer></script>`
|
||
und erzeugt `/datenschutz-analyse.html` aus einer Vorlage (Firmenname, Anschrift, E-Mail des Kunden aus
|
||
Appwrite). Ist sie aus, unterbleibt beides. Das Portal übergibt dafür im Build-Auftrag ein Feld
|
||
`analytics: { siteKey, color, privacy: { company, address, email } } | null` (validiert im Runner:
|
||
siteKey `^[a-z0-9]{16,40}$`, color `^#[0-9a-f]{6}$`, Texte HTML-maskiert, Länge begrenzt).
|
||
Einschalten/Ausschalten löst einen Neubau aus (Portal `runDeploy`).
|
||
3. **Skript** `w.js` (≤ 6 KB minifiziert, ohne Abhängigkeiten, vom Sammeldienst ausgeliefert, Cache 1 h):
|
||
Seitenaufruf ohne Einwilligung; Banner; nach „Annehmen“ Zusatzereignisse (Abschnitt 2); Footer-Link
|
||
„Cookie-Einstellungen“ (fest unten rechts, klein, wenn kein Element `[data-webklar-consent]` auf der
|
||
Seite existiert – sonst wird dieses Element zum Auslöser); Overlay-Modus für die Heatmap
|
||
(Parameter `__wk_heat=<token>`, lädt `h.js` nach).
|
||
4. **Portal:** Funktionen-Kachel „Analyse“ (Abo-Prüfung, Projektwahl, Erklärung, Akzentfarbe mit
|
||
Banner-Vorschau, AVV-Haken, Ein/Aus), Ansicht „Analyse“ (Abschnitt 3), Admin-API für WOMS.
|
||
5. **WOMS:** Zeile „Website-Analyse an/aus“ auf der Projektkarte; Services zeigt die Funktion als aktiv.
|
||
6. **Rechtliches:** AVV Website-Analyse, Leistungsbeschreibung „Website-Analyse“, neue Fassung der
|
||
Abo-Bedingungen, Vorlage Zusatzseite, Absatz in WEBklar-Datenschutzerklärung (Auftragsverarbeitung).
|
||
|
||
## 2. Daten und Datenschutz
|
||
|
||
**Ereignisse** (alle per `navigator.sendBeacon` als `text/plain` an `POST /a/e`, Body JSON ≤ 4 KB):
|
||
|
||
| Typ | Wann | Felder |
|
||
|---|---|---|
|
||
| `pv` Seitenaufruf | immer | site, path (ohne Query/Hash, ≤ 300), ref (nur Host, nur wenn ≠ eigene Domain), vp (`m` < 768 ≤ `t` < 1024 ≤ `d`) |
|
||
| `bn` Banner | Anzeige / Klick | site, a: `shown`\|`accept`\|`reject` – **keine** weiteren Felder |
|
||
| `en` Verweildauer | nur nach Annehmen, beim Verlassen | site, path, vp, sec (0–3600), scroll (0–100) |
|
||
| `ck` Klick | nur nach Annehmen | site, path, vp, x, y (0–1, relativ zur Dokumentgröße), sel (CSS-Pfad ≤ 200) |
|
||
| `ct` Kontakt-Klick | nur nach Annehmen | site, path, kind: `tel`\|`mail`\|`wa`\|`out`, host (nur bei `out`) |
|
||
|
||
**Server:** IP ⇒ Land (DB-IP), danach verworfen; User-Agent ⇒ Gerätetyp + Bot-Erkennung, danach verworfen.
|
||
Besucher-Kennung nur für `en`/`ck`/`ct`: `HMAC-SHA256(tagesSalz, siteKey|ip|ua)` gekürzt auf 16 Hex,
|
||
Salz zufällig, nur im RAM, wird um 00:00 Europe/Berlin neu erzeugt (nach Neustart neues Salz). Keine
|
||
Cookies, keine `Set-Cookie`-Header. Gespeichert auf dem Gerät nur die Banner-Wahl
|
||
(`localStorage['wk-consent'] = {v:1, a:'accept'|'reject', t}`), gültig 6 Monate.
|
||
GPC (`navigator.globalPrivacyControl`) oder DNT ⇒ wie „Ablehnen“, Banner nicht anzeigen.
|
||
|
||
**Annahme-Prüfung:** Unbekannter siteKey, Origin/Referer-Host nicht in den Domains der Website
|
||
(Subdomain `*.project.webklar.com` + `liveDomain`) ⇒ 204 ohne Speichern. Bots ⇒ verworfen. Mengenlimit
|
||
120 Ereignisse/Minute je (site, IP) im RAM. CORS nur für die eigenen Domains der Website.
|
||
|
||
**Speicherung (SQLite):** Tabellen `events_raw` (30 Tage), `daily` (site, day, dim, key, count),
|
||
`clicks` (30 Tage), `sites` (siteKey, projectId, domains, active, offSince). Nächtlicher Job 02:30:
|
||
Verdichten, Rohdaten > 30 Tage löschen, Websites mit `offSince` > 60 Tage komplett löschen.
|
||
Traefik: Zugriffsprotokoll für `/a/` ohne Client-IP (Feld filtern oder Router ohne Access-Log) – im
|
||
Livegang prüfen.
|
||
|
||
**Einwilligungsnachweis:** technischer Nachweis (ohne Annehmen sendet das Skript nachweislich nichts
|
||
Weiteres); so in AVV und Leistungsbeschreibung beschrieben; Prüfpunkt für den Anwalt.
|
||
|
||
## 3. Portal, WOMS, Abläufe
|
||
|
||
**Funktionsdaten:** vorhandenes Collection `portalFeatures` mit `featureKey: 'analyse'` je
|
||
(Kunde, Projekt): `state` (`off`|`active`|`disabled`), `siteKey`, neu `accentColor`, `avvVersion`,
|
||
`avvAcceptedAt`, `avvAcceptedBy` (`customer`|Mitarbeitername), `activatedAt`, `offSince`, `privacyTicketId`.
|
||
Fehlende Attribute per Schema-Skript (Muster `scripts/preview-runner-setup.mjs`, limit 200).
|
||
|
||
**Einschalten (Kunde):** `POST /api/funktionen/analyse/activate { projectId, accentColor, avvVersion,
|
||
consent:true }` ⇒ Prüfungen (Eigentum, Wartungs-Abo aktiv, Projekt öffentlich mit Subdomain/Live-Domain,
|
||
AVV-Fassung aktuell) ⇒ Dokument `active`, Sammeldienst-Website registrieren (`PUT /a/admin/sites/:siteKey`
|
||
mit Token), Neubau anstoßen, Ticket „Datenschutzerklärung … ergänzen“ (Muster `createFeatureTicket`),
|
||
Mail an Kunden (neue Vorlage K-Nummer laut Bestand) „Deine Website-Analyse ist aktiv“.
|
||
**Ausschalten:** `POST …/deactivate { projectId }` ⇒ `off`, `offSince`, Neubau, Sammeldienst `active=false`.
|
||
**Abo-Ende:** täglicher Job: aktive Analyse ohne aktives Wartungs-Abo ⇒ wie Ausschalten (Mail an Kunden).
|
||
**WOMS:** `POST /api/admin/analyse/:projectId { on, avvConfirmedVerbally }` (requireAppwriteStaff);
|
||
beim Einschalten durch Mitarbeiter Pflicht-Bestätigung „Kunde hat AVV bestätigt“, protokolliert.
|
||
|
||
**Auswertung:** `GET /api/funktionen/analyse/stats?projectId&range=7|30|90` (Portal holt beim Sammeldienst
|
||
`GET /a/admin/stats/:siteKey`, Token): Kennzahlen (Aufrufe, Besucher*, Kontakt-Klicks*, Zustimmungsquote),
|
||
Tagesverlauf, Top-Seiten, Herkunft, Länder, Geräte, Kontakt-Klicks nach Art, Banner-Zahlen
|
||
(*nur aus Einwilligungen – Hinweistext). Heatmap: `POST …/heatmap-link { projectId, path, vp }` ⇒ signierter
|
||
Link `https://<domain><path>?__wk_heat=<token>` (HMAC, 30 min); `h.js` lädt Punkte von
|
||
`GET /a/heat?token=…` und zeichnet sie (Canvas, eigene kleine Implementierung) über die Seite.
|
||
Ansicht handytauglich; Diagramme als leichtes SVG (keine Chart-Bibliothek nötig).
|
||
|
||
**Rechtstexte** (Entwürfe im Repo unter `docs/rechtliches/`, Upload in WOMS „Rechtliches“ durch den Nutzer):
|
||
AVV Website-Analyse (Art. 28 DSGVO; Gegenstand, Daten, Betroffene, TOMs, Unterauftragsverarbeiter
|
||
Hetzner, Löschung, Kontrollrechte), Leistungsbeschreibung (Umfang, Pflichten des Kunden: Banner nicht
|
||
umgehen, keine weiteren Tracker, Datenschutzerklärung pflegen), Abo-Bedingungen neue Fassung (Analyse als
|
||
Abo-Leistung), Vorlage `/datenschutz-analyse.html` (Art. 13: Zwecke, Rechtsgrundlagen § 25 Abs. 1 TDDDG /
|
||
Art. 6 Abs. 1 lit. a und f DSGVO, Empfänger, Speicherdauer, Widerruf, Rechte), Absatz WEBklar-
|
||
Datenschutzerklärung.
|
||
|
||
## Fehlerfälle
|
||
|
||
Sammeldienst nicht erreichbar ⇒ Website läuft normal (Skript fängt Fehler, Beacon ohne Wirkung);
|
||
Portal-Statistik zeigt „Auswertung gerade nicht erreichbar“. Länder-DB fehlt ⇒ Land „unbekannt“.
|
||
Neubau nach Ein/Aus scheitert ⇒ Zustand bleibt gespeichert, Hinweis in der Kachel, nächster Neubau holt nach.
|
||
|
||
## Tests
|
||
|
||
Sammeldienst (kein IP/UA gespeichert, Bots, Domain-Prüfung, Limit, Salz-Rotation, Verdichtung, Löschung),
|
||
Skript (jsdom/Playwright: ohne Annehmen nur `pv`+`bn`; GPC; Banner gleichwertig; Widerruf; Overlay),
|
||
Runner (Einsetzen/Entfernen, nur öffentlich, Maskierung, Zusatzseite), Portal (Abo-Pflicht, AVV-Pflicht,
|
||
Ticket, Abo-Ende-Job, Statistik-Proxy, Heatmap-Token), Playwright für Kachel, Auswertung, Banner (390 px).
|
||
|
||
## Livegang (je Schritt mit OK des Nutzers)
|
||
|
||
L1 Sammeldienst-Container + Datenverzeichnis + DB-IP + Traefik-Route `/a/` (ohne IP-Log) + Schema-Skript ·
|
||
L2 Portal/Runner pushen (Kachel noch „Kommt bald“ per Schalter `ANALYSE_ENABLED=off`) · L3 Probe auf einer
|
||
Wegwerf-Website (öffentlich) inkl. Banner, Heatmap, Löschung · L4 WOMS pushen, Rechtstexte durch den Nutzer
|
||
in „Rechtliches“, Schalter an · L5 Live-Test des Nutzers an einer echten Kunden-Website mit Wartungs-Abo.
|
||
|
||
## Nicht Teil dieser Spec
|
||
|
||
Session-Aufzeichnung, Formularanalyse, Ladezeiten/JS-Fehler, UTM/Trichter, Einwilligung für fremde
|
||
Inhalte (z. B. Google-Karte) über denselben Banner, Websites außerhalb des eigenen Hostings, Export.
|