201 lines
6.5 KiB
JavaScript
201 lines
6.5 KiB
JavaScript
/**
|
|
* Global Error Handler
|
|
* ====================
|
|
* Captures uncaught exceptions and promise rejections from Doctor code.
|
|
* Uses addEventListener (NOT assignment) to avoid breaking other extensions.
|
|
*
|
|
* R5: Frontend Error Boundaries - Phase 1: Console logging only (no telemetry)
|
|
* @module global_error_handler
|
|
*/
|
|
|
|
import { sanitizeErrorData } from './privacy_utils.js';
|
|
// NO telemetry imports in Phase 1
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// CONSTANTS
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
const DEDUP_WINDOW_MS = 5000;
|
|
const MAX_CACHE_ENTRIES = 200;
|
|
|
|
// Deduplication cache: hash -> { count, lastSeen, errorId }
|
|
const errorCache = new Map();
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// HELPER FUNCTIONS
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Generate a hash for deduplication.
|
|
*/
|
|
function hashError(message, source, lineno, colno) {
|
|
return `${message}:${source}:${lineno}:${colno}`;
|
|
}
|
|
|
|
/**
|
|
* Check if error is a duplicate within the dedup window.
|
|
*/
|
|
function isDuplicate(hash) {
|
|
const now = Date.now();
|
|
const cached = errorCache.get(hash);
|
|
|
|
if (cached && (now - cached.lastSeen < DEDUP_WINDOW_MS)) {
|
|
cached.count++;
|
|
cached.lastSeen = now;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Add error to cache with LRU-style eviction.
|
|
*/
|
|
function addToCache(hash, errorId) {
|
|
// Evict oldest entries if cache is full
|
|
if (errorCache.size >= MAX_CACHE_ENTRIES) {
|
|
let oldestHash = null;
|
|
let oldestTime = Infinity;
|
|
for (const [h, data] of errorCache.entries()) {
|
|
if (data.lastSeen < oldestTime) {
|
|
oldestTime = data.lastSeen;
|
|
oldestHash = h;
|
|
}
|
|
}
|
|
if (oldestHash) {
|
|
errorCache.delete(oldestHash);
|
|
}
|
|
}
|
|
|
|
errorCache.set(hash, { count: 1, lastSeen: Date.now(), errorId });
|
|
}
|
|
|
|
/**
|
|
* Generate a unique error ID.
|
|
*/
|
|
function generateErrorId() {
|
|
const timestamp = Date.now();
|
|
const random = Math.random().toString(36).substring(2, 11);
|
|
return `err-${timestamp}-${random}`;
|
|
}
|
|
|
|
/**
|
|
* Check if error originates from Doctor code.
|
|
* Filters out ComfyUI core and other extension errors to reduce noise.
|
|
*/
|
|
function isDoctorError(filename, stack) {
|
|
if (!filename && !stack) return false;
|
|
|
|
// Check filename for Doctor resources
|
|
const doctorPatterns = [
|
|
'/doctor', // /extensions/ComfyUI-Doctor/...
|
|
'doctor_', // doctor_*.js files
|
|
'ErrorBoundary', // ErrorBoundary.js
|
|
'chat-island', // island files
|
|
'statistics-island',
|
|
'privacy_utils',
|
|
'global_error_handler'
|
|
];
|
|
|
|
const fullTrace = `${filename || ''} ${stack || ''}`;
|
|
|
|
return doctorPatterns.some(pattern =>
|
|
fullTrace.toLowerCase().includes(pattern.toLowerCase())
|
|
);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// PUBLIC API
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Install global error handlers.
|
|
* Uses addEventListener to avoid breaking other extensions.
|
|
* Idempotent - safe to call multiple times.
|
|
*/
|
|
export function installGlobalErrorHandlers() {
|
|
// Idempotent guard: prevent duplicate installation
|
|
if (window.__doctorGlobalErrorHandlersInstalled) {
|
|
console.warn('[ComfyUI-Doctor] Global error handlers already installed, skipping');
|
|
return;
|
|
}
|
|
|
|
// 1. Uncaught exceptions
|
|
window.addEventListener('error', (event) => {
|
|
const { message, filename, lineno, colno, error } = event;
|
|
|
|
// Filter: Only log Doctor errors (avoid ComfyUI/other extension noise)
|
|
if (!isDoctorError(filename, error?.stack)) {
|
|
return; // Not a Doctor error, ignore
|
|
}
|
|
|
|
const hash = hashError(message, filename, lineno, colno);
|
|
|
|
if (isDuplicate(hash)) {
|
|
return; // Duplicate, ignore
|
|
}
|
|
|
|
const errorId = generateErrorId();
|
|
addToCache(hash, errorId);
|
|
|
|
// Sanitize before logging
|
|
const sanitized = sanitizeErrorData({
|
|
message,
|
|
source: filename,
|
|
lineno,
|
|
colno,
|
|
stack: error?.stack
|
|
});
|
|
|
|
console.error(`[ComfyUI-Doctor] Uncaught exception (${errorId}):`, sanitized);
|
|
|
|
// Phase 1: NO telemetry (console only)
|
|
// Phase 2: Add reportToTelemetry(sanitized) if enabled
|
|
});
|
|
|
|
// 2. Unhandled promise rejections
|
|
window.addEventListener('unhandledrejection', (event) => {
|
|
const message = event.reason?.message || String(event.reason);
|
|
const stack = event.reason?.stack || '';
|
|
|
|
// Filter: Only log Doctor errors
|
|
if (!isDoctorError('', stack)) {
|
|
return;
|
|
}
|
|
|
|
const hash = hashError(message, 'promise', 0, 0);
|
|
|
|
if (isDuplicate(hash)) {
|
|
return;
|
|
}
|
|
|
|
const errorId = generateErrorId();
|
|
addToCache(hash, errorId);
|
|
|
|
const sanitized = sanitizeErrorData({ message, stack });
|
|
|
|
console.error(`[ComfyUI-Doctor] Unhandled rejection (${errorId}):`, sanitized);
|
|
|
|
// Phase 1: NO telemetry
|
|
});
|
|
|
|
// Mark as installed
|
|
window.__doctorGlobalErrorHandlersInstalled = true;
|
|
|
|
console.log('[ComfyUI-Doctor] Global error handlers installed');
|
|
}
|
|
|
|
/**
|
|
* Clear the error cache (for testing).
|
|
*/
|
|
export function _clearErrorCache() {
|
|
errorCache.clear();
|
|
}
|
|
|
|
/**
|
|
* Reset handlers (for testing).
|
|
*/
|
|
export function _resetHandlers() {
|
|
window.__doctorGlobalErrorHandlersInstalled = false;
|
|
}
|