/** * Usage Analytics — Aggregation functions for the analytics dashboard * * Processes usage.json history entries into dashboard-ready data: * summary cards, daily trends, activity heatmap, model breakdown, etc. */ import { normalizeModelName as normModel, computeCostFromPricing, } from "@/lib/usage/costCalculator"; /** * Compute date range boundaries * @param {string} range - "1d" | "7d" | "30d" | "90d" | "ytd" | "all" | "custom" * @param {string} [startDate] - ISO string for custom range start * @param {string} [endDate] - ISO string for custom range end * @returns {{ start: Date, end: Date }} */ function getDateRange(range: string, startDate?: string, endDate?: string) { const end = new Date(); let start; switch (range) { case "custom": start = startDate ? new Date(startDate) : new Date(0); return { start, end: endDate ? new Date(endDate) : end, }; case "1d": start = new Date(end); start.setDate(start.getDate() - 1); break; case "7d": start = new Date(end); start.setDate(start.getDate() - 7); break; case "30d": start = new Date(end); start.setDate(start.getDate() - 30); break; case "90d": start = new Date(end); start.setDate(start.getDate() - 90); break; case "ytd": start = new Date(end.getFullYear(), 0, 1); break; case "all": default: start = new Date(0); break; } return { start, end }; } /** * Format a Date to "YYYY-MM-DD" string */ function toDateKey(date: Date) { const y = date.getFullYear(); const m = String(date.getMonth() + 1).padStart(2, "0"); const d = String(date.getDate()).padStart(2, "0"); return `${y}-${m}-${d}`; } /** * Short model name (strip provider prefix paths) */ function shortModelName(model: string) { if (!model) return "unknown"; // "accounts/fireworks/models/gpt-oss-120b" → "gpt-oss-120b" const parts = model.split("/"); return parts[parts.length - 1] || model; } function getApiKeyAnalyticsKey( apiKeyId: string | null | undefined, apiKeyName: string | null | undefined ) { return apiKeyId ? `id:${apiKeyId}` : `name:${apiKeyName || "unknown"}`; } /** * Compute all analytics data from usage history * @param {Array} history - Array of usage entries * @param {string} range - Time range filter * @param {Object} connectionMap - Map of connectionId → account name * @returns {Object} Analytics data */ export async function computeAnalytics( history: any[], range = "30d", connectionMap: Record = {}, options: { startDate?: string; endDate?: string } = {} ) { const { start, end } = getDateRange(range, options.startDate, options.endDate); // ---- Filtered entries ---- const entries = history.filter((e) => { const t = new Date(e.timestamp); return t >= start && t <= end; }); // ---- Summary ---- const summary = { totalTokens: 0, promptTokens: 0, completionTokens: 0, totalCost: 0, totalRequests: entries.length, uniqueModels: new Set(), uniqueAccounts: new Set(), uniqueApiKeys: new Set(), }; // ---- Daily trend ---- const dailyMap: Record = {}; // "YYYY-MM-DD" → { requests, promptTokens, completionTokens, cost } const dailyByModelMap: Record> = {}; // "YYYY-MM-DD" → { modelShort → tokens } // ---- Activity heatmap (always last 365 days, regardless of range filter) ---- const heatmapStart = new Date(); heatmapStart.setDate(heatmapStart.getDate() - 364); const activityMap: Record = {}; // ---- By model / account / provider ---- const byModelMap: Record = {}; const byAccountMap: Record = {}; const byProviderMap: Record = {}; const byApiKeyMap: Record = {}; // ---- Weekly pattern (0=Sun..6=Sat) ---- const weeklyTokens = [0, 0, 0, 0, 0, 0, 0]; const weeklyCounts = [0, 0, 0, 0, 0, 0, 0]; // ---- Single pass over ALL history for heatmap ---- for (const entry of history) { const entryDate = new Date(entry.timestamp); if (entryDate >= heatmapStart) { const key = toDateKey(entryDate); const tokens = (entry.tokens?.input ?? entry.tokens?.prompt_tokens ?? 0) + (entry.tokens?.output ?? entry.tokens?.completion_tokens ?? 0); activityMap[key] = (activityMap[key] || 0) + tokens; } } // Pre-fetch pricing for all unique (provider, model) pairs — one DB round-trip // per unique pair instead of one per entry, then compute costs synchronously. const { getPricingForModel } = await import("@/lib/localDb"); const pricingCache = new Map | null>(); const uniquePairs = new Set(entries.map((e) => `${e.provider}|||${e.model}`)); await Promise.all( [...uniquePairs].map(async (key) => { const [provider, model] = key.split("|||"); let pricing = (await getPricingForModel(provider, model)) as Record | null; if (!pricing) { const normalized = normModel(model); if (normalized !== model) { pricing = (await getPricingForModel(provider, normalized)) as Record< string, unknown > | null; } } pricingCache.set(key, pricing ?? null); }) ); // ---- Single pass over filtered entries for everything else ---- for (let i = 0; i < entries.length; i++) { const entry = entries[i]; const pt = entry.tokens?.input ?? entry.tokens?.prompt_tokens ?? 0; const ct = entry.tokens?.output ?? entry.tokens?.completion_tokens ?? 0; const totalTkns = pt + ct; const entryDate = new Date(entry.timestamp); const dateKey = toDateKey(entryDate); const dayOfWeek = entryDate.getDay(); const modelShort = shortModelName(entry.model); const pricingKey = `${entry.provider}|||${entry.model}`; const cost = computeCostFromPricing(pricingCache.get(pricingKey), entry.tokens, { provider: entry.provider, model: entry.model, flatRateAsZero: true, }); // Summary summary.promptTokens += pt; summary.completionTokens += ct; summary.totalTokens += totalTkns; summary.totalCost += cost; if (entry.model) summary.uniqueModels.add(modelShort); if (entry.connectionId) summary.uniqueAccounts.add(entry.connectionId); if (entry.apiKeyId || entry.apiKeyName) { summary.uniqueApiKeys.add(getApiKeyAnalyticsKey(entry.apiKeyId, entry.apiKeyName)); } // Daily trend if (!dailyMap[dateKey]) { dailyMap[dateKey] = { date: dateKey, requests: 0, promptTokens: 0, completionTokens: 0, cost: 0, }; } dailyMap[dateKey].requests++; dailyMap[dateKey].promptTokens += pt; dailyMap[dateKey].completionTokens += ct; dailyMap[dateKey].cost += cost; // Daily by model if (!dailyByModelMap[dateKey]) dailyByModelMap[dateKey] = {}; dailyByModelMap[dateKey][modelShort] = (dailyByModelMap[dateKey][modelShort] || 0) + totalTkns; // Weekly pattern weeklyTokens[dayOfWeek] += totalTkns; weeklyCounts[dayOfWeek]++; // By model if (!byModelMap[modelShort]) { byModelMap[modelShort] = { model: modelShort, provider: entry.provider, requests: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0, cost: 0, }; } byModelMap[modelShort].requests++; byModelMap[modelShort].promptTokens += pt; byModelMap[modelShort].completionTokens += ct; byModelMap[modelShort].totalTokens += totalTkns; byModelMap[modelShort].cost += cost; // By account const accountName = entry.connectionId ? connectionMap[entry.connectionId] || `Account ${entry.connectionId.slice(0, 8)}` : entry.provider || "unknown"; if (!byAccountMap[accountName]) { byAccountMap[accountName] = { account: accountName, totalTokens: 0, requests: 0, cost: 0 }; } byAccountMap[accountName].totalTokens += totalTkns; byAccountMap[accountName].requests++; byAccountMap[accountName].cost += cost; // By provider const prov = entry.provider || "unknown"; if (!byProviderMap[prov]) { byProviderMap[prov] = { provider: prov, requests: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0, cost: 0, }; } byProviderMap[prov].requests++; byProviderMap[prov].promptTokens += pt; byProviderMap[prov].completionTokens += ct; byProviderMap[prov].totalTokens += totalTkns; byProviderMap[prov].cost += cost; // By API key if (entry.apiKeyId || entry.apiKeyName) { const keyName = entry.apiKeyName || entry.apiKeyId || "unknown"; const key = getApiKeyAnalyticsKey(entry.apiKeyId, entry.apiKeyName); const keyLabel = entry.apiKeyId ? `${keyName} (${entry.apiKeyId})` : keyName; if (!byApiKeyMap[key]) { byApiKeyMap[key] = { apiKey: keyLabel, apiKeyId: entry.apiKeyId || null, apiKeyName: keyName, historicalApiKeyNames: [], requests: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0, cost: 0, }; } if (entry.apiKeyName && !byApiKeyMap[key].historicalApiKeyNames.includes(entry.apiKeyName)) { byApiKeyMap[key].historicalApiKeyNames.push(entry.apiKeyName); } byApiKeyMap[key].requests++; byApiKeyMap[key].promptTokens += pt; byApiKeyMap[key].completionTokens += ct; byApiKeyMap[key].totalTokens += totalTkns; byApiKeyMap[key].cost += cost; } } // ---- Build sorted arrays ---- const dailyTrend = Object.values(dailyMap).sort((a, b) => a.date.localeCompare(b.date)); // Daily by model — collect all unique model names const allModels = new Set(); for (const day of Object.values(dailyByModelMap)) { for (const m of Object.keys(day)) allModels.add(m); } const dailyByModel = dailyTrend.map((d) => { const row = { date: d.date }; for (const m of allModels) { row[m] = dailyByModelMap[d.date]?.[m] || 0; } return row; }); const byModel = Object.values(byModelMap) .sort((a, b) => b.totalTokens - a.totalTokens) .map((m) => ({ ...m, pct: summary.totalTokens > 0 ? ((m.totalTokens / summary.totalTokens) * 100).toFixed(1) : "0", })); const byAccount = Object.values(byAccountMap).sort((a, b) => b.totalTokens - a.totalTokens); const byProvider = Object.values(byProviderMap).sort((a, b) => b.totalTokens - a.totalTokens); const byApiKey = Object.values(byApiKeyMap).sort((a, b) => b.totalTokens - a.totalTokens); // Weekly pattern (avg tokens per day of week) const weekDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; const weeklyPattern = weekDays.map((name, i) => ({ day: name, avgTokens: weeklyCounts[i] > 0 ? Math.round(weeklyTokens[i] / weeklyCounts[i]) : 0, totalTokens: weeklyTokens[i], })); // Streak — consecutive days with activity (from today going back) let streak = 0; const today = new Date(); for (let i = 0; i < 365; i++) { const d = new Date(today); d.setDate(d.getDate() - i); const key = toDateKey(d); if (activityMap[key] && activityMap[key] > 0) { streak++; } else if (i > 0) { break; // Stop at first gap (skip today if no activity yet) } } return { summary: { totalTokens: summary.totalTokens, promptTokens: summary.promptTokens, completionTokens: summary.completionTokens, totalCost: summary.totalCost, totalRequests: summary.totalRequests, uniqueModels: summary.uniqueModels.size, uniqueAccounts: summary.uniqueAccounts.size, uniqueApiKeys: summary.uniqueApiKeys.size, streak, }, dailyTrend, dailyByModel, modelNames: [...allModels], byModel, byAccount, byProvider, byApiKey, activityMap, weeklyPattern, range, }; }