fix(observability): preserve admin upstream error diagnostics

This commit is contained in:
elky
2026-09-07 10:39:48 +08:00
parent 470c59e197
commit 062e111c03
17 changed files with 690 additions and 116 deletions
@@ -477,6 +477,18 @@
HTTP {{ currentAttemptRequestError.statusCode }}
</span>
</div>
<div
v-if="currentAttemptRequestError.upstreamStatusCode != null && currentAttemptRequestError.upstreamStatusCode !== currentAttemptRequestError.statusCode"
class="error-upstream-status text-xs text-muted-foreground mb-1"
>
上游响应状态:HTTP {{ currentAttemptRequestError.upstreamStatusCode }}(本次尝试状态见上方)
</div>
<div
v-if="currentAttemptRequestError.finalStatusCode != null"
class="error-final-status text-xs text-muted-foreground mb-1"
>
请求最终状态:HTTP {{ currentAttemptRequestError.finalStatusCode }}(与本次尝试状态不同)
</div>
<div
v-if="currentAttemptRequestError.message"
class="error-msg"
@@ -1630,21 +1642,19 @@ const shouldShowAttemptMessageWithUpstreamResponse = (
rawMessage: string,
upstreamResponse: Record<string, unknown> | null,
): boolean => {
if (!upstreamResponse) return true
if (!upstreamResponse || !hasRenderableValue(upstreamResponse.body)) return true
const normalized = rawMessage.trim()
if (!normalized) return false
if (isLocalSyncFinalizeDiagnostic(normalized)) return true
if (isConversionDiagnosticMessage(normalized)) return true
if (isGenericExecutionRuntimeStatusMessage(normalized)) return false
const hasBody = hasRenderableValue(upstreamResponse.body)
const bodyState = (readStringField(upstreamResponse, 'body_state') ?? '').toLowerCase()
return !hasBody && bodyState === 'disabled'
return false
}
const currentAttemptRequestError = computed<{
message: string
statusCode?: number
upstreamStatusCode?: number
finalStatusCode?: number
upstreamResponse: Record<string, unknown> | null
diagnostic: Record<string, unknown> | null
} | null>(() => {
@@ -1654,11 +1664,16 @@ const currentAttemptRequestError = computed<{
const extra = extractObject(attempt.extra_data)
const upstreamResponse = extractObject(extra?.upstream_response)
const errorFlow = extractObject(extra?.error_flow)
const statusCode = readNumberField(upstreamResponse ?? {}, 'status_code')
const upstreamStatusCode = readNumberField(upstreamResponse ?? {}, 'status_code')
?? readNumberField(upstreamResponse ?? {}, 'statusCode')
const statusCode = attempt.status_code
?? upstreamStatusCode
?? readNumberField(errorFlow ?? {}, 'status_code')
?? readNumberField(errorFlow ?? {}, 'statusCode')
?? attempt.status_code
const finalStatusCode = !['pending', 'streaming'].includes(computedFinalStatus.value)
&& props.overrideStatusCode != null && props.overrideStatusCode !== statusCode
? props.overrideStatusCode
: undefined
const flowMessage = errorFlow
? readStringField(errorFlow, 'message')
: ''
@@ -1671,6 +1686,7 @@ const currentAttemptRequestError = computed<{
const diagnosticMessage = extractVisibleDiagnosticMessage(extra)
const rawMessage = chooseAttemptRawErrorMessage(flowMessage || '', fallbackMessage, diagnosticMessage)
const message = formatAttemptErrorMessage(rawMessage, statusCode) || fallbackType
|| '本次尝试失败,链路追踪未包含详细错误内容。'
const upstreamResponseDisplay = normalizeUpstreamResponseDisplay(extra?.upstream_response)
const visibleDiagnosticObjects = extractVisibleDiagnosticObjects(extra)
const shouldAttachDiagnostic = Boolean(
@@ -1695,20 +1711,16 @@ const currentAttemptRequestError = computed<{
const response = Object.keys(upstreamResponseData).length > 0
? upstreamResponseData
: null
if (
!message
&& statusCode == null
&& !response
&& !diagnostic
) return null
const showMessage = shouldShowAttemptMessageWithUpstreamResponse(
rawMessage || fallbackType,
upstreamResponseDisplay,
)
return {
message: showMessage ? (message || '未知错误') : '',
message: showMessage ? message : '',
statusCode,
upstreamStatusCode,
finalStatusCode,
upstreamResponse: response,
diagnostic,
}
@@ -659,6 +659,98 @@ describe('HorizontalRequestTimeline', () => {
expect(root.textContent).not.toContain('该错误被标记为敏感上游错误')
})
it.each(['inline', 'reference', 'disabled', 'unavailable', 'none', undefined])(
'shows a fallback for redacted errors with body state %s',
async (bodyState) => {
const trace = buildTrace([
buildCandidate({
status_code: 400,
extra_data: {
upstream_response: {
status_code: 400,
body_state: bodyState,
},
error_flow: {
source: 'upstream_response',
status_code: 400,
decision: 'retry_next_candidate',
},
},
}),
])
trace.final_status = 'failed'
const root = mountTimeline(trace, {
overrideStatusCode: 503,
requestStatus: 'failed',
})
await nextTick()
expect(root.querySelector('.status-tag')?.textContent?.trim()).toBe('400')
expect(root.querySelector('.error-status-badge')?.textContent?.trim()).toBe('HTTP 400')
expect(root.querySelector('.error-msg')?.textContent).toContain('链路追踪未包含详细错误内容')
expect(root.querySelector('.error-final-status')?.textContent).toContain('请求最终状态:HTTP 503')
expect(root.querySelector('.error-upstream-response-json')).toBeNull()
},
)
it('keeps the attempt status separate from the upstream transport status', async () => {
const root = mountTimeline(buildTrace([
buildCandidate({
status_code: 502,
error_type: 'stream_error',
extra_data: {
upstream_response: { status_code: 200, body_state: 'disabled' },
},
}),
]))
await nextTick()
expect(root.querySelector('.status-tag')?.textContent?.trim()).toBe('502')
expect(root.querySelector('.error-status-badge')?.textContent?.trim()).toBe('HTTP 502')
expect(root.querySelector('.error-upstream-status')?.textContent).toContain('上游响应状态:HTTP 200')
expect(root.querySelector('.error-final-status')).toBeNull()
})
it('does not label an active request status as final', async () => {
const root = mountTimeline(buildTrace([
buildCandidate({ status_code: 400 }),
]), {
overrideStatusCode: 200,
requestStatus: 'streaming',
})
await nextTick()
expect(root.querySelector('.error-final-status')).toBeNull()
})
it('shows failure details even when no status or error message was retained', async () => {
const root = mountTimeline(buildTrace([buildCandidate()]))
await nextTick()
expect(root.querySelector('.error-msg')?.textContent).toContain('链路追踪未包含详细错误内容')
})
it('keeps a generic error visible when only upstream headers are available', async () => {
const root = mountTimeline(buildTrace([
buildCandidate({
status_code: 400,
error_message: 'execution runtime stream returned non-success status 400',
extra_data: {
upstream_response: {
status_code: 400,
headers: { 'content-type': 'application/json' },
body_state: 'reference',
},
},
}),
]))
await nextTick()
expect(root.querySelector('.error-msg')?.textContent).toContain('上游返回非成功状态 400')
expect(root.querySelector('.error-upstream-response-json')?.textContent).toContain('application/json')
})
it('keeps local sync diagnostics visible when upstream response body capture is disabled', async () => {
const trace = buildTrace([
buildCandidate({