Add Claude observability tracing and diagnostics UI

This commit is contained in:
2026-02-24 12:50:31 -05:00
parent 6863c1da0b
commit 691591d279
22 changed files with 1898 additions and 32 deletions

View File

@@ -39,6 +39,9 @@ const dom = {
eventsLimit: document.querySelector("#events-limit"),
eventsRefresh: document.querySelector("#events-refresh"),
eventFeed: document.querySelector("#event-feed"),
claudeEventsLimit: document.querySelector("#claude-events-limit"),
claudeEventsRefresh: document.querySelector("#claude-events-refresh"),
claudeEventFeed: document.querySelector("#claude-event-feed"),
historyRefresh: document.querySelector("#history-refresh"),
historyBody: document.querySelector("#history-body"),
notificationsForm: document.querySelector("#notifications-form"),
@@ -147,6 +150,7 @@ const LABEL_HELP_BY_CONTROL = Object.freeze({
"session-project-path": "Absolute project path used when creating an explicit managed session.",
"session-close-merge": "When enabled, close will merge the session base branch back into the project branch.",
"events-limit": "Set how many recent runtime events are loaded per refresh.",
"claude-events-limit": "Set how many Claude SDK trace records are loaded per refresh.",
"cfg-webhook-url": "Webhook endpoint that receives runtime event notifications.",
"cfg-webhook-severity": "Minimum severity level that triggers webhook notifications.",
"cfg-webhook-always": "Event types that should always notify, regardless of severity.",
@@ -1493,6 +1497,43 @@ function renderEventFeed(events) {
dom.eventFeed.innerHTML = rows || '<div class="event-row"><div class="event-time">-</div><div class="event-type">-</div><div>No runtime events.</div></div>';
}
function toClaudeRowSeverity(event) {
const stage = String(event?.stage || "");
const type = String(event?.sdkMessageType || "");
if (stage === "query.error") {
return "critical";
}
if (stage === "query.stderr" || (type === "result" && String(event?.sdkMessageSubtype || "").startsWith("error_"))) {
return "warning";
}
return "info";
}
function renderClaudeTraceFeed(events) {
const rows = [...events]
.reverse()
.map((event) => {
const ts = new Date(event.timestamp).toLocaleTimeString();
const stage = String(event.stage || "query.message");
const sdkMessageType = String(event.sdkMessageType || "");
const sdkMessageSubtype = String(event.sdkMessageSubtype || "");
const typeLabel = sdkMessageType
? `${stage}/${sdkMessageType}${sdkMessageSubtype ? `:${sdkMessageSubtype}` : ""}`
: stage;
const message = typeof event.message === "string" ? event.message : JSON.stringify(event.message || "");
return `
<div class="event-row ${escapeHtml(toClaudeRowSeverity(event))}">
<div class="event-time">${escapeHtml(ts)}</div>
<div class="event-type">${escapeHtml(typeLabel)}</div>
<div>${escapeHtml(message)}</div>
</div>
`;
})
.join("");
dom.claudeEventFeed.innerHTML = rows || '<div class="event-row"><div class="event-time">-</div><div class="event-type">-</div><div>No Claude trace events.</div></div>';
}
async function refreshEvents() {
const limit = Number(dom.eventsLimit.value || "150");
const params = new URLSearchParams({
@@ -1507,6 +1548,20 @@ async function refreshEvents() {
renderEventFeed(payload.events || []);
}
async function refreshClaudeTrace() {
const limit = Number(dom.claudeEventsLimit.value || "150");
const params = new URLSearchParams({
limit: String(limit),
});
if (state.selectedSessionId) {
params.set("sessionId", state.selectedSessionId);
}
const payload = await apiRequest(`/api/claude-trace?${params.toString()}`);
renderClaudeTraceFeed(payload.events || []);
}
async function startRun(event) {
event.preventDefault();
@@ -1581,6 +1636,7 @@ async function startRun(event) {
dom.sessionSelect.value = run.sessionId;
await refreshGraph();
await refreshEvents();
await refreshClaudeTrace();
} catch (error) {
showRunStatus(error instanceof Error ? error.message : String(error), true);
}
@@ -1601,6 +1657,7 @@ async function cancelActiveRun() {
await loadSessions();
await refreshGraph();
await refreshEvents();
await refreshClaudeTrace();
} catch (error) {
showRunStatus(error instanceof Error ? error.message : String(error), true);
}
@@ -1633,6 +1690,7 @@ async function createSessionFromUi() {
dom.sessionSelect.value = state.selectedSessionId;
await refreshGraph();
await refreshEvents();
await refreshClaudeTrace();
}
} catch (error) {
showRunStatus(error instanceof Error ? error.message : String(error), true);
@@ -1659,6 +1717,7 @@ async function closeSelectedSessionFromUi() {
await loadSessions();
await refreshGraph();
await refreshEvents();
await refreshClaudeTrace();
} catch (error) {
showRunStatus(error instanceof Error ? error.message : String(error), true);
}
@@ -1808,6 +1867,7 @@ function bindUiEvents() {
state.selectedSessionId = dom.sessionSelect.value;
await refreshGraph();
await refreshEvents();
await refreshClaudeTrace();
});
dom.graphManifestSelect.addEventListener("change", async () => {
@@ -1827,9 +1887,14 @@ function bindUiEvents() {
await refreshEvents();
});
dom.claudeEventsRefresh.addEventListener("click", async () => {
await refreshClaudeTrace();
});
dom.historyRefresh.addEventListener("click", async () => {
await loadSessions();
await refreshGraph();
await refreshClaudeTrace();
});
dom.runForm.addEventListener("submit", startRun);
@@ -1949,6 +2014,7 @@ async function refreshAll() {
await refreshGraph();
await refreshEvents();
await refreshClaudeTrace();
}
async function initialize() {
@@ -1979,6 +2045,10 @@ async function initialize() {
void refreshEvents();
}, 3000);
setInterval(() => {
void refreshClaudeTrace();
}, 3000);
setInterval(() => {
void refreshGraph();
}, 7000);

View File

@@ -130,6 +130,24 @@
<div id="event-feed" class="event-feed"></div>
</section>
<section class="panel claude-panel">
<div class="panel-head">
<h2>Claude Trace</h2>
<div class="panel-actions">
<label>
Limit
<select id="claude-events-limit">
<option value="80">80</option>
<option value="150" selected>150</option>
<option value="300">300</option>
</select>
</label>
<button id="claude-events-refresh" type="button">Refresh</button>
</div>
</div>
<div id="claude-event-feed" class="event-feed claude-event-feed"></div>
</section>
<section class="panel history-panel">
<div class="panel-head">
<h2>Run History</h2>

View File

@@ -79,7 +79,8 @@ p {
grid-template-columns: minmax(0, 2fr) minmax(280px, 1fr);
grid-template-areas:
"graph side"
"feed history"
"feed claude"
"history history"
"config config";
}
@@ -129,6 +130,10 @@ p {
grid-area: history;
}
.claude-panel {
grid-area: claude;
}
.config-panel {
grid-area: config;
}
@@ -314,6 +319,14 @@ button.danger {
color: var(--critical);
}
.claude-event-feed .event-row {
grid-template-columns: 110px 150px 1fr;
}
.claude-event-feed .event-type {
font-size: 0.7rem;
}
.history-table {
width: 100%;
border-collapse: collapse;
@@ -485,6 +498,7 @@ button.danger {
"graph"
"side"
"feed"
"claude"
"history"
"config";
}