UNPKG

claude-usage-tracker

Version:

Advanced analytics for Claude Code usage with cost optimization, conversation length analysis, and rate limit tracking

511 lines 24.5 kB
export class ConversationLengthAnalyzer { conversations = new Map(); extractProjectName(conversationId) { // Extract project name from the conversation data using instanceId // Find the first entry for this conversation to get its instanceId const entries = this.conversations.get(conversationId); if (entries && entries.length > 0) { return entries[0].instanceId || "unknown-project"; } return "unknown-project"; } loadConversations(entries) { // Group entries by conversation ID this.conversations.clear(); for (const entry of entries) { const convId = entry.conversationId; if (!this.conversations.has(convId)) { this.conversations.set(convId, []); } this.conversations.get(convId).push(entry); } } analyzeConversationLengths() { const insights = this.generateConversationInsights(); const projectProfiles = this.generateProjectProfiles(insights); return { totalConversations: insights.length, overallOptimalRange: this.calculateOptimalRange(insights), projectProfiles, lengthDistribution: this.calculateLengthDistribution(insights), costAnalysis: this.calculateCostAnalysis(insights), insights: this.generateInsights(insights, projectProfiles), recommendations: this.generateRecommendations(insights, projectProfiles), }; } generateConversationInsights() { const insights = []; for (const [conversationId, entries] of this.conversations) { if (entries.length === 0) continue; const messageCount = entries.length; const tokenCount = entries.reduce((sum, entry) => sum + (entry.prompt_tokens || 0) + (entry.completion_tokens || 0), 0); const sortedEntries = entries.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()); const startTime = new Date(sortedEntries[0].timestamp); const endTime = new Date(sortedEntries[sortedEntries.length - 1].timestamp); const duration = (endTime.getTime() - startTime.getTime()) / (1000 * 60); // minutes const project = this.extractProjectName(conversationId); const totalCost = entries.reduce((sum, entry) => sum + (entry.cost || entry.costUSD || 0), 0); const efficiency = duration > 0 ? tokenCount / duration : 0; const costEfficiency = totalCost > 0 ? tokenCount / totalCost : 0; insights.push({ conversationId, messageCount, tokenCount, duration: Math.max(duration, 1), // Minimum 1 minute project, efficiency, costEfficiency, lengthCategory: this.categorizeLengthByMessages(messageCount), followUpPattern: this.analyzeFollowUpPattern(conversationId, entries), successIndicators: this.analyzeSuccessIndicators(conversationId, entries), }); } return insights; } categorizeLengthByMessages(messageCount) { if (messageCount <= 5) return "quick"; if (messageCount <= 20) return "medium"; if (messageCount <= 100) return "deep"; return "marathon"; } analyzeFollowUpPattern(conversationId, entries) { // Simple heuristic: if conversation has many messages, likely builds on previous // In real implementation, would analyze content similarity with previous conversations if (entries.length > 50) return "builds-on-previous"; if (entries.length > 10) return "builds-on-previous"; return "standalone"; } analyzeSuccessIndicators(conversationId, entries) { // Heuristics for success: // - Quick follow-up suggests previous conversation was incomplete // - Conversation completed if it has a clear ending pattern // - Topic resolved if no immediate follow-up on same topic const hasQuickFollowUp = this.hasQuickFollowUp(conversationId, entries); const conversationCompleted = this.seemsCompleted(entries); const topicResolved = !hasQuickFollowUp && conversationCompleted; return { hasQuickFollowUp, conversationCompleted, topicResolved, }; } hasQuickFollowUp(conversationId, entries) { // Check if there's an explicit follow-up conversation (indicated by naming pattern) // or a very quick subsequent conversation that suggests the previous one was incomplete const lastEntry = entries[entries.length - 1]; const lastTime = new Date(lastEntry.timestamp); const project = this.extractProjectName(conversationId); for (const [otherConvId, otherEntries] of this.conversations) { if (otherConvId === conversationId) continue; // Check for explicit follow-up pattern (e.g., "conv1_followup") if (otherConvId.startsWith(conversationId + "_")) { return true; } const otherProject = this.extractProjectName(otherConvId); if (otherProject !== project) continue; const otherStartTime = new Date(otherEntries[0].timestamp); const timeDiff = (otherStartTime.getTime() - lastTime.getTime()) / (1000 * 60 * 60); // hours // Only consider it a quick follow-up if it's very close in time (within 2 hours) // and the current conversation was relatively short (suggesting it was incomplete) if (timeDiff > 0 && timeDiff < 2 && entries.length < 50) { return true; } } return false; } seemsCompleted(entries) { // Heuristic: conversation seems completed if it's not extremely long // Very long conversations (>200 messages) often indicate getting stuck return entries.length < 200; } generateProjectProfiles(insights) { const projectGroups = new Map(); for (const insight of insights) { if (!projectGroups.has(insight.project)) { projectGroups.set(insight.project, []); } projectGroups.get(insight.project).push(insight); } const profiles = []; for (const [project, projectInsights] of projectGroups) { const profile = this.generateSingleProjectProfile(project, projectInsights); profiles.push(profile); } return profiles.sort((a, b) => b.totalConversations - a.totalConversations); } generateSingleProjectProfile(project, insights) { const totalConversations = insights.length; const avgMessageCount = insights.reduce((sum, i) => sum + i.messageCount, 0) / totalConversations; const avgDuration = insights.reduce((sum, i) => sum + i.duration, 0) / totalConversations; const avgTokens = insights.reduce((sum, i) => sum + i.tokenCount, 0) / totalConversations; const efficiencyByLength = this.calculateEfficiencyByLength(insights); const optimalRange = this.calculateProjectOptimalRange(insights, efficiencyByLength); const recommendations = this.generateProjectRecommendations(project, insights, efficiencyByLength, optimalRange); return { project, totalConversations, avgMessageCount, avgDuration, avgTokens, optimalRange, efficiencyByLength, recommendations, }; } calculateEfficiencyByLength(insights) { const groups = { quick: insights.filter((i) => i.lengthCategory === "quick"), medium: insights.filter((i) => i.lengthCategory === "medium"), deep: insights.filter((i) => i.lengthCategory === "deep"), marathon: insights.filter((i) => i.lengthCategory === "marathon"), }; const result = {}; for (const [category, items] of Object.entries(groups)) { const count = items.length; const avgEfficiency = count > 0 ? items.reduce((sum, i) => sum + i.efficiency, 0) / count : 0; const successRate = count > 0 ? items.filter((i) => i.successIndicators.topicResolved).length / count : 0; // Calculate cost metrics for this category const totalCost = items.reduce((sum, i) => { const conversationEntries = this.conversations.get(i.conversationId) || []; return sum + conversationEntries.reduce((entrySum, entry) => entrySum + (entry.cost || entry.costUSD || 0), 0); }, 0); const avgCost = count > 0 ? totalCost / count : 0; const avgCostEfficiency = count > 0 ? items.reduce((sum, i) => sum + i.costEfficiency, 0) / count : 0; result[category] = { count, avgEfficiency, successRate, avgCost, costEfficiency: avgCostEfficiency }; } return result; } calculateProjectOptimalRange(insights, efficiencyByLength) { // Find the length category with highest success rate and good efficiency const categories = ["quick", "medium", "deep", "marathon"]; let bestCategory = "medium"; let bestScore = 0; for (const category of categories) { const data = efficiencyByLength[category]; if (data.count === 0) continue; // Score combines success rate and efficiency (weighted toward success) const score = data.successRate * 0.7 + (data.avgEfficiency / 1000) * 0.3; if (score > bestScore) { bestScore = score; bestCategory = category; } } const ranges = { quick: { min: 1, max: 5, explanation: "Quick, focused questions work best for this project", }, medium: { min: 6, max: 20, explanation: "Medium-length conversations provide good balance", }, deep: { min: 21, max: 100, explanation: "Deep exploration conversations are most effective", }, marathon: { min: 101, max: 500, explanation: "Complex problems require extended conversations", }, }; const range = ranges[bestCategory]; return { minMessages: range.min, maxMessages: range.max, explanation: range.explanation, }; } generateProjectRecommendations(project, insights, efficiencyByLength, optimalRange) { const recommendations = []; // Check if user has too many marathon conversations const marathonRate = efficiencyByLength.marathon.count / insights.length; if (marathonRate > 0.2) { recommendations.push(`Break down complex tasks into smaller conversations`); } // Check success rates const categoryOrder = { quick: 1, medium: 2, deep: 3, marathon: 4 }; const bestCategory = Object.entries(efficiencyByLength) .filter(([_, data]) => data.count > 0) .sort(([categoryA, a], [categoryB, b]) => { const successDiff = b.successRate - a.successRate; if (Math.abs(successDiff) < 0.01) { // If success rates are very close, prefer longer conversations (higher order) return categoryOrder[categoryB] - categoryOrder[categoryA]; } return successDiff; })[0]; if (bestCategory) { const [category, data] = bestCategory; if (data.successRate > 0.8) { recommendations.push(`${category} conversations work well for ${project} - consider this approach more often`); } } // Check efficiency const avgEfficiency = insights.reduce((sum, i) => sum + i.efficiency, 0) / insights.length; if (avgEfficiency < 100) { recommendations.push(`Consider being more focused in ${project} conversations to improve efficiency`); } return recommendations; } calculateOptimalRange(insights) { // Calculate overall optimal range based on success rates const successByLength = insights.reduce((acc, insight) => { const category = insight.lengthCategory; if (!acc[category]) { acc[category] = { total: 0, successful: 0 }; } acc[category].total++; if (insight.successIndicators.topicResolved) { acc[category].successful++; } return acc; }, {}); let bestCategory = "medium"; let bestSuccessRate = 0; for (const [category, data] of Object.entries(successByLength)) { const successRate = data.successful / data.total; if (successRate > bestSuccessRate) { bestSuccessRate = successRate; bestCategory = category; } } const ranges = { quick: { min: 1, max: 5, explanation: "Quick, focused questions tend to be most effective overall", }, medium: { min: 6, max: 20, explanation: "Medium-length conversations provide the best balance of depth and efficiency", }, deep: { min: 21, max: 100, explanation: "Deep exploration is most effective for complex problems", }, marathon: { min: 101, max: 500, explanation: "Extended conversations needed for very complex topics", }, }; const range = ranges[bestCategory]; return { minMessages: range.min, maxMessages: range.max, explanation: range.explanation, }; } calculateLengthDistribution(insights) { const total = insights.length; if (total === 0) { return { quick: 0, medium: 0, deep: 0, marathon: 0 }; } const counts = insights.reduce((acc, insight) => { acc[insight.lengthCategory]++; return acc; }, { quick: 0, medium: 0, deep: 0, marathon: 0 }); return { quick: counts.quick / total, medium: counts.medium / total, deep: counts.deep / total, marathon: counts.marathon / total, }; } calculateCostAnalysis(insights) { if (insights.length === 0) { return { totalCost: 0, avgCostPerConversation: 0, costByLength: { quick: { totalCost: 0, avgCost: 0, costEfficiency: 0 }, medium: { totalCost: 0, avgCost: 0, costEfficiency: 0 }, deep: { totalCost: 0, avgCost: 0, costEfficiency: 0 }, marathon: { totalCost: 0, avgCost: 0, costEfficiency: 0 }, }, mostCostEfficient: "medium", }; } // Calculate total cost across all conversations const totalCost = insights.reduce((sum, insight) => { const conversationEntries = this.conversations.get(insight.conversationId) || []; return sum + conversationEntries.reduce((entrySum, entry) => entrySum + (entry.cost || entry.costUSD || 0), 0); }, 0); const avgCostPerConversation = totalCost / insights.length; // Group by length category and calculate cost metrics const groups = { quick: insights.filter((i) => i.lengthCategory === "quick"), medium: insights.filter((i) => i.lengthCategory === "medium"), deep: insights.filter((i) => i.lengthCategory === "deep"), marathon: insights.filter((i) => i.lengthCategory === "marathon"), }; const costByLength = {}; let bestCostEfficiency = 0; let mostCostEfficient = "medium"; for (const [category, items] of Object.entries(groups)) { const count = items.length; const categoryTotalCost = items.reduce((sum, insight) => { const conversationEntries = this.conversations.get(insight.conversationId) || []; return sum + conversationEntries.reduce((entrySum, entry) => entrySum + (entry.cost || entry.costUSD || 0), 0); }, 0); const avgCost = count > 0 ? categoryTotalCost / count : 0; const avgCostEfficiency = count > 0 ? items.reduce((sum, i) => sum + i.costEfficiency, 0) / count : 0; costByLength[category] = { totalCost: categoryTotalCost, avgCost, costEfficiency: avgCostEfficiency, }; // Track most cost-efficient category if (avgCostEfficiency > bestCostEfficiency && count > 0) { bestCostEfficiency = avgCostEfficiency; mostCostEfficient = category; } } return { totalCost, avgCostPerConversation, costByLength, mostCostEfficient, }; } generateInsights(insights, projectProfiles) { const generatedInsights = []; if (insights.length === 0) { return generatedInsights; } // Overall patterns const avgMessageCount = insights.reduce((sum, i) => sum + i.messageCount, 0) / insights.length; generatedInsights.push(`Your average conversation length is ${Math.round(avgMessageCount)} messages`); // Success patterns const successfulConversations = insights.filter((i) => i.successIndicators.topicResolved); if (successfulConversations.length === 0) { return generatedInsights; } const avgSuccessfulLength = successfulConversations.reduce((sum, i) => sum + i.messageCount, 0) / successfulConversations.length; if (avgSuccessfulLength < avgMessageCount) { generatedInsights.push(`Your most successful conversations average ${Math.round(avgSuccessfulLength)} messages - shorter than average`); } else { generatedInsights.push(`Your most successful conversations average ${Math.round(avgSuccessfulLength)} messages - you benefit from deeper exploration`); } // Cost efficiency insights const totalCost = insights.reduce((sum, insight) => { const conversationEntries = this.conversations.get(insight.conversationId) || []; return sum + conversationEntries.reduce((entrySum, entry) => entrySum + (entry.cost || entry.costUSD || 0), 0); }, 0); if (totalCost > 0) { const avgCost = totalCost / insights.length; generatedInsights.push(`Average cost per conversation: $${avgCost.toFixed(4)}`); // Find most cost-efficient conversation length const costByLength = { quick: insights.filter((i) => i.lengthCategory === "quick"), medium: insights.filter((i) => i.lengthCategory === "medium"), deep: insights.filter((i) => i.lengthCategory === "deep"), marathon: insights.filter((i) => i.lengthCategory === "marathon"), }; let bestCostEfficiency = 0; let bestCategory = "medium"; for (const [category, items] of Object.entries(costByLength)) { if (items.length > 0) { const avgCostEfficiency = items.reduce((sum, i) => sum + i.costEfficiency, 0) / items.length; if (avgCostEfficiency > bestCostEfficiency) { bestCostEfficiency = avgCostEfficiency; bestCategory = category; } } } if (bestCostEfficiency > 0) { generatedInsights.push(`${bestCategory} conversations show the best cost efficiency (${Math.round(bestCostEfficiency)} tokens per dollar)`); } } // Project-specific insights if (projectProfiles.length > 0) { const mostEfficientProject = projectProfiles.reduce((best, current) => { const bestEfficiency = best.efficiencyByLength.medium.avgEfficiency || 0; const currentEfficiency = current.efficiencyByLength.medium.avgEfficiency || 0; return currentEfficiency > bestEfficiency ? current : best; }, projectProfiles[0]); if (mostEfficientProject) { generatedInsights.push(`${mostEfficientProject.project} shows your highest conversation efficiency`); } } return generatedInsights; } generateRecommendations(insights, projectProfiles) { const recommendations = []; // Marathon conversation check const marathonConversations = insights.filter((i) => i.lengthCategory === "marathon"); if (marathonConversations.length >= insights.length * 0.1) { recommendations.push("Consider breaking down complex problems into multiple focused conversations"); } // Quick follow-up pattern const hasQuickFollowUps = insights.filter((i) => i.successIndicators.hasQuickFollowUp); if (hasQuickFollowUps.length >= insights.length * 0.3) { recommendations.push("Many conversations require quick follow-ups - try being more thorough in initial conversations"); } // Efficiency recommendations const lowEfficiencyConversations = insights.filter((i) => i.efficiency < 50); if (lowEfficiencyConversations.length >= insights.length * 0.2) { recommendations.push("Focus conversations with specific questions to improve time efficiency"); } // Cost efficiency recommendations const lowCostEfficiencyConversations = insights.filter((i) => i.costEfficiency < 1000); if (lowCostEfficiencyConversations.length >= insights.length * 0.3) { recommendations.push("Consider optimizing conversation length for better cost efficiency"); } // Expensive marathon conversations const expensiveMarathons = marathonConversations.filter((conv) => { const conversationEntries = this.conversations.get(conv.conversationId) || []; const cost = conversationEntries.reduce((sum, entry) => sum + (entry.cost || entry.costUSD || 0), 0); return cost > 0.50; // More than $0.50 per conversation }); if (expensiveMarathons.length > 0) { recommendations.push(`${expensiveMarathons.length} marathon conversation(s) cost over $0.50 each - consider breaking these down`); } // Project-specific recommendations const projectsWithLowSuccess = projectProfiles.filter((p) => { const overallSuccess = Object.values(p.efficiencyByLength).reduce((sum, cat) => sum + cat.successRate * cat.count, 0) / p.totalConversations; return overallSuccess < 0.6; }); for (const project of projectsWithLowSuccess) { recommendations.push(`Consider adjusting approach for ${project.project} - success rate could be improved`); } // Cost-based project recommendations const projectsWithHighCosts = projectProfiles.filter((p) => { const avgCost = Object.values(p.efficiencyByLength).reduce((sum, cat) => sum + cat.avgCost * cat.count, 0) / p.totalConversations; return avgCost > 0.25; // More than $0.25 average per conversation }); for (const project of projectsWithHighCosts) { recommendations.push(`${project.project} has high average conversation costs - consider shorter, more focused interactions`); } return recommendations; } } //# sourceMappingURL=conversation-length-analytics.js.map