Add Claude observability tracing and diagnostics UI
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user