Skip to main content
Glama
error-tracker.ts7.52 kB
/** * Error Tracker * * Tracks and aggregates errors for monitoring and alerting. * * Requirements: 11.1, 11.2, 11.3, 11.4, 11.5 */ import { randomUUID } from "crypto"; import type { ErrorTrackingEntry } from "./types.js"; /** * Error fingerprint for deduplication */ interface ErrorFingerprint { name: string; message: string; component: string; code?: string; } /** * Error Tracker class * * Tracks errors with deduplication, aggregation, and alerting support. */ export class ErrorTracker { private errors: Map<string, ErrorTrackingEntry> = new Map(); private recentErrors: ErrorTrackingEntry[] = []; private maxErrors: number; private maxRecentErrors: number; private alertCallbacks: ((entry: ErrorTrackingEntry) => void)[] = []; constructor(options?: { maxErrors?: number; maxRecentErrors?: number }) { this.maxErrors = options?.maxErrors ?? 100; this.maxRecentErrors = options?.maxRecentErrors ?? 1000; } /** * Generate a fingerprint for error deduplication */ private generateFingerprint(fp: ErrorFingerprint): string { return `${fp.name}:${fp.message}:${fp.component}:${fp.code ?? ""}`; } /** * Track an error */ trackError( error: Error, options: { component: string; operation?: string; traceId?: string; context?: Record<string, unknown>; } ): ErrorTrackingEntry { const fingerprint = this.generateFingerprint({ name: error.name, message: error.message, component: options.component, code: (error as Error & { code?: string }).code, }); const now = new Date(); const existing = this.errors.get(fingerprint); if (existing) { // Update existing error entry existing.count += 1; existing.lastSeen = now; if (options.traceId) { existing.traceId = options.traceId; } if (options.context) { existing.context = { ...existing.context, ...options.context }; } this.addToRecent(existing); this.notifyAlertCallbacks(existing); return existing; } // Create new error entry const entry: ErrorTrackingEntry = { id: randomUUID(), name: error.name, message: error.message, stack: error.stack, code: (error as Error & { code?: string }).code, component: options.component, operation: options.operation, timestamp: now, traceId: options.traceId, context: options.context, count: 1, firstSeen: now, lastSeen: now, }; this.errors.set(fingerprint, entry); this.addToRecent(entry); this.trimErrors(); this.notifyAlertCallbacks(entry); return entry; } /** * Add error to recent list */ private addToRecent(entry: ErrorTrackingEntry): void { this.recentErrors.push({ ...entry, timestamp: new Date() }); if (this.recentErrors.length > this.maxRecentErrors) { this.recentErrors = this.recentErrors.slice(-this.maxRecentErrors); } } /** * Trim errors to max limit */ private trimErrors(): void { if (this.errors.size <= this.maxErrors) { return; } // Remove oldest errors by firstSeen const entries = Array.from(this.errors.entries()).sort( ([, a], [, b]) => a.firstSeen.getTime() - b.firstSeen.getTime() ); const toRemove = entries.slice(0, entries.length - this.maxErrors); for (const [key] of toRemove) { this.errors.delete(key); } } /** * Notify alert callbacks */ private notifyAlertCallbacks(entry: ErrorTrackingEntry): void { for (const callback of this.alertCallbacks) { try { callback(entry); } catch { // Ignore callback errors } } } /** * Register an alert callback */ onError(callback: (entry: ErrorTrackingEntry) => void): void { this.alertCallbacks.push(callback); } /** * Remove an alert callback */ offError(callback: (entry: ErrorTrackingEntry) => void): void { const index = this.alertCallbacks.indexOf(callback); if (index !== -1) { this.alertCallbacks.splice(index, 1); } } /** * Get error by ID */ getError(id: string): ErrorTrackingEntry | undefined { for (const entry of this.errors.values()) { if (entry.id === id) { return entry; } } return undefined; } /** * Get all tracked errors */ getAllErrors(): ErrorTrackingEntry[] { return Array.from(this.errors.values()); } /** * Get recent errors */ getRecentErrors(options?: { limit?: number; since?: Date; component?: string; }): ErrorTrackingEntry[] { let filtered = this.recentErrors; if (options?.since) { const since = options.since; filtered = filtered.filter((e) => e.timestamp >= since); } if (options?.component) { filtered = filtered.filter((e) => e.component === options.component); } if (options?.limit) { filtered = filtered.slice(-options.limit); } return filtered; } /** * Get error count by component */ getErrorCountByComponent(): Record<string, number> { const counts: Record<string, number> = {}; for (const entry of this.errors.values()) { counts[entry.component] = (counts[entry.component] ?? 0) + entry.count; } return counts; } /** * Get error count by name */ getErrorCountByName(): Record<string, number> { const counts: Record<string, number> = {}; for (const entry of this.errors.values()) { counts[entry.name] = (counts[entry.name] ?? 0) + entry.count; } return counts; } /** * Get total error count */ getTotalErrorCount(): number { let total = 0; for (const entry of this.errors.values()) { total += entry.count; } return total; } /** * Get error rate (errors per minute) over a time window */ getErrorRate(windowMinutes: number = 5): number { const windowStart = new Date(Date.now() - windowMinutes * 60 * 1000); const recentCount = this.recentErrors.filter((e) => e.timestamp >= windowStart).length; return recentCount / windowMinutes; } /** * Get top errors by count */ getTopErrors(limit: number = 10): ErrorTrackingEntry[] { return Array.from(this.errors.values()) .sort((a, b) => b.count - a.count) .slice(0, limit); } /** * Clear all tracked errors */ clear(): void { this.errors.clear(); this.recentErrors = []; } /** * Clear errors older than a given date */ clearOlderThan(date: Date): number { let cleared = 0; for (const [key, entry] of this.errors) { if (entry.lastSeen < date) { this.errors.delete(key); cleared++; } } this.recentErrors = this.recentErrors.filter((e) => e.timestamp >= date); return cleared; } /** * Export error summary */ exportSummary(): { totalUniqueErrors: number; totalErrorCount: number; errorsByComponent: Record<string, number>; errorsByName: Record<string, number>; topErrors: ErrorTrackingEntry[]; errorRate: number; } { return { totalUniqueErrors: this.errors.size, totalErrorCount: this.getTotalErrorCount(), errorsByComponent: this.getErrorCountByComponent(), errorsByName: this.getErrorCountByName(), topErrors: this.getTopErrors(5), errorRate: this.getErrorRate(), }; } } /** * Global error tracker instance */ export const errorTracker = new ErrorTracker();

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/keyurgolani/ThoughtMcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server