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,