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>
84 lines
3.1 KiB
PHP
84 lines
3.1 KiB
PHP
<?php
|
|
|
|
use App\Http\Controllers\Api\ClientLogController;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Laravel\Passport\Passport;
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
test('client logs require a token', function () {
|
|
$this->postJson('/api/client-logs', ['entries' => []])
|
|
->assertUnauthorized();
|
|
});
|
|
|
|
test('client log entries are written to the client channel with the user attached', function () {
|
|
$user = User::factory()->create();
|
|
Passport::actingAs($user);
|
|
|
|
$logger = Mockery::mock(LoggerInterface::class);
|
|
Log::shouldReceive('channel')->once()->with(ClientLogController::CHANNEL)->andReturn($logger);
|
|
|
|
$logger->shouldReceive('log')->once()->withArgs(function (string $level, string $message, array $context) use ($user): bool {
|
|
return $level === 'error'
|
|
&& $message === '[api] PUT /settings failed'
|
|
&& $context['user_id'] === $user->id
|
|
&& $context['app_version'] === '0.0.1'
|
|
&& $context['url'] === '/#/'
|
|
&& $context['context'] === ['status' => 429, 'path' => '/settings'];
|
|
});
|
|
$logger->shouldReceive('log')->once()->withArgs(function (string $level, string $message, array $context): bool {
|
|
return $level === 'warning'
|
|
&& $message === '[image] Thumbnail load failed'
|
|
&& ! array_key_exists('context', $context);
|
|
});
|
|
|
|
$this->postJson('/api/client-logs', [
|
|
'appVersion' => '0.0.1',
|
|
'sessionId' => 'abc',
|
|
'userAgent' => 'Safari',
|
|
'entries' => [
|
|
[
|
|
'level' => 'error',
|
|
'scope' => 'api',
|
|
'message' => 'PUT /settings failed',
|
|
'url' => '/#/',
|
|
'occurredAt' => '2026-09-25T10:00:00.000Z',
|
|
'context' => ['status' => 429, 'path' => '/settings'],
|
|
],
|
|
[
|
|
'level' => 'warning',
|
|
'scope' => 'image',
|
|
'message' => 'Thumbnail load failed',
|
|
'occurredAt' => '2026-09-25T10:00:01.000Z',
|
|
],
|
|
],
|
|
])->assertNoContent();
|
|
});
|
|
|
|
test('client logs reject unknown levels, oversized batches and nested context', function () {
|
|
Passport::actingAs(User::factory()->create());
|
|
|
|
$entry = fn (array $overrides = []) => [
|
|
'level' => 'error',
|
|
'scope' => 'api',
|
|
'message' => 'x',
|
|
'occurredAt' => '2026-09-25T10:00:00.000Z',
|
|
...$overrides,
|
|
];
|
|
|
|
$this->postJson('/api/client-logs', ['entries' => [$entry(['level' => 'debug'])]])
|
|
->assertStatus(422)
|
|
->assertJsonValidationErrors(['entries.0.level']);
|
|
|
|
$this->postJson('/api/client-logs', ['entries' => array_fill(0, 51, $entry())])
|
|
->assertStatus(422)
|
|
->assertJsonValidationErrors(['entries']);
|
|
|
|
$this->postJson('/api/client-logs', ['entries' => [$entry(['context' => ['nested' => ['a' => 1]]])]])
|
|
->assertStatus(422)
|
|
->assertJsonValidationErrors(['entries.0.context.nested']);
|
|
|
|
$this->postJson('/api/client-logs', ['entries' => []])
|
|
->assertStatus(422)
|
|
->assertJsonValidationErrors(['entries']);
|
|
});
|