fix: ignore malformed bar analytics date keys (#1556)

* fix: ignore malformed bar analytics date keys

* style: apply prettier formatting to dynamic imports
This commit is contained in:
Kai (Tam Nhu) Tran authored and GitHub committed 2026-06-20 16:23:22 -04:00
1 parent b811cacaef
commit e37a87c3a2
2 files changed
+41 -9

No files matched your search

+23 -9
View File
@@ -259,9 +259,20 @@ function surfaceLabel(source: string): string {
}
/** Local-midnight Date from a YYYY-MM-DD day key (calendar-day anchored). */
function dateFromDayKey(key: string): Date {
const [y, m, d] = key.split('-').map((n) => parseInt(n, 10));
return new Date(y, (m || 1) - 1, d || 1);
function dateFromDayKey(key: string): Date | null {
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(key);
if (!match) return null;
const y = Number(match[1]);
const m = Number(match[2]);
const d = Number(match[3]);
const date = new Date(y, m - 1, d);
if (date.getFullYear() !== y || date.getMonth() !== m - 1 || date.getDate() !== d) {
return null;
}
return date;
}
/**
@@ -329,7 +340,9 @@ export function computeBarAnalyticsFromDaily(
// Pass 1 — daily: cost, per-model spend, per-surface spend, sparkline cost.
for (const d of daily) {
if (!d || !d.date) continue;
const delta = dayDelta(now, dateFromDayKey(d.date));
const activityDate = dateFromDayKey(d.date);
if (!activityDate) continue;
const delta = dayDelta(now, activityDate);
if (delta < 0) continue; // ignore future-dated noise
const cost = Number.isFinite(d.totalCost) ? d.totalCost : Number.isFinite(d.cost) ? d.cost : 0;
const source = d.source || '';
@@ -362,7 +375,9 @@ export function computeBarAnalyticsFromDaily(
for (const h of hourly) {
if (!h || !h.hour) continue;
const dayKey = h.hour.slice(0, 10);
const delta = dayDelta(now, dateFromDayKey(dayKey));
const activityDate = dateFromDayKey(dayKey);
if (!activityDate) continue;
const delta = dayDelta(now, activityDate);
if (delta < 0) continue;
const requests = Number.isFinite(h.requestCount) ? (h.requestCount as number) : 0;
if (requests <= 0) continue;
@@ -396,10 +411,9 @@ export function computeBarAnalyticsFromDaily(
.filter((s) => s.cost > 0 || s.requests > 0)
.sort((a, b) => b.cost - a.cost);
const lastActivityAt = lastActivityKey ? dateFromDayKey(lastActivityKey).toISOString() : null;
const daysSinceLastActivity = lastActivityKey
? dayDelta(now, dateFromDayKey(lastActivityKey))
: null;
const lastActivityDate = lastActivityKey ? dateFromDayKey(lastActivityKey) : null;
const lastActivityAt = lastActivityDate ? lastActivityDate.toISOString() : null;
const daysSinceLastActivity = lastActivityDate ? dayDelta(now, lastActivityDate) : null;
return {
today,
@@ -228,6 +228,24 @@ describe('computeBarAnalyticsFromDaily — monthToDate', () => {
expect(a.last30d.cost).toBe(20); // rolling 30d still populated
});
it('ignores malformed aggregate date keys instead of throwing', () => {
const a = computeBarAnalyticsFromDaily(
[daily({ date: 'May 1 2026', totalCost: 10 }), daily({ date: '2026-06-08', totalCost: 4 })],
[
hourly({ hour: 'May 1 2026 00:00', requestCount: 5 }),
hourly({ hour: '2026-06-08 09:00', requestCount: 3 }),
],
NOW
);
expect(a.today.cost).toBe(4);
expect(a.today.requests).toBe(3);
expect(a.allTime.cost).toBe(4);
expect(a.allTime.requests).toBe(3);
expect(a.lastActivityAt).toBe(new Date(2026, 5, 8).toISOString());
expect(a.daysSinceLastActivity).toBe(0);
});
it('returns zeroed monthToDate for empty daily and hourly input', () => {
const a = computeBarAnalyticsFromDaily([], [], NOW);
expect(a.monthToDate).toEqual({ cost: 0, requests: 0 });