forked from boos/BoosAPI
Fix proxy debug trace truncation previews (#313)
This commit is contained in:
@@ -150,4 +150,37 @@ describe('proxyDebugTraceStore', () => {
|
|||||||
downgradeDecision: true,
|
downgradeDecision: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('stores truncated debug payload previews as valid JSON text for json-capable databases', async () => {
|
||||||
|
const trace = await store.createProxyDebugTrace({
|
||||||
|
downstreamPath: '/v1/responses',
|
||||||
|
clientKind: 'codex',
|
||||||
|
requestedModel: 'gpt-5.4',
|
||||||
|
requestHeaders: {
|
||||||
|
authorization: `Bearer ${'x'.repeat(5000)}`,
|
||||||
|
'x-client': 'Codex Desktop',
|
||||||
|
},
|
||||||
|
requestBody: {
|
||||||
|
model: 'gpt-5.4',
|
||||||
|
input: [{ role: 'user', content: 'hello '.repeat(2000) }],
|
||||||
|
},
|
||||||
|
maxBodyBytes: 1024,
|
||||||
|
});
|
||||||
|
|
||||||
|
const detail = await store.getProxyDebugTraceDetail(trace.id);
|
||||||
|
const headersPayload = JSON.parse(detail?.trace.requestHeadersJson || 'null');
|
||||||
|
const bodyPayload = JSON.parse(detail?.trace.requestBodyJson || 'null');
|
||||||
|
|
||||||
|
expect(headersPayload).toMatchObject({
|
||||||
|
__metapiTruncated: true,
|
||||||
|
});
|
||||||
|
expect(typeof headersPayload.preview).toBe('string');
|
||||||
|
expect(headersPayload.preview).toContain('authorization');
|
||||||
|
|
||||||
|
expect(bodyPayload).toMatchObject({
|
||||||
|
__metapiTruncated: true,
|
||||||
|
});
|
||||||
|
expect(typeof bodyPayload.preview).toBe('string');
|
||||||
|
expect(bodyPayload.preview).toContain('"model": "gpt-5.4"');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,6 +25,24 @@ export type ProxyDebugTraceSession = {
|
|||||||
let lastPruneAtMs = 0;
|
let lastPruneAtMs = 0;
|
||||||
const PRUNE_INTERVAL_MS = 5 * 60 * 1000;
|
const PRUNE_INTERVAL_MS = 5 * 60 * 1000;
|
||||||
|
|
||||||
|
type TruncatedDebugPreview = {
|
||||||
|
__metapiTruncated: true;
|
||||||
|
preview: string;
|
||||||
|
originalBytes: number;
|
||||||
|
storedBytes: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function buildTruncatedDebugPreview(text: string, maxBytes: number, originalBytes: number): string {
|
||||||
|
const truncated = Buffer.from(text, 'utf8').subarray(0, Math.max(0, maxBytes)).toString('utf8');
|
||||||
|
const payload: TruncatedDebugPreview = {
|
||||||
|
__metapiTruncated: true,
|
||||||
|
preview: truncated,
|
||||||
|
originalBytes,
|
||||||
|
storedBytes: maxBytes,
|
||||||
|
};
|
||||||
|
return JSON.stringify(payload, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
function stringifyDebugValue(value: unknown, maxBytes: number): string | null {
|
function stringifyDebugValue(value: unknown, maxBytes: number): string | null {
|
||||||
if (value == null) return null;
|
if (value == null) return null;
|
||||||
|
|
||||||
@@ -45,8 +63,7 @@ function stringifyDebugValue(value: unknown, maxBytes: number): string | null {
|
|||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
const truncated = buffer.subarray(0, Math.max(0, maxBytes)).toString('utf8');
|
return buildTruncatedDebugPreview(text, maxBytes, buffer.length);
|
||||||
return `${truncated}\n...[truncated ${buffer.length - maxBytes} bytes]`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeHeadersValue(value: HeadersLike): Record<string, unknown> | null {
|
function normalizeHeadersValue(value: HeadersLike): Record<string, unknown> | null {
|
||||||
|
|||||||
@@ -97,6 +97,15 @@ function buildListResponse(overrides?: Partial<{
|
|||||||
describe('ProxyLogs server-driven page', () => {
|
describe('ProxyLogs server-driven page', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
Object.defineProperty(globalThis, 'navigator', {
|
||||||
|
value: {
|
||||||
|
clipboard: {
|
||||||
|
writeText: vi.fn().mockResolvedValue(undefined),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
apiMock.getSites.mockResolvedValue([
|
apiMock.getSites.mockResolvedValue([
|
||||||
{ id: 9, name: 'main-site', status: 'active' },
|
{ id: 9, name: 'main-site', status: 'active' },
|
||||||
{ id: 12, name: 'backup-site', status: 'active' },
|
{ id: 12, name: 'backup-site', status: 'active' },
|
||||||
@@ -185,6 +194,7 @@ describe('ProxyLogs server-driven page', () => {
|
|||||||
id: 701,
|
id: 701,
|
||||||
requestedModel: 'gpt-4o',
|
requestedModel: 'gpt-4o',
|
||||||
sessionId: 'sess-debug-1',
|
sessionId: 'sess-debug-1',
|
||||||
|
requestHeadersJson: '{\n "authorization": "Bearer demo"\n}',
|
||||||
},
|
},
|
||||||
attempts: [],
|
attempts: [],
|
||||||
});
|
});
|
||||||
@@ -493,6 +503,49 @@ describe('ProxyLogs server-driven page', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('copies the saved request headers content from the trace detail modal', async () => {
|
||||||
|
let root!: WebTestRenderer;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await act(async () => {
|
||||||
|
root = create(
|
||||||
|
<MemoryRouter initialEntries={['/logs']}>
|
||||||
|
<ToastProvider>
|
||||||
|
<ProxyLogs />
|
||||||
|
</ToastProvider>
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await flushMicrotasks();
|
||||||
|
|
||||||
|
const viewDetailButton = root.root.find((node) => (
|
||||||
|
node.type === 'button'
|
||||||
|
&& typeof node.props.onClick === 'function'
|
||||||
|
&& collectText(node).trim() === '查看详情'
|
||||||
|
));
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
viewDetailButton.props.onClick();
|
||||||
|
});
|
||||||
|
await flushMicrotasks();
|
||||||
|
|
||||||
|
const copyButton = root.root.find((node) => (
|
||||||
|
node.type === 'button'
|
||||||
|
&& typeof node.props.onClick === 'function'
|
||||||
|
&& node.props['aria-label'] === '复制原始下游请求头'
|
||||||
|
));
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
copyButton.props.onClick({ stopPropagation: () => undefined, preventDefault: () => undefined });
|
||||||
|
});
|
||||||
|
await flushMicrotasks();
|
||||||
|
|
||||||
|
expect(globalThis.navigator.clipboard.writeText).toHaveBeenCalledWith('{\n "authorization": "Bearer demo"\n}');
|
||||||
|
} finally {
|
||||||
|
root?.unmount();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps the model badge sized to the model name in desktop rows', async () => {
|
it('keeps the model badge sized to the model name in desktop rows', async () => {
|
||||||
let root!: WebTestRenderer;
|
let root!: WebTestRenderer;
|
||||||
|
|
||||||
|
|||||||
+187
-46
@@ -64,6 +64,12 @@ type ProxyDebugTraceDetailState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type ProxyDebugTraceAttempt = ProxyDebugTraceDetail['attempts'][number];
|
type ProxyDebugTraceAttempt = ProxyDebugTraceDetail['attempts'][number];
|
||||||
|
type StoredDebugPreviewPayload = {
|
||||||
|
__metapiTruncated?: boolean;
|
||||||
|
preview?: string;
|
||||||
|
originalBytes?: number;
|
||||||
|
storedBytes?: number;
|
||||||
|
};
|
||||||
|
|
||||||
const PAGE_SIZES = [20, 50, 100];
|
const PAGE_SIZES = [20, 50, 100];
|
||||||
const DEFAULT_PAGE_SIZE = 50;
|
const DEFAULT_PAGE_SIZE = 50;
|
||||||
@@ -185,6 +191,24 @@ const detailExpandableSummaryStyle: React.CSSProperties = {
|
|||||||
background: 'color-mix(in srgb, var(--color-bg-card) 86%, var(--color-bg) 14%)',
|
background: 'color-mix(in srgb, var(--color-bg-card) 86%, var(--color-bg) 14%)',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function copyTextToClipboard(text: string) {
|
||||||
|
if (navigator.clipboard?.writeText) {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
textarea.value = text;
|
||||||
|
textarea.style.position = 'fixed';
|
||||||
|
textarea.style.opacity = '0';
|
||||||
|
textarea.style.left = '-9999px';
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.focus();
|
||||||
|
textarea.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
}
|
||||||
|
|
||||||
function formatLatency(ms: number) {
|
function formatLatency(ms: number) {
|
||||||
if (ms >= 1000) {
|
if (ms >= 1000) {
|
||||||
return `${(ms / 1000).toFixed(ms >= 10000 ? 0 : 1)}s`;
|
return `${(ms / 1000).toFixed(ms >= 10000 ? 0 : 1)}s`;
|
||||||
@@ -465,6 +489,67 @@ function formatProxyDebugTargetSummary(settings: ProxyDebugSettingsState) {
|
|||||||
return parts.length > 0 ? parts.join(',') : '不过滤,记录所有命中的新请求';
|
return parts.length > 0 ? parts.join(',') : '不过滤,记录所有命中的新请求';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stringifyStoredDebugValue(value: unknown): string | null {
|
||||||
|
if (value == null) return null;
|
||||||
|
if (typeof value === 'string') return value;
|
||||||
|
try {
|
||||||
|
return JSON.stringify(value, null, 2);
|
||||||
|
} catch {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseStoredDebugPreview(value: unknown): {
|
||||||
|
raw: string | null;
|
||||||
|
displayText: string;
|
||||||
|
truncated: boolean;
|
||||||
|
note: string | null;
|
||||||
|
} {
|
||||||
|
const raw = stringifyStoredDebugValue(value);
|
||||||
|
if (!raw) {
|
||||||
|
return {
|
||||||
|
raw: null,
|
||||||
|
displayText: '-',
|
||||||
|
truncated: false,
|
||||||
|
note: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw) as StoredDebugPreviewPayload | string;
|
||||||
|
if (typeof parsed === 'string') {
|
||||||
|
return {
|
||||||
|
raw,
|
||||||
|
displayText: parsed || '-',
|
||||||
|
truncated: false,
|
||||||
|
note: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed && typeof parsed === 'object' && parsed.__metapiTruncated && typeof parsed.preview === 'string') {
|
||||||
|
const originalBytes = Number(parsed.originalBytes || 0);
|
||||||
|
const storedBytes = Number(parsed.storedBytes || 0);
|
||||||
|
return {
|
||||||
|
raw,
|
||||||
|
displayText: parsed.preview || '-',
|
||||||
|
truncated: true,
|
||||||
|
note: originalBytes > 0 && storedBytes > 0
|
||||||
|
? `内容已截断展示,原始 ${originalBytes} bytes,当前保留 ${storedBytes} bytes。复制按钮会复制当前数据库里保存的内容。`
|
||||||
|
: '内容已截断展示。复制按钮会复制当前数据库里保存的内容。',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fall through to display the saved raw value directly.
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
raw,
|
||||||
|
displayText: raw,
|
||||||
|
truncated: false,
|
||||||
|
note: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function CompactSummaryMetric({
|
function CompactSummaryMetric({
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
@@ -921,6 +1006,19 @@ export default function ProxyLogs() {
|
|||||||
setSelectedDebugTraceId(traceId);
|
setSelectedDebugTraceId(traceId);
|
||||||
setShowDebugTraceDetailModal(true);
|
setShowDebugTraceDetailModal(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
const handleCopyStoredDebugValue = useCallback(async (label: string, value: unknown) => {
|
||||||
|
const normalized = parseStoredDebugPreview(value);
|
||||||
|
if (!normalized.raw) {
|
||||||
|
toast.error(`${label}为空,无法复制`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await copyTextToClipboard(normalized.raw);
|
||||||
|
toast.success(`已复制${label}`);
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(error?.message || `复制${label}失败`);
|
||||||
|
}
|
||||||
|
}, [toast]);
|
||||||
|
|
||||||
function renderTraceStatusBadge(trace: ProxyDebugTraceListItem) {
|
function renderTraceStatusBadge(trace: ProxyDebugTraceListItem) {
|
||||||
const failed = trace.finalStatus === 'failed';
|
const failed = trace.finalStatus === 'failed';
|
||||||
@@ -932,6 +1030,32 @@ export default function ProxyLogs() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderAttemptDetail(attempt: ProxyDebugTraceAttempt) {
|
function renderAttemptDetail(attempt: ProxyDebugTraceAttempt) {
|
||||||
|
const serializedAttempt = [
|
||||||
|
`targetUrl: ${attempt.targetUrl}`,
|
||||||
|
`runtimeExecutor: ${attempt.runtimeExecutor || '-'}`,
|
||||||
|
`recoverApplied: ${attempt.recoverApplied ? 'true' : 'false'}`,
|
||||||
|
`downgradeDecision: ${attempt.downgradeDecision ? 'true' : 'false'}`,
|
||||||
|
`downgradeReason: ${attempt.downgradeReason || '-'}`,
|
||||||
|
'',
|
||||||
|
'requestHeaders:',
|
||||||
|
stringifyStoredDebugValue(attempt.requestHeadersJson) || '-',
|
||||||
|
'',
|
||||||
|
'requestBody:',
|
||||||
|
stringifyStoredDebugValue(attempt.requestBodyJson) || '-',
|
||||||
|
'',
|
||||||
|
'responseHeaders:',
|
||||||
|
stringifyStoredDebugValue(attempt.responseHeadersJson) || '-',
|
||||||
|
'',
|
||||||
|
'responseBody:',
|
||||||
|
stringifyStoredDebugValue(attempt.responseBodyJson) || '-',
|
||||||
|
'',
|
||||||
|
'rawErrorText:',
|
||||||
|
attempt.rawErrorText || '-',
|
||||||
|
'',
|
||||||
|
'memoryWrite:',
|
||||||
|
stringifyStoredDebugValue(attempt.memoryWriteJson) || '-',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<details key={attempt.id} style={detailExpandableCardStyle}>
|
<details key={attempt.id} style={detailExpandableCardStyle}>
|
||||||
<summary style={detailExpandableSummaryStyle}>
|
<summary style={detailExpandableSummaryStyle}>
|
||||||
@@ -961,31 +1085,41 @@ export default function ProxyLogs() {
|
|||||||
降级原因:{attempt.downgradeReason}
|
降级原因:{attempt.downgradeReason}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<pre style={debugCodeBlockStyle}>
|
<pre style={debugCodeBlockStyle}>{serializedAttempt}</pre>
|
||||||
{`targetUrl: ${attempt.targetUrl}
|
</div>
|
||||||
runtimeExecutor: ${attempt.runtimeExecutor || '-'}
|
</details>
|
||||||
recoverApplied: ${attempt.recoverApplied ? 'true' : 'false'}
|
);
|
||||||
downgradeDecision: ${attempt.downgradeDecision ? 'true' : 'false'}
|
}
|
||||||
downgradeReason: ${attempt.downgradeReason || '-'}
|
|
||||||
|
|
||||||
requestHeaders:
|
function renderStoredDebugDetails(title: string, value: unknown, options?: { defaultOpen?: boolean; copyLabel?: string }) {
|
||||||
${attempt.requestHeadersJson || '-'}
|
const normalized = parseStoredDebugPreview(value);
|
||||||
|
const copyLabel = options?.copyLabel || title;
|
||||||
|
|
||||||
requestBody:
|
return (
|
||||||
${attempt.requestBodyJson || '-'}
|
<details open={options?.defaultOpen} style={detailExpandableCardStyle}>
|
||||||
|
<summary style={detailExpandableSummaryStyle}>
|
||||||
responseHeaders:
|
<span>{title}</span>
|
||||||
${attempt.responseHeadersJson || '-'}
|
</summary>
|
||||||
|
<div style={{ padding: 12, display: 'grid', gap: 10 }}>
|
||||||
responseBody:
|
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
${attempt.responseBodyJson || '-'}
|
<button
|
||||||
|
type="button"
|
||||||
rawErrorText:
|
className="btn btn-ghost"
|
||||||
${attempt.rawErrorText || '-'}
|
style={{ border: '1px solid var(--color-border)', padding: '6px 12px' }}
|
||||||
|
aria-label={`复制${copyLabel}`}
|
||||||
memoryWrite:
|
onClick={(event) => {
|
||||||
${attempt.memoryWriteJson || '-'}`}
|
event.preventDefault();
|
||||||
</pre>
|
event.stopPropagation();
|
||||||
|
void handleCopyStoredDebugValue(copyLabel, value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
复制当前保存内容
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{normalized.note ? (
|
||||||
|
<div style={{ fontSize: 12, color: 'var(--color-text-muted)' }}>{normalized.note}</div>
|
||||||
|
) : null}
|
||||||
|
<pre style={debugCodeBlockStyle}>{normalized.displayText}</pre>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
);
|
);
|
||||||
@@ -1012,6 +1146,8 @@ ${attempt.memoryWriteJson || '-'}`}
|
|||||||
return <div style={{ color: 'var(--color-text-muted)', fontSize: 13 }}>暂无追踪详情。</div>;
|
return <div style={{ color: 'var(--color-text-muted)', fontSize: 13 }}>暂无追踪详情。</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const traceDetail = selectedDebugTraceDetail.data.trace;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'grid', gap: 12 }}>
|
<div style={{ display: 'grid', gap: 12 }}>
|
||||||
<div style={{ ...formSectionStyle, gap: 10 }}>
|
<div style={{ ...formSectionStyle, gap: 10 }}>
|
||||||
@@ -1019,48 +1155,53 @@ ${attempt.memoryWriteJson || '-'}`}
|
|||||||
<div style={detailInfoGridStyle}>
|
<div style={detailInfoGridStyle}>
|
||||||
<div style={detailInfoItemStyle}>
|
<div style={detailInfoItemStyle}>
|
||||||
<div style={detailInfoLabelStyle}>下游路径</div>
|
<div style={detailInfoLabelStyle}>下游路径</div>
|
||||||
<div style={detailInfoValueStyle}>{selectedDebugTraceDetail.data.trace.downstreamPath || '-'}</div>
|
<div style={detailInfoValueStyle}>{traceDetail.downstreamPath || '-'}</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={detailInfoItemStyle}>
|
<div style={detailInfoItemStyle}>
|
||||||
<div style={detailInfoLabelStyle}>Session</div>
|
<div style={detailInfoLabelStyle}>Session</div>
|
||||||
<div style={detailInfoValueStyle}>{selectedDebugTraceDetail.data.trace.sessionId || '-'}</div>
|
<div style={detailInfoValueStyle}>{traceDetail.sessionId || '-'}</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={detailInfoItemStyle}>
|
<div style={detailInfoItemStyle}>
|
||||||
<div style={detailInfoLabelStyle}>模型</div>
|
<div style={detailInfoLabelStyle}>模型</div>
|
||||||
<div style={detailInfoValueStyle}>{selectedDebugTraceDetail.data.trace.requestedModel || '-'}</div>
|
<div style={detailInfoValueStyle}>{traceDetail.requestedModel || '-'}</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={detailInfoItemStyle}>
|
<div style={detailInfoItemStyle}>
|
||||||
<div style={detailInfoLabelStyle}>最终上游路径</div>
|
<div style={detailInfoLabelStyle}>最终上游路径</div>
|
||||||
<div style={detailInfoValueStyle}>{selectedDebugTraceDetail.data.trace.finalUpstreamPath || '-'}</div>
|
<div style={detailInfoValueStyle}>{traceDetail.finalUpstreamPath || '-'}</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ ...detailInfoItemStyle, gridColumn: '1 / -1' }}>
|
<div style={{ ...detailInfoItemStyle, gridColumn: '1 / -1' }}>
|
||||||
<div style={detailInfoLabelStyle}>候选 endpoint</div>
|
<div style={detailInfoLabelStyle}>候选 endpoint</div>
|
||||||
|
<div style={{ display: 'grid', gap: 8 }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-ghost"
|
||||||
|
style={{ border: '1px solid var(--color-border)', padding: '6px 12px' }}
|
||||||
|
aria-label="复制候选 endpoint"
|
||||||
|
onClick={() => void handleCopyStoredDebugValue('候选 endpoint', traceDetail.endpointCandidatesJson)}
|
||||||
|
>
|
||||||
|
复制当前保存内容
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<code style={{ ...debugCodeBlockStyle, margin: 0, fontSize: 11 }}>
|
<code style={{ ...debugCodeBlockStyle, margin: 0, fontSize: 11 }}>
|
||||||
{selectedDebugTraceDetail.data.trace.endpointCandidatesJson || '-'}
|
{parseStoredDebugPreview(traceDetail.endpointCandidatesJson).displayText}
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'grid', gap: 10 }}>
|
<div style={{ display: 'grid', gap: 10 }}>
|
||||||
<details open style={detailExpandableCardStyle}>
|
{renderStoredDebugDetails('原始下游请求头', traceDetail.requestHeadersJson, {
|
||||||
<summary style={detailExpandableSummaryStyle}>原始下游请求头</summary>
|
defaultOpen: true,
|
||||||
<div style={{ padding: 12 }}>
|
copyLabel: '原始下游请求头',
|
||||||
<pre style={debugCodeBlockStyle}>{selectedDebugTraceDetail.data.trace.requestHeadersJson || '-'}</pre>
|
})}
|
||||||
</div>
|
{renderStoredDebugDetails('原始下游请求体', traceDetail.requestBodyJson, {
|
||||||
</details>
|
copyLabel: '原始下游请求体',
|
||||||
<details style={detailExpandableCardStyle}>
|
})}
|
||||||
<summary style={detailExpandableSummaryStyle}>原始下游请求体</summary>
|
{renderStoredDebugDetails('最终响应', traceDetail.finalResponseBodyJson, {
|
||||||
<div style={{ padding: 12 }}>
|
copyLabel: '最终响应',
|
||||||
<pre style={debugCodeBlockStyle}>{selectedDebugTraceDetail.data.trace.requestBodyJson || '-'}</pre>
|
})}
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
<details style={detailExpandableCardStyle}>
|
|
||||||
<summary style={detailExpandableSummaryStyle}>最终响应</summary>
|
|
||||||
<div style={{ padding: 12 }}>
|
|
||||||
<pre style={debugCodeBlockStyle}>{selectedDebugTraceDetail.data.trace.finalResponseBodyJson || '-'}</pre>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'grid', gap: 8 }}>
|
<div style={{ display: 'grid', gap: 8 }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user