feat: optimize usage body viewing and provider card layout

This commit is contained in:
elky
2026-09-08 02:49:06 +08:00
parent 7aa0c89244
commit 099b810a2f
36 changed files with 3113 additions and 739 deletions
@@ -0,0 +1,56 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { getMock } = vi.hoisted(() => ({ getMock: vi.fn() }))
vi.mock('@/api/client', () => ({ default: { get: getMock } }))
import { dashboardApi } from '@/api/dashboard'
import { cache } from '@/utils/cache'
beforeEach(() => {
cache.clear()
getMock.mockReset()
getMock.mockResolvedValue({ data: { id: 'usage-1' } })
})
describe('dashboard body loading', () => {
it('requests opaque body bytes, outside the JSON detail cache', async () => {
const bytes = new ArrayBuffer(20)
const controller = new AbortController()
getMock.mockResolvedValue({ data: bytes, headers: { 'x-aether-body-encoding': 'gzip', 'x-aether-usage-id': 'usage-1', 'x-aether-body-field': 'request_body' } })
await expect(dashboardApi.getRequestBody('usage-1', 'request_body', controller.signal)).resolves.toEqual({ bytes, encoding: 'gzip' })
expect(getMock).toHaveBeenCalledWith('/api/admin/usage/usage-1', { params: { include_bodies: true, body_field: 'request_body', body_format: 'raw' }, responseType: 'arraybuffer', signal: controller.signal })
})
it.each([
{ 'x-aether-body-encoding': 'br', 'x-aether-usage-id': 'usage-1', 'x-aether-body-field': 'request_body' },
{ 'x-aether-body-encoding': 'gzip', 'x-aether-usage-id': 'usage-other', 'x-aether-body-field': 'request_body' },
{ 'x-aether-body-encoding': 'gzip', 'x-aether-usage-id': 'usage-1', 'x-aether-body-field': 'response_body' },
{},
])('rejects mismatched or invalid body response headers', async headers => {
getMock.mockResolvedValue({ data: new ArrayBuffer(10), headers })
await expect(dashboardApi.getRequestBody('usage-1', 'request_body')).rejects.toThrow('Invalid body response')
})
it('defaults to shallow details and isolates cached records', async () => {
await dashboardApi.getRequestDetail('usage-1')
expect(getMock).toHaveBeenLastCalledWith('/api/admin/usage/usage-1', { params: { include_bodies: false } })
await dashboardApi.getRequestDetail('usage-1', { cacheTtlMs: 5000 })
await dashboardApi.getRequestDetail('usage-2', { cacheTtlMs: 5000 })
await dashboardApi.getRequestDetail('usage-1', { cacheTtlMs: 5000 })
expect(getMock).toHaveBeenCalledTimes(3)
expect(getMock).toHaveBeenLastCalledWith('/api/admin/usage/usage-2', { params: { include_bodies: false } })
})
it('does not share abortable requests with another caller or a cancelled request', async () => {
const first = new AbortController()
const second = new AbortController()
const firstRequest = dashboardApi.getRequestDetail('usage-1', { signal: first.signal })
first.abort()
const secondRequest = dashboardApi.getRequestDetail('usage-1', { signal: second.signal })
await Promise.all([firstRequest, secondRequest])
expect(getMock).toHaveBeenCalledTimes(2)
expect(getMock).toHaveBeenLastCalledWith('/api/admin/usage/usage-1', {
params: { include_bodies: false }, signal: second.signal,
})
})
})
+34 -12
View File
@@ -190,6 +190,16 @@ export interface RequestSettlementSnapshot {
[key: string]: unknown
}
export type RequestBodyField = 'request_body' | 'provider_request_body' | 'response_body' | 'client_response_body'
export type RequestBodyLoadErrorCode = 'too_large' | 'decode_failed' | 'missing' | 'storage_unavailable'
export class RequestBodyProtocolError extends Error {
constructor() {
super('Invalid body response')
this.name = 'RequestBodyProtocolError'
}
}
export interface RequestDetail {
id: string // UUID
request_id: string
@@ -296,6 +306,7 @@ export interface RequestDetail {
response_body?: boolean
client_response_body?: boolean
} | null
body_load_error_codes?: Partial<Record<RequestBodyField, RequestBodyLoadErrorCode>> | null
metadata?: Record<string, unknown>
routing?: Record<string, unknown>
body_capture?: Record<string, unknown>
@@ -455,21 +466,32 @@ export const dashboardApi = {
// NOTE: This method now calls the new RESTful API at /api/admin/usage/{id}
async getRequestDetail(
requestId: string,
options: { includeBodies?: boolean, cacheTtlMs?: number } = {}
options: { includeBodies?: boolean, cacheTtlMs?: number, signal?: AbortSignal } = {}
): Promise<RequestDetail> {
const includeBodies = options.includeBodies ?? true
const includeBodies = options.includeBodies ?? false
const cacheTtlMs = options.cacheTtlMs ?? 0
const cacheKey = buildCacheKey('dashboard:request-detail', { requestId, includeBodies })
return cachedRequest(
cacheKey,
async () => {
const response = await apiClient.get<RequestDetail>(`/api/admin/usage/${requestId}`, {
params: { include_bodies: includeBodies },
})
return response.data
},
cacheTtlMs
)
const fetchDetail = async () => {
const response = await apiClient.get<RequestDetail>(`/api/admin/usage/${requestId}`, {
params: { include_bodies: includeBodies },
...(options.signal ? { signal: options.signal } : {}),
})
return response.data
}
return options.signal ? fetchDetail() : cachedRequest(cacheKey, fetchDetail, cacheTtlMs)
},
async getRequestBody(requestId: string, field: RequestBodyField, signal?: AbortSignal) {
const response = await apiClient.get<ArrayBuffer>(`/api/admin/usage/${requestId}`, {
params: { include_bodies: true, body_field: field, body_format: 'raw' },
responseType: 'arraybuffer',
signal,
})
const encoding = response.headers['x-aether-body-encoding']
if ((encoding !== 'gzip' && encoding !== 'json') || response.headers['x-aether-usage-id'] !== requestId || response.headers['x-aether-body-field'] !== field) {
throw new RequestBodyProtocolError()
}
return { bytes: response.data, encoding: encoding as 'gzip' | 'json' }
},
async prefetchRequestDetail(requestId: string): Promise<void> {