thats-me/backend/app/Http/Controllers/Api/ClientLogController.php
Kevin Adametz ab23c9f4ef Client-Logging, getrennte Rate-Limits und Ladezustand für Bilder
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>
2026-09-25 09:31:35 +00:00

46 lines
1.5 KiB
PHP

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreClientLogsRequest;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log;
class ClientLogController extends Controller
{
/**
* Log channel that receives browser-side warnings and errors.
*/
public const CHANNEL = 'client';
/**
* POST /api/client-logs — a batch of warnings and errors collected in the
* browser. Each entry becomes one line in the `client` log channel, tagged
* with the user, the app version and where in the app it happened.
*/
public function store(StoreClientLogsRequest $request): Response
{
$userId = (int) $request->user()->id;
$batch = [
'user_id' => $userId,
'app_version' => $request->validated('appVersion'),
'session_id' => $request->validated('sessionId'),
'user_agent' => $request->validated('userAgent'),
'ip' => $request->ip(),
];
$logger = Log::channel(self::CHANNEL);
foreach ($request->validated('entries') as $entry) {
$logger->log($entry['level'], "[{$entry['scope']}] {$entry['message']}", array_filter([
...$batch,
'url' => $entry['url'] ?? null,
'occurred_at' => $entry['occurredAt'],
'context' => $entry['context'] ?? null,
], fn (mixed $value): bool => $value !== null && $value !== []));
}
return response()->noContent();
}
}