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,
|
'status' => $log->status,
|
||||||
'duration_ms' => $log->duration_ms,
|
'duration_ms' => $log->duration_ms,
|
||||||
'error_message' => $log->error_message,
|
'error_message' => $log->error_message,
|
||||||
|
'request_context' => $log->request_context,
|
||||||
|
'response_summary' => $log->response_summary,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return Inertia::render('ApiLogs/Index', [
|
return Inertia::render('ApiLogs/Index', [
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,12 @@ function statusBadgeClass(logStatus) {
|
||||||
function statusText(logStatus) {
|
function statusText(logStatus) {
|
||||||
return logStatus === 'error' ? 'Fehler' : 'Erfolg'
|
return logStatus === 'error' ? 'Fehler' : 'Erfolg'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const expandedId = ref(null)
|
||||||
|
|
||||||
|
function toggleExpanded(id) {
|
||||||
|
expandedId.value = expandedId.value === id ? null : id
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -122,11 +128,11 @@ function statusText(logStatus) {
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody class="divide-y divide-gray-100 bg-white">
|
<tbody class="divide-y divide-gray-100 bg-white">
|
||||||
|
<template v-for="log in logs.data" :key="log.id">
|
||||||
<tr
|
<tr
|
||||||
v-for="log in logs.data"
|
:class="[log.status === 'error' ? 'bg-red-50/70' : '', 'cursor-pointer hover:bg-gray-50 transition-colors']"
|
||||||
:key="log.id"
|
|
||||||
:class="log.status === 'error' ? 'bg-red-50/70' : ''"
|
|
||||||
data-testid="api-log-row"
|
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 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 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>
|
<td class="px-4 py-3 text-sm text-red-700">{{ log.error_message || '—' }}</td>
|
||||||
</tr>
|
</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">
|
<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>
|
<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>
|
</tr>
|
||||||
|
|
|
||||||
|
|
@ -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
|
public function test_api_request_log_scopes_funktionieren(): void
|
||||||
{
|
{
|
||||||
ApiRequestLog::create([
|
ApiRequestLog::create([
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue