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>
44 lines
1.5 KiB
PHP
44 lines
1.5 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Requests;
|
|
|
|
use App\Rules\ScalarSetting;
|
|
use Illuminate\Foundation\Http\FormRequest;
|
|
|
|
class StoreClientLogsRequest extends FormRequest
|
|
{
|
|
/**
|
|
* Levels the client may report. Anything below a warning stays in the
|
|
* browser; the server log is for things somebody has to look at.
|
|
*
|
|
* @var array<int, string>
|
|
*/
|
|
public const LEVELS = ['warning', 'error'];
|
|
|
|
public function authorize(): bool
|
|
{
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
|
*/
|
|
public function rules(): array
|
|
{
|
|
return [
|
|
'appVersion' => ['nullable', 'string', 'max:40'],
|
|
'sessionId' => ['nullable', 'string', 'max:64'],
|
|
'userAgent' => ['nullable', 'string', 'max:400'],
|
|
'entries' => ['required', 'array', 'min:1', 'max:50'],
|
|
'entries.*.level' => ['required', 'string', 'in:'.implode(',', self::LEVELS)],
|
|
'entries.*.scope' => ['required', 'string', 'max:60'],
|
|
'entries.*.message' => ['required', 'string', 'max:1000'],
|
|
'entries.*.url' => ['nullable', 'string', 'max:500'],
|
|
'entries.*.occurredAt' => ['required', 'date'],
|
|
'entries.*.context' => ['nullable', 'array', 'max:20'],
|
|
// Context values are flat: a status code, a path, a message. Nested
|
|
// structures are stringified by the client before sending.
|
|
'entries.*.context.*' => ['nullable', new ScalarSetting(500)],
|
|
];
|
|
}
|
|
}
|