Zum Hauptinhalt springen

QR-Link-Tracking — Impression, Confirmed, Privacy

QR-Links liefern Statistiken — wie viele Menschen scannen einen Code, wer klickt weiter, woher kommen sie? SpeamCore baut diese Auswertung auf einem zweistufigen Modell auf, das DSGVO-konformes Tracking ohne Personenbezug ermöglicht. Dieser Artikel erklärt die Architektur, Cookie-Lebenszeit und Privacy-Garantien.

Die zwei Scan-Arten

Jeder Scan eines QR-Codes löst potenziell zwei Datensätze in der Tabelle qr_link_scans aus:

Art (kind)Wann?CookieIP-HashUA / Geo / Device
impressionsobald die Landing-Page /q/:slug aufgerufen wirdnein, neue UUID pro Aufrufneinja
confirmedwenn der User auf „Fortfahren" klickt und auf die Ziel-URL weitergeleitet wirdja, persistenter qr_device_id (1 Jahr HttpOnly)ja, mit täglich rotierendem Saltja

Warum zwei Stufen? Eine reine „Page-View"-Statistik wäre verzerrt — viele Scans führen aus Versehen oder bewusst nicht zum eigentlichen Ziel. Das Confirmed-Modell zwingt zu einer expliziten User-Aktion und ermöglicht damit:

  • Klar getrennte Reichweite (Impressions) vs. Conversion (Confirmed).
  • Unique Devices verlässlich nur auf Confirmed-Basis zählen.
  • Eine Einwilligungs-Flow-Geste vor dem persistenten Cookie — der User sieht den Konsent-Hinweis und klickt aktiv zu.

Der Public-Flow

Die Public-Route /q/:slug läuft ohne Auth, ohne App-Layout — also auch komplett ohne SpeamCore-Login. Marketing-Empfänger müssen keine Accounts haben.

Privacy-Garantien

Anonyme Impression

Der erste Track-Aufruf legt eine frische UUID als deviceCookieId an — der User wird zwischen einzelnen Impressions also nicht wiedererkannt. Das verhindert Profilbildung beim bloßen Anschauen der Landing-Page.

Zusätzlich:

  • Kein IP-Hash beim Impression-Track.
  • Trotzdem werden anonyme Demografie-Felder (Browser, OS, Device, Country) erfasst, weil sie aus dem User-Agent-Header und Cloudflare-Geo-Header kommen und keinen Personenbezug haben.

Erst beim Klick auf „Fortfahren" setzt der Backend-Service einen HttpOnly-Cookie qr_device_id mit der UUID + SameSite=Lax + 1 Jahr Lebenszeit. Bei wiederholten Confirmed-Scans desselben Browsers wird die Cookie-UUID wiederverwendet — so entsteht der Unique-Devices-Count.

Der Cookie ist:

  • HttpOnly — nicht aus JavaScript lesbar (kein XSS-Vektor).
  • SameSite=Lax — kommt nicht bei Cross-Site-Embeds mit.
  • Pro SpeamCore-Mandanten-Domain — kein domainübergreifendes Tracking.

IP-Hashing mit täglichem Salt

Beim Confirmed-Scan wird die IP-Adresse als Hash gespeichert — nicht im Klartext und nicht persistent verkettbar:

dailySalt = QR_IP_SALT::YYYYMMDD
ipHash = HMAC-SHA256(ip, dailySalt) [64 Zeichen]

Die QR_IP_SALT-Konstante lebt nur im Server-Env. Jeden Tag rotiert das YYYYMMDD-Postfix — der Hash einer IP am 19.05. unterscheidet sich vom Hash derselben IP am 20.05..

Konsequenz:

  • Tages-Dedup ist möglich (z. B. wiederholte Confirmed-Scans vom selben Anschluss am gleichen Tag).
  • Persistente Verfolgung über mehrere Tage hinweg ist ausgeschlossen — selbst der Datenbank-Inhalt erlaubt keine Wieder-Verkettung.

Was NICHT gespeichert wird

  • Klartext-IP (weder im DB-Datensatz noch in den Logs der Resolve-/Impression-/Confirm-Endpoints).
  • Auth-Token oder Identifikatoren des Scannenden (Public-Route hat keine Session).
  • Browser-Fingerprints (Canvas, WebGL, AudioContext) — SpeamCore verwendet nichts davon.
  • Geo-Daten unterhalb Land-Granularität.

Was die Statistik zeigt

Der Stats-Endpoint GET /api/qr-links/:id/stats?from=&to= aggregiert ausschließlich anonym:

KPIBerechnungAussagekraft
totalImpressionsCOUNT(kind = 'impression')Reichweite — wie oft hat jemand die Landing-Page geöffnet
totalScansCOUNT(kind = 'confirmed')Conversion — wie oft ist jemand auf die Ziel-URL weiter
uniqueDevicesCOUNT(DISTINCT deviceCookieId) FILTER (kind = 'confirmed')Wie viele unterschiedliche Browser haben überhaupt confirmed
byCountryGROUP BY countryCode (Confirmed)Geografische Verteilung
byUaFamilyGROUP BY uaFamily (Confirmed)Browser-Mix
byDeviceTypeGROUP BY deviceType (Confirmed)Mobile vs. Desktop
dailyGROUP BY DATE(scannedAt), kindTages-Verlauf, beide Stufen

Die Conversion-Rate ergibt sich als totalScans / totalImpressions. Sie ist keine Persistent-Linkung zwischen einem Impression-Datensatz und seinem späteren Confirmed-Pendant — beide Datensätze leben isoliert nebeneinander.

Datenhaltung & Löschung

  • QrLinkScan ist append-only, nicht paranoid → keine Soft-Delete-Spalte. Statistik-Datensätze werden zur Auswertung gehalten.
  • Wird ein QrLink (Soft-)gelöscht, bleiben die zugehörigen Scans erhalten — Auswertungen sind weiterhin lesbar (CASL view:QrLinkScan vorausgesetzt). Public-Resolve liefert ab dann 404.
  • Wer einen Aufbewahrungs-Cutoff benötigt (z. B. „nach 13 Monaten DSGVO-konform löschen"), kann einen Cron-Job auf qr_link_scans aufsetzen — aktuell nicht aus SpeamCore heraus exponiert.

Verknüpfungen

Versionshinweise

  • 2026-05-20 (Welle 130): Initiale Veröffentlichung. Quelle: BE-Controller qrLink.controller.ts (Zeilen 117–250+ für Track-Logik), Migration 20260519150001-add-kind-to-qr-link-scans.js. Tracking-Modell entstand mit dem QR-Link-Modul aus Commit 2813f706.