feat(logs): add expandable request/response details in API log

This commit is contained in:
Thorsten Bus 2026-03-02 12:14:34 +01:00
parent f775589f32
commit e2e1723b99
3 changed files with 95 additions and 17 deletions

View file

@ -27,6 +27,8 @@ public function index(): Response
'status' => $log->status,
'duration_ms' => $log->duration_ms,
'error_message' => $log->error_message,
'request_context' => $log->request_context,
'response_summary' => $log->response_summary,
]);
return Inertia::render('ApiLogs/Index', [

View file

@ -64,6 +64,12 @@ function statusBadgeClass(logStatus) {
function statusText(logStatus) {
return logStatus === 'error' ? 'Fehler' : 'Erfolg'
}
const expandedId = ref(null)
function toggleExpanded(id) {
expandedId.value = expandedId.value === id ? null : id
}
</script>
<template>
@ -122,11 +128,11 @@ function statusText(logStatus) {
</thead>
<tbody class="divide-y divide-gray-100 bg-white">
<template v-for="log in logs.data" :key="log.id">
<tr
v-for="log in logs.data"
:key="log.id"
:class="log.status === 'error' ? 'bg-red-50/70' : ''"
:class="[log.status === 'error' ? 'bg-red-50/70' : '', 'cursor-pointer hover:bg-gray-50 transition-colors']"
data-testid="api-log-row"
@click="toggleExpanded(log.id)"
>
<td class="px-4 py-3 text-sm text-gray-700">{{ formatDateTime(log.created_at) }}</td>
<td class="px-4 py-3 text-sm font-medium text-gray-900">{{ log.method }}</td>
@ -140,6 +146,24 @@ function statusText(logStatus) {
<td class="px-4 py-3 text-sm text-red-700">{{ log.error_message || '—' }}</td>
</tr>
<tr v-if="expandedId === log.id" class="bg-gray-50/50" data-testid="api-log-detail">
<td colspan="6" class="px-4 py-4">
<div class="space-y-3">
<div>
<span class="text-xs font-semibold uppercase tracking-wide text-gray-500">Anfrage-Kontext</span>
<pre v-if="log.request_context" class="mt-1 overflow-x-auto rounded-lg bg-gray-100 p-3 text-xs text-gray-800">{{ JSON.stringify(log.request_context, null, 2) }}</pre>
<p v-else class="mt-1 text-sm italic text-gray-400">Kein Kontext verfügbar</p>
</div>
<div>
<span class="text-xs font-semibold uppercase tracking-wide text-gray-500">Antwort-Zusammenfassung</span>
<p v-if="log.response_summary" class="mt-1 text-sm text-gray-700">{{ log.response_summary }}</p>
<p v-else class="mt-1 text-sm italic text-gray-400">Keine Zusammenfassung verfügbar</p>
</div>
</div>
</td>
</tr>
</template>
<tr v-if="logs.data.length === 0">
<td colspan="6" class="px-4 py-8 text-center text-sm text-gray-500">Keine API-Logs für deinen Filter gefunden.</td>
</tr>

View file

@ -100,6 +100,58 @@ public function test_api_log_index_filtert_nach_status(): void
);
}
public function test_api_log_index_enthaelt_request_context_und_response_summary(): void
{
$this->withoutVite();
$user = User::factory()->create();
ApiRequestLog::create([
'method' => 'fetchEvents',
'endpoint' => 'events',
'status' => 'success',
'request_context' => ['eventId' => 42, 'includeAgenda' => true],
'response_summary' => 'Array mit 3 Eintraegen',
'duration_ms' => 150,
]);
$response = $this->actingAs($user)->get(route('api-logs.index'));
$response->assertOk();
$response->assertInertia(
fn ($page) => $page
->component('ApiLogs/Index')
->has('logs.data', 1)
->where('logs.data.0.request_context', ['eventId' => 42, 'includeAgenda' => true])
->where('logs.data.0.response_summary', 'Array mit 3 Eintraegen')
);
}
public function test_api_log_index_behandelt_null_context_und_summary(): void
{
$this->withoutVite();
$user = User::factory()->create();
ApiRequestLog::create([
'method' => 'fetchSongs',
'endpoint' => 'songs',
'status' => 'success',
'request_context' => null,
'response_summary' => null,
'duration_ms' => 80,
]);
$response = $this->actingAs($user)->get(route('api-logs.index'));
$response->assertOk();
$response->assertInertia(
fn ($page) => $page
->component('ApiLogs/Index')
->has('logs.data', 1)
->where('logs.data.0.request_context', null)
->where('logs.data.0.response_summary', null)
);
}
public function test_api_request_log_scopes_funktionieren(): void
{
ApiRequestLog::create([