From 9f90341dcb7d99758f58dc54630d814229935c90 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Thu, 26 Mar 2026 00:05:44 +0800 Subject: [PATCH] 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. --- .../monitoring/hooks/useMonitoringData.ts | 17 +++++++++-------- .../app/home/monitoring/utils/dateUtils.ts | 19 +++++++++++++++++++ .../monitoring-tab/PipelineMonitoringTab.tsx | 7 ++++--- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/web/src/app/home/monitoring/hooks/useMonitoringData.ts b/web/src/app/home/monitoring/hooks/useMonitoringData.ts index f6dfc985..315cd5bb 100644 --- a/web/src/app/home/monitoring/hooks/useMonitoringData.ts +++ b/web/src/app/home/monitoring/hooks/useMonitoringData.ts @@ -7,6 +7,7 @@ import { EmbeddingCall, } from '../types/monitoring'; import { backendClient } from '@/app/infra/http'; +import { parseUTCTimestamp } from '../utils/dateUtils'; /** * Custom hook for fetching and managing monitoring data @@ -120,7 +121,7 @@ export function useMonitoringData(filterState: FilterState) { variables?: string; }) => ({ id: msg.id, - timestamp: new Date(msg.timestamp), + timestamp: parseUTCTimestamp(msg.timestamp), botId: msg.bot_id, botName: msg.bot_name, pipelineId: msg.pipeline_id, @@ -154,7 +155,7 @@ export function useMonitoringData(filterState: FilterState) { message_id?: string; }) => ({ id: call.id, - timestamp: new Date(call.timestamp), + timestamp: parseUTCTimestamp(call.timestamp), modelName: call.model_name, tokens: { input: call.input_tokens, @@ -190,7 +191,7 @@ export function useMonitoringData(filterState: FilterState) { call_type?: string; }) => ({ id: call.id, - timestamp: new Date(call.timestamp), + timestamp: parseUTCTimestamp(call.timestamp), modelName: call.model_name, promptTokens: call.prompt_tokens, totalTokens: call.total_tokens, @@ -227,10 +228,10 @@ export function useMonitoringData(filterState: FilterState) { pipelineName: session.pipeline_name, messageCount: session.message_count, duration: - new Date(session.last_activity).getTime() - - new Date(session.start_time).getTime(), - lastActivity: new Date(session.last_activity), - startTime: new Date(session.start_time), + 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, }), @@ -250,7 +251,7 @@ export function useMonitoringData(filterState: FilterState) { message_id?: string; }) => ({ id: error.id, - timestamp: new Date(error.timestamp), + timestamp: parseUTCTimestamp(error.timestamp), errorType: error.error_type, errorMessage: error.error_message, botId: error.bot_id, diff --git a/web/src/app/home/monitoring/utils/dateUtils.ts b/web/src/app/home/monitoring/utils/dateUtils.ts index 42ef8039..b246d3cf 100644 --- a/web/src/app/home/monitoring/utils/dateUtils.ts +++ b/web/src/app/home/monitoring/utils/dateUtils.ts @@ -97,3 +97,22 @@ export function isDateInRange(date: Date, range: DateRange | null): boolean { export function parseDate(dateStr: string): Date { return new Date(dateStr); } + +/** + * Parse a UTC timestamp string from the backend into a Date object. + * + * The backend stores all monitoring timestamps in UTC but serializes them + * as naive ISO strings (e.g. "2026-03-25T14:30:00") without a timezone + * designator. JavaScript's `new Date()` would treat such strings as local + * time, causing the displayed time to be off by the user's UTC offset. + * + * This function appends 'Z' when the string has no timezone info, so that + * `new Date()` correctly interprets it as UTC. + */ +export function parseUTCTimestamp(timestamp: string): Date { + // If the string already contains timezone info ('Z', '+', or '-' offset), parse as-is + if (/Z|[+-]\d{2}:\d{2}$/.test(timestamp)) { + return new Date(timestamp); + } + return new Date(timestamp + 'Z'); +} diff --git a/web/src/app/home/pipelines/components/monitoring-tab/PipelineMonitoringTab.tsx b/web/src/app/home/pipelines/components/monitoring-tab/PipelineMonitoringTab.tsx index 8f5b1f2b..b2c65f8f 100644 --- a/web/src/app/home/pipelines/components/monitoring-tab/PipelineMonitoringTab.tsx +++ b/web/src/app/home/pipelines/components/monitoring-tab/PipelineMonitoringTab.tsx @@ -10,6 +10,7 @@ import { MessageContentRenderer } from '@/app/home/monitoring/components/Message import { LoadingSpinner } from '@/components/ui/loading-spinner'; import { httpClient } from '@/app/infra/http/HttpClient'; import { MessageDetails } from '@/app/home/monitoring/types/monitoring'; +import { parseUTCTimestamp } from '@/app/home/monitoring/utils/dateUtils'; interface PipelineMonitoringTabProps { pipelineId: string; @@ -120,7 +121,7 @@ export default function PipelineMonitoringTab({ message: result.message ? { id: result.message.id, - timestamp: new Date(result.message.timestamp), + timestamp: parseUTCTimestamp(result.message.timestamp), botId: result.message.bot_id, botName: result.message.bot_name, pipelineId: result.message.pipeline_id, @@ -137,7 +138,7 @@ export default function PipelineMonitoringTab({ : undefined, llmCalls: result.llm_calls.map((call: RawLLMCallData) => ({ id: call.id, - timestamp: new Date(call.timestamp), + timestamp: parseUTCTimestamp(call.timestamp), modelName: call.model_name, status: call.status, duration: call.duration, @@ -150,7 +151,7 @@ export default function PipelineMonitoringTab({ })), errors: result.errors.map((error: RawErrorData) => ({ id: error.id, - timestamp: new Date(error.timestamp), + timestamp: parseUTCTimestamp(error.timestamp), errorType: error.error_type, errorMessage: error.error_message, stackTrace: error.stack_trace,