Files
LangBot/web/src/app/home/monitoring/hooks/useMonitoringData.ts
Junyan Qin 9f90341dcb fix(web): correct UTC timestamp parsing in monitoring panel
Backend serializes monitoring timestamps as naive ISO strings without
timezone designator. JavaScript's new Date() treats such strings as
local time, causing displayed times to be off by the user's UTC offset.
Add parseUTCTimestamp() utility that appends 'Z' to ensure correct UTC
interpretation.
2026-03-26 00:05:44 +08:00

354 lines
11 KiB
TypeScript

import { useState, useEffect, useCallback, useMemo } from 'react';
import {
FilterState,
MonitoringData,
ModelCall,
LLMCall,
EmbeddingCall,
} from '../types/monitoring';
import { backendClient } from '@/app/infra/http';
import { parseUTCTimestamp } from '../utils/dateUtils';
/**
* Custom hook for fetching and managing monitoring data
*/
export function useMonitoringData(filterState: FilterState) {
const [data, setData] = useState<MonitoringData | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
// Memoize filter parameters to prevent unnecessary re-renders
const selectedBotsStr = useMemo(
() => JSON.stringify(filterState.selectedBots),
[filterState.selectedBots],
);
const selectedPipelinesStr = useMemo(
() => JSON.stringify(filterState.selectedPipelines),
[filterState.selectedPipelines],
);
const customDateRangeStr = useMemo(
() => JSON.stringify(filterState.customDateRange),
[filterState.customDateRange],
);
// Convert time range to datetime strings
const getTimeRange = useCallback(() => {
const now = new Date();
let startTime: Date | null = null;
switch (filterState.timeRange) {
case 'lastHour':
startTime = new Date(now.getTime() - 60 * 60 * 1000);
break;
case 'last6Hours':
startTime = new Date(now.getTime() - 6 * 60 * 60 * 1000);
break;
case 'last24Hours':
startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000);
break;
case 'last7Days':
startTime = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
break;
case 'last30Days':
startTime = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
break;
case 'custom':
if (filterState.customDateRange) {
startTime = filterState.customDateRange.from;
}
break;
}
const endTime =
filterState.timeRange === 'custom' && filterState.customDateRange
? filterState.customDateRange.to
: now;
return {
startTime: startTime?.toISOString(),
endTime: endTime.toISOString(),
};
}, [filterState.timeRange, filterState.customDateRange]);
// Fetch data based on filters
const fetchData = useCallback(async () => {
setLoading(true);
setError(null);
try {
const { startTime, endTime } = getTimeRange();
const response = await backendClient.getMonitoringData({
botId:
filterState.selectedBots.length > 0
? filterState.selectedBots
: undefined,
pipelineId:
filterState.selectedPipelines.length > 0
? filterState.selectedPipelines
: undefined,
startTime,
endTime,
limit: 50,
});
// Transform the response to match MonitoringData interface
const transformedData: MonitoringData = {
overview: {
totalMessages: response.overview.total_messages,
llmCalls: response.overview.llm_calls,
embeddingCalls: response.overview.embedding_calls || 0,
modelCalls:
response.overview.model_calls || response.overview.llm_calls,
successRate: response.overview.success_rate,
activeSessions: response.overview.active_sessions,
},
messages: response.messages.map(
(msg: {
id: string;
timestamp: string;
bot_id: string;
bot_name: string;
pipeline_id: string;
pipeline_name: string;
message_content: string;
session_id: string;
status: string;
level: string;
platform?: string;
user_id?: string;
runner_name?: string;
variables?: string;
}) => ({
id: msg.id,
timestamp: parseUTCTimestamp(msg.timestamp),
botId: msg.bot_id,
botName: msg.bot_name,
pipelineId: msg.pipeline_id,
pipelineName: msg.pipeline_name,
messageContent: msg.message_content,
sessionId: msg.session_id,
status: msg.status as 'success' | 'error' | 'pending',
level: msg.level as 'info' | 'warning' | 'error' | 'debug',
platform: msg.platform,
userId: msg.user_id,
runnerName: msg.runner_name,
variables: msg.variables,
}),
),
llmCalls: response.llmCalls.map(
(call: {
id: string;
timestamp: string;
model_name: string;
input_tokens: number;
output_tokens: number;
total_tokens: number;
duration: number;
cost?: number;
status: string;
bot_id: string;
bot_name: string;
pipeline_id: string;
pipeline_name: string;
error_message?: string;
message_id?: string;
}) => ({
id: call.id,
timestamp: parseUTCTimestamp(call.timestamp),
modelName: call.model_name,
tokens: {
input: call.input_tokens,
output: call.output_tokens,
total: call.total_tokens,
},
duration: call.duration,
cost: call.cost,
status: call.status as 'success' | 'error',
botId: call.bot_id,
botName: call.bot_name,
pipelineId: call.pipeline_id,
pipelineName: call.pipeline_name,
errorMessage: call.error_message,
messageId: call.message_id,
}),
),
embeddingCalls: (response.embeddingCalls || []).map(
(call: {
id: string;
timestamp: string;
model_name: string;
prompt_tokens: number;
total_tokens: number;
duration: number;
input_count: number;
status: string;
error_message?: string;
knowledge_base_id?: string;
query_text?: string;
session_id?: string;
message_id?: string;
call_type?: string;
}) => ({
id: call.id,
timestamp: parseUTCTimestamp(call.timestamp),
modelName: call.model_name,
promptTokens: call.prompt_tokens,
totalTokens: call.total_tokens,
duration: call.duration,
inputCount: call.input_count,
status: call.status as 'success' | 'error',
errorMessage: call.error_message,
knowledgeBaseId: call.knowledge_base_id,
queryText: call.query_text,
sessionId: call.session_id,
messageId: call.message_id,
callType: call.call_type as 'embedding' | 'retrieve' | undefined,
}),
),
// Create merged modelCalls array from llmCalls and embeddingCalls
modelCalls: [] as ModelCall[], // Will be populated after transform
sessions: response.sessions.map(
(session: {
session_id: string;
bot_id: string;
bot_name: string;
pipeline_id: string;
pipeline_name: string;
message_count: number;
last_activity: string;
start_time: string;
platform?: string;
user_id?: string;
}) => ({
sessionId: session.session_id,
botId: session.bot_id,
botName: session.bot_name,
pipelineId: session.pipeline_id,
pipelineName: session.pipeline_name,
messageCount: session.message_count,
duration:
parseUTCTimestamp(session.last_activity).getTime() -
parseUTCTimestamp(session.start_time).getTime(),
lastActivity: parseUTCTimestamp(session.last_activity),
startTime: parseUTCTimestamp(session.start_time),
platform: session.platform,
userId: session.user_id,
}),
),
errors: response.errors.map(
(error: {
id: string;
timestamp: string;
error_type: string;
error_message: string;
bot_id: string;
bot_name: string;
pipeline_id: string;
pipeline_name: string;
session_id?: string;
stack_trace?: string;
message_id?: string;
}) => ({
id: error.id,
timestamp: parseUTCTimestamp(error.timestamp),
errorType: error.error_type,
errorMessage: error.error_message,
botId: error.bot_id,
botName: error.bot_name,
pipelineId: error.pipeline_id,
pipelineName: error.pipeline_name,
sessionId: error.session_id,
stackTrace: error.stack_trace,
messageId: error.message_id,
}),
),
totalCount: {
messages: response.totalCount.messages,
llmCalls: response.totalCount.llmCalls,
embeddingCalls: response.totalCount.embeddingCalls || 0,
sessions: response.totalCount.sessions,
errors: response.totalCount.errors,
},
};
// Merge LLM calls and embedding calls into modelCalls
const llmModelCalls: ModelCall[] = transformedData.llmCalls.map(
(call: LLMCall): ModelCall => ({
id: call.id,
timestamp: call.timestamp,
modelName: call.modelName,
modelType: 'llm',
status: call.status,
duration: call.duration,
errorMessage: call.errorMessage,
messageId: call.messageId,
tokens: call.tokens,
cost: call.cost,
botId: call.botId,
botName: call.botName,
pipelineId: call.pipelineId,
pipelineName: call.pipelineName,
}),
);
const embeddingModelCalls: ModelCall[] =
transformedData.embeddingCalls.map(
(call: EmbeddingCall): ModelCall => ({
id: call.id,
timestamp: call.timestamp,
modelName: call.modelName,
modelType: 'embedding',
status: call.status,
duration: call.duration,
errorMessage: call.errorMessage,
messageId: call.messageId,
callType: call.callType,
promptTokens: call.promptTokens,
totalTokens: call.totalTokens,
inputCount: call.inputCount,
knowledgeBaseId: call.knowledgeBaseId,
queryText: call.queryText,
sessionId: call.sessionId,
}),
);
// Combine and sort by timestamp (newest first)
transformedData.modelCalls = [
...llmModelCalls,
...embeddingModelCalls,
].sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
setData(transformedData);
} catch (err) {
setError(err as Error);
console.error('Failed to fetch monitoring data:', err);
} finally {
setLoading(false);
}
}, [getTimeRange, filterState.selectedBots, filterState.selectedPipelines]);
// Fetch data when filter state changes
useEffect(() => {
fetchData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
selectedBotsStr,
selectedPipelinesStr,
filterState.timeRange,
customDateRangeStr,
]);
// Manual refetch function
const refetch = () => {
fetchData();
};
return {
data,
loading,
error,
refetch,
};
}