diff --git a/claude.md b/claude.md index 66a6832..ae02bbe 100644 --- a/claude.md +++ b/claude.md @@ -25,6 +25,14 @@ --- +## Console Wrapping Removal + +- We no longer monkey-patch `console.*` methods; global `error`/`unhandledrejection` handlers remain the only capture points. +- Rationale: console wrapping risks recursion, breaks DevTools expectations, mutates global browser state, and adds per-call overhead. +- Security posture is improved by avoiding interception of developer tooling while still capturing unhandled errors/rejections. + +--- + ## Rule of Thumb: Priority Fixes **When adversarial analysis identifies issues, categorize and handle as:** @@ -3288,4 +3296,3 @@ Hera is a **monitoring tool**, not an **authentication library**. Signature veri **Date:** 2025-10-30 **Verdict:** ✅ STRONG FOUNDATION, 5 CRITICAL GAPS TO ADDRESS **Recommendation:** Prioritize P0 (DPoP integration) + P1 (PKCE severity, rotation UI, debugger scoping) - diff --git a/docs/runbooks/RUNBOOK-error-collection.md b/docs/runbooks/RUNBOOK-error-collection.md new file mode 100644 index 0000000..c22ae0b --- /dev/null +++ b/docs/runbooks/RUNBOOK-error-collection.md @@ -0,0 +1,21 @@ +# Error Collection Runbook + +## Overview +- Error collection now relies solely on global `error` and `unhandledrejection` handlers; console method wrapping was removed for security and stability. +- Purpose: capture actionable runtime failures (message, stack, filename/line) and persist recent entries without mutating browser globals. + +## What Is Captured +- `UNHANDLED_ERROR`: window-level errors with message, stack, filename, line, column, and timestamp. +- `UNHANDLED_REJECTION`: unhandled promise rejections with message, stack (when available), and timestamp. +- Warnings and info logs from inside the collector itself; no interception of `console.*`. + +## Expected Behavior +- Errors are de-duped within a 5s window to prevent bursts. +- Persistence is debounced to minimize storage writes; only the most recent entries are kept. +- Console calls (`console.error|warn|log`) are **not** intercepted—this is intentional to avoid recursion and DevTools breakage. + +## Operator Checks +1) Trigger an uncaught error in the extension context; confirm it appears as `UNHANDLED_ERROR`. +2) Trigger an unhandled promise rejection; confirm it appears as `UNHANDLED_REJECTION`. +3) Call `console.error` and verify it is **not** captured (by design). +4) Export errors (JSON or text) from the UI and ensure timestamps and stacks are present. diff --git a/modules/error-collector.js b/modules/error-collector.js index 3037c94..0bbeae9 100644 --- a/modules/error-collector.js +++ b/modules/error-collector.js @@ -16,14 +16,22 @@ class ErrorCollector { // Dedupe cache for noisy repeats this._lastSeen = new Map(); // key -> timestamp - // OFF by default; can be toggled by message or storage - this.captureDebugLogs = false; - // FIX: Debounce timer for persistence (prevent rate limit violations) this._persistTimer = null; this._PERSIST_DELAY = 1000; // 1 second - // Intercept console errors + // Periodic cleanup for dedupe cache + if (chrome?.alarms) { + // Register cleanup alarm (runs every 60 seconds) + chrome.alarms.create('error-collector-cleanup', { periodInMinutes: 1 }); + chrome.alarms.onAlarm.addListener((alarm) => { + if (alarm?.name === 'error-collector-cleanup') { + this._cleanupLastSeen(); + } + }); + } + + // Intercept global errors this.setupErrorHandlers(); } @@ -48,6 +56,18 @@ class ErrorCollector { return true; } + /** + * Remove expired dedupe entries + */ + _cleanupLastSeen() { + const now = Date.now(); + for (const [key, timestamp] of this._lastSeen.entries()) { + if (now - timestamp > DEDUPE_WINDOW_MS) { + this._lastSeen.delete(key); + } + } + } + /** * Safely stringify a value with size limit */ @@ -164,65 +184,6 @@ class ErrorCollector { } }); } - - // Wrap console methods - this.wrapConsole(); - } - - /** - * Wrap console methods to capture errors - */ - wrapConsole() { - const originalError = console.error; - const originalWarn = console.warn; - const originalLog = console.log; - - console.error = (...args) => { - // BUGFIX: Don't log storage quota errors to prevent infinite loop - const message = args.map(a => String(a)).join(' '); - if (!message.includes('Storage rate limit') && !message.includes('QUOTA_BYTES')) { - const entry = { - type: 'CONSOLE_ERROR', - message: message, - args: args, - stack: new Error().stack, - timestamp: new Date().toISOString() - }; - if (this._shouldLog(entry)) { - this.logError(entry); - } - } - originalError.apply(console, args); - }; - - console.warn = (...args) => { - const entry = { - type: 'CONSOLE_WARN', - message: args.map(a => String(a)).join(' '), - args: args, - timestamp: new Date().toISOString() - }; - if (this._shouldLog(entry)) { - this.logWarning(entry); - } - originalWarn.apply(console, args); - }; - - // Optionally capture logs for debugging - if (this.captureDebugLogs) { - console.log = (...args) => { - const entry = { - type: 'CONSOLE_LOG', - message: args.map(a => String(a)).join(' '), - args: args, - timestamp: new Date().toISOString() - }; - if (this._shouldLog(entry)) { - this.logInfo(entry); - } - originalLog.apply(console, args); - }; - } } /**