diff --git a/src/db/repositories.ts b/src/db/repositories.ts index 4c1f4d7831..16fcbbaaed 100644 --- a/src/db/repositories.ts +++ b/src/db/repositories.ts @@ -3606,6 +3606,13 @@ async function upsertProductUsageDailyRollup(env: Env, day: string, generatedAt: return record; } +// Bounded enum dimensions (surface / outcome / eventName) are consumed by exact-name lookups +// (e.g. the weekly value report's sumEvent over byEvent), so they must be stored complete: +// frequency-truncating a bounded exact-lookup dimension silently zeroes any value below the +// top-N cut on a high-diversity day. Only the genuinely-unbounded repo/command/tool/route +// dimensions keep a display top-N. +const FULL_DIMENSION_LIMIT = Number.MAX_SAFE_INTEGER; + function buildProductUsageDailyRollupRecord(args: { day: string; generatedAt: string; @@ -3633,9 +3640,9 @@ function buildProductUsageDailyRollupRecord(args: { maxEventCapacity: PRODUCT_USAGE_ROLLUP_EVENT_SCAN_LIMIT, firstEventAt: args.events[0]?.occurredAt ?? null, lastEventAt: args.events.at(-1)?.occurredAt ?? null, - bySurface: countProductUsageDimensions(args.events.map((event) => event.surface)).map(({ key, count }) => ({ surface: normalizeProductUsageSurface(key), count })), - byOutcome: countProductUsageDimensions(args.events.map((event) => event.outcome)).map(({ key, count }) => ({ outcome: normalizeProductUsageOutcome(key), count })), - byEvent: countProductUsageDimensions(args.events.map((event) => event.eventName)).map(({ key, count }) => ({ eventName: key, count })), + bySurface: countProductUsageDimensions(args.events.map((event) => event.surface), FULL_DIMENSION_LIMIT).map(({ key, count }) => ({ surface: normalizeProductUsageSurface(key), count })), + byOutcome: countProductUsageDimensions(args.events.map((event) => event.outcome), FULL_DIMENSION_LIMIT).map(({ key, count }) => ({ outcome: normalizeProductUsageOutcome(key), count })), + byEvent: countProductUsageDimensions(args.events.map((event) => event.eventName), FULL_DIMENSION_LIMIT).map(({ key, count }) => ({ eventName: key, count })), byRepo: countProductUsageDimensions(args.events.map((event) => event.repoFullName)), byCommand: countProductUsageDimensions(args.events.map((event) => productUsageMetadataString(event, "command"))), byTool: countProductUsageDimensions(args.events.map((event) => productUsageMetadataString(event, "toolName"))), diff --git a/test/unit/product-usage.test.ts b/test/unit/product-usage.test.ts index 55a9992df3..8e7e503d5d 100644 --- a/test/unit/product-usage.test.ts +++ b/test/unit/product-usage.test.ts @@ -472,6 +472,24 @@ describe("product usage events", () => { await expect(getProductUsageRollupStatus(env, { nowIso: "2026-05-31T00:40:00.000Z" })).resolves.toMatchObject({ status: "ready", warnings: [] }); }); + it("retains low-frequency bounded events in byEvent on a high-diversity day (no top-20 truncation)", async () => { + const env = createTestEnv(); + const day = "2026-05-30"; + // 20 distinct filler events, each recorded twice (count 2), occupy the highest-frequency slots. + for (let i = 0; i < 20; i++) { + const eventName = `filler_event_${String(i).padStart(2, "0")}`; + await recordProductUsageEvent(env, { surface: "control_panel", eventName, actor: "user", outcome: "success", occurredAt: `${day}T00:00:00.000Z` }); + await recordProductUsageEvent(env, { surface: "control_panel", eventName, actor: "user", outcome: "success", occurredAt: `${day}T01:00:00.000Z` }); + } + // A low-frequency flagship event the weekly report looks up by exact name (count 1, ranks 21st). + await recordProductUsageEvent(env, { surface: "control_panel", eventName: "agent_pr_packet_completed", actor: "user", outcome: "success", occurredAt: `${day}T05:00:00.000Z` }); + + const run = await rollupProductUsageDaily(env, { day, nowIso: "2026-05-31T00:10:00.000Z" }); + // Without the fix, byEvent was frequency-truncated to the top 20 and dropped this event. + expect(run.rollups[0]?.byEvent).toEqual(expect.arrayContaining([{ eventName: "agent_pr_packet_completed", count: 1 }])); + expect((run.rollups[0]?.byEvent ?? []).length).toBe(21); + }); + it("builds empty role and retention rollups for days without product usage", async () => { const env = createTestEnv({ PRODUCT_USAGE_HASH_SALT: "fixed-test-salt" }); const result = await rollupProductUsageDaily(env, { day: "2026-06-01", nowIso: "2026-06-02T00:00:00.000Z" });