summarizely-cli
Version:
YouTube summarizer that respects your existing subscriptions. No API keys required.
319 lines • 11.3 kB
JavaScript
;
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