feat(logs): add expandable request/response details in API log
This commit is contained in:
parent
f775589f32
commit
e2e1723b99
|
|
@ -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', [
|
||||
|
|
|
|||
|
|
@ -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,23 +128,41 @@ function statusText(logStatus) {
|
|||
</thead>
|
||||
|
||||
<tbody class="divide-y divide-gray-100 bg-white">
|
||||
<tr
|
||||
v-for="log in logs.data"
|
||||
:key="log.id"
|
||||
:class="log.status === 'error' ? 'bg-red-50/70' : ''"
|
||||
data-testid="api-log-row"
|
||||
>
|
||||
<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>
|
||||
<td class="px-4 py-3 text-sm text-gray-700">{{ log.endpoint }}</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-semibold ring-1 ring-inset" :class="statusBadgeClass(log.status)">
|
||||
{{ statusText(log.status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-700">{{ log.duration_ms }}</td>
|
||||
<td class="px-4 py-3 text-sm text-red-700">{{ log.error_message || '—' }}</td>
|
||||
</tr>
|
||||
<template v-for="log in logs.data" :key="log.id">
|
||||
<tr
|
||||
: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>
|
||||
<td class="px-4 py-3 text-sm text-gray-700">{{ log.endpoint }}</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-semibold ring-1 ring-inset" :class="statusBadgeClass(log.status)">
|
||||
{{ statusText(log.status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-700">{{ log.duration_ms }}</td>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
|
|
|
|||
Loading…
Reference in a new issue