#!/usr/bin/env node /** * BMAD Telemetry & Analytics System * Tracks agent usage patterns, performance metrics, and optimization opportunities */ const fs = require('fs').promises; const path = require('path'); const crypto = require('crypto'); class TelemetryAnalytics { constructor(options = {}) { this.dataDir = options.dataDir || path.join(__dirname, '../telemetry-data'); this.sessionId = crypto.randomBytes(16).toString('hex'); this.startTime = Date.now(); // Initialize collectors this.collectors = { agentUsage: new Map(), fileAccess: new Map(), tokenPatterns: [], checkpoints: [], decisions: [], errors: [], performance: [] }; // Configuration this.config = { autoSave: options.autoSave !== false, saveInterval: options.saveInterval || 60000, // 1 minute anonymize: options.anonymize !== false, verbose: options.verbose || false }; this.initializeDataDir(); if (this.config.autoSave) { this.startAutoSave(); } } /** * Initialize telemetry data directory */ async initializeDataDir() { try { await fs.mkdir(this.dataDir, { recursive: true }); await fs.mkdir(path.join(this.dataDir, 'sessions'), { recursive: true }); await fs.mkdir(path.join(this.dataDir, 'aggregated'), { recursive: true }); } catch (error) { console.error('Failed to initialize telemetry directory:', error); } } /** * Track agent usage */ trackAgentUsage(agentName, action, metadata = {}) { const agent = this.collectors.agentUsage.get(agentName) || { invocations: 0, totalTokensUsed: 0, totalTimeMs: 0, actions: new Map(), errors: 0 }; agent.invocations++; agent.totalTokensUsed += metadata.tokensUsed || 0; agent.totalTimeMs += metadata.timeMs || 0; // Track specific actions const actionCount = agent.actions.get(action) || 0; agent.actions.set(action, actionCount + 1); this.collectors.agentUsage.set(agentName, agent); // Log event this.logEvent('agent_usage', { agent: agentName, action, ...metadata, timestamp: Date.now() }); } /** * Track file access patterns */ trackFileAccess(filepath, operation, metadata = {}) { const filename = path.basename(filepath); const file = this.collectors.fileAccess.get(filename) || { accessCount: 0, operations: new Map(), totalTokens: 0, agents: new Set(), firstAccess: Date.now(), lastAccess: Date.now() }; file.accessCount++; file.lastAccess = Date.now(); file.totalTokens += metadata.tokens || 0; if (metadata.agent) { file.agents.add(metadata.agent); } const opCount = file.operations.get(operation) || 0; file.operations.set(operation, opCount + 1); this.collectors.fileAccess.set(filename, file); this.logEvent('file_access', { file: filename, operation, ...metadata, timestamp: Date.now() }); } /** * Track token usage patterns */ trackTokenUsage(context, tokens, metadata = {}) { const pattern = { context, tokens, timestamp: Date.now(), phase: metadata.phase || 'unknown', agent: metadata.agent || 'unknown', efficiency: metadata.efficiency || this.calculateEfficiency(tokens, metadata) }; this.collectors.tokenPatterns.push(pattern); // Track peak usage if (!this.peakTokenUsage || tokens > this.peakTokenUsage.tokens) { this.peakTokenUsage = pattern; } this.logEvent('token_usage', pattern); } /** * Track checkpoint creation */ trackCheckpoint(checkpointId, originalTokens, compressedTokens, metadata = {}) { const checkpoint = { id: checkpointId, originalTokens, compressedTokens, compressionRatio: ((originalTokens - compressedTokens) / originalTokens * 100).toFixed(1), timestamp: Date.now(), phase: metadata.phase || 'unknown', agent: metadata.agent || 'unknown', artifacts: metadata.artifacts || [] }; this.collectors.checkpoints.push(checkpoint); this.logEvent('checkpoint', checkpoint); } /** * Track decision points */ trackDecision(decisionType, choice, metadata = {}) { const decision = { type: decisionType, choice, timestamp: Date.now(), agent: metadata.agent || 'unknown', rationale: metadata.rationale || '', alternatives: metadata.alternatives || [], confidence: metadata.confidence || 1.0 }; this.collectors.decisions.push(decision); this.logEvent('decision', decision); } /** * Track errors and failures */ trackError(errorType, message, metadata = {}) { const error = { type: errorType, message: this.config.anonymize ? this.anonymizeError(message) : message, timestamp: Date.now(), agent: metadata.agent || 'unknown', phase: metadata.phase || 'unknown', severity: metadata.severity || 'medium', resolved: metadata.resolved || false }; this.collectors.errors.push(error); this.logEvent('error', error); } /** * Track performance metrics */ trackPerformance(operation, duration, metadata = {}) { const perf = { operation, duration, timestamp: Date.now(), success: metadata.success !== false, tokensConsumed: metadata.tokens || 0, efficiency: duration > 0 ? (metadata.tokens || 0) / duration : 0 }; this.collectors.performance.push(perf); this.logEvent('performance', perf); } /** * Calculate efficiency metrics */ calculateEfficiency(tokens, metadata) { if (!metadata.outputValue) return 1.0; // Simple efficiency: output value / tokens used return Math.min(metadata.outputValue / tokens, 2.0); } /** * Anonymize error messages */ anonymizeError(message) { // Remove file paths, URLs, and potentially sensitive data return message .replace(/\/[^\s]+/g, '/[path]') .replace(/https?:\/\/[^\s]+/g, '[url]') .replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, '[email]'); } /** * Log event to memory */ logEvent(type, data) { if (this.config.verbose) { console.log(`[Telemetry] ${type}:`, data); } } /** * Analyze collected data */ analyze() { const duration = Date.now() - this.startTime; const analysis = { session: { id: this.sessionId, duration, startTime: this.startTime, endTime: Date.now() }, agents: this.analyzeAgents(), files: this.analyzeFiles(), tokens: this.analyzeTokens(), checkpoints: this.analyzeCheckpoints(), decisions: this.analyzeDecisions(), errors: this.analyzeErrors(), performance: this.analyzePerformance(), recommendations: this.generateRecommendations() }; return analysis; } /** * Analyze agent usage patterns */ analyzeAgents() { const agents = Array.from(this.collectors.agentUsage.entries()).map(([name, data]) => ({ name, invocations: data.invocations, avgTokensPerInvocation: data.invocations > 0 ? Math.round(data.totalTokensUsed / data.invocations) : 0, avgTimeMs: data.invocations > 0 ? Math.round(data.totalTimeMs / data.invocations) : 0, topActions: Array.from(data.actions.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 5) .map(([action, count]) => ({ action, count })), errorRate: data.invocations > 0 ? (data.errors / data.invocations * 100).toFixed(1) : 0 })); return { totalAgents: agents.length, mostUsed: agents.sort((a, b) => b.invocations - a.invocations)[0], leastUsed: agents.sort((a, b) => a.invocations - b.invocations)[0], agents }; } /** * Analyze file access patterns */ analyzeFiles() { const files = Array.from(this.collectors.fileAccess.entries()).map(([name, data]) => ({ name, accessCount: data.accessCount, avgTokens: data.accessCount > 0 ? Math.round(data.totalTokens / data.accessCount) : 0, agentCount: data.agents.size, accessPattern: this.detectAccessPattern(data), operations: Array.from(data.operations.entries()) })); return { totalFiles: files.length, mostAccessed: files.sort((a, b) => b.accessCount - a.accessCount)[0], largestFile: files.sort((a, b) => b.avgTokens - a.avgTokens)[0], accessPatterns: this.summarizeAccessPatterns(files), files }; } /** * Detect file access patterns */ detectAccessPattern(fileData) { const duration = fileData.lastAccess - fileData.firstAccess; const accessRate = fileData.accessCount / (duration / 1000 / 60); // per minute if (accessRate > 10) return 'hot'; if (accessRate > 1) return 'frequent'; if (fileData.accessCount === 1) return 'one-time'; return 'occasional'; } /** * Analyze token usage patterns */ analyzeTokens() { if (this.collectors.tokenPatterns.length === 0) { return { totalTokens: 0, patterns: [] }; } const totalTokens = this.collectors.tokenPatterns.reduce((sum, p) => sum + p.tokens, 0); const avgTokens = Math.round(totalTokens / this.collectors.tokenPatterns.length); // Group by phase const byPhase = {}; for (const pattern of this.collectors.tokenPatterns) { if (!byPhase[pattern.phase]) { byPhase[pattern.phase] = { count: 0, tokens: 0 }; } byPhase[pattern.phase].count++; byPhase[pattern.phase].tokens += pattern.tokens; } return { totalTokens, avgTokensPerOperation: avgTokens, peakUsage: this.peakTokenUsage, byPhase: Object.entries(byPhase).map(([phase, data]) => ({ phase, operations: data.count, totalTokens: data.tokens, avgTokens: Math.round(data.tokens / data.count) })), efficiency: this.calculateOverallEfficiency() }; } /** * Analyze checkpoints */ analyzeCheckpoints() { if (this.collectors.checkpoints.length === 0) { return { count: 0, avgCompression: 0 }; } const avgCompression = this.collectors.checkpoints .reduce((sum, cp) => sum + parseFloat(cp.compressionRatio), 0) / this.collectors.checkpoints.length; const totalSaved = this.collectors.checkpoints .reduce((sum, cp) => sum + (cp.originalTokens - cp.compressedTokens), 0); return { count: this.collectors.checkpoints.length, avgCompression: avgCompression.toFixed(1), totalTokensSaved: totalSaved, checkpoints: this.collectors.checkpoints }; } /** * Analyze decisions */ analyzeDecisions() { const decisionTypes = {}; for (const decision of this.collectors.decisions) { if (!decisionTypes[decision.type]) { decisionTypes[decision.type] = []; } decisionTypes[decision.type].push(decision.choice); } return { totalDecisions: this.collectors.decisions.length, byType: Object.entries(decisionTypes).map(([type, choices]) => ({ type, count: choices.length, choices: [...new Set(choices)] })), avgConfidence: this.collectors.decisions.length > 0 ? (this.collectors.decisions.reduce((sum, d) => sum + d.confidence, 0) / this.collectors.decisions.length).toFixed(2) : 1.0 }; } /** * Analyze errors */ analyzeErrors() { const byType = {}; const bySeverity = { low: 0, medium: 0, high: 0, critical: 0 }; for (const error of this.collectors.errors) { byType[error.type] = (byType[error.type] || 0) + 1; bySeverity[error.severity]++; } return { totalErrors: this.collectors.errors.length, byType, bySeverity, resolvedCount: this.collectors.errors.filter(e => e.resolved).length, criticalErrors: this.collectors.errors.filter(e => e.severity === 'critical') }; } /** * Analyze performance */ analyzePerformance() { if (this.collectors.performance.length === 0) { return { operations: 0 }; } const successful = this.collectors.performance.filter(p => p.success); const avgDuration = this.collectors.performance .reduce((sum, p) => sum + p.duration, 0) / this.collectors.performance.length; return { operations: this.collectors.performance.length, successRate: (successful.length / this.collectors.performance.length * 100).toFixed(1), avgDuration: Math.round(avgDuration), slowestOperation: this.collectors.performance .sort((a, b) => b.duration - a.duration)[0], totalTokensConsumed: this.collectors.performance .reduce((sum, p) => sum + p.tokensConsumed, 0) }; } /** * Calculate overall efficiency */ calculateOverallEfficiency() { const patterns = this.collectors.tokenPatterns; if (patterns.length === 0) return 1.0; const avgEfficiency = patterns .reduce((sum, p) => sum + p.efficiency, 0) / patterns.length; return avgEfficiency.toFixed(2); } /** * Summarize access patterns */ summarizeAccessPatterns(files) { const patterns = { hot: 0, frequent: 0, occasional: 0, 'one-time': 0 }; for (const file of files) { patterns[file.accessPattern]++; } return patterns; } /** * Generate recommendations */ generateRecommendations() { const recommendations = []; const analysis = { agents: this.analyzeAgents(), files: this.analyzeFiles(), tokens: this.analyzeTokens(), errors: this.analyzeErrors() }; // Token optimization if (analysis.tokens.peakUsage && analysis.tokens.peakUsage.tokens > 4000) { recommendations.push({ type: 'optimization', priority: 'high', message: `Peak token usage (${analysis.tokens.peakUsage.tokens}) exceeds recommended limit. Consider more aggressive checkpointing.` }); } // File access optimization const hotFiles = analysis.files.files.filter(f => f.accessPattern === 'hot'); if (hotFiles.length > 0) { recommendations.push({ type: 'caching', priority: 'medium', message: `Files accessed frequently: ${hotFiles.map(f => f.name).join(', ')}. Consider caching these.` }); } // Error handling if (analysis.errors.criticalErrors.length > 0) { recommendations.push({ type: 'error', priority: 'critical', message: `${analysis.errors.criticalErrors.length} critical errors detected. Immediate attention required.` }); } // Agent optimization if (analysis.agents.mostUsed && analysis.agents.mostUsed.avgTokensPerInvocation > 2000) { recommendations.push({ type: 'agent', priority: 'medium', message: `Agent '${analysis.agents.mostUsed.name}' uses high tokens per invocation. Consider splitting responsibilities.` }); } // Checkpoint frequency const checkpointAnalysis = this.analyzeCheckpoints(); if (checkpointAnalysis.count === 0 && analysis.tokens.totalTokens > 10000) { recommendations.push({ type: 'checkpoint', priority: 'high', message: 'No checkpoints created despite high token usage. Implement regular checkpointing.' }); } return recommendations; } /** * Save telemetry data */ async save() { try { const analysis = this.analyze(); const filename = `session-${this.sessionId}-${Date.now()}.json`; const filepath = path.join(this.dataDir, 'sessions', filename); await fs.writeFile(filepath, JSON.stringify(analysis, null, 2)); if (this.config.verbose) { console.log(`Telemetry saved to ${filepath}`); } // Also update aggregated data await this.updateAggregatedData(analysis); return filepath; } catch (error) { console.error('Failed to save telemetry:', error); return null; } } /** * Update aggregated data */ async updateAggregatedData(sessionAnalysis) { const aggregatedPath = path.join(this.dataDir, 'aggregated', 'summary.json'); let aggregated = {}; try { const existing = await fs.readFile(aggregatedPath, 'utf-8'); aggregated = JSON.parse(existing); } catch (error) { // File doesn't exist yet, start fresh } // Update aggregated metrics aggregated.sessions = (aggregated.sessions || 0) + 1; aggregated.totalTokens = (aggregated.totalTokens || 0) + sessionAnalysis.tokens.totalTokens; aggregated.totalErrors = (aggregated.totalErrors || 0) + sessionAnalysis.errors.totalErrors; aggregated.lastUpdated = Date.now(); // Track top files if (!aggregated.topFiles) aggregated.topFiles = {}; for (const file of sessionAnalysis.files.files) { aggregated.topFiles[file.name] = (aggregated.topFiles[file.name] || 0) + file.accessCount; } // Track agent usage if (!aggregated.agentUsage) aggregated.agentUsage = {}; for (const agent of sessionAnalysis.agents.agents) { aggregated.agentUsage[agent.name] = (aggregated.agentUsage[agent.name] || 0) + agent.invocations; } await fs.writeFile(aggregatedPath, JSON.stringify(aggregated, null, 2)); } /** * Start auto-save timer */ startAutoSave() { this.autoSaveTimer = setInterval(() => { this.save(); }, this.config.saveInterval); } /** * Stop auto-save timer */ stopAutoSave() { if (this.autoSaveTimer) { clearInterval(this.autoSaveTimer); this.autoSaveTimer = null; } } /** * Generate report */ generateReport() { const analysis = this.analyze(); console.log('\n' + '='.repeat(80)); console.log('TELEMETRY ANALYTICS REPORT'); console.log('='.repeat(80)); // Session info console.log('\nšŸ“Š SESSION'); console.log('-'.repeat(40)); console.log(`Session ID: ${analysis.session.id}`); console.log(`Duration: ${Math.round(analysis.session.duration / 1000)}s`); // Agent usage console.log('\nšŸ¤– AGENT USAGE'); console.log('-'.repeat(40)); if (analysis.agents.mostUsed) { console.log(`Most Used: ${analysis.agents.mostUsed.name} (${analysis.agents.mostUsed.invocations} invocations)`); } console.log(`Total Agents: ${analysis.agents.totalAgents}`); // Token usage console.log('\nšŸ’° TOKEN USAGE'); console.log('-'.repeat(40)); console.log(`Total Tokens: ${analysis.tokens.totalTokens}`); console.log(`Avg per Operation: ${analysis.tokens.avgTokensPerOperation}`); console.log(`Efficiency Score: ${analysis.tokens.efficiency}`); // File access console.log('\nšŸ“ FILE ACCESS'); console.log('-'.repeat(40)); console.log(`Total Files: ${analysis.files.totalFiles}`); if (analysis.files.mostAccessed) { console.log(`Most Accessed: ${analysis.files.mostAccessed.name} (${analysis.files.mostAccessed.accessCount} times)`); } // Checkpoints console.log('\nšŸ“ CHECKPOINTS'); console.log('-'.repeat(40)); console.log(`Created: ${analysis.checkpoints.count}`); console.log(`Avg Compression: ${analysis.checkpoints.avgCompression}%`); console.log(`Tokens Saved: ${analysis.checkpoints.totalTokensSaved}`); // Errors if (analysis.errors.totalErrors > 0) { console.log('\nāš ļø ERRORS'); console.log('-'.repeat(40)); console.log(`Total: ${analysis.errors.totalErrors}`); console.log(`Critical: ${analysis.errors.bySeverity.critical}`); console.log(`Resolved: ${analysis.errors.resolvedCount}`); } // Recommendations if (analysis.recommendations.length > 0) { console.log('\nšŸ’” RECOMMENDATIONS'); console.log('-'.repeat(40)); for (const rec of analysis.recommendations) { const icon = rec.priority === 'critical' ? 'šŸ”“' : rec.priority === 'high' ? '🟠' : '🟔'; console.log(`${icon} [${rec.type}] ${rec.message}`); } } console.log('\n' + '='.repeat(80)); } /** * Clean up and save final data */ async cleanup() { this.stopAutoSave(); await this.save(); } } // CLI for testing if (require.main === module) { const telemetry = new TelemetryAnalytics({ verbose: true }); // Simulate usage console.log('Simulating telemetry collection...\n'); // Agent usage telemetry.trackAgentUsage('react-developer', 'create_component', { tokensUsed: 1200, timeMs: 3000 }); telemetry.trackAgentUsage('node-backend-developer', 'implement_api', { tokensUsed: 1800, timeMs: 5000 }); // File access telemetry.trackFileAccess('core-principles.md', 'load', { tokens: 5000, agent: 'react-developer' }); telemetry.trackFileAccess('core-principles.md', 'load', { tokens: 5000, agent: 'node-backend-developer' }); // Token usage telemetry.trackTokenUsage('architecture_phase', 4500, { phase: 'architecture', agent: 'solution-architect' }); // Checkpoint telemetry.trackCheckpoint('checkpoint-arch-001', 4500, 800, { phase: 'architecture', agent: 'solution-architect' }); // Decision telemetry.trackDecision('technology', 'Next.js', { agent: 'solution-architect', alternatives: ['Gatsby', 'Vite'], confidence: 0.9 }); // Error telemetry.trackError('api_error', 'Failed to connect to database', { agent: 'node-backend-developer', severity: 'high' }); // Generate report telemetry.generateReport(); // Save and cleanup telemetry.cleanup().then(() => { console.log('\nTelemetry data saved successfully.'); }); } module.exports = TelemetryAnalytics;