feat(frontend-usage): enhance usage UI with first byte latency metrics

- Update usage records table to display first_byte_time_ms metrics
- Improve request timeline visualization for latency tracking
- Extend usage types for new timing information
This commit is contained in:
fawney19
2025-12-16 02:39:54 +08:00
parent 4e2ba0e57f
commit f1e3c2ab11
3 changed files with 40 additions and 10 deletions

View File

@@ -479,10 +479,25 @@ const groupedTimeline = computed<NodeGroup[]>(() => {
return groups return groups
}) })
// 计算链路总耗时(从第一个节点开始到最后一个节点结束 // 计算链路总耗时(使用成功候选的 latency_ms 字段
// 优先使用 latency_ms因为它与 Usage.response_time_ms 使用相同的时间基准
// 避免 finished_at - started_at 带来的额外延迟(数据库操作时间)
const totalTraceLatency = computed(() => { const totalTraceLatency = computed(() => {
if (!timeline.value || timeline.value.length === 0) return 0 if (!timeline.value || timeline.value.length === 0) return 0
// 查找成功的候选,使用其 latency_ms
const successCandidate = timeline.value.find(c => c.status === 'success')
if (successCandidate?.latency_ms != null) {
return successCandidate.latency_ms
}
// 如果没有成功的候选,查找失败但有 latency_ms 的候选
const failedWithLatency = timeline.value.find(c => c.status === 'failed' && c.latency_ms != null)
if (failedWithLatency?.latency_ms != null) {
return failedWithLatency.latency_ms
}
// 回退:使用 finished_at - started_at 计算
let earliestStart: number | null = null let earliestStart: number | null = null
let latestEnd: number | null = null let latestEnd: number | null = null

View File

@@ -177,8 +177,9 @@
费用 费用
</TableHead> </TableHead>
<TableHead class="h-12 font-semibold w-[70px] text-right"> <TableHead class="h-12 font-semibold w-[70px] text-right">
<div class="inline-block max-w-[2rem] leading-tight"> <div class="flex flex-col items-end text-xs gap-0.5">
响应时间 <span>首字</span>
<span class="text-muted-foreground font-normal">总耗时</span>
</div> </div>
</TableHead> </TableHead>
</TableRow> </TableRow>
@@ -356,15 +357,28 @@
</div> </div>
</TableCell> </TableCell>
<TableCell class="text-right py-4 w-[70px]"> <TableCell class="text-right py-4 w-[70px]">
<span <div
v-if="record.status === 'pending' || record.status === 'streaming'" v-if="record.status === 'pending' || record.status === 'streaming'"
class="text-primary tabular-nums" class="flex flex-col items-end text-xs gap-0.5"
> >
<span class="text-primary tabular-nums">
{{ getElapsedTime(record) }} {{ getElapsedTime(record) }}
</span> </span>
<span v-else-if="record.response_time_ms"> </div>
{{ (record.response_time_ms / 1000).toFixed(2) }}s <div
</span> v-else-if="record.response_time_ms != null"
class="flex flex-col items-end text-xs gap-0.5"
>
<span
v-if="record.first_byte_time_ms != null"
class="tabular-nums"
>{{ (record.first_byte_time_ms / 1000).toFixed(2) }}s</span>
<span
v-else
class="text-muted-foreground"
>-</span>
<span class="text-muted-foreground tabular-nums">{{ (record.response_time_ms / 1000).toFixed(2) }}s</span>
</div>
<span <span
v-else v-else
class="text-muted-foreground" class="text-muted-foreground"

View File

@@ -78,6 +78,7 @@ export interface UsageRecord {
cost: number cost: number
actual_cost?: number actual_cost?: number
response_time_ms?: number response_time_ms?: number
first_byte_time_ms?: number // 首字时间 (TTFB)
is_stream: boolean is_stream: boolean
status_code?: number status_code?: number
error_message?: string error_message?: string