Skip to content

Commit d75dac4

Browse files
JOHNJOHN
authored andcommitted
Add automatic browser error capture and display
- Add error-capture utility that captures all browser errors - Display errors in DebugPanel with Errors tab - Errors automatically saved to chrome.storage for inspection - No more need for screenshots - errors visible in extension popup - Fix AnnotationManager init order (event listeners before async load) - Run npm run test:all to validate before manual testing
1 parent 1e3df9a commit d75dac4

File tree

5 files changed

+353
-20
lines changed

5 files changed

+353
-20
lines changed

src/content/content.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@ import { AnnotationManager } from './AnnotationManager';
33
import { DrawingManager } from './DrawingManager';
44
import { PubkyURLHandler } from './PubkyURLHandler';
55

6+
// Initialize error capture for content script
7+
// Use dynamic import to avoid bundling issues in content script
8+
import('../utils/error-capture').then(() => {
9+
logger.info('ContentScript', 'Error capture initialized');
10+
}).catch(() => {
11+
// Error capture not available, continue anyway
12+
});
13+
614
/**
715
* @fileoverview Content script bootstrapper that wires together the interactive
816
* experiences for annotations, drawings, and Pubky links.

src/popup/components/DebugPanel.tsx

Lines changed: 135 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useState, useEffect } from 'react';
22
import { logger } from '../../utils/logger';
3+
import { errorCapture } from '../../utils/error-capture';
34

45
interface LogEntry {
56
timestamp: string;
@@ -10,15 +11,31 @@ interface LogEntry {
1011
error?: any;
1112
}
1213

14+
interface CapturedError {
15+
timestamp: number;
16+
message: string;
17+
source: string;
18+
lineno?: number;
19+
colno?: number;
20+
stack?: string;
21+
url?: string;
22+
}
23+
1324
function DebugPanel() {
1425
const [logs, setLogs] = useState<LogEntry[]>([]);
26+
const [errors, setErrors] = useState<CapturedError[]>([]);
1527
const [filter, setFilter] = useState<string>('all');
28+
const [activeTab, setActiveTab] = useState<'logs' | 'errors'>('errors');
1629

1730
useEffect(() => {
1831
loadLogs();
32+
loadErrors();
1933

20-
// Refresh logs every 2 seconds
21-
const interval = setInterval(loadLogs, 2000);
34+
// Refresh every 2 seconds
35+
const interval = setInterval(() => {
36+
loadLogs();
37+
loadErrors();
38+
}, 2000);
2239
return () => clearInterval(interval);
2340
}, []);
2441

@@ -27,6 +44,15 @@ function DebugPanel() {
2744
setLogs(allLogs);
2845
};
2946

47+
const loadErrors = async () => {
48+
try {
49+
const capturedErrors = await errorCapture.getErrors();
50+
setErrors(capturedErrors);
51+
} catch (e) {
52+
// Error capture not available
53+
}
54+
};
55+
3056
const handleClearLogs = async () => {
3157
if (confirm('Clear all debug logs?')) {
3258
await logger.clearLogs();
@@ -60,10 +86,49 @@ function DebugPanel() {
6086
}
6187
};
6288

89+
const handleClearErrors = async () => {
90+
if (confirm('Clear all captured errors?')) {
91+
await errorCapture.clearErrors();
92+
setErrors([]);
93+
}
94+
};
95+
96+
const handleExportErrors = async () => {
97+
const exported = await errorCapture.exportErrors();
98+
const blob = new Blob([exported], { type: 'text/plain' });
99+
const url = URL.createObjectURL(blob);
100+
const a = document.createElement('a');
101+
a.href = url;
102+
a.download = `graphiti-errors-${new Date().toISOString()}.txt`;
103+
a.click();
104+
URL.revokeObjectURL(url);
105+
};
106+
63107
return (
64108
<div className="bg-gray-900 text-gray-100 p-4 border-b border-gray-700">
65109
<div className="flex items-center justify-between mb-3">
66-
<h3 className="text-sm font-bold">Debug Logs</h3>
110+
<div className="flex gap-2">
111+
<button
112+
onClick={() => setActiveTab('errors')}
113+
className={`text-xs px-3 py-1 rounded ${
114+
activeTab === 'errors'
115+
? 'bg-red-600 text-white'
116+
: 'bg-gray-800 text-gray-400 hover:bg-gray-700'
117+
}`}
118+
>
119+
Errors {errors.length > 0 && `(${errors.length})`}
120+
</button>
121+
<button
122+
onClick={() => setActiveTab('logs')}
123+
className={`text-xs px-3 py-1 rounded ${
124+
activeTab === 'logs'
125+
? 'bg-blue-600 text-white'
126+
: 'bg-gray-800 text-gray-400 hover:bg-gray-700'
127+
}`}
128+
>
129+
Logs
130+
</button>
131+
</div>
67132
<div className="flex gap-2">
68133
<select
69134
value={filter}
@@ -76,25 +141,77 @@ function DebugPanel() {
76141
<option value="WARN">Warn</option>
77142
<option value="ERROR">Error</option>
78143
</select>
79-
<button
80-
onClick={handleExportLogs}
81-
className="text-xs px-2 py-1 bg-blue-600 hover:bg-blue-700 rounded"
82-
title="Export logs"
83-
>
84-
💾 Export
85-
</button>
86-
<button
87-
onClick={handleClearLogs}
88-
className="text-xs px-2 py-1 bg-red-600 hover:bg-red-700 rounded"
89-
title="Clear logs"
90-
>
91-
🗑️ Clear
92-
</button>
144+
{activeTab === 'errors' ? (
145+
<>
146+
<button
147+
onClick={handleExportErrors}
148+
className="text-xs px-2 py-1 bg-blue-600 hover:bg-blue-700 rounded"
149+
title="Export errors"
150+
>
151+
💾 Export
152+
</button>
153+
<button
154+
onClick={handleClearErrors}
155+
className="text-xs px-2 py-1 bg-red-600 hover:bg-red-700 rounded"
156+
title="Clear errors"
157+
>
158+
🗑️ Clear
159+
</button>
160+
</>
161+
) : (
162+
<>
163+
<button
164+
onClick={handleExportLogs}
165+
className="text-xs px-2 py-1 bg-blue-600 hover:bg-blue-700 rounded"
166+
title="Export logs"
167+
>
168+
💾 Export
169+
</button>
170+
<button
171+
onClick={handleClearLogs}
172+
className="text-xs px-2 py-1 bg-red-600 hover:bg-red-700 rounded"
173+
title="Clear logs"
174+
>
175+
🗑️ Clear
176+
</button>
177+
</>
178+
)}
93179
</div>
94180
</div>
95181

96182
<div className="bg-gray-800 rounded p-2 max-h-64 overflow-y-auto text-xs font-mono">
97-
{filteredLogs.length === 0 ? (
183+
{activeTab === 'errors' ? (
184+
errors.length === 0 ? (
185+
<p className="text-gray-500">No errors captured</p>
186+
) : (
187+
<div className="space-y-2">
188+
{errors.slice(-20).map((error, idx) => (
189+
<div key={idx} className="border-b border-gray-700 pb-2 mb-2">
190+
<div className="flex items-start gap-2 mb-1">
191+
<span className="text-red-400 font-bold">[ERROR]</span>
192+
<span className="text-gray-500 text-[10px]">
193+
{new Date(error.timestamp).toLocaleTimeString()}
194+
</span>
195+
<span className="text-yellow-400 text-[10px]">
196+
{error.source}{error.lineno ? `:${error.lineno}:${error.colno}` : ''}
197+
</span>
198+
</div>
199+
<div className="text-red-300 mb-1">{error.message}</div>
200+
{error.stack && (
201+
<div className="text-gray-400 text-[10px] ml-2 whitespace-pre-wrap">
202+
{error.stack}
203+
</div>
204+
)}
205+
{error.url && (
206+
<div className="text-gray-500 text-[10px] ml-2">
207+
URL: {error.url}
208+
</div>
209+
)}
210+
</div>
211+
))}
212+
</div>
213+
)
214+
) : filteredLogs.length === 0 ? (
98215
<p className="text-gray-500">No logs to display</p>
99216
) : (
100217
<div className="space-y-1">

src/utils/error-capture.ts

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/**
2+
* Error capture utility - captures browser errors and stores them for debugging
3+
* This allows us to see errors without requiring screenshots
4+
*/
5+
6+
interface CapturedError {
7+
timestamp: number;
8+
message: string;
9+
source: string;
10+
lineno?: number;
11+
colno?: number;
12+
stack?: string;
13+
url?: string;
14+
}
15+
16+
interface ErrorInput {
17+
message: string;
18+
source: string;
19+
lineno?: number;
20+
colno?: number;
21+
stack?: string;
22+
url?: string;
23+
}
24+
25+
class ErrorCapture {
26+
private static instance: ErrorCapture;
27+
private errors: CapturedError[] = [];
28+
private maxErrors = 100;
29+
30+
private constructor() {
31+
this.setupErrorCapture();
32+
}
33+
34+
static getInstance(): ErrorCapture {
35+
if (!ErrorCapture.instance) {
36+
ErrorCapture.instance = new ErrorCapture();
37+
}
38+
return ErrorCapture.instance;
39+
}
40+
41+
private setupErrorCapture() {
42+
// Capture unhandled errors
43+
if (typeof window !== 'undefined') {
44+
window.addEventListener('error', (event) => {
45+
this.captureError({
46+
message: event.message,
47+
source: event.filename || 'unknown',
48+
lineno: event.lineno,
49+
colno: event.colno,
50+
stack: event.error?.stack,
51+
url: event.filename,
52+
});
53+
});
54+
55+
// Capture unhandled promise rejections
56+
window.addEventListener('unhandledrejection', (event) => {
57+
this.captureError({
58+
message: `Unhandled Promise Rejection: ${event.reason}`,
59+
source: 'promise',
60+
stack: event.reason?.stack,
61+
});
62+
});
63+
}
64+
65+
// Capture console errors if possible
66+
if (typeof console !== 'undefined' && console.error) {
67+
const originalError = console.error;
68+
console.error = (...args: any[]) => {
69+
const message = args.map(arg =>
70+
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
71+
).join(' ');
72+
this.captureError({
73+
message: `Console Error: ${message}`,
74+
source: 'console',
75+
});
76+
originalError.apply(console, args);
77+
};
78+
}
79+
}
80+
81+
private captureError(error: ErrorInput) {
82+
const captured: CapturedError = {
83+
...error,
84+
timestamp: Date.now(),
85+
};
86+
87+
this.errors.push(captured);
88+
89+
// Keep only recent errors
90+
if (this.errors.length > this.maxErrors) {
91+
this.errors = this.errors.slice(-this.maxErrors);
92+
}
93+
94+
// Try to save to storage (non-blocking)
95+
this.saveErrors().catch(() => {
96+
// Ignore storage errors
97+
});
98+
99+
// Log to console for immediate visibility
100+
console.error('[ErrorCapture]', captured.message, captured);
101+
}
102+
103+
private async saveErrors() {
104+
if (typeof chrome !== 'undefined' && chrome.storage?.local) {
105+
try {
106+
await chrome.storage.local.set({
107+
capturedErrors: this.errors,
108+
lastErrorCapture: Date.now()
109+
});
110+
} catch (e) {
111+
// Ignore storage errors
112+
}
113+
}
114+
}
115+
116+
async getErrors(): Promise<CapturedError[]> {
117+
// Try to load from storage
118+
if (typeof chrome !== 'undefined' && chrome.storage?.local) {
119+
try {
120+
const result = await chrome.storage.local.get('capturedErrors');
121+
if (result.capturedErrors) {
122+
this.errors = result.capturedErrors;
123+
}
124+
} catch (e) {
125+
// Ignore
126+
}
127+
}
128+
return [...this.errors];
129+
}
130+
131+
async clearErrors() {
132+
this.errors = [];
133+
if (typeof chrome !== 'undefined' && chrome.storage?.local) {
134+
try {
135+
await chrome.storage.local.remove('capturedErrors');
136+
} catch (e) {
137+
// Ignore
138+
}
139+
}
140+
}
141+
142+
// Export errors as text for debugging
143+
async exportErrors(): Promise<string> {
144+
const errors = await this.getErrors();
145+
return errors.map((err, i) => {
146+
return `Error ${i + 1} (${new Date(err.timestamp).toISOString()}):
147+
Message: ${err.message}
148+
Source: ${err.source}${err.lineno ? `:${err.lineno}:${err.colno}` : ''}
149+
${err.stack ? `Stack:\n${err.stack}` : ''}
150+
${err.url ? `URL: ${err.url}` : ''}
151+
---`;
152+
}).join('\n\n');
153+
}
154+
}
155+
156+
export const errorCapture = ErrorCapture.getInstance();

0 commit comments

Comments
 (0)