UNPKG

@gork-labs/secondbrain-mcp

Version:

Second Brain MCP Server - Agent team orchestration with dynamic tool discovery

252 lines (251 loc) 9.95 kB
import * as fs from 'fs'; import * as path from 'path'; import { config } from '../utils/config.js'; import { logger } from '../utils/logger.js'; /** * Analytics Storage Manager * Handles persistence and retrieval of analytics data for the SecondBrain MCP server */ export class AnalyticsStorage { storePath; qualityMetrics = new Map(); performanceMetrics = []; usageMetrics = []; maxMetricsPerType = 10000; // Prevent unbounded growth constructor() { this.storePath = config.sessionStorePath; // Reuse existing storage path this.ensureAnalyticsDirectory(); this.loadExistingAnalytics(); this.startCleanupTimer(); } ensureAnalyticsDirectory() { try { const analyticsPath = path.join(this.storePath, 'analytics'); if (!fs.existsSync(analyticsPath)) { fs.mkdirSync(analyticsPath, { recursive: true }); logger.info('Created analytics storage directory', { path: analyticsPath }); } } catch (error) { logger.error('Failed to create analytics storage directory', { error: error instanceof Error ? error.message : String(error) }); throw error; } } loadExistingAnalytics() { try { this.loadQualityMetrics(); this.loadPerformanceMetrics(); this.loadUsageMetrics(); logger.info('Loaded analytics data from storage', { qualityMetricTypes: this.qualityMetrics.size, performanceMetrics: this.performanceMetrics.length, usageMetrics: this.usageMetrics.length }); } catch (error) { logger.warn('Failed to load existing analytics data', { error: error instanceof Error ? error.message : String(error) }); } } loadQualityMetrics() { const qualityFile = path.join(this.storePath, 'analytics', 'quality-metrics.json'); if (fs.existsSync(qualityFile)) { const data = fs.readFileSync(qualityFile, 'utf-8'); const qualityData = JSON.parse(data); this.qualityMetrics = new Map(Object.entries(qualityData)); } } loadPerformanceMetrics() { const performanceFile = path.join(this.storePath, 'analytics', 'performance-metrics.json'); if (fs.existsSync(performanceFile)) { const data = fs.readFileSync(performanceFile, 'utf-8'); this.performanceMetrics = JSON.parse(data); } } loadUsageMetrics() { const usageFile = path.join(this.storePath, 'analytics', 'usage-metrics.json'); if (fs.existsSync(usageFile)) { const data = fs.readFileSync(usageFile, 'utf-8'); this.usageMetrics = JSON.parse(data); } } persistAnalytics() { try { this.persistQualityMetrics(); this.persistPerformanceMetrics(); this.persistUsageMetrics(); } catch (error) { logger.error('Failed to persist analytics data', { error: error instanceof Error ? error.message : String(error) }); } } persistQualityMetrics() { const qualityFile = path.join(this.storePath, 'analytics', 'quality-metrics.json'); const qualityData = Object.fromEntries(this.qualityMetrics); fs.writeFileSync(qualityFile, JSON.stringify(qualityData, null, 2)); } persistPerformanceMetrics() { const performanceFile = path.join(this.storePath, 'analytics', 'performance-metrics.json'); fs.writeFileSync(performanceFile, JSON.stringify(this.performanceMetrics, null, 2)); } persistUsageMetrics() { const usageFile = path.join(this.storePath, 'analytics', 'usage-metrics.json'); fs.writeFileSync(usageFile, JSON.stringify(this.usageMetrics, null, 2)); } startCleanupTimer() { // Clean up old metrics every hour setInterval(() => { this.cleanupOldMetrics(); }, 60 * 60 * 1000); } cleanupOldMetrics() { const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - 30); // Keep 30 days of data let cleaned = 0; // Clean up performance metrics const originalPerformanceCount = this.performanceMetrics.length; this.performanceMetrics = this.performanceMetrics.filter(metric => new Date(metric.timestamp) > cutoffDate); cleaned += originalPerformanceCount - this.performanceMetrics.length; // Clean up usage metrics const originalUsageCount = this.usageMetrics.length; this.usageMetrics = this.usageMetrics.filter(metric => new Date(metric.timestamp) > cutoffDate); cleaned += originalUsageCount - this.usageMetrics.length; // Clean up quality metrics for (const [chatmode, metrics] of this.qualityMetrics) { const originalCount = metrics.length; const filtered = metrics.filter(metric => new Date(metric.timestamp) > cutoffDate); this.qualityMetrics.set(chatmode, filtered); cleaned += originalCount - filtered.length; } if (cleaned > 0) { logger.info('Cleaned up old analytics metrics', { count: cleaned }); this.persistAnalytics(); } } // Quality Metrics Operations recordQualityMetric(subagent, metric) { if (!this.qualityMetrics.has(subagent)) { this.qualityMetrics.set(subagent, []); } const metrics = this.qualityMetrics.get(subagent); metrics.push(metric); // Prevent unbounded growth if (metrics.length > this.maxMetricsPerType) { metrics.splice(0, metrics.length - this.maxMetricsPerType); } this.qualityMetrics.set(subagent, metrics); this.persistQualityMetrics(); logger.debug('Recorded quality metric', { subagent, score: metric.qualityScore, passed: metric.passed, timestamp: metric.timestamp }); } getQualityMetrics(subagent, limit) { if (subagent) { const metrics = this.qualityMetrics.get(subagent) || []; return limit ? metrics.slice(-limit) : metrics; } // Return all metrics across all subagents const allMetrics = []; for (const metrics of this.qualityMetrics.values()) { allMetrics.push(...metrics); } // Sort by timestamp and apply limit const sorted = allMetrics.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()); return limit ? sorted.slice(-limit) : sorted; } // Performance Metrics Operations recordPerformanceMetric(metric) { this.performanceMetrics.push(metric); // Prevent unbounded growth if (this.performanceMetrics.length > this.maxMetricsPerType) { this.performanceMetrics.splice(0, this.performanceMetrics.length - this.maxMetricsPerType); } this.persistPerformanceMetrics(); logger.debug('Recorded performance metric', { operation: metric.operation, duration: metric.duration, success: metric.success, timestamp: metric.timestamp }); } getPerformanceMetrics(operation, limit) { let metrics = this.performanceMetrics; if (operation) { metrics = metrics.filter(m => m.operation === operation); } return limit ? metrics.slice(-limit) : metrics; } // Usage Metrics Operations recordUsageMetric(metric) { this.usageMetrics.push(metric); // Prevent unbounded growth if (this.usageMetrics.length > this.maxMetricsPerType) { this.usageMetrics.splice(0, this.usageMetrics.length - this.maxMetricsPerType); } this.persistUsageMetrics(); logger.debug('Recorded usage metric', { subagent: metric.subagent, sessionId: metric.sessionId, timestamp: metric.timestamp }); } getUsageMetrics(subagent, limit) { let metrics = this.usageMetrics; if (subagent) { metrics = metrics.filter(m => m.subagent === subagent); } return limit ? metrics.slice(-limit) : metrics; } // Analytics Data Aggregation getAnalyticsData() { const now = new Date().toISOString(); return { qualityMetrics: Object.fromEntries(this.qualityMetrics), performanceMetrics: this.performanceMetrics, usageMetrics: this.usageMetrics, lastUpdated: now, totalRecords: this.getTotalRecordCount() }; } getTotalRecordCount() { let total = this.performanceMetrics.length + this.usageMetrics.length; for (const metrics of this.qualityMetrics.values()) { total += metrics.length; } return total; } // Health and Status getStorageHealth() { const totalRecords = this.getTotalRecordCount(); const memoryUsage = process.memoryUsage(); return { status: totalRecords < this.maxMetricsPerType * 3 ? 'healthy' : 'warning', totalRecords, memoryUsage: memoryUsage.heapUsed, lastCleanup: new Date().toISOString(), storageLocation: path.join(this.storePath, 'analytics') }; } // Cleanup and Maintenance clearAnalytics() { this.qualityMetrics.clear(); this.performanceMetrics = []; this.usageMetrics = []; this.persistAnalytics(); logger.info('Cleared all analytics data'); } exportAnalytics() { logger.info('Exporting analytics data', { totalRecords: this.getTotalRecordCount() }); return this.getAnalyticsData(); } }