fix(usage): key live byApiKey stats by full api key to prevent team-key collision

maskApiKey kept only the first 8 characters of the key. API keys minted
from the same machine id share that prefix, so every key of an instance
collapsed into one sk-XXXXXXX*** bucket per model/provider, and the
dashboard attributed one holder's usage to another.

Key the live path by the full api key - matching the daily rollup
(aggregateEntryToDay) and the lastUsed overlay - and keep the last 4
characters in maskApiKey so masked keys stay distinguishable in the UI.
This commit is contained in:
Rafli Ahmad Zulfikar
2026-09-23 14:41:03 +07:00
parent a406381fad
commit 4a57df8bf9

View File

@@ -5,8 +5,9 @@ import { getMeta, setMeta } from "../helpers/metaStore.js";
function maskApiKey(key) { function maskApiKey(key) {
if (!key || typeof key !== "string") return null; if (!key || typeof key !== "string") return null;
if (key.length <= 8) return key.charAt(0) + "***"; if (key.length <= 12) return key.charAt(0) + "***";
return key.slice(0, 8) + "***"; // Keep the tail: keys sharing a machine-id prefix (team keys) must not collide.
return key.slice(0, 8) + "***" + key.slice(-4);
} }
const PENDING_TIMEOUT_MS = 60 * 1000; const PENDING_TIMEOUT_MS = 60 * 1000;
@@ -634,6 +635,8 @@ export async function getUsageStats(period = "all") {
const keyInfo = apiKeyMap[r.apiKey]; const keyInfo = apiKeyMap[r.apiKey];
const keyName = keyInfo?.name || r.apiKey.slice(0, 8) + "..."; const keyName = keyInfo?.name || r.apiKey.slice(0, 8) + "...";
const apiKeyMasked = maskApiKey(r.apiKey); const apiKeyMasked = maskApiKey(r.apiKey);
// Key by the FULL api key (same as the daily rollup + lastUsed overlay)
// — masking here collided all keys sharing a prefix into one bucket.
const akKey = `${r.apiKey}|${r.model}|${r.provider || "unknown"}`; const akKey = `${r.apiKey}|${r.model}|${r.provider || "unknown"}`;
if (!stats.byApiKey[akKey]) { if (!stats.byApiKey[akKey]) {
stats.byApiKey[akKey] = { requests: 0, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cost: 0, rawModel: r.model, provider: providerDisplayName, apiKeyMasked, keyName, apiKeyKey: apiKeyMasked, lastUsed: r.timestamp }; stats.byApiKey[akKey] = { requests: 0, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cost: 0, rawModel: r.model, provider: providerDisplayName, apiKeyMasked, keyName, apiKeyKey: apiKeyMasked, lastUsed: r.timestamp };