Auf dem Testserver lief die App beim Scrollen langer Timelines in ein 429: nach jeder Scroll-Pause ging sofort ein PUT /settings raus, und jedes sichtbare Vorschaubild zählte auf dasselbe Limit von 300/min. Danach blieben Bilder leer und "Einstellungen konnten nicht gespeichert werden" erschien. Backend: Medien-Reads bekommen einen eigenen Limiter (media, 1200/min), POST /client-logs nimmt Warnungen und Fehler der App an (client-logs, 30/min) und schreibt sie in den täglichen Kanal `client`; Exceptions tragen user_id, path und method im Kontext. Frontend: Einstellungen gehen frühestens 1,5 s nach der letzten Änderung und nur bei geändertem Inhalt zum Server, 429/Netzfehler werden still mit Backoff wiederholt, nur eine Ablehnung wird gemeldet. Bilder laufen durch ein Gate (6 parallel, Retry-After, 3 Versuche) und fallen auf einen Platzhalter statt auf einen kaputten Pfad zurück. Neuer clientLog-Service mit IndexedDB-Puffer, Dedupe und Batch-Versand, globaler Fehlerfang im Boot, Protokoll im Entwickler-Tab. Drei pulsierende Punkte statt weißer Fläche in Timeline-Punkt, Key Image und Medienkacheln. Dokumentation in documentation/logging.md. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
4.7 KiB
Fehler-Logging
Stand: 25.09.2026. Wo Fehler landen, wie man sie liest und was der Client automatisch meldet.
Überblick
| Quelle | Ablage | Lesen |
|---|---|---|
| Browser-App (Warnungen, Fehler) | backend/storage/logs/client-YYYY-MM-DD.log |
tail -f storage/logs/client-*.log oder php artisan pail |
| Browser-App, lokal | Einstellungen → Entwickler → Protokoll | im Browser, „Kopieren“ liefert JSON |
| Laravel (Exceptions, 5xx) | backend/storage/logs/laravel.log |
php artisan pail |
Der Kanal client ist in config/logging.php definiert (täglich rotierend, 30 Tage, per LOG_CLIENT_DAYS und LOG_CLIENT_LEVEL einstellbar). Für den Server-Log empfiehlt sich auf dem Testserver LOG_STACK=daily, damit laravel.log nicht unbegrenzt wächst.
Was der Client meldet
Der Logger liegt in frontend/src/services/clientLog.js. Er hält bis zu 200 Einträge im Speicher, spiegelt sie in die IndexedDB-Tabelle clientLogs (überlebt einen Reload) und schickt Warnungen und Fehler gebündelt an POST /api/client-logs:
- frühestens 5 Sekunden nach dem ersten neuen Eintrag, höchstens 50 Einträge pro Batch,
- nur online und nur für Konten mit Server-Anbindung (Demo-Nutzer behalten alles lokal),
- beim Verlassen der Seite mit
keepalive, damit der letzte Batch noch rausgeht, - gleiche Meldung aus gleicher Quelle innerhalb von 10 Sekunden erhöht einen Zähler statt eine Zeile zu erzeugen (
(×12)im Log).
Erfasst wird automatisch:
- jede fehlgeschlagene API-Antwort außer 401 (
[api] PUT /settings → 429, mitRetry-After), - Netzwerkfehler ohne Antwort (
→ keine Antwort, mitonline: false), - Bilder, die nach drei Versuchen nicht geladen werden konnten (
[image] …, mit Status und Pfad), - Einstellungen, die nicht übertragen werden konnten, mit Versuchszähler,
- Sync-Warteschlange, Sprachnotizen, Dexie-Fehler,
- unbehandelte Fehler: Vue
errorHandler,window.onerror,unhandledrejection, fehlgeschlagene Ressourcen-Loads (src/boot/errorLog.js).
Jeder Eintrag trägt user_id, app_version, session_id, user_agent, ip, die Route in der App (url) und den Zeitpunkt im Browser (occurred_at).
Eigene Meldungen
import { clientLog } from 'src/services/clientLog'
clientLog.warn('scope', 'Was passiert ist', error) // Error oder flaches Objekt
clientLog.error('scope', 'Was passiert ist', { status: 500 })
clientLog.info('scope', 'nur lokal sichtbar')
scope ist ein kurzes Wort (api, image, settings, sync, events, voice), damit sich im Log nach Quelle filtern lässt. Kontext wird auf eine Ebene mit Skalaren abgeflacht, verschachtelte Objekte werden als JSON-String übernommen.
Rate Limits
| Limiter | Grenze | Gilt für |
|---|---|---|
api |
300/min pro Nutzer | alle Schreib- und Lesezugriffe der App außer Medien |
media |
1200/min pro Nutzer | GET /event-media/{id}/{variant}, GET /settings/media/background |
uploads |
30/min pro Nutzer | Medien- und Hintergrund-Uploads |
client-logs |
30/min pro Nutzer | POST /client-logs |
Warum getrennt: Eine lange Timeline lädt pro sichtbarem Ereignis ein Vorschaubild. Vor dem 25.09.2026 zählten diese Bilder und die Einstellungs-Schreibvorgänge auf dasselbe Limit. Beim Scrollen wurde zusätzlich nach jeder Scroll-Pause sofort ein PUT /settings gesendet. Nach etwa einer Minute Scrollen antwortete die API mit 429, die Bilder blieben leer und die App meldete „Einstellungen konnten nicht gespeichert werden“.
Verhalten des Clients bei 429 und Netzproblemen
- Einstellungen werden lokal sofort gespeichert, zum Server frühestens 1,5 Sekunden nach der letzten Änderung und nur, wenn sich der Inhalt geändert hat. Ein 429 oder Netzfehler wird still mit wachsendem Abstand wiederholt (3 s, 6 s, … bis 60 s) und beim Wieder-Online-Gehen. Nur eine Ablehnung des Inhalts (z. B. 422) zeigt die Meldung.
- Bilder laufen durch ein Gate mit höchstens 6 gleichzeitigen Anfragen. Nach einem 429 warten alle Bildanfragen die
Retry-After-Zeit ab. Jedes Bild wird bis zu dreimal versucht, dann bleibt die Kachel leer (Platzhalter-Icon), statt ein kaputtes Bild zu zeigen. Während des Ladens pulsieren drei Punkte. - Sync-Warteschlange (Events, Medien) hatte diese Logik bereits: 429 und 5xx gelten als vorübergehend und werden wiederholt.
Nachschauen auf dem Server
# Client-Meldungen der letzten Minuten
tail -n 100 storage/logs/client-$(date +%F).log
# Nur Fehler eines Nutzers
grep '"user_id":7' storage/logs/client-*.log | grep ERROR
# Live, beide Kanäle
php artisan pail --timeout=0
Jede Laravel-Exception im Server-Log trägt seit dem 25.09.2026 user_id, path und method im Kontext (bootstrap/app.php), damit sich Server- und Client-Zeilen einander zuordnen lassen.