fix: expose workbench projection failures in otel (#1824)
This commit is contained in:
@@ -1095,7 +1095,11 @@ async function handleWorkbenchTurnSnapshot(response, url, options, actor, rawTur
|
||||
}
|
||||
const readModel = createWorkbenchReadModel(options, actor);
|
||||
const result = await readModel.queryFacts({ traceId, limit: MAX_PAGE_LIMIT });
|
||||
if (result.error) return sendJson(response, 503, workbenchProjectionStoreError(result.error));
|
||||
if (result.error) {
|
||||
const body = workbenchProjectionStoreError(result.error);
|
||||
recordWorkbenchTurnReadMetric(options, url, 503, body, startedAt);
|
||||
return sendJson(response, 503, body);
|
||||
}
|
||||
const session = visibleFactSessions(result.facts, actor).find((item) => item.lastTraceId === traceId) ?? visibleFactSessions(result.facts, actor)[0] ?? null;
|
||||
const turn = factTurnForTrace(result.facts, traceId, turnId);
|
||||
const found = Boolean(session || turn);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* 职责: Workbench durable facts store adapter。统一读取 session/message/turn/trace projection facts,不在 GET 中推进 AgentRun facts。
|
||||
*/
|
||||
import { defaultCodeAgentTraceStore } from "./code-agent-trace-store.ts";
|
||||
import { emitCodeAgentOtelSpan } from "./otel-trace.ts";
|
||||
import { safeConversationId, safeSessionId, safeTraceId } from "./server-http-utils.ts";
|
||||
import { durableTraceStatus, normalizeWorkbenchStatus, TERMINAL_STATUSES, traceTerminalEvidence } from "./workbench-turn-projection.ts";
|
||||
|
||||
@@ -46,6 +47,7 @@ export function createWorkbenchFactsStore(options = {}, actor = null) {
|
||||
retryExhausted: finalAttempt && retryable
|
||||
});
|
||||
if (finalAttempt) {
|
||||
emitWorkbenchFactsQueryFailureOtel(params, annotatedError);
|
||||
logWorkbenchFactsQueryFailure(options.logger ?? console, params, annotatedError);
|
||||
return {
|
||||
facts: emptyWorkbenchFacts(),
|
||||
@@ -208,6 +210,39 @@ function logWorkbenchFactsQueryFailure(logger, params = {}, error = null) {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function emitWorkbenchFactsQueryFailureOtel(params = {}, error = null) {
|
||||
const data = objectValue(error?.data) ?? {};
|
||||
const families = Array.isArray(params.families) ? params.families : [];
|
||||
const traceId = safeTraceId(params.traceId) ?? "trc_unassigned";
|
||||
void emitCodeAgentOtelSpan("workbench.projection_store.query.failed", traceId, process.env, {
|
||||
status: "error",
|
||||
error,
|
||||
attributes: {
|
||||
"workbench.projection_store.error": true,
|
||||
"workbench.projection_store.retryable": data.retryable === true,
|
||||
"workbench.projection_store.transient": data.transient === true,
|
||||
"workbench.projection_store.retry_attempt": otelNonNegativeIntegerOrNull(data.retryAttempt ?? data.retryAttempts),
|
||||
"workbench.projection_store.retry_max": otelNonNegativeIntegerOrNull(data.retryMax),
|
||||
"workbench.projection_store.retry_exhausted": data.retryExhausted === true,
|
||||
"workbench.projection_store.retry_after_ms": otelNonNegativeIntegerOrNull(data.retryAfterMs),
|
||||
"workbench.facts.family_count": families.length,
|
||||
"workbench.facts.families": families.join(","),
|
||||
"workbench.facts.has_session_id": Boolean(params.sessionId),
|
||||
"workbench.facts.has_trace_id": Boolean(params.traceId),
|
||||
"workbench.facts.has_owner_user_id": Boolean(params.ownerUserId),
|
||||
"error.code": error?.code ?? null,
|
||||
"error.category": "projection_store_unavailable",
|
||||
"error.layer": "runtime-store",
|
||||
valuesRedacted: true
|
||||
}
|
||||
}).catch(() => undefined);
|
||||
}
|
||||
|
||||
function otelNonNegativeIntegerOrNull(value) {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) && parsed >= 0 ? Math.trunc(parsed) : null;
|
||||
}
|
||||
|
||||
async function durableTraceSnapshot(runtimeStore, traceId) {
|
||||
if (!runtimeStore || typeof runtimeStore.queryAgentTraceEvents !== "function") return null;
|
||||
const safeId = safeTraceId(traceId);
|
||||
|
||||
@@ -307,7 +307,17 @@ function formatDuration(ms: number): string {
|
||||
<template>
|
||||
<section id="conversation-list" ref="panelRef" class="conversation-panel" :data-following="following ? 'true' : 'false'" @scroll="onPanelScroll">
|
||||
<LoadingState v-if="workbench.sessionDetailLoading" class="conversation-detail-loading" label="加载中" />
|
||||
<article v-for="message in visibleMessages" :key="message.id" class="message-card" :data-role="message.role" :data-status="message.status">
|
||||
<article
|
||||
v-for="message in visibleMessages"
|
||||
:key="message.id"
|
||||
class="message-card"
|
||||
:data-role="message.role"
|
||||
:data-status="message.status"
|
||||
:data-session-id="message.sessionId || undefined"
|
||||
:data-message-id="message.id || undefined"
|
||||
:data-trace-id="message.traceId || message.runnerTrace?.traceId || undefined"
|
||||
:data-turn-id="message.turnId || message.traceId || message.runnerTrace?.traceId || undefined"
|
||||
>
|
||||
<header v-if="message.role !== 'user'">
|
||||
<div class="message-header-main">
|
||||
<strong>{{ message.title }}</strong>
|
||||
|
||||
Reference in New Issue
Block a user