/** * Analytics Service * * Privacy-first analytics that tracks: * - Command usage patterns (local only) * - Model performance metrics * - Task completion rates * - Error frequencies * * All data stored locally, never sent to cloud */ import Configstore from 'configstore'; export interface CommandUsage { command: string; timestamp: number; model?: string; success: boolean; durationMs?: number; errorType?: string; } export interface ModelPerformance { model: string; taskType: string; successCount: number; failureCount: number; avgDurationMs: number; lastUsed: number; } export interface AnalyticsSummary { totalCommands: number; commandFrequency: Record; modelUsage: Record; successRate: number; averageResponseTime: number; mostUsedCommand: string; mostUsedModel: string; topPerformingModels: ModelPerformance[]; } export class AnalyticsService { private store: Configstore; private readonly MAX_EVENTS = 1000; // Keep last 1000 events private readonly STORE_NAME = 'neohub-analytics'; constructor() { this.store = new Configstore(this.STORE_NAME, { events: [] as CommandUsage[], modelPerformance: {} as Record, enabled: true, }); } /** * Track a command execution */ trackCommand(usage: CommandUsage): void { if (!this.isEnabled()) return; const events = this.store.get('events') || []; events.push(usage); // Keep only last MAX_EVENTS if (events.length > this.MAX_EVENTS) { events.shift(); } this.store.set('events', events); // Update model performance if applicable if (usage.model && usage.durationMs) { this.updateModelPerformance(usage); } } /** * Update model performance metrics */ private updateModelPerformance(usage: CommandUsage): void { const key = `${usage.model}:${this.inferTaskType(usage.command)}`; const performance = this.store.get(`modelPerformance.${key}`) || { model: usage.model, taskType: this.inferTaskType(usage.command), successCount: 0, failureCount: 0, avgDurationMs: 0, lastUsed: 0, }; if (usage.success) { performance.successCount++; } else { performance.failureCount++; } // Update running average const totalCount = performance.successCount + performance.failureCount; performance.avgDurationMs = (performance.avgDurationMs * (totalCount - 1) + (usage.durationMs || 0)) / totalCount; performance.lastUsed = usage.timestamp; this.store.set(`modelPerformance.${key}`, performance); } /** * Infer task type from command */ private inferTaskType(command: string): string { const taskMap: Record = { edit: 'code_generation', analyze: 'code_review', chat: 'general', recommend: 'recommendation', }; return taskMap[command] || 'other'; } /** * Get analytics summary */ getSummary(): AnalyticsSummary { const events = this.store.get('events') || []; if (events.length === 0) { return { totalCommands: 0, commandFrequency: {}, modelUsage: {}, successRate: 0, averageResponseTime: 0, mostUsedCommand: '', mostUsedModel: '', topPerformingModels: [], }; } // Command frequency const commandFrequency: Record = {}; const modelUsage: Record = {}; let successCount = 0; let totalDuration = 0; let durationCount = 0; events.forEach((event: CommandUsage) => { commandFrequency[event.command] = (commandFrequency[event.command] || 0) + 1; if (event.model) { modelUsage[event.model] = (modelUsage[event.model] || 0) + 1; } if (event.success) successCount++; if (event.durationMs) { totalDuration += event.durationMs; durationCount++; } }); // Most used command const mostUsedCommand = Object.entries(commandFrequency) .sort(([, a], [, b]) => b - a)[0]?.[0] || ''; // Most used model const mostUsedModel = Object.entries(modelUsage) .sort(([, a], [, b]) => b - a)[0]?.[0] || ''; // Top performing models const modelPerformance = this.store.get('modelPerformance') || {}; const topPerformingModels = Object.values(modelPerformance as Record) .sort((a, b) => { const aScore = a.successCount / (a.successCount + a.failureCount); const bScore = b.successCount / (b.successCount + b.failureCount); return bScore - aScore; }) .slice(0, 5); return { totalCommands: events.length, commandFrequency, modelUsage, successRate: (successCount / events.length) * 100, averageResponseTime: durationCount > 0 ? totalDuration / durationCount : 0, mostUsedCommand, mostUsedModel, topPerformingModels, }; } /** * Get model recommendations based on performance */ getModelRecommendation(taskType: string): string | null { const modelPerformance = this.store.get('modelPerformance') || {}; const relevantModels = Object.values(modelPerformance as Record) .filter(perf => perf.taskType === taskType); if (relevantModels.length === 0) return null; // Sort by success rate, then by average duration const best = relevantModels.sort((a, b) => { const aSuccessRate = a.successCount / (a.successCount + a.failureCount); const bSuccessRate = b.successCount / (b.successCount + b.failureCount); if (Math.abs(aSuccessRate - bSuccessRate) > 0.1) { return bSuccessRate - aSuccessRate; } return a.avgDurationMs - b.avgDurationMs; })[0]; return best?.model || null; } /** * Get events for last N days */ getRecentEvents(days: number = 7): CommandUsage[] { const events = this.store.get('events') || []; const cutoff = Date.now() - (days * 24 * 60 * 60 * 1000); return events.filter((event: CommandUsage) => event.timestamp > cutoff); } /** * Clear all analytics data */ clear(): void { this.store.set('events', []); this.store.set('modelPerformance', {}); } /** * Enable/disable analytics */ setEnabled(enabled: boolean): void { this.store.set('enabled', enabled); } /** * Check if analytics is enabled */ isEnabled(): boolean { return this.store.get('enabled') !== false; } /** * Export analytics data */ export(): string { return JSON.stringify({ summary: this.getSummary(), events: this.store.get('events'), modelPerformance: this.store.get('modelPerformance'), }, null, 2); } }