fix(usage): normalize high-precision RFC3339 timestamps

This commit is contained in:
xin
2026-04-15 19:12:48 +08:00
Unverified
parent 70a12bba4f
commit 496f9900c6
7 changed files with 83 additions and 18 deletions
@@ -9,6 +9,7 @@ import type { GeminiKeyConfig, ProviderKeyConfig, OpenAIProviderConfig } from '@
import type { AuthFileItem } from '@/types/authFile';
import type { CredentialInfo } from '@/types/sourceInfo';
import { buildSourceInfoMap, resolveSourceDisplay } from '@/utils/sourceResolver';
import { parseTimestampMs } from '@/utils/timestamp';
import {
collectUsageDetails,
extractLatencyMs,
@@ -131,7 +132,7 @@ export function RequestEventsDetailsCard({
const timestampMs =
typeof detail.__timestampMs === 'number' && detail.__timestampMs > 0
? detail.__timestampMs
: Date.parse(timestamp);
: parseTimestampMs(timestamp);
const date = Number.isNaN(timestampMs) ? null : new Date(timestampMs);
const sourceRaw = String(detail.source ?? '').trim();
const authIndexRaw = detail.auth_index as unknown;
+2 -1
View File
@@ -9,6 +9,7 @@ import iconKimiLight from '@/assets/icons/kimi-light.svg';
import iconQwen from '@/assets/icons/qwen.svg';
import iconVertex from '@/assets/icons/vertex.svg';
import type { AuthFileItem } from '@/types';
import { parseTimestamp } from '@/utils/timestamp';
import {
normalizeAuthIndex,
normalizeUsageSourceId,
@@ -279,7 +280,7 @@ export const formatModified = (item: AuthFileItem): string => {
const date =
Number.isFinite(asNumber) && !Number.isNaN(asNumber)
? new Date(asNumber < 1e12 ? asNumber * 1000 : asNumber)
: new Date(String(raw));
: parseTimestamp(raw) ?? new Date(String(raw));
return Number.isNaN(date.getTime()) ? '-' : date.toLocaleString();
};
+2 -1
View File
@@ -5,6 +5,7 @@ import { USAGE_STATS_STALE_TIME_MS, useUsageStatsStore } from '@/stores';
import type { AuthFileItem, Config } from '@/types';
import type { CredentialInfo, SourceInfo } from '@/types/sourceInfo';
import { buildSourceInfoMap, resolveSourceDisplay } from '@/utils/sourceResolver';
import { parseTimestampMs } from '@/utils/timestamp';
import {
collectUsageDetailsWithEndpoint,
normalizeAuthIndex,
@@ -183,7 +184,7 @@ export function useTraceResolver(options: UseTraceResolverOptions): UseTraceReso
if (!logPath) return [];
const logTimestampMs = traceLogLine.timestamp
? Date.parse(traceLogLine.timestamp)
? parseTimestampMs(traceLogLine.timestamp)
: Number.NaN;
// Step 1: filter by path match
+2 -1
View File
@@ -5,6 +5,7 @@
import { apiClient } from './client';
import type { AuthFilesResponse } from '@/types/authFile';
import type { OAuthModelAliasEntry } from '@/types';
import { parseTimestampMs } from '@/utils/timestamp';
type StatusError = { status?: number };
type AuthFileStatusResponse = { status: string; disabled: boolean };
@@ -185,7 +186,7 @@ const readDateField = (entry: AuthFileEntry): number => {
if (Number.isFinite(asNumber)) {
return asNumber < 1e12 ? asNumber * 1000 : asNumber;
}
const parsed = Date.parse(trimmed);
const parsed = parseTimestampMs(trimmed);
if (!Number.isNaN(parsed)) {
return parsed;
}
+4 -2
View File
@@ -1,3 +1,5 @@
import { parseTimestamp } from './timestamp';
/**
* 格式化工具函数
* 从原项目 src/utils/string.js 迁移
@@ -47,7 +49,7 @@ export function formatFileSize(bytes: number): string {
* 格式化日期时间
*/
export function formatDateTime(date: string | Date, locale?: string): string {
const d = typeof date === 'string' ? new Date(date) : date;
const d = typeof date === 'string' ? parseTimestamp(date) ?? new Date(date) : date;
if (isNaN(d.getTime())) {
return 'Invalid Date';
@@ -73,7 +75,7 @@ export function formatUnixTimestamp(value: unknown, locale?: string): string {
const asNumber = typeof value === 'number' ? value : Number(value);
const date = (() => {
if (!Number.isFinite(asNumber) || Number.isNaN(asNumber)) {
return new Date(String(value));
return parseTimestamp(value) ?? new Date(String(value));
}
const abs = Math.abs(asNumber);
+58
View File
@@ -0,0 +1,58 @@
const RFC3339_HIGH_PRECISION_REGEX =
/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(\.(\d+))?(Z|[+-]\d{2}:\d{2})?$/i;
/**
* Some browsers mis-handle RFC3339 timestamps that include sub-millisecond
* precision. Normalize them to millisecond precision before parsing.
*/
export function normalizeTimestampForDateParse(value: string): string {
const trimmed = value.trim();
if (!trimmed) return '';
const match = trimmed.match(RFC3339_HIGH_PRECISION_REGEX);
if (!match) return trimmed;
const [, base, , fractionDigits = '', timezone = ''] = match;
if (fractionDigits.length <= 3) {
return trimmed;
}
return `${base}.${fractionDigits.slice(0, 3)}${timezone}`;
}
export function parseTimestampMs(value: unknown): number {
if (typeof value === 'number' && Number.isFinite(value)) {
return value;
}
if (typeof value !== 'string') {
return Number.NaN;
}
const trimmed = value.trim();
if (!trimmed) {
return Number.NaN;
}
const normalized = normalizeTimestampForDateParse(trimmed);
const normalizedParsed = Date.parse(normalized);
if (!Number.isNaN(normalizedParsed)) {
return normalizedParsed;
}
if (normalized !== trimmed) {
const originalParsed = Date.parse(trimmed);
if (!Number.isNaN(originalParsed)) {
return originalParsed;
}
}
return Number.NaN;
}
export function parseTimestamp(value: unknown): Date | null {
const timestampMs = parseTimestampMs(value);
if (!Number.isFinite(timestampMs)) {
return null;
}
return new Date(timestampMs);
}
+13 -12
View File
@@ -13,6 +13,7 @@ import {
finalizeLatencyStats,
} from './usage/latency';
import { maskApiKey } from './format';
import { parseTimestampMs } from './timestamp';
export type { DurationFormatOptions, LatencyStats } from './usage/latency';
export {
@@ -195,7 +196,7 @@ export function filterUsageByTimeRange<T>(
if (!detailRecord || typeof detailRecord.timestamp !== 'string') {
return;
}
const timestamp = Date.parse(detailRecord.timestamp);
const timestamp = parseTimestampMs(detailRecord.timestamp);
if (Number.isNaN(timestamp) || timestamp < windowStart || timestamp > nowMs) {
return;
}
@@ -545,7 +546,7 @@ export function collectUsageDetails(usageData: unknown): UsageDetail[] {
modelDetails.forEach((detailRaw) => {
if (!isRecord(detailRaw) || typeof detailRaw.timestamp !== 'string') return;
const timestamp = detailRaw.timestamp;
const timestampMs = Date.parse(timestamp);
const timestampMs = parseTimestampMs(timestamp);
const tokensRaw = isRecord(detailRaw.tokens) ? detailRaw.tokens : {};
const latencyMs = extractLatencyMs(detailRaw);
details.push({
@@ -618,7 +619,7 @@ export function collectUsageDetailsWithEndpoint(usageData: unknown): UsageDetail
modelDetails.forEach((detailRaw) => {
if (!isRecord(detailRaw) || typeof detailRaw.timestamp !== 'string') return;
const timestamp = detailRaw.timestamp;
const timestampMs = Date.parse(timestamp);
const timestampMs = parseTimestampMs(timestamp);
const tokensRaw = isRecord(detailRaw.tokens) ? detailRaw.tokens : {};
const latencyMs = extractLatencyMs(detailRaw);
details.push({
@@ -721,7 +722,7 @@ export function calculateRecentPerMinuteRates(
const timestamp =
typeof detail.__timestampMs === 'number'
? detail.__timestampMs
: Date.parse(detail.timestamp);
: parseTimestampMs(detail.timestamp);
if (!Number.isFinite(timestamp) || timestamp < windowStart || timestamp > now) {
return;
}
@@ -1131,7 +1132,7 @@ export function buildHourlySeriesByModel(
const timestamp =
typeof detail.__timestampMs === 'number'
? detail.__timestampMs
: Date.parse(detail.timestamp);
: parseTimestampMs(detail.timestamp);
if (!Number.isFinite(timestamp) || timestamp <= 0) {
return;
}
@@ -1190,7 +1191,7 @@ export function buildDailySeriesByModel(
const timestamp =
typeof detail.__timestampMs === 'number'
? detail.__timestampMs
: Date.parse(detail.timestamp);
: parseTimestampMs(detail.timestamp);
if (!Number.isFinite(timestamp) || timestamp <= 0) {
return;
}
@@ -1416,7 +1417,7 @@ export function calculateStatusBarData(
const timestamp =
typeof detail.__timestampMs === 'number'
? detail.__timestampMs
: Date.parse(detail.timestamp);
: parseTimestampMs(detail.timestamp);
if (
!Number.isFinite(timestamp) ||
timestamp <= 0 ||
@@ -1524,7 +1525,7 @@ export function calculateServiceHealthData(usageDetails: UsageDetail[]): Service
const timestamp =
typeof detail.__timestampMs === 'number'
? detail.__timestampMs
: Date.parse(detail.timestamp);
: parseTimestampMs(detail.timestamp);
if (
!Number.isFinite(timestamp) ||
timestamp <= 0 ||
@@ -1734,7 +1735,7 @@ export function buildHourlyTokenBreakdown(
const timestamp =
typeof detail.__timestampMs === 'number'
? detail.__timestampMs
: Date.parse(detail.timestamp);
: parseTimestampMs(detail.timestamp);
if (!Number.isFinite(timestamp) || timestamp <= 0) return;
const normalized = new Date(timestamp);
normalized.setMinutes(0, 0, 0);
@@ -1776,7 +1777,7 @@ export function buildDailyTokenBreakdown(usageData: unknown): TokenBreakdownSeri
const timestamp =
typeof detail.__timestampMs === 'number'
? detail.__timestampMs
: Date.parse(detail.timestamp);
: parseTimestampMs(detail.timestamp);
if (!Number.isFinite(timestamp) || timestamp <= 0) return;
const dayLabel = formatDayLabel(new Date(timestamp));
if (!dayLabel) return;
@@ -1853,7 +1854,7 @@ export function buildHourlyCostSeries(
const timestamp =
typeof detail.__timestampMs === 'number'
? detail.__timestampMs
: Date.parse(detail.timestamp);
: parseTimestampMs(detail.timestamp);
if (!Number.isFinite(timestamp) || timestamp <= 0) return;
const normalized = new Date(timestamp);
normalized.setMinutes(0, 0, 0);
@@ -1888,7 +1889,7 @@ export function buildDailyCostSeries(
const timestamp =
typeof detail.__timestampMs === 'number'
? detail.__timestampMs
: Date.parse(detail.timestamp);
: parseTimestampMs(detail.timestamp);
if (!Number.isFinite(timestamp) || timestamp <= 0) return;
const dayLabel = formatDayLabel(new Date(timestamp));
if (!dayLabel) return;