fix(ui): block the global usage export when the aggregated key cap is reached

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
yassin 2026-09-16 09:22:20 +00:00
parent f863e74612
commit 61b3611b8c
4 changed files with 56 additions and 4 deletions

View file

@ -664,7 +664,7 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
{ key: "endpoints", label: "Endpoint Activity", content: <EndpointUsage userSpendData={spendData} /> },
];
const spendFetchState = { coversRange, cancelled, failed };
const spendFetchState = { coversRange, cancelled, failed, apiKeyLimitReached: undefined };
return (
<div style={{ width: "100%" }} className="relative">

View file

@ -30,7 +30,7 @@ import { ActivityMetrics, processActivityData } from "@/components/activity_metr
import CloudZeroExportModal from "@/components/cloudzero_export_modal";
import UserDropdown from "@/components/common_components/UserDropdown";
import EntityUsageExportModal from "@/components/EntityUsageExport";
import { getExportBlockedReason } from "@/components/EntityUsageExport/exportBlockedReason";
import { getApiKeyLimitReached, getExportBlockedReason } from "@/components/EntityUsageExport/exportBlockedReason";
import KeyActivityPanel from "@/components/UsagePage/components/KeyActivityPanel";
import { Team } from "@/components/key_team_helpers/key_list";
import {
@ -256,6 +256,7 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
coversRange: activeAggregated !== null || paginatedResult.coversRange,
cancelled: paginatedResult.cancelled,
failed: paginatedResult.failed,
apiKeyLimitReached: getApiKeyLimitReached(userSpendData.results, userSpendData.metadata?.api_key_limit),
};
const exportBlockedReason = getExportBlockedReason(spendFetchState);

View file

@ -1,14 +1,24 @@
import { describe, expect, it } from "vitest";
import { getExportBlockedReason, type UsageFetchState } from "./exportBlockedReason";
import type { DailyData } from "@/components/UsagePage/types";
import { getApiKeyLimitReached, getExportBlockedReason, type UsageFetchState } from "./exportBlockedReason";
const state = (overrides: Partial<UsageFetchState> = {}): UsageFetchState => ({
coversRange: true,
cancelled: false,
failed: false,
apiKeyLimitReached: undefined,
...overrides,
});
const dayWithKeys = (date: string, ...keys: string[]): DailyData =>
({
date,
metrics: {},
breakdown: { api_keys: Object.fromEntries(keys.map((k) => [k, { metrics: {}, metadata: {} }])) },
}) as unknown as DailyData;
describe("getExportBlockedReason", () => {
it("lets the export through once the data on screen covers the range", () => {
expect(getExportBlockedReason(state())).toBeUndefined();
@ -31,4 +41,29 @@ describe("getExportBlockedReason", () => {
expect(reason).toMatch(/failed to load/i);
expect(reason).not.toMatch(/stopped/i);
});
it("blocks when the aggregated endpoint hit its key cap, since a per-team CSV would miss keys", () => {
const reason = getExportBlockedReason(state({ apiKeyLimitReached: 100 }));
expect(reason).toMatch(/100 highest-spend keys/);
expect(reason).toMatch(/USAGE_TOP_API_KEYS_LIMIT/);
});
});
describe("getApiKeyLimitReached", () => {
it("reports the cap once the distinct keys across every day reach it", () => {
const results = [dayWithKeys("2026-06-01", "key-1", "key-2"), dayWithKeys("2026-06-02", "key-2", "key-3")];
expect(getApiKeyLimitReached(results, 3)).toBe(3);
});
it("stays quiet while fewer keys than the cap came back, which means every key is on screen", () => {
const results = [dayWithKeys("2026-06-01", "key-1", "key-2"), dayWithKeys("2026-06-02", "key-2")];
expect(getApiKeyLimitReached(results, 3)).toBeUndefined();
});
it("stays quiet when the response carries no cap, as the paginated fallback does", () => {
expect(getApiKeyLimitReached([dayWithKeys("2026-06-01", "key-1")], undefined)).toBeUndefined();
});
});

View file

@ -1,13 +1,29 @@
import type { DailyData } from "@/components/UsagePage/types";
export interface UsageFetchState {
coversRange: boolean;
cancelled: boolean;
failed: boolean;
apiKeyLimitReached: number | undefined;
}
export const getExportBlockedReason = ({ coversRange, cancelled, failed }: UsageFetchState): string | undefined => {
export const getApiKeyLimitReached = (results: DailyData[], apiKeyLimit: unknown): number | undefined => {
if (typeof apiKeyLimit !== "number") return undefined;
const keys = new Set(results.flatMap((day) => Object.keys(day.breakdown.api_keys ?? {})));
return keys.size >= apiKeyLimit ? apiKeyLimit : undefined;
};
export const getExportBlockedReason = ({
coversRange,
cancelled,
failed,
apiKeyLimitReached,
}: UsageFetchState): string | undefined => {
if (failed) return "Some spend data failed to load, so an export would under-report. Reload the page to try again.";
if (cancelled)
return "Loading was stopped before the whole range arrived, so an export would under-report. Reload the page to load it all.";
if (!coversRange) return "Spend data is still loading, so an export would under-report. Wait for it to finish.";
if (apiKeyLimitReached !== undefined)
return `Only the ${apiKeyLimitReached} highest-spend keys were loaded, so a per-team export would under-report. Raise USAGE_TOP_API_KEYS_LIMIT on the proxy to load more keys.`;
return undefined;
};