fix(monitoring): tolerate null API payloads

Normalize monitoring API responses before rendering so empty or error payloads with data:null cannot crash the dashboard. Also guard chart, token, and box session arrays before reading length/map.
This commit is contained in:
dadachann
2026-06-25 08:21:33 -04:00
parent 04628d93cb
commit 53b20e2b13
4 changed files with 57 additions and 26 deletions
@@ -164,7 +164,7 @@ export default function TokenMonitoring({
}, [fetchStats]); }, [fetchStats]);
const chartData = useMemo(() => { const chartData = useMemo(() => {
if (!stats) return []; if (!stats || !Array.isArray(stats.timeseries)) return [];
return stats.timeseries.map((p) => ({ return stats.timeseries.map((p) => ({
bucket: p.bucket, bucket: p.bucket,
input: p.input_tokens, input: p.input_tokens,
@@ -198,7 +198,7 @@ export default function TokenMonitoring({
); );
} }
if (!stats || stats.summary.total_calls === 0) { if (!stats || !stats.summary || stats.summary.total_calls === 0) {
return ( return (
<div className="bg-card rounded-xl border p-6"> <div className="bg-card rounded-xl border p-6">
<div className="h-[260px] flex flex-col items-center justify-center text-muted-foreground gap-2"> <div className="h-[260px] flex flex-col items-center justify-center text-muted-foreground gap-2">
@@ -209,7 +209,8 @@ export default function TokenMonitoring({
); );
} }
const { summary, by_model } = stats; const summary = stats.summary;
const by_model = Array.isArray(stats.by_model) ? stats.by_model : [];
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -74,7 +74,7 @@ export default function SystemStatusCard({
: await httpClient.getBoxSessions().catch(() => [] as BoxSessionInfo[]); : await httpClient.getBoxSessions().catch(() => [] as BoxSessionInfo[]);
setPluginStatus(plugin); setPluginStatus(plugin);
setBoxStatus(box); setBoxStatus(box);
setBoxSessions(sessions); setBoxSessions(Array.isArray(sessions) ? sessions : []);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -34,14 +34,16 @@ export default function TrafficChart({
const { t } = useTranslation(); const { t } = useTranslation();
const chartData = useMemo(() => { const chartData = useMemo(() => {
if (!messages.length && !llmCalls.length) { const safeMessages = Array.isArray(messages) ? messages : [];
const safeLlmCalls = Array.isArray(llmCalls) ? llmCalls : [];
if (!safeMessages.length && !safeLlmCalls.length) {
return []; return [];
} }
// Combine all timestamps and find the range // Combine all timestamps and find the range
const allTimestamps = [ const allTimestamps = [
...messages.map((m) => m.timestamp.getTime()), ...safeMessages.map((m) => m.timestamp.getTime()),
...llmCalls.map((c) => c.timestamp.getTime()), ...safeLlmCalls.map((c) => c.timestamp.getTime()),
]; ];
if (allTimestamps.length === 0) return []; if (allTimestamps.length === 0) return [];
@@ -99,7 +101,7 @@ export default function TrafficChart({
} }
// Count messages per bucket // Count messages per bucket
messages.forEach((msg) => { safeMessages.forEach((msg) => {
const bucket = const bucket =
Math.floor(msg.timestamp.getTime() / bucketSize) * bucketSize; Math.floor(msg.timestamp.getTime() / bucketSize) * bucketSize;
const point = buckets.get(bucket); const point = buckets.get(bucket);
@@ -109,7 +111,7 @@ export default function TrafficChart({
}); });
// Count LLM calls per bucket // Count LLM calls per bucket
llmCalls.forEach((call) => { safeLlmCalls.forEach((call) => {
const bucket = const bucket =
Math.floor(call.timestamp.getTime() / bucketSize) * bucketSize; Math.floor(call.timestamp.getTime() / bucketSize) * bucketSize;
const point = buckets.get(bucket); const point = buckets.get(bucket);
@@ -92,18 +92,46 @@ export function useMonitoringData(filterState: FilterState) {
limit: 50, limit: 50,
}); });
const overview = response?.overview ?? {
total_messages: 0,
llm_calls: 0,
embedding_calls: 0,
model_calls: 0,
success_rate: 100,
active_sessions: 0,
};
const messages = Array.isArray(response?.messages)
? response.messages
: [];
const llmCalls = Array.isArray(response?.llmCalls)
? response.llmCalls
: [];
const embeddingCalls = Array.isArray(response?.embeddingCalls)
? response.embeddingCalls
: [];
const sessions = Array.isArray(response?.sessions)
? response.sessions
: [];
const errors = Array.isArray(response?.errors) ? response.errors : [];
const totalCount = response?.totalCount ?? {
messages: messages.length,
llmCalls: llmCalls.length,
embeddingCalls: embeddingCalls.length,
sessions: sessions.length,
errors: errors.length,
};
// Transform the response to match MonitoringData interface // Transform the response to match MonitoringData interface
const transformedData: MonitoringData = { const transformedData: MonitoringData = {
overview: { overview: {
totalMessages: response.overview.total_messages, totalMessages: overview.total_messages,
llmCalls: response.overview.llm_calls, llmCalls: overview.llm_calls,
embeddingCalls: response.overview.embedding_calls || 0, embeddingCalls: overview.embedding_calls || 0,
modelCalls: modelCalls: overview.model_calls || overview.llm_calls,
response.overview.model_calls || response.overview.llm_calls, successRate: overview.success_rate,
successRate: response.overview.success_rate, activeSessions: overview.active_sessions,
activeSessions: response.overview.active_sessions,
}, },
messages: response.messages.map( messages: messages.map(
(msg: { (msg: {
id: string; id: string;
timestamp: string; timestamp: string;
@@ -136,7 +164,7 @@ export function useMonitoringData(filterState: FilterState) {
variables: msg.variables, variables: msg.variables,
}), }),
), ),
llmCalls: response.llmCalls.map( llmCalls: llmCalls.map(
(call: { (call: {
id: string; id: string;
timestamp: string; timestamp: string;
@@ -173,7 +201,7 @@ export function useMonitoringData(filterState: FilterState) {
messageId: call.message_id, messageId: call.message_id,
}), }),
), ),
embeddingCalls: (response.embeddingCalls || []).map( embeddingCalls: embeddingCalls.map(
(call: { (call: {
id: string; id: string;
timestamp: string; timestamp: string;
@@ -208,7 +236,7 @@ export function useMonitoringData(filterState: FilterState) {
), ),
// Create merged modelCalls array from llmCalls and embeddingCalls // Create merged modelCalls array from llmCalls and embeddingCalls
modelCalls: [] as ModelCall[], // Will be populated after transform modelCalls: [] as ModelCall[], // Will be populated after transform
sessions: response.sessions.map( sessions: sessions.map(
(session: { (session: {
session_id: string; session_id: string;
bot_id: string; bot_id: string;
@@ -236,7 +264,7 @@ export function useMonitoringData(filterState: FilterState) {
userId: session.user_id, userId: session.user_id,
}), }),
), ),
errors: response.errors.map( errors: errors.map(
(error: { (error: {
id: string; id: string;
timestamp: string; timestamp: string;
@@ -264,11 +292,11 @@ export function useMonitoringData(filterState: FilterState) {
}), }),
), ),
totalCount: { totalCount: {
messages: response.totalCount.messages, messages: totalCount.messages,
llmCalls: response.totalCount.llmCalls, llmCalls: totalCount.llmCalls,
embeddingCalls: response.totalCount.embeddingCalls || 0, embeddingCalls: totalCount.embeddingCalls || 0,
sessions: response.totalCount.sessions, sessions: totalCount.sessions,
errors: response.totalCount.errors, errors: totalCount.errors,
}, },
}; };