fix(ui/usage): guard undefined breakdown metrics when aggregating usage

This commit is contained in:
Devin AI 2026-07-15 14:12:25 +00:00
parent d6f498ff5c
commit 5fe78056a6
4 changed files with 130 additions and 66 deletions

View file

@ -574,6 +574,45 @@ describe("UsagePage", () => {
expect(successfulRequestElements.length).toBeGreaterThan(0);
});
it("does not crash when a breakdown entry is missing its metrics (issue #33381)", async () => {
const malformedSpendData = {
results: [
{
date: "2025-01-01",
metrics: {
spend: 125.75,
api_requests: 1500,
successful_requests: 1450,
failed_requests: 50,
total_tokens: 75000,
prompt_tokens: 45000,
completion_tokens: 30000,
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0,
},
breakdown: {
models: { "gpt-4": { metadata: {}, api_key_breakdown: {} } },
model_groups: { "gpt-4": { metadata: {}, api_key_breakdown: {} } },
api_keys: { "sk-test123": { metadata: {} } },
providers: { openai: {} },
mcp_servers: {},
},
},
],
metadata: { total_spend: 125.75 },
};
mockUserDailyActivityAggregatedCall.mockResolvedValueOnce(malformedSpendData as any);
renderWithProviders(<UsagePage {...defaultProps} />);
await waitFor(() => {
expect(mockUserDailyActivityAggregatedCall).toHaveBeenCalled();
});
expect(screen.getByText("Daily Spend")).toBeInTheDocument();
expect(screen.getByText("Top Virtual Keys")).toBeInTheDocument();
});
it("should display usage metrics and charts", async () => {
renderWithProviders(<UsagePage {...defaultProps} />);

View file

@ -269,15 +269,15 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
api_key_breakdown: {},
};
}
modelSpend[model].metrics.spend += metrics.metrics.spend;
modelSpend[model].metrics.prompt_tokens += metrics.metrics.prompt_tokens;
modelSpend[model].metrics.completion_tokens += metrics.metrics.completion_tokens;
modelSpend[model].metrics.total_tokens += metrics.metrics.total_tokens;
modelSpend[model].metrics.api_requests += metrics.metrics.api_requests;
modelSpend[model].metrics.successful_requests += metrics.metrics.successful_requests || 0;
modelSpend[model].metrics.failed_requests += metrics.metrics.failed_requests || 0;
modelSpend[model].metrics.cache_read_input_tokens += metrics.metrics.cache_read_input_tokens || 0;
modelSpend[model].metrics.cache_creation_input_tokens += metrics.metrics.cache_creation_input_tokens || 0;
modelSpend[model].metrics.spend += metrics.metrics?.spend ?? 0;
modelSpend[model].metrics.prompt_tokens += metrics.metrics?.prompt_tokens ?? 0;
modelSpend[model].metrics.completion_tokens += metrics.metrics?.completion_tokens ?? 0;
modelSpend[model].metrics.total_tokens += metrics.metrics?.total_tokens ?? 0;
modelSpend[model].metrics.api_requests += metrics.metrics?.api_requests ?? 0;
modelSpend[model].metrics.successful_requests += metrics.metrics?.successful_requests ?? 0;
modelSpend[model].metrics.failed_requests += metrics.metrics?.failed_requests ?? 0;
modelSpend[model].metrics.cache_read_input_tokens += metrics.metrics?.cache_read_input_tokens ?? 0;
modelSpend[model].metrics.cache_creation_input_tokens += metrics.metrics?.cache_creation_input_tokens ?? 0;
});
});
@ -315,16 +315,16 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
api_key_breakdown: {},
};
}
modelGroupSpend[modelGroup].metrics.spend += metrics.metrics.spend;
modelGroupSpend[modelGroup].metrics.prompt_tokens += metrics.metrics.prompt_tokens;
modelGroupSpend[modelGroup].metrics.completion_tokens += metrics.metrics.completion_tokens;
modelGroupSpend[modelGroup].metrics.total_tokens += metrics.metrics.total_tokens;
modelGroupSpend[modelGroup].metrics.api_requests += metrics.metrics.api_requests;
modelGroupSpend[modelGroup].metrics.successful_requests += metrics.metrics.successful_requests || 0;
modelGroupSpend[modelGroup].metrics.failed_requests += metrics.metrics.failed_requests || 0;
modelGroupSpend[modelGroup].metrics.cache_read_input_tokens += metrics.metrics.cache_read_input_tokens || 0;
modelGroupSpend[modelGroup].metrics.spend += metrics.metrics?.spend ?? 0;
modelGroupSpend[modelGroup].metrics.prompt_tokens += metrics.metrics?.prompt_tokens ?? 0;
modelGroupSpend[modelGroup].metrics.completion_tokens += metrics.metrics?.completion_tokens ?? 0;
modelGroupSpend[modelGroup].metrics.total_tokens += metrics.metrics?.total_tokens ?? 0;
modelGroupSpend[modelGroup].metrics.api_requests += metrics.metrics?.api_requests ?? 0;
modelGroupSpend[modelGroup].metrics.successful_requests += metrics.metrics?.successful_requests ?? 0;
modelGroupSpend[modelGroup].metrics.failed_requests += metrics.metrics?.failed_requests ?? 0;
modelGroupSpend[modelGroup].metrics.cache_read_input_tokens += metrics.metrics?.cache_read_input_tokens ?? 0;
modelGroupSpend[modelGroup].metrics.cache_creation_input_tokens +=
metrics.metrics.cache_creation_input_tokens || 0;
metrics.metrics?.cache_creation_input_tokens ?? 0;
});
});
@ -363,16 +363,16 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
api_key_breakdown: {},
};
}
providerSpendMap[provider].metrics.spend += metrics.metrics.spend;
providerSpendMap[provider].metrics.prompt_tokens += metrics.metrics.prompt_tokens;
providerSpendMap[provider].metrics.completion_tokens += metrics.metrics.completion_tokens;
providerSpendMap[provider].metrics.total_tokens += metrics.metrics.total_tokens;
providerSpendMap[provider].metrics.api_requests += metrics.metrics.api_requests;
providerSpendMap[provider].metrics.successful_requests += metrics.metrics.successful_requests || 0;
providerSpendMap[provider].metrics.failed_requests += metrics.metrics.failed_requests || 0;
providerSpendMap[provider].metrics.cache_read_input_tokens += metrics.metrics.cache_read_input_tokens || 0;
providerSpendMap[provider].metrics.spend += metrics.metrics?.spend ?? 0;
providerSpendMap[provider].metrics.prompt_tokens += metrics.metrics?.prompt_tokens ?? 0;
providerSpendMap[provider].metrics.completion_tokens += metrics.metrics?.completion_tokens ?? 0;
providerSpendMap[provider].metrics.total_tokens += metrics.metrics?.total_tokens ?? 0;
providerSpendMap[provider].metrics.api_requests += metrics.metrics?.api_requests ?? 0;
providerSpendMap[provider].metrics.successful_requests += metrics.metrics?.successful_requests ?? 0;
providerSpendMap[provider].metrics.failed_requests += metrics.metrics?.failed_requests ?? 0;
providerSpendMap[provider].metrics.cache_read_input_tokens += metrics.metrics?.cache_read_input_tokens ?? 0;
providerSpendMap[provider].metrics.cache_creation_input_tokens +=
metrics.metrics.cache_creation_input_tokens || 0;
metrics.metrics?.cache_creation_input_tokens ?? 0;
});
});
@ -405,21 +405,21 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
cache_creation_input_tokens: 0,
},
metadata: {
key_alias: metrics.metadata.key_alias,
key_alias: metrics.metadata?.key_alias,
team_id: null,
tags: metrics.metadata.tags || [],
tags: metrics.metadata?.tags || [],
},
};
}
keySpend[key].metrics.spend += metrics.metrics.spend;
keySpend[key].metrics.prompt_tokens += metrics.metrics.prompt_tokens;
keySpend[key].metrics.completion_tokens += metrics.metrics.completion_tokens;
keySpend[key].metrics.total_tokens += metrics.metrics.total_tokens;
keySpend[key].metrics.api_requests += metrics.metrics.api_requests;
keySpend[key].metrics.successful_requests += metrics.metrics.successful_requests;
keySpend[key].metrics.failed_requests += metrics.metrics.failed_requests;
keySpend[key].metrics.cache_read_input_tokens += metrics.metrics.cache_read_input_tokens || 0;
keySpend[key].metrics.cache_creation_input_tokens += metrics.metrics.cache_creation_input_tokens || 0;
keySpend[key].metrics.spend += metrics.metrics?.spend ?? 0;
keySpend[key].metrics.prompt_tokens += metrics.metrics?.prompt_tokens ?? 0;
keySpend[key].metrics.completion_tokens += metrics.metrics?.completion_tokens ?? 0;
keySpend[key].metrics.total_tokens += metrics.metrics?.total_tokens ?? 0;
keySpend[key].metrics.api_requests += metrics.metrics?.api_requests ?? 0;
keySpend[key].metrics.successful_requests += metrics.metrics?.successful_requests ?? 0;
keySpend[key].metrics.failed_requests += metrics.metrics?.failed_requests ?? 0;
keySpend[key].metrics.cache_read_input_tokens += metrics.metrics?.cache_read_input_tokens ?? 0;
keySpend[key].metrics.cache_creation_input_tokens += metrics.metrics?.cache_creation_input_tokens ?? 0;
});
});

View file

@ -716,6 +716,31 @@ describe("processActivityData", () => {
expect(result["gpt-4"].total_spend).toBe(100.5);
});
it("does not crash when a breakdown entry is missing its metrics (issue #33381)", () => {
const dailyActivityWithMissingMetrics = {
results: [
{
date: "2025-01-01",
metrics: { ...EMPTY_SPEND_METRICS, spend: 100.5, api_requests: 100 },
breakdown: {
...EMPTY_BREAKDOWN,
models: {
"gpt-4": { metadata: {}, api_key_breakdown: {} },
},
},
},
],
} as unknown as { results: DailyData[] };
const result = processActivityData(dailyActivityWithMissingMetrics, "models");
expect(result).toHaveProperty("gpt-4");
expect(result["gpt-4"].total_spend).toBe(0);
expect(result["gpt-4"].total_requests).toBe(0);
expect(result["gpt-4"].daily_data[0].metrics.spend).toBe(0);
expect(result["gpt-4"].daily_data[0].metrics.api_requests).toBe(0);
});
it("should process data for mcp_servers key", () => {
const dailyActivityWithMCP: { results: DailyData[] } = {
results: [

View file

@ -392,29 +392,29 @@ export const processActivityData = (
};
}
// Update totals
modelMetrics[model].total_requests += modelData.metrics.api_requests;
modelMetrics[model].prompt_tokens += modelData.metrics.prompt_tokens;
modelMetrics[model].completion_tokens += modelData.metrics.completion_tokens;
modelMetrics[model].total_tokens += modelData.metrics.total_tokens;
modelMetrics[model].total_spend += modelData.metrics.spend;
modelMetrics[model].total_successful_requests += modelData.metrics.successful_requests;
modelMetrics[model].total_failed_requests += modelData.metrics.failed_requests;
modelMetrics[model].total_cache_read_input_tokens += modelData.metrics.cache_read_input_tokens || 0;
modelMetrics[model].total_cache_creation_input_tokens += modelData.metrics.cache_creation_input_tokens || 0;
modelMetrics[model].total_requests += modelData.metrics?.api_requests ?? 0;
modelMetrics[model].prompt_tokens += modelData.metrics?.prompt_tokens ?? 0;
modelMetrics[model].completion_tokens += modelData.metrics?.completion_tokens ?? 0;
modelMetrics[model].total_tokens += modelData.metrics?.total_tokens ?? 0;
modelMetrics[model].total_spend += modelData.metrics?.spend ?? 0;
modelMetrics[model].total_successful_requests += modelData.metrics?.successful_requests ?? 0;
modelMetrics[model].total_failed_requests += modelData.metrics?.failed_requests ?? 0;
modelMetrics[model].total_cache_read_input_tokens += modelData.metrics?.cache_read_input_tokens ?? 0;
modelMetrics[model].total_cache_creation_input_tokens += modelData.metrics?.cache_creation_input_tokens ?? 0;
// Add daily data
modelMetrics[model].daily_data.push({
date: day.date,
metrics: {
prompt_tokens: modelData.metrics.prompt_tokens,
completion_tokens: modelData.metrics.completion_tokens,
total_tokens: modelData.metrics.total_tokens,
api_requests: modelData.metrics.api_requests,
spend: modelData.metrics.spend,
successful_requests: modelData.metrics.successful_requests,
failed_requests: modelData.metrics.failed_requests,
cache_read_input_tokens: modelData.metrics.cache_read_input_tokens || 0,
cache_creation_input_tokens: modelData.metrics.cache_creation_input_tokens || 0,
prompt_tokens: modelData.metrics?.prompt_tokens ?? 0,
completion_tokens: modelData.metrics?.completion_tokens ?? 0,
total_tokens: modelData.metrics?.total_tokens ?? 0,
api_requests: modelData.metrics?.api_requests ?? 0,
spend: modelData.metrics?.spend ?? 0,
successful_requests: modelData.metrics?.successful_requests ?? 0,
failed_requests: modelData.metrics?.failed_requests ?? 0,
cache_read_input_tokens: modelData.metrics?.cache_read_input_tokens ?? 0,
cache_creation_input_tokens: modelData.metrics?.cache_creation_input_tokens ?? 0,
},
});
});
@ -433,17 +433,17 @@ export const processActivityData = (
if (!apiKeyBreakdown[apiKey]) {
apiKeyBreakdown[apiKey] = {
api_key: apiKey,
key_alias: keyData.metadata.key_alias,
team_id: keyData.metadata.team_id,
key_alias: keyData.metadata?.key_alias,
team_id: keyData.metadata?.team_id,
spend: 0,
requests: 0,
tokens: 0,
};
}
apiKeyBreakdown[apiKey].spend += keyData.metrics.spend;
apiKeyBreakdown[apiKey].requests += keyData.metrics.api_requests;
apiKeyBreakdown[apiKey].tokens += keyData.metrics.total_tokens;
apiKeyBreakdown[apiKey].spend += keyData.metrics?.spend ?? 0;
apiKeyBreakdown[apiKey].requests += keyData.metrics?.api_requests ?? 0;
apiKeyBreakdown[apiKey].tokens += keyData.metrics?.total_tokens ?? 0;
});
}
});
@ -478,11 +478,11 @@ export const processActivityData = (
};
}
modelBreakdown[modelName].spend += keyDataForModel.metrics.spend;
modelBreakdown[modelName].requests += keyDataForModel.metrics.api_requests;
modelBreakdown[modelName].successful_requests += keyDataForModel.metrics.successful_requests || 0;
modelBreakdown[modelName].failed_requests += keyDataForModel.metrics.failed_requests || 0;
modelBreakdown[modelName].tokens += keyDataForModel.metrics.total_tokens;
modelBreakdown[modelName].spend += keyDataForModel.metrics?.spend ?? 0;
modelBreakdown[modelName].requests += keyDataForModel.metrics?.api_requests ?? 0;
modelBreakdown[modelName].successful_requests += keyDataForModel.metrics?.successful_requests ?? 0;
modelBreakdown[modelName].failed_requests += keyDataForModel.metrics?.failed_requests ?? 0;
modelBreakdown[modelName].tokens += keyDataForModel.metrics?.total_tokens ?? 0;
}
}
});