fix(ui): use precise hourly message counts for Peak Error Hours (#49396)

Merged via squash.

Prepared head SHA: fbbf43b84a
Co-authored-by: konanok <30515586+konanok@users.noreply.github.com>
Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
Reviewed-by: @hxy91819
This commit is contained in:
konanok
2026-04-29 15:22:42 +08:00
committed by GitHub
parent a0fd105e5e
commit bd5afadc5c
6 changed files with 462 additions and 16 deletions

View File

@@ -33,6 +33,7 @@ Docs: https://docs.openclaw.ai
- CLI/update: tolerate stale memory-runtime import failures during best-effort CLI process teardown, so `openclaw update` replacing hashed runtime chunks before the finalizer runs no longer surfaces as exit-time `Cannot find module` noise. Thanks @vincentkoc.
- CLI/channels logs: reuse the rolling log-file resolver so `openclaw channels logs` falls back to the active dated log across date boundaries without reading unrelated custom log files. Fixes #42875; carries forward #42904 and #43043. Thanks @ethanclaw and @wdskuki.
- CLI/update: skip tracked plugins disabled in config during post-update plugin sync before npm, ClawHub, or marketplace update checks, preserving their install records without failing the update. Fixes #73880. Thanks @islandpreneur007.
- Control UI: fix Peak Error Hours showing incorrect hourly rates when the browser's timezone observes DST, by storing hourly message counts with UTC date keys and using DST-aware `Date.getHours()` for local conversion. Also extract `accumulateMessageCounts` helper to reduce duplicated daily/hourly aggregation logic. (#49396) Thanks @konanok.
- iMessage: normalize known leading attributedBody corruption markers on sent-message echo text keys so delayed reflected echoes with U+FFFD/U+FFFE/U+FFFF/FEFF prefixes are dropped without collapsing interior text. Fixes #59973; carries forward #59980 and #62191. Thanks @neeravmakwana and @maguilar631697.
- Security/audit: recognize dangerous node command IDs as valid `gateway.nodes.denyCommands` entries, so audit only warns on real typos or unsupported patterns. (#56923) Thanks @chziyue.
- Telegram/exec approvals: stop treating general Telegram chat allowlists and `defaultTo` routes as native exec approvers; Telegram now uses explicit `execApprovals.approvers` or owner identity from `commands.ownerAllowFrom`, matching the first-pairing owner bootstrap path. Thanks @pashpashpash.

View File

@@ -1,4 +1,5 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
@@ -274,6 +275,17 @@ describe("session cost usage", () => {
expect(summary?.dailyLatency?.[0]?.count).toBe(1);
expect(summary?.dailyModelUsage?.[0]?.date).toBe("2026-02-01");
expect(summary?.dailyModelUsage?.[0]?.model).toBe("gpt-5.4");
// 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?.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 () => {
@@ -753,6 +765,83 @@ example
expect(logs?.[0]?.content).toBe("hello there");
});
it("buckets hourly message counts into UTC quarter-hour slots", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cost-quarter-"));
const sessionFile = path.join(root, "session.jsonl");
// Messages at different UTC quarter-hour boundaries:
// 00:14 UTC → quarterIndex = floor((0*60+14)/15) = 0
// 00:15 UTC → quarterIndex = floor((0*60+15)/15) = 1
// 06:30 UTC → quarterIndex = floor((6*60+30)/15) = 26
// 23:59 UTC → quarterIndex = floor((23*60+59)/15) = 95
const entries = [
{
type: "message",
timestamp: "2026-03-15T00:14:00.000Z",
message: { role: "user", content: "a" },
},
{
type: "message",
timestamp: "2026-03-15T00:15:00.000Z",
message: { role: "user", content: "b" },
},
{
type: "message",
timestamp: "2026-03-15T06:30:00.000Z",
message: {
role: "assistant",
provider: "openai",
model: "gpt-5.2",
usage: { input: 5, output: 5, totalTokens: 10, cost: { total: 0.001 } },
},
},
{
type: "message",
timestamp: "2026-03-15T23:59:00.000Z",
message: {
role: "assistant",
provider: "openai",
model: "gpt-5.2",
stopReason: "error",
usage: { input: 3, output: 3, totalTokens: 6, cost: { total: 0.001 } },
},
},
];
await fs.writeFile(
sessionFile,
entries.map((entry) => JSON.stringify(entry)).join("\n"),
"utf-8",
);
const summary = await loadSessionCostSummary({ sessionFile });
const quarterHourly = summary?.utcQuarterHourMessageCounts;
expect(quarterHourly).toBeDefined();
expect(quarterHourly?.length).toBe(4);
// Sort by quarterIndex for deterministic checks
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
expect(sorted[1]?.user).toBe(1);
expect(sorted[2]?.quarterIndex).toBe(26); // 06:30
expect(sorted[2]?.assistant).toBe(1);
expect(sorted[3]?.quarterIndex).toBe(95); // 23:59
expect(sorted[3]?.assistant).toBe(1);
expect(sorted[3]?.errors).toBe(1); // stopReason "error"
});
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?.utcQuarterHourMessageCounts).toBeUndefined();
});
it("preserves totals and cumulative values when downsampling timeseries", async () => {
const root = await makeSessionCostRoot("timeseries-downsample");
const sessionsDir = path.join(root, "agents", "main", "sessions");

View File

@@ -38,6 +38,7 @@ import type {
SessionLogEntry,
SessionMessageCounts,
SessionModelUsage,
SessionUtcQuarterHourMessageCounts,
SessionToolUsage,
SessionUsageTimePoint,
SessionUsageTimeSeries,
@@ -57,6 +58,7 @@ export type {
SessionLogEntry,
SessionMessageCounts,
SessionModelUsage,
SessionUtcQuarterHourMessageCounts,
SessionToolUsage,
SessionUsageTimePoint,
SessionUsageTimeSeries,
@@ -165,6 +167,39 @@ const parseTranscriptEntry = (entry: Record<string, unknown>): ParsedTranscriptE
const formatDayKey = (date: Date): string =>
date.toLocaleDateString("en-CA", { timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone });
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 UTC quarter-hour).
* Avoids duplicating the same logic for both daily and quarter-hour message counts.
*/
const accumulateMessageCounts = (
bucket: {
total: number;
user: number;
assistant: number;
toolCalls: number;
toolResults: number;
errors: number;
},
entry: ParsedTranscriptEntry,
errorStopReasons: Set<string>,
) => {
bucket.total += entry.role === "user" || entry.role === "assistant" ? 1 : 0;
if (entry.role === "user") {
bucket.user += 1;
} else if (entry.role === "assistant") {
bucket.assistant += 1;
}
bucket.toolCalls += entry.toolNames.length;
bucket.toolResults += entry.toolResultCounts.total;
bucket.errors += entry.toolResultCounts.errors;
if (entry.stopReason && errorStopReasons.has(entry.stopReason)) {
bucket.errors += 1;
}
};
const computeLatencyStats = (values: number[]): SessionLatencyStats | undefined => {
if (!values.length) {
return undefined;
@@ -572,6 +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 utcQuarterHourMessageMap = new Map<string, SessionUtcQuarterHourMessageCounts>();
const dailyLatencyMap = new Map<string, number[]>();
const dailyModelUsageMap = new Map<string, SessionDailyModelUsage>();
const messageCounts: SessionMessageCounts = {
@@ -669,19 +705,27 @@ export async function loadSessionCostSummary(params: {
toolResults: 0,
errors: 0,
};
daily.total += entry.role === "user" || entry.role === "assistant" ? 1 : 0;
if (entry.role === "user") {
daily.user += 1;
} else if (entry.role === "assistant") {
daily.assistant += 1;
}
daily.toolCalls += entry.toolNames.length;
daily.toolResults += entry.toolResultCounts.total;
daily.errors += entry.toolResultCounts.errors;
if (entry.stopReason && errorStopReasons.has(entry.stopReason)) {
daily.errors += 1;
}
accumulateMessageCounts(daily, entry, errorStopReasons);
dailyMessageMap.set(dayKey, daily);
// Per-quarter-hour message counts for precise hourly stats (UTC-based)
const quarterIndex = Math.floor(
(entry.timestamp.getUTCHours() * 60 + entry.timestamp.getUTCMinutes()) / 15,
);
const utcDayKey = formatUtcDayKey(entry.timestamp);
const quarterKey = `${utcDayKey}::${quarterIndex}`;
const utcQuarterHour = utcQuarterHourMessageMap.get(quarterKey) ?? {
date: utcDayKey,
quarterIndex,
total: 0,
user: 0,
assistant: 0,
toolCalls: 0,
toolResults: 0,
errors: 0,
};
accumulateMessageCounts(utcQuarterHour, entry, errorStopReasons);
utcQuarterHourMessageMap.set(quarterKey, utcQuarterHour);
}
if (!entry.usage) {
@@ -767,6 +811,10 @@ export async function loadSessionCostSummary(params: {
dailyMessageMap.values(),
).toSorted((a, b) => a.date.localeCompare(b.date));
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())
.map(([date, values]) => {
const stats = computeLatencyStats(values);
@@ -814,6 +862,9 @@ export async function loadSessionCostSummary(params: {
activityDates: Array.from(activityDatesSet).toSorted(),
dailyBreakdown,
dailyMessageCounts,
utcQuarterHourMessageCounts: utcQuarterHourMessageCounts.length
? utcQuarterHourMessageCounts
: undefined,
dailyLatency: dailyLatency.length ? dailyLatency : undefined,
dailyModelUsage: dailyModelUsage.length ? dailyModelUsage : undefined,
messageCounts,

View File

@@ -78,6 +78,17 @@ export type SessionDailyMessageCounts = {
errors: number;
};
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;
user: number;
assistant: number;
toolCalls: number;
toolResults: number;
errors: number;
};
export type SessionLatencyStats = {
count: number;
avgMs: number;
@@ -130,6 +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[];
utcQuarterHourMessageCounts?: SessionUtcQuarterHourMessageCounts[]; // UTC quarter-hour buckets for precise hourly stats
dailyLatency?: SessionDailyLatency[];
dailyModelUsage?: SessionDailyModelUsage[];
messageCounts?: SessionMessageCounts;

View File

@@ -0,0 +1,272 @@
import { describe, expect, it, vi, afterEach } from "vitest";
import { buildPeakErrorHours } from "./usage-metrics.ts";
import type { UsageSessionEntry } from "./usageTypes.ts";
/**
* Helper: build a minimal UsageSessionEntry with utcQuarterHourMessageCounts
* using the new UTC quarter-hour bucket format.
*/
function makeSessionWithQuarterHourly(
buckets: Array<{
date: string;
quarterIndex: number;
total: number;
errors: number;
}>,
): UsageSessionEntry {
return {
key: "test-session",
usage: {
totalTokens: 100,
totalCost: 0.01,
input: 50,
output: 50,
cacheRead: 0,
cacheWrite: 0,
inputCost: 0,
outputCost: 0,
cacheReadCost: 0,
cacheWriteCost: 0,
missingCostEntries: 0,
firstActivity: Date.now() - 3600_000,
lastActivity: Date.now(),
messageCounts: {
total: buckets.reduce((sum, b) => sum + b.total, 0),
user: 0,
assistant: 0,
toolCalls: 0,
toolResults: 0,
errors: buckets.reduce((sum, b) => sum + b.errors, 0),
},
utcQuarterHourMessageCounts: buckets.map((b) => ({
date: b.date,
quarterIndex: b.quarterIndex,
total: b.total,
user: 0,
assistant: 0,
toolCalls: 0,
toolResults: 0,
errors: b.errors,
})),
},
} as unknown as UsageSessionEntry;
}
describe("buildPeakErrorHours", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("maps UTC quarter-hour buckets to correct hours in UTC mode", () => {
// quarterIndex 0 → 00:00-00:14 UTC → hour 0
// quarterIndex 4 → 01:00-01:14 UTC → hour 1
// quarterIndex 36 → 09:00-09:14 UTC → hour 9
// quarterIndex 95 → 23:45-23:59 UTC → hour 23
const session = makeSessionWithQuarterHourly([
{ date: "2026-03-15", quarterIndex: 0, total: 10, errors: 5 },
{ date: "2026-03-15", quarterIndex: 4, total: 20, errors: 2 },
{ date: "2026-03-15", quarterIndex: 36, total: 15, errors: 3 },
{ date: "2026-03-15", quarterIndex: 95, total: 8, errors: 4 },
]);
const result = buildPeakErrorHours([session], "utc");
// All hours with errors should appear, sorted by error rate desc
expect(result.length).toBeGreaterThan(0);
expect(result.length).toBeLessThanOrEqual(5);
// Verify that hour mappings are correct by checking all returned entries
const _hourSet = new Set(result.map((r) => r.label));
// The hours present should correspond to UTC hours 0, 1, 9, 23
// formatHourLabel uses Date.setHours so labels depend on locale,
// but we can verify error rates and sub info
const highestRate = result[0];
expect(highestRate).toBeDefined();
// hour 0: 5/10 = 50%, hour 23: 4/8 = 50%, hour 9: 3/15 = 20%, hour 1: 2/20 = 10%
expect(highestRate.value).toMatch(/50\.00%/);
});
it("aggregates multiple quarter-hour buckets into the same hour in UTC mode", () => {
// quarterIndex 0 (00:00) and quarterIndex 3 (00:45) both map to hour 0
const session = makeSessionWithQuarterHourly([
{ date: "2026-03-15", quarterIndex: 0, total: 10, errors: 2 },
{ date: "2026-03-15", quarterIndex: 3, total: 5, errors: 3 },
]);
const result = buildPeakErrorHours([session], "utc");
expect(result.length).toBe(1);
// Aggregated: 5 errors / 15 total = 33.33%
expect(result[0].value).toBe("33.33%");
expect(result[0].sub).toContain("5 errors");
expect(result[0].sub).toContain("15 msgs");
});
it("shifts UTC quarter-hour buckets to local timezone in local mode", () => {
// Simulate UTC+5: UTC hour 0 → local hour 5, UTC hour 10 → local hour 15
vi.spyOn(Date.prototype, "getHours").mockImplementation(function (this: Date) {
return (this.getUTCHours() + 5) % 24;
});
// quarterIndex 0 → UTC 00:00 → local 05:00
// quarterIndex 40 → UTC 10:00 → local 15:00
const session = makeSessionWithQuarterHourly([
{ date: "2026-03-15", quarterIndex: 0, total: 10, errors: 3 },
{ date: "2026-03-15", quarterIndex: 40, total: 20, errors: 4 },
]);
const result = buildPeakErrorHours([session], "local");
expect(result.length).toBe(2);
// Verify the sub info matches aggregated values
const subs = result.map((r) => r.sub);
expect(subs).toContain("3 errors · 10 msgs"); // local hour 5
expect(subs).toContain("4 errors · 20 msgs"); // local hour 15
});
it("wraps correctly for negative local timezone (UTC-8)", () => {
// Simulate UTC-8: UTC hour 0 → local hour 16 (previous day)
vi.spyOn(Date.prototype, "getHours").mockImplementation(function (this: Date) {
return (this.getUTCHours() - 8 + 24) % 24;
});
// quarterIndex 0 → UTC 00:00 → local 16:00
const session = makeSessionWithQuarterHourly([
{ date: "2026-03-15", quarterIndex: 0, total: 10, errors: 5 },
]);
const result = buildPeakErrorHours([session], "local");
expect(result.length).toBe(1);
expect(result[0].value).toBe("50.00%");
expect(result[0].sub).toContain("5 errors");
expect(result[0].sub).toContain("10 msgs");
});
it("wraps correctly for positive local timezone near midnight (UTC+8, late quarter)", () => {
// Simulate UTC+8: UTC hour 17 → local hour 1 (next day)
vi.spyOn(Date.prototype, "getHours").mockImplementation(function (this: Date) {
return (this.getUTCHours() + 8) % 24;
});
// quarterIndex 68 → UTC 17:00 → local 01:00 (next day)
const session = makeSessionWithQuarterHourly([
{ date: "2026-03-15", quarterIndex: 68, total: 12, errors: 6 },
]);
const result = buildPeakErrorHours([session], "local");
expect(result.length).toBe(1);
expect(result[0].value).toBe("50.00%");
expect(result[0].sub).toContain("6 errors");
expect(result[0].sub).toContain("12 msgs");
});
it("returns empty array when no sessions have errors", () => {
const session = makeSessionWithQuarterHourly([
{ date: "2026-03-15", quarterIndex: 10, total: 50, errors: 0 },
]);
const result = buildPeakErrorHours([session], "utc");
expect(result).toEqual([]);
});
it("returns empty array when sessions have no message counts", () => {
const session: UsageSessionEntry = {
key: "empty",
usage: {
totalTokens: 0,
totalCost: 0,
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
inputCost: 0,
outputCost: 0,
cacheReadCost: 0,
cacheWriteCost: 0,
missingCostEntries: 0,
messageCounts: { total: 0, user: 0, assistant: 0, toolCalls: 0, toolResults: 0, errors: 0 },
},
} as unknown as UsageSessionEntry;
const result = buildPeakErrorHours([session], "utc");
expect(result).toEqual([]);
});
it("limits results to at most 5 entries sorted by error rate", () => {
// Create 8 different hours with errors
const buckets = Array.from({ length: 8 }, (_, i) => ({
date: "2026-03-15",
quarterIndex: i * 8, // hours 0,2,4,6,8,10,12,14
total: 100,
errors: (i + 1) * 2, // increasing error counts
}));
const session = makeSessionWithQuarterHourly(buckets);
const result = buildPeakErrorHours([session], "utc");
expect(result.length).toBe(5);
// Should be sorted by rate descending — highest rate first
const rates = result.map((r) => Number.parseFloat(r.value));
for (let i = 1; i < rates.length; i++) {
expect(rates[i - 1]).toBeGreaterThanOrEqual(rates[i]);
}
});
it("aggregates across multiple sessions", () => {
const session1 = makeSessionWithQuarterHourly([
{ date: "2026-03-15", quarterIndex: 20, total: 10, errors: 3 },
]);
const session2 = makeSessionWithQuarterHourly([
{ date: "2026-03-16", quarterIndex: 20, total: 20, errors: 7 },
]);
const result = buildPeakErrorHours([session1, session2], "utc");
expect(result.length).toBe(1);
// quarterIndex 20 → hour 5: aggregated 10 errors / 30 msgs = 33.33%
expect(result[0].value).toBe("33.33%");
expect(result[0].sub).toContain("10 errors");
expect(result[0].sub).toContain("30 msgs");
});
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",
updatedAt: now,
usage: {
totalTokens: 100,
totalCost: 0.01,
input: 50,
output: 50,
cacheRead: 0,
cacheWrite: 0,
inputCost: 0,
outputCost: 0,
cacheReadCost: 0,
cacheWriteCost: 0,
missingCostEntries: 0,
firstActivity: now - 3600_000,
lastActivity: now,
messageCounts: {
total: 10,
user: 5,
assistant: 5,
toolCalls: 0,
toolResults: 0,
errors: 3,
},
// No utcQuarterHourMessageCounts -> fallback path
},
} as unknown as UsageSessionEntry;
const result = buildPeakErrorHours([session], "utc");
// Should still produce results via the proportional allocation fallback
expect(result.length).toBeGreaterThan(0);
// All errors (3) should be distributed proportionally
const totalErrors = result.reduce((sum, r) => {
const match = r.sub.match(/(\d+) errors/);
return sum + (match ? Number.parseInt(match[1], 10) : 0);
}, 0);
expect(totalErrors).toBe(3);
});
});

View File

@@ -79,13 +79,34 @@ function buildPeakErrorHours(sessions: UsageSessionEntry[], timeZone: "local" |
const hourMsgs = Array.from({ length: 24 }, () => 0);
for (const session of sessions) {
const messageCounts = session.usage?.messageCounts;
if (!messageCounts || messageCounts.total === 0) {
const usage = session.usage;
if (!usage?.messageCounts || usage.messageCounts.total === 0) {
continue;
}
// Prefer precise quarter-hour message counts when available.
// 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.utcQuarterHourMessageCounts && usage.utcQuarterHourMessageCounts.length > 0) {
for (const quarterHour of usage.utcQuarterHourMessageCounts) {
const hour =
timeZone === "utc"
? Math.floor(quarterHour.quarterIndex / 4)
: (() => {
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] += quarterHour.errors;
hourMsgs[hour] += quarterHour.total;
}
continue;
}
// Fallback: time-based proportional allocation (legacy algorithm)
forEachSessionHourSlice(session, timeZone, ({ hour, share }) => {
hourErrors[hour] += messageCounts.errors * share;
hourMsgs[hour] += messageCounts.total * share;
hourErrors[hour] += usage.messageCounts!.errors * share;
hourMsgs[hour] += usage.messageCounts!.total * share;
});
}