UNPKG

summarizely-cli

Version:

YouTube summarizer that respects your existing subscriptions. No API keys required.

319 lines 11.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.LocalMetrics = void 0; exports.getMetrics = getMetrics; const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const os_1 = __importDefault(require("os")); class LocalMetrics { constructor() { this.maxFileSize = 50 * 1024 * 1024; // 50MB this.rotateCount = 5; const metricsDir = path_1.default.join(os_1.default.homedir(), '.summarizely', 'metrics'); if (!fs_1.default.existsSync(metricsDir)) { fs_1.default.mkdirSync(metricsDir, { recursive: true }); } this.metricsFile = path_1.default.join(metricsDir, 'events.jsonl'); this.aggregatedFile = path_1.default.join(metricsDir, 'aggregated.json'); // Check if rotation needed on startup this.checkRotation(); } record(event) { const fullEvent = { ...event, timestamp: Date.now() }; try { const line = JSON.stringify(fullEvent) + '\n'; fs_1.default.appendFileSync(this.metricsFile, line, 'utf8'); // Check if rotation needed this.checkRotation(); } catch (error) { // Silently fail - metrics should never crash the app console.error('Failed to record metric:', error); } } recordCaptionFetch(url, duration, success) { this.record({ type: 'caption_fetch', url, duration, success }); } recordSummaryGenerated(url, provider, model, duration, success) { this.record({ type: 'summary_generated', url, provider, model, duration, success }); } recordCacheHit(url) { this.record({ type: 'cache_hit', url }); } recordCacheMiss(url) { this.record({ type: 'cache_miss', url }); } recordError(error, url) { this.record({ type: 'error', error, url, success: false }); } recordBatchComplete(totalVideos, successful, duration) { this.record({ type: 'batch_complete', duration, metadata: { totalVideos, successful, failed: totalVideos - successful } }); } async aggregate() { const events = await this.readEvents(); const metrics = { totalEvents: events.length, captionsFetched: 0, summariesGenerated: 0, cacheHits: 0, cacheMisses: 0, errors: 0, batchesCompleted: 0, averageDuration: 0, providerUsage: {}, modelUsage: {}, successRate: 0, errorTypes: {}, dailyStats: {} }; let totalDuration = 0; let durationCount = 0; let successCount = 0; for (const event of events) { // Count event types switch (event.type) { case 'caption_fetch': metrics.captionsFetched++; break; case 'summary_generated': metrics.summariesGenerated++; if (event.provider) { metrics.providerUsage[event.provider] = (metrics.providerUsage[event.provider] || 0) + 1; } if (event.model) { metrics.modelUsage[event.model] = (metrics.modelUsage[event.model] || 0) + 1; } break; case 'cache_hit': metrics.cacheHits++; break; case 'cache_miss': metrics.cacheMisses++; break; case 'error': metrics.errors++; if (event.error) { metrics.errorTypes[event.error] = (metrics.errorTypes[event.error] || 0) + 1; } break; case 'batch_complete': metrics.batchesCompleted++; break; } // Track durations if (event.duration) { totalDuration += event.duration; durationCount++; } // Track success if (event.success !== undefined) { if (event.success) successCount++; } // Daily stats const date = new Date(event.timestamp).toISOString().split('T')[0]; if (!metrics.dailyStats[date]) { metrics.dailyStats[date] = { date, events: 0, summaries: 0, errors: 0, averageDuration: 0 }; } metrics.dailyStats[date].events++; if (event.type === 'summary_generated') { metrics.dailyStats[date].summaries++; } if (event.type === 'error') { metrics.dailyStats[date].errors++; } } // Calculate averages metrics.averageDuration = durationCount > 0 ? totalDuration / durationCount : 0; const totalWithSuccess = metrics.summariesGenerated + metrics.captionsFetched; metrics.successRate = totalWithSuccess > 0 ? (successCount / totalWithSuccess) * 100 : 0; // Calculate daily averages for (const date in metrics.dailyStats) { const dayEvents = events.filter(e => new Date(e.timestamp).toISOString().startsWith(date)); const dayDurations = dayEvents .map(e => e.duration) .filter(d => d !== undefined); if (dayDurations.length > 0) { metrics.dailyStats[date].averageDuration = dayDurations.reduce((a, b) => a + b, 0) / dayDurations.length; } } // Save aggregated metrics try { fs_1.default.writeFileSync(this.aggregatedFile, JSON.stringify(metrics, null, 2), 'utf8'); } catch { // Silently fail } return metrics; } async report() { const metrics = await this.aggregate(); const lines = [ '\n========== Metrics Report ==========', `Total Events: ${metrics.totalEvents}`, '', '📊 Operations:', ` Captions Fetched: ${metrics.captionsFetched}`, ` Summaries Generated: ${metrics.summariesGenerated}`, ` Cache Hits: ${metrics.cacheHits}`, ` Cache Misses: ${metrics.cacheMisses}`, ` Errors: ${metrics.errors}`, ` Batches Completed: ${metrics.batchesCompleted}`, '', '⚡ Performance:', ` Average Duration: ${Math.round(metrics.averageDuration)}ms`, ` Success Rate: ${metrics.successRate.toFixed(1)}%`, '', '🔧 Provider Usage:' ]; for (const [provider, count] of Object.entries(metrics.providerUsage)) { lines.push(` ${provider}: ${count}`); } if (Object.keys(metrics.modelUsage).length > 0) { lines.push('', '🤖 Model Usage:'); for (const [model, count] of Object.entries(metrics.modelUsage)) { lines.push(` ${model}: ${count}`); } } if (metrics.errors > 0) { lines.push('', '❌ Error Types:'); const topErrors = Object.entries(metrics.errorTypes) .sort((a, b) => b[1] - a[1]) .slice(0, 5); for (const [error, count] of topErrors) { const truncated = error.length > 40 ? error.substring(0, 37) + '...' : error; lines.push(` ${truncated}: ${count}`); } } // Recent daily stats (last 7 days) const recentDays = Object.keys(metrics.dailyStats) .sort() .slice(-7); if (recentDays.length > 0) { lines.push('', '📅 Recent Daily Stats:'); for (const date of recentDays) { const stats = metrics.dailyStats[date]; lines.push(` ${date}: ${stats.summaries} summaries, ${stats.errors} errors`); } } lines.push('====================================\n'); return lines.join('\n'); } async readEvents() { if (!fs_1.default.existsSync(this.metricsFile)) { return []; } try { const content = fs_1.default.readFileSync(this.metricsFile, 'utf8'); const lines = content.split('\n').filter(line => line.trim()); return lines.map(line => JSON.parse(line)); } catch { return []; } } checkRotation() { try { if (!fs_1.default.existsSync(this.metricsFile)) return; const stats = fs_1.default.statSync(this.metricsFile); if (stats.size > this.maxFileSize) { this.rotateFiles(); } } catch { // Silently fail } } rotateFiles() { try { // Rotate existing files for (let i = this.rotateCount - 1; i >= 1; i--) { const oldFile = `${this.metricsFile}.${i}`; const newFile = `${this.metricsFile}.${i + 1}`; if (fs_1.default.existsSync(oldFile)) { if (i === this.rotateCount - 1) { fs_1.default.unlinkSync(oldFile); // Delete oldest } else { fs_1.default.renameSync(oldFile, newFile); } } } // Rotate current file fs_1.default.renameSync(this.metricsFile, `${this.metricsFile}.1`); } catch { // Silently fail } } async cleanup(daysToKeep = 30) { const cutoff = Date.now() - (daysToKeep * 24 * 60 * 60 * 1000); const events = await this.readEvents(); const filtered = events.filter(e => e.timestamp > cutoff); if (filtered.length < events.length) { try { const content = filtered.map(e => JSON.stringify(e)).join('\n') + '\n'; fs_1.default.writeFileSync(this.metricsFile, content, 'utf8'); console.log(`Cleaned up ${events.length - filtered.length} old metric events`); } catch { // Silently fail } } } } exports.LocalMetrics = LocalMetrics; // Singleton instance let metricsInstance = null; function getMetrics() { if (!metricsInstance) { metricsInstance = new LocalMetrics(); } return metricsInstance; } //# sourceMappingURL=metrics.js.map