UNPKG

claude-usage-tracker

Version:

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

353 lines 17.2 kB
import { describe, expect, it } from "vitest"; import { PatternAnalyzer } from "./pattern-analysis.js"; const createMockEntry = (overrides = {}) => ({ timestamp: "2025-07-31T12:00:00.000Z", conversationId: "test-conversation", requestId: "test-request", model: "claude-opus-4-20250514", prompt_tokens: 1000, completion_tokens: 2000, total_tokens: 3000, cache_creation_input_tokens: 0, cache_read_input_tokens: 0, ...overrides, }); const createDateEntry = (daysAgo, overrides = {}) => { const date = new Date(); date.setDate(date.getDate() - daysAgo); return createMockEntry({ timestamp: date.toISOString(), ...overrides, }); }; const createTimeEntry = (hoursAgo, overrides = {}) => { const date = new Date(); date.setHours(date.getHours() - hoursAgo); return createMockEntry({ timestamp: date.toISOString(), ...overrides, }); }; describe("PatternAnalyzer", () => { const analyzer = new PatternAnalyzer(); describe("analyzeConversationLengthPatterns", () => { it("should classify conversation types correctly", () => { const entries = [ // Quick questions (1-2 exchanges) createMockEntry({ conversationId: "quick-1", prompt_tokens: 100, completion_tokens: 200, }), createMockEntry({ conversationId: "quick-1", prompt_tokens: 150, completion_tokens: 300, }), // Detailed discussions (3-8 exchanges) ...Array.from({ length: 5 }, (_, i) => createMockEntry({ conversationId: "detailed-1", prompt_tokens: 800, completion_tokens: 1200, })), // Deep dives (9+ exchanges) ...Array.from({ length: 12 }, (_, i) => createMockEntry({ conversationId: "deep-1", prompt_tokens: 1500, completion_tokens: 2500, })), ]; const result = analyzer.analyzeConversationLengthPatterns(entries); expect(result.conversationTypes).toBeDefined(); expect(result.conversationTypes.quickQuestions.count).toBe(1); expect(result.conversationTypes.detailedDiscussions.count).toBe(1); expect(result.conversationTypes.deepDives.count).toBe(1); expect(result.avgLengthByType.quickQuestions).toBe(2); expect(result.avgLengthByType.detailedDiscussions).toBe(5); expect(result.avgLengthByType.deepDives).toBe(12); expect(result.recommendations).toBeInstanceOf(Array); expect(result.recommendations.length).toBeGreaterThan(0); }); it("should calculate accurate cost distributions", () => { const entries = [ // Expensive quick question createMockEntry({ conversationId: "expensive-quick", model: "claude-opus-4-20250514", prompt_tokens: 500, completion_tokens: 1000, }), // Cheap detailed discussion ...Array.from({ length: 6 }, (_, i) => createMockEntry({ conversationId: "cheap-detailed", model: "claude-3.5-sonnet-20241022", prompt_tokens: 300, completion_tokens: 600, })), ]; const result = analyzer.analyzeConversationLengthPatterns(entries); expect(result.costDistribution.quickQuestions.avgCost).toBeGreaterThan(0); expect(result.costDistribution.detailedDiscussions.avgCost).toBeGreaterThan(0); expect(result.costDistribution.quickQuestions.totalCost).toBeGreaterThan(0); expect(result.costDistribution.detailedDiscussions.totalCost).toBeGreaterThan(0); }); it("should provide efficiency insights", () => { const entries = [ // Efficient conversation createMockEntry({ conversationId: "efficient", prompt_tokens: 1000, completion_tokens: 3000, // High completion ratio }), // Inefficient conversation createMockEntry({ conversationId: "inefficient", prompt_tokens: 3000, completion_tokens: 500, // Low completion ratio }), ]; const result = analyzer.analyzeConversationLengthPatterns(entries); expect(result.efficiencyInsights.mostEfficientType).toBeTruthy(); expect(result.efficiencyInsights.leastEfficientType).toBeTruthy(); expect(result.efficiencyInsights.avgTokensPerExchange).toBeGreaterThan(0); }); }); describe("identifyLearningCurves", () => { it("should track learning progression over time", () => { const entries = []; // Simulate learning curve: start with many questions, improve over time for (let week = 0; week < 12; week++) { const questionsThisWeek = Math.max(1, 10 - week); // Decreasing questions const complexityThisWeek = 0.3 + week * 0.05; // Increasing complexity for (let q = 0; q < questionsThisWeek; q++) { entries.push(createDateEntry(week * 7 + q, { conversationId: `week-${week}-q-${q}`, prompt_tokens: Math.floor(500 + complexityThisWeek * 2000), completion_tokens: Math.floor(1000 + complexityThisWeek * 3000), })); } } const result = analyzer.identifyLearningCurves(entries); expect(result.periods).toBeInstanceOf(Array); expect(result.periods.length).toBeGreaterThan(0); expect(result.overallTrend).toMatch(/improving|stable|declining/); expect(result.insights).toBeInstanceOf(Array); result.periods.forEach((period) => { expect(period.startDate).toBeTruthy(); expect(period.endDate).toBeTruthy(); expect(period.metrics.avgQuestionsPerDay).toBeGreaterThanOrEqual(0); expect(period.metrics.avgComplexityScore).toBeGreaterThanOrEqual(0); expect(period.metrics.avgComplexityScore).toBeLessThanOrEqual(1); expect(period.characteristics).toBeInstanceOf(Array); }); }); it("should detect plateau periods", () => { const entries = Array.from({ length: 30 }, (_, i) => createDateEntry(i, { conversationId: `plateau-${i}`, prompt_tokens: 1000 + Math.random() * 100, // Minimal variation completion_tokens: 2000 + Math.random() * 200, })); const result = analyzer.identifyLearningCurves(entries); expect(result.overallTrend).toBe("stable"); expect(result.insights.some((insight) => insight.includes("consistent"))).toBe(true); }); it("should identify declining patterns", () => { const entries = Array.from({ length: 14 }, (_, i) => createDateEntry(i, { conversationId: `decline-${i}`, prompt_tokens: 3000 - i * 150, // More pronounced decreasing complexity completion_tokens: 6000 - i * 300, })); const result = analyzer.identifyLearningCurves(entries); if (result.periods.length >= 2) { const recent = result.periods[result.periods.length - 1]; const earlier = result.periods[0]; expect(recent.metrics.avgComplexityScore).toBeLessThanOrEqual(earlier.metrics.avgComplexityScore); } }); }); describe("analyzeTaskSwitchingPatterns", () => { it("should identify rapid task switching", () => { const entries = [ // Rapid switching between different conversation types createTimeEntry(5, { conversationId: "coding-task", prompt_tokens: 2000, completion_tokens: 3000, }), createTimeEntry(4.5, { conversationId: "writing-task", prompt_tokens: 800, completion_tokens: 1200, }), createTimeEntry(4, { conversationId: "analysis-task", prompt_tokens: 1500, completion_tokens: 2000, }), createTimeEntry(3.5, { conversationId: "quick-question", prompt_tokens: 200, completion_tokens: 400, }), ]; const result = analyzer.analyzeTaskSwitchingPatterns(entries); expect(result.switchFrequency).toBeGreaterThan(0); expect(result.avgTimeBetweenSwitches).toBeGreaterThan(0); expect(result.costOfSwitching).toBeGreaterThanOrEqual(0); expect(result.mostCommonTransitions).toBeInstanceOf(Array); expect(result.recommendations).toBeInstanceOf(Array); }); it("should detect focused work sessions", () => { const entries = [ // Long focused session on similar tasks ...Array.from({ length: 8 }, (_, i) => createTimeEntry(8 - i * 0.5, { conversationId: `focused-${i}`, prompt_tokens: 1500 + Math.random() * 300, completion_tokens: 2500 + Math.random() * 500, })), ]; const result = analyzer.analyzeTaskSwitchingPatterns(entries); expect(result.switchFrequency).toBeLessThan(3); expect(result.avgTimeBetweenSwitches).toBeGreaterThan(0); expect(result.recommendations.some((r) => r.includes("focused"))).toBe(true); }); it("should classify task types accurately", () => { const entries = [ // Different task types createTimeEntry(5, { conversationId: "code-review", prompt_tokens: 3000, completion_tokens: 4000, }), createTimeEntry(4, { conversationId: "quick-help", prompt_tokens: 200, completion_tokens: 300, }), createTimeEntry(3, { conversationId: "research-deep", prompt_tokens: 2500, completion_tokens: 5000, }), createTimeEntry(2, { conversationId: "writing-assist", prompt_tokens: 1000, completion_tokens: 1500, }), ]; const result = analyzer.analyzeTaskSwitchingPatterns(entries); expect(result.mostCommonTransitions.length).toBeGreaterThanOrEqual(0); result.mostCommonTransitions.forEach((transition) => { expect(transition.from).toBeTruthy(); expect(transition.to).toBeTruthy(); expect(transition.frequency).toBeGreaterThan(0); expect(transition.avgGapTime).toBeGreaterThanOrEqual(0); }); }); it("should calculate efficiency metrics", () => { const entries = Array.from({ length: 10 }, (_, i) => createTimeEntry(10 - i, { conversationId: `efficiency-test-${i}`, prompt_tokens: 1000, completion_tokens: 2000, })); const result = analyzer.analyzeTaskSwitchingPatterns(entries); expect(result.costOfSwitching).toBeGreaterThanOrEqual(0); expect(result.switchFrequency).toBeGreaterThanOrEqual(0); expect(result.avgTimeBetweenSwitches).toBeGreaterThanOrEqual(0); }); }); describe("edge cases", () => { it("should handle empty entries array", () => { const lengthResult = analyzer.analyzeConversationLengthPatterns([]); expect(lengthResult.conversationTypes.quickQuestions.count).toBe(0); expect(lengthResult.conversationTypes.detailedDiscussions.count).toBe(0); expect(lengthResult.conversationTypes.deepDives.count).toBe(0); const learningResult = analyzer.identifyLearningCurves([]); expect(learningResult.periods).toHaveLength(0); expect(learningResult.overallTrend).toBe("stable"); const switchingResult = analyzer.analyzeTaskSwitchingPatterns([]); expect(switchingResult.switchFrequency).toBe(0); expect(switchingResult.mostCommonTransitions).toHaveLength(0); }); it("should handle single conversation", () => { const entries = [createMockEntry()]; expect(() => analyzer.analyzeConversationLengthPatterns(entries)).not.toThrow(); expect(() => analyzer.identifyLearningCurves(entries)).not.toThrow(); expect(() => analyzer.analyzeTaskSwitchingPatterns(entries)).not.toThrow(); }); it("should handle conversations with zero tokens", () => { const entries = [ createMockEntry({ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0, }), ]; const lengthResult = analyzer.analyzeConversationLengthPatterns(entries); expect(lengthResult.conversationTypes.quickQuestions.count).toBe(1); expect(() => analyzer.identifyLearningCurves(entries)).not.toThrow(); expect(() => analyzer.analyzeTaskSwitchingPatterns(entries)).not.toThrow(); }); it("should handle invalid timestamps gracefully", () => { const entries = [ createMockEntry({ timestamp: "invalid-date" }), createMockEntry({ timestamp: "2025-07-31T25:00:00.000Z" }), ]; expect(() => analyzer.identifyLearningCurves(entries)).not.toThrow(); expect(() => analyzer.analyzeTaskSwitchingPatterns(entries)).not.toThrow(); }); it("should handle conversations with same timestamp", () => { const sameTimestamp = "2025-07-31T12:00:00.000Z"; const entries = [ createMockEntry({ conversationId: "same-time-1", timestamp: sameTimestamp, }), createMockEntry({ conversationId: "same-time-2", timestamp: sameTimestamp, }), ]; expect(() => analyzer.analyzeTaskSwitchingPatterns(entries)).not.toThrow(); }); it("should validate metrics ranges", () => { const entries = Array.from({ length: 5 }, (_, i) => createDateEntry(i, { conversationId: `validation-${i}`, prompt_tokens: 1000, completion_tokens: 2000, })); const lengthResult = analyzer.analyzeConversationLengthPatterns(entries); // Validate efficiency insights are within reasonable ranges expect(lengthResult.efficiencyInsights.avgTokensPerExchange).toBeGreaterThan(0); const learningResult = analyzer.identifyLearningCurves(entries); learningResult.periods.forEach((period) => { expect(period.metrics.avgComplexityScore).toBeGreaterThanOrEqual(0); expect(period.metrics.avgComplexityScore).toBeLessThanOrEqual(1); }); const switchingResult = analyzer.analyzeTaskSwitchingPatterns(entries); expect(switchingResult.switchFrequency).toBeGreaterThanOrEqual(0); expect(switchingResult.avgTimeBetweenSwitches).toBeGreaterThanOrEqual(0); }); it("should handle extreme conversation lengths", () => { const entries = [ // Very short conversation createMockEntry({ conversationId: "very-short", prompt_tokens: 1, completion_tokens: 1, }), // Very long conversation ...Array.from({ length: 100 }, (_, i) => createMockEntry({ conversationId: "very-long", prompt_tokens: 1000, completion_tokens: 2000, })), ]; const result = analyzer.analyzeConversationLengthPatterns(entries); expect(result.conversationTypes.quickQuestions.count).toBe(1); expect(result.conversationTypes.deepDives.count).toBe(1); expect(result.avgLengthByType.deepDives).toBe(100); }); }); }); //# sourceMappingURL=pattern-analysis.test.js.map