claude-usage-tracker
Version:
Advanced analytics for Claude Code usage with cost optimization, conversation length analysis, and rate limit tracking
461 lines • 19.5 kB
JavaScript
import { calculateCost } from "./analyzer.js";
export class DeepAnalyzer {
generateDataQualityReport(entries) {
const validEntries = entries.filter((e) => e.prompt_tokens > 0 && e.completion_tokens > 0 && e.total_tokens > 0);
const timestamps = entries
.map((e) => new Date(e.timestamp).getTime())
.filter((t) => !isNaN(t));
const earliestTime = Math.min(...timestamps);
const latestTime = Math.max(...timestamps);
const spanDays = Math.ceil((latestTime - earliestTime) / (1000 * 60 * 60 * 24));
// Model distribution
const modelDistribution = {};
entries.forEach((e) => {
modelDistribution[e.model] = (modelDistribution[e.model] || 0) + 1;
});
// Conversation analysis
const conversations = new Map();
entries.forEach((e) => {
if (!conversations.has(e.conversationId)) {
conversations.set(e.conversationId, []);
}
conversations.get(e.conversationId).push(e);
});
let shortConversations = 0;
let mediumConversations = 0;
let longConversations = 0;
let totalMessages = 0;
conversations.forEach((conv) => {
totalMessages += conv.length;
if (conv.length < 3)
shortConversations++;
else if (conv.length <= 20)
mediumConversations++;
else
longConversations++;
});
// Cost analysis
const costs = entries.map((e) => calculateCost(e));
const totalCost = costs.reduce((sum, cost) => sum + cost, 0);
const sortedCosts = [...costs].sort((a, b) => a - b);
const medianCost = sortedCosts[Math.floor(sortedCosts.length / 2)];
const top10PercentIndex = Math.floor(sortedCosts.length * 0.9);
const top10PercentCost = sortedCosts
.slice(top10PercentIndex)
.reduce((sum, cost) => sum + cost, 0);
const costPerModel = {};
entries.forEach((e) => {
const cost = calculateCost(e);
costPerModel[e.model] = (costPerModel[e.model] || 0) + cost;
});
// Token analysis
const totalTokens = entries.reduce((sum, e) => sum + e.total_tokens, 0);
const avgPromptTokens = entries.reduce((sum, e) => sum + e.prompt_tokens, 0) / entries.length;
const avgCompletionTokens = entries.reduce((sum, e) => sum + e.completion_tokens, 0) / entries.length;
const cacheTokens = entries.reduce((sum, e) => sum +
(e.cache_creation_input_tokens || 0) +
(e.cache_read_input_tokens || 0), 0);
// Find extreme entries
const extremeEntries = [];
const highTokenThreshold = 50000;
const lowTokenThreshold = 10;
const highCostThreshold = 10;
entries.forEach((e) => {
const cost = calculateCost(e);
if (e.total_tokens > highTokenThreshold) {
extremeEntries.push({
type: "high_tokens",
conversationId: e.conversationId,
value: e.total_tokens,
});
}
if (e.total_tokens < lowTokenThreshold) {
extremeEntries.push({
type: "low_tokens",
conversationId: e.conversationId,
value: e.total_tokens,
});
}
if (cost > highCostThreshold) {
extremeEntries.push({
type: "high_cost",
conversationId: e.conversationId,
value: cost,
});
}
});
// Detect anomalies
const anomalies = this.detectDataAnomalies(entries);
return {
totalEntries: entries.length,
validEntries: validEntries.length,
invalidEntries: entries.length - validEntries.length,
duplicateEntries: this.countDuplicates(entries),
dateRange: {
earliest: new Date(earliestTime).toISOString(),
latest: new Date(latestTime).toISOString(),
spanDays,
},
modelDistribution,
conversationDistribution: {
totalConversations: conversations.size,
avgMessagesPerConversation: totalMessages / conversations.size,
shortConversations,
mediumConversations,
longConversations,
},
costDistribution: {
totalCost,
medianCost,
top10PercentCost,
costPerModel,
},
tokenDistribution: {
totalTokens,
avgPromptTokens,
avgCompletionTokens,
cacheTokens,
extremeEntries: extremeEntries.slice(0, 10),
},
anomalies,
};
}
generateUsageHeatmap(entries) {
// Hourly heatmap
const heatmapData = new Map();
entries.forEach((e) => {
const date = new Date(e.timestamp);
const hour = date.getHours();
const dayOfWeek = date.getDay();
const key = `${hour}-${dayOfWeek}`;
if (!heatmapData.has(key)) {
heatmapData.set(key, {
totalCost: 0,
conversations: new Set(),
totalTokens: 0,
});
}
const data = heatmapData.get(key);
data.totalCost += calculateCost(e);
data.conversations.add(e.conversationId);
data.totalTokens += e.total_tokens;
});
const hourlyHeatmap = Array.from(heatmapData.entries()).map(([key, data]) => {
const [hour, dayOfWeek] = key.split("-").map(Number);
return {
hour,
dayOfWeek,
totalCost: data.totalCost,
conversationCount: data.conversations.size,
avgEfficiency: data.totalTokens / Math.max(data.totalCost, 0.001),
};
});
// Monthly trends
const monthlyData = new Map();
entries.forEach((e) => {
const date = new Date(e.timestamp);
const month = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, "0")}`;
if (!monthlyData.has(month)) {
monthlyData.set(month, {
totalCost: 0,
totalTokens: 0,
conversations: new Set(),
modelCounts: {},
});
}
const data = monthlyData.get(month);
data.totalCost += calculateCost(e);
data.totalTokens += e.total_tokens;
data.conversations.add(e.conversationId);
data.modelCounts[e.model] = (data.modelCounts[e.model] || 0) + 1;
});
const monthlyTrends = Array.from(monthlyData.entries()).map(([month, data]) => {
const topModel = Object.entries(data.modelCounts).sort(([, a], [, b]) => b - a)[0]?.[0] || "unknown";
return {
month,
totalCost: data.totalCost,
totalTokens: data.totalTokens,
conversationCount: data.conversations.size,
avgEfficiency: data.totalTokens / Math.max(data.totalCost, 0.001),
topModel,
};
});
// Efficiency distribution
const efficiencies = entries.map((e) => e.total_tokens / Math.max(calculateCost(e), 0.001));
const ranges = [
{ range: "0-1000", min: 0, max: 1000 },
{ range: "1000-5000", min: 1000, max: 5000 },
{ range: "5000-10000", min: 5000, max: 10000 },
{ range: "10000-20000", min: 10000, max: 20000 },
{ range: "20000+", min: 20000, max: Infinity },
];
const efficiencyDistribution = ranges.map((range) => {
const count = efficiencies.filter((eff) => eff >= range.min && eff < range.max).length;
return {
range: range.range,
count,
percentage: (count / efficiencies.length) * 100,
};
});
return {
hourlyHeatmap,
monthlyTrends,
efficiencyDistribution,
};
}
analyzeConversationFlow(entries) {
const conversations = new Map();
entries.forEach((e) => {
if (!conversations.has(e.conversationId)) {
conversations.set(e.conversationId, []);
}
conversations.get(e.conversationId).push(e);
});
const conversationJourneys = Array.from(conversations.entries()).map(([id, convEntries]) => {
const sorted = [...convEntries].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
const startTime = sorted[0].timestamp;
const endTime = sorted[sorted.length - 1].timestamp;
const messageCount = sorted.length;
const totalCost = sorted.reduce((sum, e) => sum + calculateCost(e), 0);
const totalTokens = sorted.reduce((sum, e) => sum + e.total_tokens, 0);
const efficiency = totalTokens / Math.max(totalCost, 0.001);
// Count model switches
let modelSwitches = 0;
for (let i = 1; i < sorted.length; i++) {
if (sorted[i].model !== sorted[i - 1].model) {
modelSwitches++;
}
}
// Calculate average response time
let totalGaps = 0;
for (let i = 1; i < sorted.length; i++) {
const gap = new Date(sorted[i].timestamp).getTime() -
new Date(sorted[i - 1].timestamp).getTime();
totalGaps += gap;
}
const avgResponseTime = sorted.length > 1 ? totalGaps / (sorted.length - 1) / 1000 : 0;
// Find peak activity
const hourCounts = {};
sorted.forEach((e) => {
const hour = new Date(e.timestamp).getHours();
hourCounts[hour] = (hourCounts[hour] || 0) + 1;
});
const peakHour = Object.entries(hourCounts).sort(([, a], [, b]) => b - a)[0];
// Classify conversation type
const duration = new Date(endTime).getTime() - new Date(startTime).getTime();
const durationHours = duration / (1000 * 60 * 60);
let conversationType;
if (durationHours < 1 && messageCount > 10) {
conversationType = "sprint";
}
else if (durationHours > 8) {
conversationType = "marathon";
}
else if (avgResponseTime > 30 * 60 * 1000) {
// 30+ minutes between messages
conversationType = "interrupted";
}
else {
conversationType = "focused";
}
return {
conversationId: id,
startTime,
endTime,
messageCount,
totalCost,
efficiency,
modelSwitches,
avgResponseTime,
peakActivity: {
hour: peakHour ? parseInt(peakHour[0]) : 0,
burstCount: peakHour ? peakHour[1] : 0,
},
conversationType,
};
});
// Analyze flow patterns
const patterns = new Map();
conversationJourneys.forEach((journey) => {
const pattern = `${journey.conversationType}_${journey.messageCount < 10 ? "short" : journey.messageCount < 30 ? "medium" : "long"}`;
if (!patterns.has(pattern)) {
patterns.set(pattern, { count: 0, totalCost: 0, successes: 0 });
}
const data = patterns.get(pattern);
data.count++;
data.totalCost += journey.totalCost;
if (journey.efficiency > 3000)
data.successes++; // Arbitrary success threshold
});
const flowPatterns = Array.from(patterns.entries()).map(([pattern, data]) => ({
pattern,
frequency: data.count,
avgCost: data.totalCost / data.count,
successRate: data.successes / data.count,
}));
return {
conversationJourneys,
flowPatterns,
};
}
analyzeCostDrivers(entries) {
const totalCost = entries.reduce((sum, e) => sum + calculateCost(e), 0);
// Analyze model usage patterns
const opusEntries = entries.filter((e) => e.model.includes("opus"));
const sonnetEntries = entries.filter((e) => e.model.includes("sonnet"));
const opusCost = opusEntries.reduce((sum, e) => sum + calculateCost(e), 0);
const sonnetCost = sonnetEntries.reduce((sum, e) => sum + calculateCost(e), 0);
const opusPercentage = (opusCost / totalCost) * 100;
const sonnetPercentage = (sonnetCost / totalCost) * 100;
// Analyze appropriate usage (simple heuristic)
const opusConversations = new Map();
opusEntries.forEach((e) => {
if (!opusConversations.has(e.conversationId)) {
opusConversations.set(e.conversationId, []);
}
opusConversations.get(e.conversationId).push(e);
});
let appropriateOpusUsage = 0;
let wastedOpusSpend = 0;
opusConversations.forEach((conv) => {
const avgTokens = conv.reduce((sum, e) => sum + e.total_tokens, 0) / conv.length;
const isComplexTask = avgTokens > 3000 || conv.length > 15;
if (isComplexTask) {
appropriateOpusUsage++;
}
else {
wastedOpusSpend += conv.reduce((sum, e) => sum + calculateCost(e), 0);
}
});
const primaryDrivers = [
{
factor: "Model Selection",
impact: opusPercentage > 70 ? 0.8 : 0.4,
description: `${opusPercentage.toFixed(1)}% of cost from Opus usage`,
recommendedAction: opusPercentage > 70
? "Review Opus usage - consider Sonnet for simpler tasks"
: "Current model mix appears reasonable",
},
{
factor: "Conversation Length",
impact: 0.6,
description: "Long conversations drive higher costs",
recommendedAction: "Break complex tasks into focused sessions",
},
{
factor: "Cache Utilization",
impact: 0.3,
description: "Opportunity to reduce costs through better caching",
recommendedAction: "Use context continuation for related tasks",
},
];
return {
primaryDrivers,
modelUsagePattern: {
opusUsage: {
percentage: opusPercentage,
appropriateUsage: (appropriateOpusUsage / opusConversations.size) * 100,
wastedSpend: wastedOpusSpend,
},
sonnetUsage: {
percentage: sonnetPercentage,
missedOpportunities: 0, // Could be calculated
underutilization: 0,
},
},
seasonalPatterns: [], // Could be implemented with more time analysis
};
}
generateDeepInsights(entries) {
const dataQuality = this.generateDataQualityReport(entries);
const usageHeatmap = this.generateUsageHeatmap(entries);
const conversationFlow = this.analyzeConversationFlow(entries);
const costDrivers = this.analyzeCostDrivers(entries);
const recommendations = this.generateRecommendations(dataQuality, costDrivers, conversationFlow);
return {
dataQuality,
usageHeatmap,
conversationFlow,
costDrivers,
recommendations,
};
}
detectDataAnomalies(entries) {
const anomalies = [];
// Check for zero token entries
const zeroTokenEntries = entries.filter((e) => e.total_tokens === 0).length;
if (zeroTokenEntries > 0) {
anomalies.push({
type: "zero_tokens",
description: `${zeroTokenEntries} entries have zero tokens`,
severity: "medium",
affectedEntries: zeroTokenEntries,
});
}
// Check for extremely high costs
const highCostEntries = entries.filter((e) => calculateCost(e) > 50).length;
if (highCostEntries > 0) {
anomalies.push({
type: "high_cost",
description: `${highCostEntries} entries have unusually high costs (>$50)`,
severity: "high",
affectedEntries: highCostEntries,
});
}
return anomalies;
}
countDuplicates(entries) {
const seen = new Set();
let duplicates = 0;
entries.forEach((e) => {
const key = `${e.requestId}-${e.timestamp}`;
if (seen.has(key)) {
duplicates++;
}
else {
seen.add(key);
}
});
return duplicates;
}
generateRecommendations(dataQuality, costDrivers, conversationFlow) {
const recommendations = [];
// Cost optimization recommendations
if (costDrivers.modelUsagePattern.opusUsage.wastedSpend > 100) {
recommendations.push({
category: "cost",
priority: "high",
title: "Optimize Opus Usage",
description: `$${costDrivers.modelUsagePattern.opusUsage.wastedSpend.toFixed(2)} could be saved by using Sonnet for simpler tasks`,
potentialSavings: costDrivers.modelUsagePattern.opusUsage.wastedSpend,
implementationEffort: "low",
});
}
// Efficiency recommendations
const sprintConversations = conversationFlow.conversationJourneys.filter((j) => j.conversationType === "sprint");
if (sprintConversations.length > 10) {
recommendations.push({
category: "efficiency",
priority: "medium",
title: "Optimize Sprint Conversations",
description: `${sprintConversations.length} sprint conversations detected - consider breaking down complex tasks`,
implementationEffort: "medium",
});
}
// Data quality recommendations
if (dataQuality.invalidEntries > dataQuality.totalEntries * 0.1) {
recommendations.push({
category: "data_quality",
priority: "medium",
title: "Improve Data Quality",
description: `${dataQuality.invalidEntries} invalid entries detected - review data collection`,
implementationEffort: "high",
});
}
return recommendations.sort((a, b) => {
const priorityOrder = { high: 3, medium: 2, low: 1 };
return priorityOrder[b.priority] - priorityOrder[a.priority];
});
}
}
//# sourceMappingURL=deep-analysis.js.map