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.
This commit is contained in:
Junyan Qin
2026-03-26 00:05:44 +08:00
parent 67b726afb2
commit 9f90341dcb
3 changed files with 32 additions and 11 deletions

View File

@@ -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,

View File

@@ -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');
}

View File

@@ -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,