mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 13:00:44 +00:00
refactor: rename hourlyMessageCounts to utcQuarterHourMessageCounts
This commit is contained in:
@@ -276,16 +276,16 @@ describe("session cost usage", () => {
|
||||
expect(summary?.dailyModelUsage?.[0]?.date).toBe("2026-02-01");
|
||||
expect(summary?.dailyModelUsage?.[0]?.model).toBe("gpt-5.4");
|
||||
|
||||
// hourlyMessageCounts should use UTC quarter-hour buckets
|
||||
// utcQuarterHourMessageCounts should use UTC quarter-hour buckets
|
||||
// start = 2026-02-01T10:00Z → quarterIndex = floor((10*60+0)/15) = 40
|
||||
// end = 2026-02-01T10:05Z → quarterIndex = floor((10*60+5)/15) = 40
|
||||
expect(summary?.hourlyMessageCounts).toBeDefined();
|
||||
expect(summary?.hourlyMessageCounts?.length).toBe(1);
|
||||
expect(summary?.hourlyMessageCounts?.[0]?.quarterIndex).toBe(40);
|
||||
expect(summary?.hourlyMessageCounts?.[0]?.date).toBe("2026-02-01");
|
||||
expect(summary?.hourlyMessageCounts?.[0]?.total).toBe(2);
|
||||
expect(summary?.hourlyMessageCounts?.[0]?.user).toBe(1);
|
||||
expect(summary?.hourlyMessageCounts?.[0]?.assistant).toBe(1);
|
||||
expect(summary?.utcQuarterHourMessageCounts).toBeDefined();
|
||||
expect(summary?.utcQuarterHourMessageCounts?.length).toBe(1);
|
||||
expect(summary?.utcQuarterHourMessageCounts?.[0]?.quarterIndex).toBe(40);
|
||||
expect(summary?.utcQuarterHourMessageCounts?.[0]?.date).toBe("2026-02-01");
|
||||
expect(summary?.utcQuarterHourMessageCounts?.[0]?.total).toBe(2);
|
||||
expect(summary?.utcQuarterHourMessageCounts?.[0]?.user).toBe(1);
|
||||
expect(summary?.utcQuarterHourMessageCounts?.[0]?.assistant).toBe(1);
|
||||
});
|
||||
|
||||
it("does not exclude sessions with mtime after endMs during discovery", async () => {
|
||||
@@ -815,12 +815,12 @@ example
|
||||
);
|
||||
|
||||
const summary = await loadSessionCostSummary({ sessionFile });
|
||||
const hourly = summary?.hourlyMessageCounts;
|
||||
expect(hourly).toBeDefined();
|
||||
expect(hourly?.length).toBe(4);
|
||||
const quarterHourly = summary?.utcQuarterHourMessageCounts;
|
||||
expect(quarterHourly).toBeDefined();
|
||||
expect(quarterHourly?.length).toBe(4);
|
||||
|
||||
// Sort by quarterIndex for deterministic checks
|
||||
const sorted = [...(hourly ?? [])].toSorted((a, b) => a.quarterIndex - b.quarterIndex);
|
||||
const sorted = [...(quarterHourly ?? [])].toSorted((a, b) => a.quarterIndex - b.quarterIndex);
|
||||
expect(sorted[0]?.quarterIndex).toBe(0); // 00:14
|
||||
expect(sorted[0]?.user).toBe(1);
|
||||
expect(sorted[1]?.quarterIndex).toBe(1); // 00:15
|
||||
@@ -832,14 +832,14 @@ example
|
||||
expect(sorted[3]?.errors).toBe(1); // stopReason "error"
|
||||
});
|
||||
|
||||
it("returns undefined hourlyMessageCounts when session has no messages", async () => {
|
||||
it("returns undefined utcQuarterHourMessageCounts when session has no messages", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cost-empty-hourly-"));
|
||||
const sessionFile = path.join(root, "session.jsonl");
|
||||
// Empty file — no entries at all
|
||||
await fs.writeFile(sessionFile, "", "utf-8");
|
||||
|
||||
const summary = await loadSessionCostSummary({ sessionFile });
|
||||
expect(summary?.hourlyMessageCounts).toBeUndefined();
|
||||
expect(summary?.utcQuarterHourMessageCounts).toBeUndefined();
|
||||
});
|
||||
|
||||
it("preserves totals and cumulative values when downsampling timeseries", async () => {
|
||||
|
||||
@@ -34,11 +34,11 @@ import type {
|
||||
SessionDailyMessageCounts,
|
||||
SessionDailyModelUsage,
|
||||
SessionDailyUsage,
|
||||
SessionHourlyMessageCounts,
|
||||
SessionLatencyStats,
|
||||
SessionLogEntry,
|
||||
SessionMessageCounts,
|
||||
SessionModelUsage,
|
||||
SessionUtcQuarterHourMessageCounts,
|
||||
SessionToolUsage,
|
||||
SessionUsageTimePoint,
|
||||
SessionUsageTimeSeries,
|
||||
@@ -54,11 +54,11 @@ export type {
|
||||
SessionDailyMessageCounts,
|
||||
SessionDailyModelUsage,
|
||||
SessionDailyUsage,
|
||||
SessionHourlyMessageCounts,
|
||||
SessionLatencyStats,
|
||||
SessionLogEntry,
|
||||
SessionMessageCounts,
|
||||
SessionModelUsage,
|
||||
SessionUtcQuarterHourMessageCounts,
|
||||
SessionToolUsage,
|
||||
SessionUsageTimePoint,
|
||||
SessionUsageTimeSeries,
|
||||
@@ -171,8 +171,8 @@ const formatUtcDayKey = (date: Date): string =>
|
||||
`${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, "0")}-${String(date.getUTCDate()).padStart(2, "0")}`;
|
||||
|
||||
/**
|
||||
* Accumulate message-level counts into a bucket (daily or hourly).
|
||||
* Avoids duplicating the same logic for both SessionDailyMessageCounts and SessionHourlyMessageCounts.
|
||||
* Accumulate message-level counts into a bucket (daily or UTC quarter-hour).
|
||||
* Avoids duplicating the same logic for both daily and quarter-hour message counts.
|
||||
*/
|
||||
const accumulateMessageCounts = (
|
||||
bucket: {
|
||||
@@ -607,7 +607,7 @@ export async function loadSessionCostSummary(params: {
|
||||
const activityDatesSet = new Set<string>();
|
||||
const dailyMap = new Map<string, { tokens: number; cost: number }>();
|
||||
const dailyMessageMap = new Map<string, SessionDailyMessageCounts>();
|
||||
const hourlyMessageMap = new Map<string, SessionHourlyMessageCounts>();
|
||||
const utcQuarterHourMessageMap = new Map<string, SessionUtcQuarterHourMessageCounts>();
|
||||
const dailyLatencyMap = new Map<string, number[]>();
|
||||
const dailyModelUsageMap = new Map<string, SessionDailyModelUsage>();
|
||||
const messageCounts: SessionMessageCounts = {
|
||||
@@ -714,7 +714,7 @@ export async function loadSessionCostSummary(params: {
|
||||
);
|
||||
const utcDayKey = formatUtcDayKey(entry.timestamp);
|
||||
const quarterKey = `${utcDayKey}::${quarterIndex}`;
|
||||
const hourly = hourlyMessageMap.get(quarterKey) ?? {
|
||||
const utcQuarterHour = utcQuarterHourMessageMap.get(quarterKey) ?? {
|
||||
date: utcDayKey,
|
||||
quarterIndex,
|
||||
total: 0,
|
||||
@@ -724,8 +724,8 @@ export async function loadSessionCostSummary(params: {
|
||||
toolResults: 0,
|
||||
errors: 0,
|
||||
};
|
||||
accumulateMessageCounts(hourly, entry, errorStopReasons);
|
||||
hourlyMessageMap.set(quarterKey, hourly);
|
||||
accumulateMessageCounts(utcQuarterHour, entry, errorStopReasons);
|
||||
utcQuarterHourMessageMap.set(quarterKey, utcQuarterHour);
|
||||
}
|
||||
|
||||
if (!entry.usage) {
|
||||
@@ -811,8 +811,8 @@ export async function loadSessionCostSummary(params: {
|
||||
dailyMessageMap.values(),
|
||||
).toSorted((a, b) => a.date.localeCompare(b.date));
|
||||
|
||||
const hourlyMessageCounts: SessionHourlyMessageCounts[] = Array.from(
|
||||
hourlyMessageMap.values(),
|
||||
const utcQuarterHourMessageCounts: SessionUtcQuarterHourMessageCounts[] = Array.from(
|
||||
utcQuarterHourMessageMap.values(),
|
||||
).toSorted((a, b) => a.date.localeCompare(b.date) || a.quarterIndex - b.quarterIndex);
|
||||
|
||||
const dailyLatency: SessionDailyLatency[] = Array.from(dailyLatencyMap.entries())
|
||||
@@ -862,7 +862,9 @@ export async function loadSessionCostSummary(params: {
|
||||
activityDates: Array.from(activityDatesSet).toSorted(),
|
||||
dailyBreakdown,
|
||||
dailyMessageCounts,
|
||||
hourlyMessageCounts: hourlyMessageCounts.length ? hourlyMessageCounts : undefined,
|
||||
utcQuarterHourMessageCounts: utcQuarterHourMessageCounts.length
|
||||
? utcQuarterHourMessageCounts
|
||||
: undefined,
|
||||
dailyLatency: dailyLatency.length ? dailyLatency : undefined,
|
||||
dailyModelUsage: dailyModelUsage.length ? dailyModelUsage : undefined,
|
||||
messageCounts,
|
||||
|
||||
@@ -78,7 +78,7 @@ export type SessionDailyMessageCounts = {
|
||||
errors: number;
|
||||
};
|
||||
|
||||
export type SessionHourlyMessageCounts = {
|
||||
export type SessionUtcQuarterHourMessageCounts = {
|
||||
date: string; // YYYY-MM-DD (UTC)
|
||||
quarterIndex: number; // 0-95, UTC quarter-hour bucket (index = floor((utcH * 60 + utcM) / 15))
|
||||
total: number;
|
||||
@@ -141,7 +141,7 @@ export type SessionCostSummary = CostUsageTotals & {
|
||||
activityDates?: string[]; // YYYY-MM-DD dates when session had activity
|
||||
dailyBreakdown?: SessionDailyUsage[]; // Per-day token/cost breakdown
|
||||
dailyMessageCounts?: SessionDailyMessageCounts[];
|
||||
hourlyMessageCounts?: SessionHourlyMessageCounts[]; // Per-hour message counts for precise hourly stats
|
||||
utcQuarterHourMessageCounts?: SessionUtcQuarterHourMessageCounts[]; // UTC quarter-hour buckets for precise hourly stats
|
||||
dailyLatency?: SessionDailyLatency[];
|
||||
dailyModelUsage?: SessionDailyModelUsage[];
|
||||
messageCounts?: SessionMessageCounts;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { buildPeakErrorHours } from "./usage-metrics.ts";
|
||||
import type { UsageSessionEntry } from "./usageTypes.ts";
|
||||
|
||||
/**
|
||||
* Helper: build a minimal UsageSessionEntry with hourlyMessageCounts
|
||||
* Helper: build a minimal UsageSessionEntry with utcQuarterHourMessageCounts
|
||||
* using the new UTC quarter-hour bucket format.
|
||||
*/
|
||||
function makeSessionWithQuarterHourly(
|
||||
@@ -38,7 +38,7 @@ function makeSessionWithQuarterHourly(
|
||||
toolResults: 0,
|
||||
errors: buckets.reduce((sum, b) => sum + b.errors, 0),
|
||||
},
|
||||
hourlyMessageCounts: buckets.map((b) => ({
|
||||
utcQuarterHourMessageCounts: buckets.map((b) => ({
|
||||
date: b.date,
|
||||
quarterIndex: b.quarterIndex,
|
||||
total: b.total,
|
||||
@@ -227,8 +227,8 @@ describe("buildPeakErrorHours", () => {
|
||||
expect(result[0].sub).toContain("30 msgs");
|
||||
});
|
||||
|
||||
it("falls back to proportional allocation when hourlyMessageCounts is absent", () => {
|
||||
// Session without hourlyMessageCounts should use forEachSessionHourSlice
|
||||
it("falls back to proportional allocation when utcQuarterHourMessageCounts is absent", () => {
|
||||
// Session without utcQuarterHourMessageCounts should use forEachSessionHourSlice
|
||||
const now = Date.now();
|
||||
const session: UsageSessionEntry = {
|
||||
key: "fallback-session",
|
||||
@@ -255,7 +255,7 @@ describe("buildPeakErrorHours", () => {
|
||||
toolResults: 0,
|
||||
errors: 3,
|
||||
},
|
||||
// No hourlyMessageCounts → fallback path
|
||||
// No utcQuarterHourMessageCounts -> fallback path
|
||||
},
|
||||
} as unknown as UsageSessionEntry;
|
||||
|
||||
|
||||
@@ -88,17 +88,17 @@ function buildPeakErrorHours(sessions: UsageSessionEntry[], timeZone: "local" |
|
||||
// Data is stored as UTC quarter-hour buckets (quarterIndex 0-95) with UTC date keys.
|
||||
// For local view, construct a Date from the UTC components and use getHours()
|
||||
// so the browser's DST-aware timezone logic handles offset automatically.
|
||||
if (usage.hourlyMessageCounts && usage.hourlyMessageCounts.length > 0) {
|
||||
for (const hourly of usage.hourlyMessageCounts) {
|
||||
if (usage.utcQuarterHourMessageCounts && usage.utcQuarterHourMessageCounts.length > 0) {
|
||||
for (const quarterHour of usage.utcQuarterHourMessageCounts) {
|
||||
const hour =
|
||||
timeZone === "utc"
|
||||
? Math.floor(hourly.quarterIndex / 4)
|
||||
? Math.floor(quarterHour.quarterIndex / 4)
|
||||
: (() => {
|
||||
const [y, m, d] = hourly.date.split("-").map(Number);
|
||||
return new Date(Date.UTC(y, m - 1, d, 0, hourly.quarterIndex * 15)).getHours();
|
||||
const [y, m, d] = quarterHour.date.split("-").map(Number);
|
||||
return new Date(Date.UTC(y, m - 1, d, 0, quarterHour.quarterIndex * 15)).getHours();
|
||||
})();
|
||||
hourErrors[hour] += hourly.errors;
|
||||
hourMsgs[hour] += hourly.total;
|
||||
hourErrors[hour] += quarterHour.errors;
|
||||
hourMsgs[hour] += quarterHour.total;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user