Eigene Analytics in 200 Zeilen Rust

Warum ich PostHog ersetzt habe

  • analytics
  • gdpr
  • privacy
  • rust

Beim Umbau meiner Website habe ich PostHog rausgeworfen und eigene Analytics gebaut. Nicht weil PostHog schlecht ist — es ist exzellent — sondern weil manchmal die einfache Lösung die richtige ist.

Warum PostHog ersetzen?

Datenhoheit: Selbst mit PostHog EU lagen meine Daten auf Drittanbieter-Servern. Alles auf dem eigenen deutschen VPS macht DSGVO-Compliance deutlich einfacher. Keine Auftragsverarbeitungsverträge, keine Sub-Prozessoren.

Kosten: PostHog’s Free-Tier ist großzügig, kann aber schnell kippen. Meine Lösung kostet exakt 0 EUR zusätzlich zum bestehenden VPS.

Integration: Ich wollte enge Verzahnung zwischen Analytics und meinem Signatur-Feature. Wer hat was gezeichnet, wie lange, was war die Journey? Mit eigenen Analytics sind solche Joins trivial.

Overkill: PostHog hat Feature Flags, Session Replay, A/B Testing, Funnels und dutzende weitere Features. Für eine persönliche Website brauche ich Page Views und Custom Events. Mehr nicht.

Was ich tracke

Das Tracking ist bewusst minimal:

Page Views

  • URL und Referrer (nur extern — keine interne Navigation)
  • Scroll-Tiefe
  • Timestamp

Custom Events

  • Signatur-Editor: Öffnen, Speichern, Undo, Redo, Radierer
  • Karussell: Navigation, Autoplay-Status
  • Blog: Tag-Klicks, Verweildauer
  • Kontaktformular: Absendungen

Sessions

  • Anonyme Session-ID (clientseitig generiert)
  • Land und Region (GeoIP-Lookup)
  • Erste/letzte Aktivität

Die Implementierung

Das gesamte Backend-Analytics besteht aus ca. 200 Zeilen Rust:

// Event-Ingestion-Endpoint
pub async fn track_event(
    State(state): State<Arc<AppState>>,
    Json(req): Json<TrackEventRequest>,
) -> Result<Json<()>, AppError> {
    let now = chrono::Utc::now().timestamp();

    sqlx::query(
        "INSERT INTO events (id, session_id, event, properties, ts)
         VALUES (?, ?, ?, ?, ?)"
    )
    .bind(uuid::Uuid::new_v4().to_string())
    .bind(&req.session_id)
    .bind(&req.event)
    .bind(&req.properties)
    .bind(now)
    .execute(&state.db)
    .await?;

    Ok(Json(()))
}

Client-seitig sind es ca. 50 Zeilen TypeScript, die Events an /api/tracking/events mit Session-ID und Event-Daten senden.

Keine Cookies. Die Session-ID wird einmalig mit crypto.randomUUID() erzeugt und in sessionStorage gespeichert. Tab schließen, neue Session. Fertig.

DSGVO-Vorteile

Dieses Setup macht DSGVO-Compliance trivial:

  1. Alle Daten auf dem eigenen deutschen VPS — keine Drittanbieter-Transfers
  2. Keine Datenweitergabe — ich bin gleichzeitig Verantwortlicher und Auftragsverarbeiter
  3. Anonym by Design — keine personenbezogenen Daten, nur Session-IDs
  4. Einfache LöschungDELETE FROM events WHERE session_id = ?
  5. Keine Einwilligung nötig — anonymes Analytics braucht keine Consent-Abfrage

Was ich nicht tracke:

  • IP-Adressen (nur für GeoIP-Lookup, nicht gespeichert)
  • User Agents
  • Device Fingerprints
  • Cross-Site-Tracking
  • Personenbezogene Daten jeglicher Art

Analytics + Signaturen

Die eigentliche Stärke: Joins zwischen Analytics und Signatur-Daten. Jede Signatur hat eine session_id, damit kann ich fragen:

Zeichendauer: Wie lange zwischen Editor-Öffnen und Speichern?

SELECT
    s.id,
    s.name,
    (save_event.ts - open_event.ts) as duration_seconds
FROM signatures s
JOIN events open_event ON open_event.session_id = s.session_id
    AND open_event.event = 'signature_editor_open'
JOIN events save_event ON save_event.session_id = s.session_id
    AND save_event.event = 'signature_saved'
WHERE save_event.ts > open_event.ts
ORDER BY duration_seconds DESC

Conversion Funnel: Wie viel Prozent der Nutzer, die den Editor öffnen, speichern tatsächlich?

SELECT
    COUNT(DISTINCT CASE WHEN event = 'signature_editor_open' THEN session_id END) as opened,
    COUNT(DISTINCT CASE WHEN event = 'signature_saved' THEN session_id END) as saved
FROM events

Radierer-Nutzung: Wie oft wurde korrigiert?

SELECT
    s.id,
    COUNT(*) as eraser_toggles
FROM signatures s
JOIN events e ON e.session_id = s.session_id
    AND e.event = 'signature_eraser_toggled'
WHERE e.ts BETWEEN s.ts_created - 3600 AND s.ts_created + 60
GROUP BY s.id

Nachteile und Tradeoffs

Ehrlich gesagt:

Eigenimplementierung: Alles selbst bauen. PostHog liefert Dashboards, Funnels, Retention Charts out of the box. Ich habe SQL-Queries.

Keine Dashboards: Mein „Dashboard” sind Queries im SQLite-CLI. Für eine persönliche Website reicht das. Für ein Unternehmen nicht.

Kein Echtzeit: Ich frage Daten ab, wenn ich sie sehen will. Kein Live-Dashboard mit aktuellen Besuchern.

Keine horizontale Skalierung: Aber für eine Website mit ein paar hundert Besuchen am Tag ist SQLite mühelos ausreichend.

Implementierungszeit: Ein Tag Arbeit. PostHog einrichten dauert 10 Minuten. Der ROI stimmt nur bei spezifischen Anforderungen wie meinen.

SQLite reicht

„SQLite für Analytics? Wirklich?”

Ja.

Wenig Traffic: 100-500 Page Views am Tag. SQLite schafft tausende Writes pro Sekunde.

WAL-Modus: Write-Ahead Logging — Reads blockieren nie Writes. Parallele Requests kein Problem.

Single-File-Backup: cp app.db backup.db — fertig. Kein Datenbankserver, kein Connection Pooling.

Query-Performance: Mit vernünftigen Indizes laufen selbst Queries über Millionen Zeilen in Millisekunden.

Die Tech-Branche hat Datenhaltung überkompliziert. Für die meisten Anwendungen ist SQLite nicht nur ausreichend — es ist optimal.

Ergebnisse

Nach einigen Wochen Betrieb, echte Zahlen:

  • 16,7% Editor Conversion Rate — Nutzer die den Editor öffnen und eine Signatur speichern
  • Durchschnittliche Zeichenzeit: ~13 Minuten — die Leute nehmen sich Zeit
  • Längste Session: 30 Minuten — jemand war sehr engagiert
  • 5,3 durchschnittliche Radierer-Nutzungen pro Zeichnung — Fehler passieren

Diese Insights wären in PostHog’s Interface vergraben. Mit eigenen Analytics sind sie eine SQL-Query entfernt.

Wann eigene Analytics sinnvoll sind

Eigene Analytics lohnen sich wenn:

  • Volle Datenhoheit gewünscht
  • Spezifische Integrationsanforderungen bestehen
  • SQL kein Problem ist
  • Traffic moderat ist (unter 10k täglich)
  • Keine Echtzeit-Dashboards nötig sind

PostHog/Plausible/etc. sind besser wenn:

  • Ein Team Dashboards braucht
  • Plug-and-Play gewünscht ist
  • Features wie Session Replay oder A/B Testing nötig sind
  • Millionen Events anfallen
  • Kein Code gewartet werden soll

Für meine persönliche Website ist der eigene Ansatz perfekt. Volle Kontrolle, null Zusatzkosten, genau die Features die ich brauche.