From e49ed2427ab1a53d4b55ead556e0a6bfba4bfae1 Mon Sep 17 00:00:00 2001 From: Jeff Gu Date: Sun, 12 Apr 2026 03:10:09 +0800 Subject: [PATCH] =?UTF-8?q?feat(utils):=20=E6=B7=BB=E5=8A=A0=20UTC=20?= =?UTF-8?q?=E6=97=B6=E9=97=B4=E6=88=B3=E8=A7=A3=E6=9E=90=E5=87=BD=E6=95=B0?= =?UTF-8?q?=E4=BB=A5=E6=94=AF=E6=8C=81=E6=97=A5=E6=9C=9F=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/analytics/TrendBarChart.tsx | 6 ++--- frontend/src/lib/utils.ts | 19 +++++++++++++++ frontend/src/pages/UsageLogs.tsx | 6 ++--- src/analytics/query.ts | 23 +++++++++++++++++-- 4 files changed, 46 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/analytics/TrendBarChart.tsx b/frontend/src/components/analytics/TrendBarChart.tsx index 1e3eecf..73fdb1e 100644 --- a/frontend/src/components/analytics/TrendBarChart.tsx +++ b/frontend/src/components/analytics/TrendBarChart.tsx @@ -1,10 +1,10 @@ import { AnalyticsRange, AnalyticsTrendPoint } from "@/types"; -import { formatCurrency } from "@/lib/utils"; +import { formatCurrency, parseUtcTimestamp } from "@/lib/utils"; import { Card } from "../ui/card"; const formatAxisLabel = (value: string, range: AnalyticsRange): string => { - const date = new Date(value); - if (Number.isNaN(date.getTime())) { + const date = parseUtcTimestamp(value); + if (!date) { return value; } diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index 6850c0d..6ce6dff 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -5,6 +5,25 @@ export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } +const TIMESTAMP_TIMEZONE_PATTERN = /(Z|[+-]\d{2}:\d{2})$/i + +export function parseUtcTimestamp(value: string): Date | null { + const rawValue = value.trim() + if (!rawValue) { + return null + } + + const normalizedValue = rawValue.includes('T') + ? rawValue + : rawValue.replace(' ', 'T') + const candidate = TIMESTAMP_TIMEZONE_PATTERN.test(normalizedValue) + ? normalizedValue + : `${normalizedValue}Z` + const date = new Date(candidate) + + return Number.isNaN(date.getTime()) ? null : date +} + export function formatCurrency(value: number): string { return `$${(value / 1000000).toFixed(2)}` } diff --git a/frontend/src/pages/UsageLogs.tsx b/frontend/src/pages/UsageLogs.tsx index c6a2d38..53704fe 100644 --- a/frontend/src/pages/UsageLogs.tsx +++ b/frontend/src/pages/UsageLogs.tsx @@ -10,7 +10,7 @@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } f import { Input } from "@/components/ui/input"; import { Select } from "@/components/ui/select"; import { readScopedCache, writeScopedCache } from "@/lib/local-cache"; -import { cn, formatCurrency } from "@/lib/utils"; +import { cn, formatCurrency, parseUtcTimestamp } from "@/lib/utils"; import { Eye, RefreshCw, RotateCcw, Search } from "lucide-react"; type UsageLogFilterState = { @@ -89,8 +89,8 @@ const hydrateUsageLogFilters = ( }; const formatDateTime = (value: string): string => { - const date = new Date(value); - if (Number.isNaN(date.getTime())) { + const date = parseUtcTimestamp(value); + if (!date) { return value; } diff --git a/src/analytics/query.ts b/src/analytics/query.ts index ee5b033..f04d75b 100644 --- a/src/analytics/query.ts +++ b/src/analytics/query.ts @@ -235,6 +235,25 @@ const toText = (value: unknown): string => { return typeof value === "string" ? value : ""; }; +const TIMESTAMP_TIMEZONE_PATTERN = /(Z|[+-]\d{2}:\d{2})$/i; + +const normalizeAnalyticsTimestamp = (value: unknown): string => { + const rawValue = toText(value).trim(); + if (!rawValue) { + return ""; + } + + const normalizedValue = rawValue.includes("T") + ? rawValue + : rawValue.replace(" ", "T"); + const candidate = TIMESTAMP_TIMEZONE_PATTERN.test(normalizedValue) + ? normalizedValue + : `${normalizedValue}Z`; + const date = new Date(candidate); + + return Number.isNaN(date.getTime()) ? rawValue : date.toISOString(); +}; + const escapeSqlString = (value: string): string => { return value.replace(/'/g, "''"); }; @@ -785,7 +804,7 @@ LIMIT ${limit} sampled: true, compatibilityWarning, items: rows.map((row) => ({ - timestamp: toText(row.timestamp), + timestamp: normalizeAnalyticsTimestamp(row.timestamp), routeId: toText(row.route_id), tokenName: toText(row.token_name), channelKey: toText(row.channel_key), @@ -970,7 +989,7 @@ OFFSET ${offset} hasPrevPage: totalPages > 0 && page > 1, hasNextPage: totalPages > 0 && page < totalPages, items: rows.map((row) => ({ - timestamp: toText(row.timestamp), + timestamp: normalizeAnalyticsTimestamp(row.timestamp), routeId: toText(row.route_id), tokenName: toText(row.token_name), channelKey: toText(row.channel_key),