bug-report-js 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +144 -0
- package/background.js +279 -0
- package/content.js +905 -0
- package/icons/icon128.png +0 -0
- package/icons/icon16.png +0 -0
- package/icons/icon48.png +0 -0
- package/manifest.json +43 -0
- package/package.json +27 -0
- package/popup.css +289 -0
- package/popup.html +92 -0
- package/popup.js +126 -0
- package/report-template.js +623 -0
- package/sanitizer.js +262 -0
- package/website/README.md +282 -0
- package/website/bug-report.js +1089 -0
- package/website/docs.html +241 -0
- package/website/index.html +996 -0
package/content.js
ADDED
|
@@ -0,0 +1,905 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* content.js — Content Script for Bug Report Extension (v2.0.0)
|
|
3
|
+
*
|
|
4
|
+
* Captures:
|
|
5
|
+
* - User interactions (ring buffer, max 50 / 5 min)
|
|
6
|
+
* - Console logs (max 100)
|
|
7
|
+
* - JS errors (max 50)
|
|
8
|
+
* - Page metadata
|
|
9
|
+
* - Visual DOM capture via layout2vector Canvas Writer
|
|
10
|
+
*
|
|
11
|
+
* Communicates with background.js via chrome.runtime messaging.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const EXTENSION_VERSION = '2.0.0';
|
|
15
|
+
|
|
16
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
17
|
+
// RING BUFFER — User Interactions
|
|
18
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
19
|
+
|
|
20
|
+
const InteractionBuffer = (() => {
|
|
21
|
+
const MAX_ENTRIES = 50;
|
|
22
|
+
const buffer = [];
|
|
23
|
+
|
|
24
|
+
function add(entry) {
|
|
25
|
+
entry._ts = Date.now();
|
|
26
|
+
buffer.push(entry);
|
|
27
|
+
if (buffer.length > MAX_ENTRIES) {
|
|
28
|
+
buffer.shift();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getAll() {
|
|
33
|
+
return buffer.map(({ _ts, ...rest }) => rest);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getRaw() {
|
|
37
|
+
return [...buffer];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { add, getAll, getRaw };
|
|
41
|
+
})();
|
|
42
|
+
|
|
43
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
44
|
+
// CONSOLE LOG BUFFER
|
|
45
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
46
|
+
|
|
47
|
+
const ConsoleBuffer = (() => {
|
|
48
|
+
const MAX_ENTRIES = 100;
|
|
49
|
+
const buffer = [];
|
|
50
|
+
|
|
51
|
+
function add(entry) {
|
|
52
|
+
buffer.push(entry);
|
|
53
|
+
if (buffer.length > MAX_ENTRIES) {
|
|
54
|
+
buffer.shift();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getAll() {
|
|
59
|
+
return [...buffer];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { add, getAll };
|
|
63
|
+
})();
|
|
64
|
+
|
|
65
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
66
|
+
// JS ERROR BUFFER
|
|
67
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
68
|
+
|
|
69
|
+
const ErrorBuffer = (() => {
|
|
70
|
+
const MAX_ENTRIES = 50;
|
|
71
|
+
const buffer = [];
|
|
72
|
+
|
|
73
|
+
function add(entry) {
|
|
74
|
+
buffer.push(entry);
|
|
75
|
+
if (buffer.length > MAX_ENTRIES) {
|
|
76
|
+
buffer.shift();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getAll() {
|
|
81
|
+
return [...buffer];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { add, getAll };
|
|
85
|
+
})();
|
|
86
|
+
|
|
87
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
88
|
+
// HELPER UTILITIES
|
|
89
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Build a sanitized CSS selector for an element.
|
|
93
|
+
* Uses tag name, safe class names, and structural position.
|
|
94
|
+
* Avoids raw IDs that could contain PII.
|
|
95
|
+
*/
|
|
96
|
+
function buildSelector(el) {
|
|
97
|
+
if (!el || !el.tagName) return '';
|
|
98
|
+
|
|
99
|
+
const parts = [];
|
|
100
|
+
let current = el;
|
|
101
|
+
let depth = 0;
|
|
102
|
+
|
|
103
|
+
while (current && current !== document.body && depth < 4) {
|
|
104
|
+
const tag = current.tagName.toLowerCase();
|
|
105
|
+
let selector = tag;
|
|
106
|
+
|
|
107
|
+
// Add safe class names (skip anything that looks dynamic/PII-ish)
|
|
108
|
+
const safeClasses = Array.from(current.classList || [])
|
|
109
|
+
.filter(cls =>
|
|
110
|
+
cls.length < 40 &&
|
|
111
|
+
!/[0-9a-f]{8,}/i.test(cls) && // skip hash-like classes
|
|
112
|
+
!/@/.test(cls) && // skip email-like
|
|
113
|
+
!/^user|^customer|^email/i.test(cls) // skip PII-prefixed
|
|
114
|
+
)
|
|
115
|
+
.slice(0, 3);
|
|
116
|
+
|
|
117
|
+
if (safeClasses.length > 0) {
|
|
118
|
+
selector += '.' + safeClasses.join('.');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Add nth-child for disambiguation
|
|
122
|
+
if (current.parentElement) {
|
|
123
|
+
const siblings = Array.from(current.parentElement.children).filter(
|
|
124
|
+
c => c.tagName === current.tagName
|
|
125
|
+
);
|
|
126
|
+
if (siblings.length > 1) {
|
|
127
|
+
const idx = siblings.indexOf(current) + 1;
|
|
128
|
+
selector += `:nth-of-type(${idx})`;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
parts.unshift(selector);
|
|
133
|
+
current = current.parentElement;
|
|
134
|
+
depth++;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return parts.join(' > ');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get a sanitized visible label from an element.
|
|
142
|
+
* Truncated and only for explicitly allowed element types.
|
|
143
|
+
*/
|
|
144
|
+
function getVisibleLabel(el) {
|
|
145
|
+
if (!el) return undefined;
|
|
146
|
+
|
|
147
|
+
const ALLOWED_TAGS = new Set([
|
|
148
|
+
'BUTTON', 'A', 'LABEL', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6',
|
|
149
|
+
'SUMMARY', 'LEGEND', 'CAPTION', 'TH',
|
|
150
|
+
]);
|
|
151
|
+
|
|
152
|
+
if (!ALLOWED_TAGS.has(el.tagName)) return undefined;
|
|
153
|
+
|
|
154
|
+
const text = (el.textContent || '').trim().substring(0, 50);
|
|
155
|
+
return text || undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Throttle a function to execute at most once per `delay` ms.
|
|
160
|
+
*/
|
|
161
|
+
function throttle(fn, delay) {
|
|
162
|
+
let lastCall = 0;
|
|
163
|
+
let timeoutId = null;
|
|
164
|
+
return function (...args) {
|
|
165
|
+
const now = Date.now();
|
|
166
|
+
const remaining = delay - (now - lastCall);
|
|
167
|
+
if (remaining <= 0) {
|
|
168
|
+
lastCall = now;
|
|
169
|
+
fn.apply(this, args);
|
|
170
|
+
} else if (!timeoutId) {
|
|
171
|
+
timeoutId = setTimeout(() => {
|
|
172
|
+
lastCall = Date.now();
|
|
173
|
+
timeoutId = null;
|
|
174
|
+
fn.apply(this, args);
|
|
175
|
+
}, remaining);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
181
|
+
// INTERACTION TRACKING
|
|
182
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
183
|
+
|
|
184
|
+
function buildBaseEntry(type, el) {
|
|
185
|
+
return {
|
|
186
|
+
timestamp: new Date().toISOString(),
|
|
187
|
+
eventType: type,
|
|
188
|
+
url: window.location.href,
|
|
189
|
+
selector: buildSelector(el),
|
|
190
|
+
tagName: el ? el.tagName : undefined,
|
|
191
|
+
ariaRole: el ? (el.getAttribute('role') || undefined) : undefined,
|
|
192
|
+
visibleLabel: getVisibleLabel(el),
|
|
193
|
+
scrollPosition: {
|
|
194
|
+
x: Math.round(window.scrollX),
|
|
195
|
+
y: Math.round(window.scrollY),
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Click events (must explicitly capture X/Y viewport coordinates)
|
|
201
|
+
document.addEventListener('click', (e) => {
|
|
202
|
+
const entry = buildBaseEntry('click', e.target);
|
|
203
|
+
entry.viewportCoordinates = {
|
|
204
|
+
x: Math.round(e.clientX),
|
|
205
|
+
y: Math.round(e.clientY),
|
|
206
|
+
};
|
|
207
|
+
InteractionBuffer.add(entry);
|
|
208
|
+
}, true);
|
|
209
|
+
|
|
210
|
+
// Submit events
|
|
211
|
+
document.addEventListener('submit', (e) => {
|
|
212
|
+
InteractionBuffer.add(buildBaseEntry('submit', e.target));
|
|
213
|
+
}, true);
|
|
214
|
+
|
|
215
|
+
// Change events (no field values stored)
|
|
216
|
+
document.addEventListener('change', (e) => {
|
|
217
|
+
InteractionBuffer.add(buildBaseEntry('change', e.target));
|
|
218
|
+
}, true);
|
|
219
|
+
|
|
220
|
+
// High-level keyboard actions (no typed characters)
|
|
221
|
+
const ALLOWED_KEYS = new Set([
|
|
222
|
+
'Enter', 'Escape', 'Tab', 'Backspace', 'Delete',
|
|
223
|
+
'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight',
|
|
224
|
+
'Home', 'End', 'PageUp', 'PageDown',
|
|
225
|
+
'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12',
|
|
226
|
+
]);
|
|
227
|
+
|
|
228
|
+
document.addEventListener('keydown', (e) => {
|
|
229
|
+
if (!ALLOWED_KEYS.has(e.key)) return;
|
|
230
|
+
|
|
231
|
+
const entry = buildBaseEntry('keyboard', e.target);
|
|
232
|
+
entry.key = e.key;
|
|
233
|
+
entry.modifiers = {
|
|
234
|
+
ctrl: e.ctrlKey,
|
|
235
|
+
alt: e.altKey,
|
|
236
|
+
shift: e.shiftKey,
|
|
237
|
+
meta: e.metaKey,
|
|
238
|
+
};
|
|
239
|
+
InteractionBuffer.add(entry);
|
|
240
|
+
}, true);
|
|
241
|
+
|
|
242
|
+
// Scroll position snapshots (throttled 500ms)
|
|
243
|
+
const handleScroll = throttle(() => {
|
|
244
|
+
InteractionBuffer.add({
|
|
245
|
+
timestamp: new Date().toISOString(),
|
|
246
|
+
eventType: 'scroll',
|
|
247
|
+
url: window.location.href,
|
|
248
|
+
scrollPosition: {
|
|
249
|
+
x: Math.round(window.scrollX),
|
|
250
|
+
y: Math.round(window.scrollY),
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
}, 500);
|
|
254
|
+
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
255
|
+
|
|
256
|
+
// Viewport resize events (throttled 500ms)
|
|
257
|
+
const handleResize = throttle(() => {
|
|
258
|
+
InteractionBuffer.add({
|
|
259
|
+
timestamp: new Date().toISOString(),
|
|
260
|
+
eventType: 'resize',
|
|
261
|
+
url: window.location.href,
|
|
262
|
+
viewportSize: {
|
|
263
|
+
width: window.innerWidth,
|
|
264
|
+
height: window.innerHeight,
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
}, 500);
|
|
268
|
+
window.addEventListener('resize', handleResize);
|
|
269
|
+
|
|
270
|
+
// Navigation / route changes
|
|
271
|
+
let lastUrl = window.location.href;
|
|
272
|
+
const navigationObserver = new MutationObserver(() => {
|
|
273
|
+
if (window.location.href !== lastUrl) {
|
|
274
|
+
const oldUrl = lastUrl;
|
|
275
|
+
lastUrl = window.location.href;
|
|
276
|
+
InteractionBuffer.add({
|
|
277
|
+
timestamp: new Date().toISOString(),
|
|
278
|
+
eventType: 'navigation',
|
|
279
|
+
url: lastUrl,
|
|
280
|
+
previousUrl: oldUrl,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
navigationObserver.observe(document.documentElement, { childList: true, subtree: true });
|
|
285
|
+
|
|
286
|
+
window.addEventListener('popstate', () => {
|
|
287
|
+
if (window.location.href !== lastUrl) {
|
|
288
|
+
const oldUrl = lastUrl;
|
|
289
|
+
lastUrl = window.location.href;
|
|
290
|
+
InteractionBuffer.add({
|
|
291
|
+
timestamp: new Date().toISOString(),
|
|
292
|
+
eventType: 'navigation',
|
|
293
|
+
url: lastUrl,
|
|
294
|
+
previousUrl: oldUrl,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
300
|
+
// CONSOLE LOG & ERROR CAPTURE (via page-level injection)
|
|
301
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
302
|
+
|
|
303
|
+
const CHANNEL_ID = '__bugreport_ext_' + Math.random().toString(36).slice(2);
|
|
304
|
+
|
|
305
|
+
const injectedCode = `
|
|
306
|
+
(function() {
|
|
307
|
+
const CHANNEL = '${CHANNEL_ID}';
|
|
308
|
+
|
|
309
|
+
// ── Console Interception ──────────────────────────────────
|
|
310
|
+
const originalConsole = {};
|
|
311
|
+
['log', 'warn', 'error', 'info', 'debug'].forEach(level => {
|
|
312
|
+
originalConsole[level] = console[level];
|
|
313
|
+
console[level] = function(...args) {
|
|
314
|
+
try {
|
|
315
|
+
const message = args
|
|
316
|
+
.map(a => {
|
|
317
|
+
if (typeof a === 'string') return a;
|
|
318
|
+
try { return JSON.stringify(a); } catch { return String(a); }
|
|
319
|
+
})
|
|
320
|
+
.join(' ')
|
|
321
|
+
.substring(0, 500);
|
|
322
|
+
|
|
323
|
+
window.postMessage({
|
|
324
|
+
channel: CHANNEL,
|
|
325
|
+
type: 'CONSOLE_LOG',
|
|
326
|
+
data: {
|
|
327
|
+
timestamp: new Date().toISOString(),
|
|
328
|
+
level: level,
|
|
329
|
+
message: message,
|
|
330
|
+
}
|
|
331
|
+
}, '*');
|
|
332
|
+
} catch {}
|
|
333
|
+
originalConsole[level].apply(console, args);
|
|
334
|
+
};
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// ── Error Capture ─────────────────────────────────────────
|
|
338
|
+
window.addEventListener('error', (e) => {
|
|
339
|
+
try {
|
|
340
|
+
window.postMessage({
|
|
341
|
+
channel: CHANNEL,
|
|
342
|
+
type: 'JS_ERROR',
|
|
343
|
+
data: {
|
|
344
|
+
timestamp: new Date().toISOString(),
|
|
345
|
+
message: (e.message || '').substring(0, 500),
|
|
346
|
+
errorType: e.error ? e.error.constructor.name : 'Error',
|
|
347
|
+
stack: e.error && e.error.stack ? e.error.stack.substring(0, 1000) : undefined,
|
|
348
|
+
sourceFile: e.filename || undefined,
|
|
349
|
+
line: e.lineno || undefined,
|
|
350
|
+
column: e.colno || undefined,
|
|
351
|
+
}
|
|
352
|
+
}, '*');
|
|
353
|
+
} catch {}
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
window.addEventListener('unhandledrejection', (e) => {
|
|
357
|
+
try {
|
|
358
|
+
const reason = e.reason || {};
|
|
359
|
+
window.postMessage({
|
|
360
|
+
channel: CHANNEL,
|
|
361
|
+
type: 'JS_ERROR',
|
|
362
|
+
data: {
|
|
363
|
+
timestamp: new Date().toISOString(),
|
|
364
|
+
message: (typeof reason === 'string' ? reason : reason.message || 'Unhandled Promise Rejection').substring(0, 500),
|
|
365
|
+
errorType: reason.constructor ? reason.constructor.name : 'UnhandledRejection',
|
|
366
|
+
stack: reason.stack ? reason.stack.substring(0, 1000) : undefined,
|
|
367
|
+
}
|
|
368
|
+
}, '*');
|
|
369
|
+
} catch {}
|
|
370
|
+
});
|
|
371
|
+
})();
|
|
372
|
+
`;
|
|
373
|
+
|
|
374
|
+
// Inject the page-level script
|
|
375
|
+
const scriptEl = document.createElement('script');
|
|
376
|
+
scriptEl.textContent = injectedCode;
|
|
377
|
+
(document.head || document.documentElement).appendChild(scriptEl);
|
|
378
|
+
scriptEl.remove();
|
|
379
|
+
|
|
380
|
+
// Listen for relayed messages from the page-level script
|
|
381
|
+
window.addEventListener('message', (event) => {
|
|
382
|
+
if (event.source !== window) return;
|
|
383
|
+
if (!event.data || event.data.channel !== CHANNEL_ID) return;
|
|
384
|
+
|
|
385
|
+
if (event.data.type === 'CONSOLE_LOG') {
|
|
386
|
+
ConsoleBuffer.add(event.data.data);
|
|
387
|
+
} else if (event.data.type === 'JS_ERROR') {
|
|
388
|
+
ErrorBuffer.add(event.data.data);
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
393
|
+
// PAGE METADATA COLLECTION
|
|
394
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
395
|
+
|
|
396
|
+
function parseBrowserInfo() {
|
|
397
|
+
const ua = navigator.userAgent;
|
|
398
|
+
let browserName = 'Unknown';
|
|
399
|
+
let browserVersion = '';
|
|
400
|
+
|
|
401
|
+
if (ua.includes('Firefox/')) {
|
|
402
|
+
browserName = 'Firefox';
|
|
403
|
+
browserVersion = ua.match(/Firefox\/([\d.]+)/)?.[1] || '';
|
|
404
|
+
} else if (ua.includes('Edg/')) {
|
|
405
|
+
browserName = 'Edge';
|
|
406
|
+
browserVersion = ua.match(/Edg\/([\d.]+)/)?.[1] || '';
|
|
407
|
+
} else if (ua.includes('Chrome/') && !ua.includes('Edg/')) {
|
|
408
|
+
browserName = 'Chrome';
|
|
409
|
+
browserVersion = ua.match(/Chrome\/([\d.]+)/)?.[1] || '';
|
|
410
|
+
} else if (ua.includes('Safari/') && !ua.includes('Chrome/')) {
|
|
411
|
+
browserName = 'Safari';
|
|
412
|
+
browserVersion = ua.match(/Version\/([\d.]+)/)?.[1] || '';
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return { browserName, browserVersion };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function collectPageMetadata() {
|
|
419
|
+
const { browserName, browserVersion } = parseBrowserInfo();
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
url: window.location.href,
|
|
423
|
+
userAgent: navigator.userAgent,
|
|
424
|
+
viewportSize: {
|
|
425
|
+
width: window.innerWidth,
|
|
426
|
+
height: window.innerHeight,
|
|
427
|
+
},
|
|
428
|
+
screenResolution: {
|
|
429
|
+
width: window.screen.width,
|
|
430
|
+
height: window.screen.height,
|
|
431
|
+
},
|
|
432
|
+
scrollPosition: {
|
|
433
|
+
x: Math.round(window.scrollX),
|
|
434
|
+
y: Math.round(window.scrollY),
|
|
435
|
+
},
|
|
436
|
+
zoomLevel: Math.round(window.devicePixelRatio * 100) / 100,
|
|
437
|
+
browserName,
|
|
438
|
+
browserVersion,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
443
|
+
// LAYOUT2VECTOR — Canvas-Based DOM Visual Capture
|
|
444
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Capture the visual state of the page by extracting DOM geometry
|
|
448
|
+
* and rendering it onto an HTML5 Canvas. Then annotate with click
|
|
449
|
+
* coordinates from the interaction buffer.
|
|
450
|
+
*
|
|
451
|
+
* Returns a Base64 PNG data URL string.
|
|
452
|
+
*/
|
|
453
|
+
function captureVisualState() {
|
|
454
|
+
const vpWidth = window.innerWidth;
|
|
455
|
+
const vpHeight = window.innerHeight;
|
|
456
|
+
const dpr = window.devicePixelRatio || 1;
|
|
457
|
+
|
|
458
|
+
// Create offscreen canvas at device resolution for sharpness
|
|
459
|
+
const canvas = document.createElement('canvas');
|
|
460
|
+
canvas.width = vpWidth * dpr;
|
|
461
|
+
canvas.height = vpHeight * dpr;
|
|
462
|
+
const ctx = canvas.getContext('2d');
|
|
463
|
+
ctx.scale(dpr, dpr);
|
|
464
|
+
|
|
465
|
+
// Fill background (page bg)
|
|
466
|
+
const bodyStyle = window.getComputedStyle(document.body);
|
|
467
|
+
const htmlStyle = window.getComputedStyle(document.documentElement);
|
|
468
|
+
const pageBg = bodyStyle.backgroundColor !== 'rgba(0, 0, 0, 0)'
|
|
469
|
+
? bodyStyle.backgroundColor
|
|
470
|
+
: (htmlStyle.backgroundColor !== 'rgba(0, 0, 0, 0)' ? htmlStyle.backgroundColor : '#ffffff');
|
|
471
|
+
ctx.fillStyle = pageBg;
|
|
472
|
+
ctx.fillRect(0, 0, vpWidth, vpHeight);
|
|
473
|
+
|
|
474
|
+
// Walk visible DOM elements and paint them
|
|
475
|
+
const elements = document.querySelectorAll('*');
|
|
476
|
+
const scrollX = window.scrollX;
|
|
477
|
+
const scrollY = window.scrollY;
|
|
478
|
+
|
|
479
|
+
for (const el of elements) {
|
|
480
|
+
// Skip script, style, meta, head elements and hidden elements
|
|
481
|
+
const tag = el.tagName;
|
|
482
|
+
if (['SCRIPT', 'STYLE', 'META', 'LINK', 'HEAD', 'TITLE', 'NOSCRIPT', 'BR'].includes(tag)) continue;
|
|
483
|
+
|
|
484
|
+
try {
|
|
485
|
+
const rect = el.getBoundingClientRect();
|
|
486
|
+
|
|
487
|
+
// Skip elements fully outside viewport
|
|
488
|
+
if (rect.bottom < 0 || rect.top > vpHeight || rect.right < 0 || rect.left > vpWidth) continue;
|
|
489
|
+
// Skip invisible elements
|
|
490
|
+
if (rect.width === 0 || rect.height === 0) continue;
|
|
491
|
+
|
|
492
|
+
const style = window.getComputedStyle(el);
|
|
493
|
+
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') continue;
|
|
494
|
+
|
|
495
|
+
const x = rect.left;
|
|
496
|
+
const y = rect.top;
|
|
497
|
+
const w = rect.width;
|
|
498
|
+
const h = rect.height;
|
|
499
|
+
|
|
500
|
+
// Draw background if not transparent
|
|
501
|
+
const bg = style.backgroundColor;
|
|
502
|
+
if (bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent') {
|
|
503
|
+
ctx.fillStyle = bg;
|
|
504
|
+
|
|
505
|
+
// Handle border-radius
|
|
506
|
+
const br = parseFloat(style.borderRadius) || 0;
|
|
507
|
+
if (br > 0) {
|
|
508
|
+
drawRoundRect(ctx, x, y, w, h, Math.min(br, w / 2, h / 2));
|
|
509
|
+
ctx.fill();
|
|
510
|
+
} else {
|
|
511
|
+
ctx.fillRect(x, y, w, h);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Draw border if present
|
|
516
|
+
const borderWidth = parseFloat(style.borderTopWidth) || 0;
|
|
517
|
+
if (borderWidth > 0) {
|
|
518
|
+
const borderColor = style.borderTopColor;
|
|
519
|
+
if (borderColor && borderColor !== 'rgba(0, 0, 0, 0)') {
|
|
520
|
+
ctx.strokeStyle = borderColor;
|
|
521
|
+
ctx.lineWidth = borderWidth;
|
|
522
|
+
const br = parseFloat(style.borderRadius) || 0;
|
|
523
|
+
if (br > 0) {
|
|
524
|
+
drawRoundRect(ctx, x, y, w, h, Math.min(br, w / 2, h / 2));
|
|
525
|
+
ctx.stroke();
|
|
526
|
+
} else {
|
|
527
|
+
ctx.strokeRect(x, y, w, h);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Draw text for direct text-containing elements
|
|
533
|
+
if (isDirectTextNode(el)) {
|
|
534
|
+
const text = getDirectText(el).substring(0, 200);
|
|
535
|
+
if (text) {
|
|
536
|
+
const fontSize = parseFloat(style.fontSize) || 14;
|
|
537
|
+
const fontWeight = style.fontWeight || 'normal';
|
|
538
|
+
const fontFamily = style.fontFamily || 'sans-serif';
|
|
539
|
+
ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
|
|
540
|
+
ctx.fillStyle = style.color || '#000000';
|
|
541
|
+
ctx.textBaseline = 'top';
|
|
542
|
+
|
|
543
|
+
// Clip text to element bounds
|
|
544
|
+
ctx.save();
|
|
545
|
+
ctx.beginPath();
|
|
546
|
+
ctx.rect(x, y, w, h);
|
|
547
|
+
ctx.clip();
|
|
548
|
+
|
|
549
|
+
const textX = x + (parseFloat(style.paddingLeft) || 0);
|
|
550
|
+
const textY = y + (parseFloat(style.paddingTop) || 0);
|
|
551
|
+
ctx.fillText(text, textX, textY + (h - fontSize) / 2);
|
|
552
|
+
ctx.restore();
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Draw images as colored placeholder rectangles
|
|
557
|
+
if (tag === 'IMG' || tag === 'SVG' || tag === 'VIDEO' || tag === 'CANVAS') {
|
|
558
|
+
ctx.fillStyle = 'rgba(99, 102, 241, 0.1)';
|
|
559
|
+
ctx.fillRect(x, y, w, h);
|
|
560
|
+
ctx.strokeStyle = 'rgba(99, 102, 241, 0.3)';
|
|
561
|
+
ctx.lineWidth = 1;
|
|
562
|
+
ctx.strokeRect(x, y, w, h);
|
|
563
|
+
|
|
564
|
+
// Draw icon placeholder
|
|
565
|
+
ctx.fillStyle = 'rgba(99, 102, 241, 0.5)';
|
|
566
|
+
ctx.font = `${Math.min(w, h, 24) * 0.5}px sans-serif`;
|
|
567
|
+
ctx.textAlign = 'center';
|
|
568
|
+
ctx.textBaseline = 'middle';
|
|
569
|
+
ctx.fillText(tag === 'IMG' ? '🖼' : tag === 'SVG' ? '◇' : '▶', x + w / 2, y + h / 2);
|
|
570
|
+
ctx.textAlign = 'start';
|
|
571
|
+
}
|
|
572
|
+
} catch (e) {
|
|
573
|
+
// Skip elements that throw
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// ── Draw click-path annotations ──────────────────────────
|
|
578
|
+
const rawInteractions = InteractionBuffer.getRaw();
|
|
579
|
+
const clicks = rawInteractions.filter(e => e.eventType === 'click' && e.viewportCoordinates);
|
|
580
|
+
let clickIndex = 1;
|
|
581
|
+
|
|
582
|
+
for (const click of clicks) {
|
|
583
|
+
const pageX = click.viewportCoordinates.x + (click.scrollPosition ? click.scrollPosition.x : 0);
|
|
584
|
+
const pageY = click.viewportCoordinates.y + (click.scrollPosition ? click.scrollPosition.y : 0);
|
|
585
|
+
const cx = pageX - window.scrollX;
|
|
586
|
+
const cy = pageY - window.scrollY;
|
|
587
|
+
|
|
588
|
+
// Draw outer red circle with glow
|
|
589
|
+
ctx.beginPath();
|
|
590
|
+
ctx.arc(cx, cy, 16, 0, Math.PI * 2);
|
|
591
|
+
ctx.fillStyle = 'rgba(239, 68, 68, 0.25)';
|
|
592
|
+
ctx.fill();
|
|
593
|
+
|
|
594
|
+
// Draw red circle
|
|
595
|
+
ctx.beginPath();
|
|
596
|
+
ctx.arc(cx, cy, 12, 0, Math.PI * 2);
|
|
597
|
+
ctx.fillStyle = '#ef4444';
|
|
598
|
+
ctx.fill();
|
|
599
|
+
|
|
600
|
+
// Draw white border
|
|
601
|
+
ctx.strokeStyle = '#ffffff';
|
|
602
|
+
ctx.lineWidth = 2;
|
|
603
|
+
ctx.stroke();
|
|
604
|
+
|
|
605
|
+
// Draw number
|
|
606
|
+
ctx.fillStyle = '#ffffff';
|
|
607
|
+
ctx.font = 'bold 10px sans-serif';
|
|
608
|
+
ctx.textAlign = 'center';
|
|
609
|
+
ctx.textBaseline = 'middle';
|
|
610
|
+
ctx.fillText(String(clickIndex), cx, cy);
|
|
611
|
+
ctx.textAlign = 'start';
|
|
612
|
+
|
|
613
|
+
clickIndex++;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Export as Base64 PNG
|
|
617
|
+
return canvas.toDataURL('image/png');
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Helper: draw a rounded rectangle path.
|
|
622
|
+
*/
|
|
623
|
+
function drawRoundRect(ctx, x, y, w, h, r) {
|
|
624
|
+
ctx.beginPath();
|
|
625
|
+
ctx.moveTo(x + r, y);
|
|
626
|
+
ctx.lineTo(x + w - r, y);
|
|
627
|
+
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
|
|
628
|
+
ctx.lineTo(x + w, y + h - r);
|
|
629
|
+
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
|
|
630
|
+
ctx.lineTo(x + r, y + h);
|
|
631
|
+
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
|
|
632
|
+
ctx.lineTo(x, y + r);
|
|
633
|
+
ctx.quadraticCurveTo(x, y, x + r, y);
|
|
634
|
+
ctx.closePath();
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Check if an element directly contains text (not via child elements).
|
|
639
|
+
*/
|
|
640
|
+
function isDirectTextNode(el) {
|
|
641
|
+
for (const node of el.childNodes) {
|
|
642
|
+
if (node.nodeType === Node.TEXT_NODE && node.textContent.trim().length > 0) {
|
|
643
|
+
return true;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return false;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Get direct text content from an element (excluding child elements).
|
|
651
|
+
*/
|
|
652
|
+
function getDirectText(el) {
|
|
653
|
+
let text = '';
|
|
654
|
+
for (const node of el.childNodes) {
|
|
655
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
656
|
+
text += node.textContent;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return text.trim();
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function showReportForm(dataUrl) {
|
|
663
|
+
return new Promise((resolve) => {
|
|
664
|
+
if (!dataUrl) {
|
|
665
|
+
resolve(null);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const overlay = document.createElement('div');
|
|
670
|
+
overlay.style.position = 'fixed';
|
|
671
|
+
overlay.style.inset = '0';
|
|
672
|
+
overlay.style.zIndex = '2147483647';
|
|
673
|
+
overlay.style.background = 'rgba(15, 17, 23, 0.95)';
|
|
674
|
+
overlay.style.display = 'flex';
|
|
675
|
+
overlay.style.flexDirection = 'column';
|
|
676
|
+
overlay.style.fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
|
677
|
+
|
|
678
|
+
const header = document.createElement('div');
|
|
679
|
+
header.style.padding = '16px 24px';
|
|
680
|
+
header.style.background = '#1a1d27';
|
|
681
|
+
header.style.color = '#e8eaf0';
|
|
682
|
+
header.style.display = 'flex';
|
|
683
|
+
header.style.justifyContent = 'space-between';
|
|
684
|
+
header.style.alignItems = 'center';
|
|
685
|
+
header.style.borderBottom = '1px solid #2d3348';
|
|
686
|
+
header.innerHTML = `
|
|
687
|
+
<div style="font-size: 16px; font-weight: 700; display: flex; align-items: center; gap: 10px;">
|
|
688
|
+
🐛 Bug Report erstellen
|
|
689
|
+
</div>
|
|
690
|
+
<div style="display: flex; gap: 12px;">
|
|
691
|
+
<button id="br-editor-cancel" style="padding: 10px 16px; background: #222636; border: 1px solid #2d3348; color: #e8eaf0; border-radius: 8px; cursor: pointer; font-size: 13px; font-weight: 600; transition: all 0.2s;">Abbrechen</button>
|
|
692
|
+
<button id="br-editor-save" disabled style="padding: 10px 20px; background: #6366f1; border: none; color: #ffffff; border-radius: 8px; cursor: not-allowed; opacity: 0.5; font-size: 13px; font-weight: 700; box-shadow: 0 4px 12px rgba(99,102,241,0.3); transition: all 0.2s;">Report herunterladen</button>
|
|
693
|
+
</div>
|
|
694
|
+
`;
|
|
695
|
+
|
|
696
|
+
const scrollbarStyle = document.createElement('style');
|
|
697
|
+
scrollbarStyle.textContent = `
|
|
698
|
+
.br-modal-main { scrollbar-width: auto; scrollbar-color: #6366f1 #1a1d27; }
|
|
699
|
+
.br-modal-main::-webkit-scrollbar { width: 10px; }
|
|
700
|
+
.br-modal-main::-webkit-scrollbar-track { background: #1a1d27; border-radius: 5px; }
|
|
701
|
+
.br-modal-main::-webkit-scrollbar-thumb { background: #6366f1; border-radius: 5px; border: 2px solid #1a1d27; }
|
|
702
|
+
.br-modal-main::-webkit-scrollbar-thumb:hover { background: #818cf8; }
|
|
703
|
+
`;
|
|
704
|
+
overlay.appendChild(scrollbarStyle);
|
|
705
|
+
|
|
706
|
+
const main = document.createElement('div');
|
|
707
|
+
main.className = 'br-modal-main';
|
|
708
|
+
main.style.flex = '1';
|
|
709
|
+
main.style.display = 'flex';
|
|
710
|
+
main.style.overflowY = 'scroll';
|
|
711
|
+
main.style.flexWrap = 'wrap';
|
|
712
|
+
|
|
713
|
+
const canvasContainer = document.createElement('div');
|
|
714
|
+
canvasContainer.style.flex = '2 1 600px';
|
|
715
|
+
canvasContainer.style.overflow = 'auto';
|
|
716
|
+
canvasContainer.style.display = 'flex';
|
|
717
|
+
canvasContainer.style.alignItems = 'flex-start';
|
|
718
|
+
canvasContainer.style.justifyContent = 'center';
|
|
719
|
+
canvasContainer.style.padding = '24px';
|
|
720
|
+
canvasContainer.style.background = '#0f1117';
|
|
721
|
+
|
|
722
|
+
const canvas = document.createElement('canvas');
|
|
723
|
+
canvas.style.boxShadow = '0 10px 40px rgba(0,0,0,0.5)';
|
|
724
|
+
canvas.style.cursor = 'crosshair';
|
|
725
|
+
canvas.style.maxWidth = '100%';
|
|
726
|
+
canvas.style.height = 'auto';
|
|
727
|
+
canvas.style.background = '#ffffff';
|
|
728
|
+
canvas.style.borderRadius = '4px';
|
|
729
|
+
|
|
730
|
+
const formContainer = document.createElement('div');
|
|
731
|
+
formContainer.style.flex = '1 1 350px';
|
|
732
|
+
formContainer.style.background = '#1a1d27';
|
|
733
|
+
formContainer.style.borderLeft = '1px solid #2d3348';
|
|
734
|
+
formContainer.style.padding = '24px';
|
|
735
|
+
formContainer.style.display = 'flex';
|
|
736
|
+
formContainer.style.flexDirection = 'column';
|
|
737
|
+
formContainer.style.gap = '20px';
|
|
738
|
+
formContainer.style.overflowY = 'auto';
|
|
739
|
+
|
|
740
|
+
formContainer.innerHTML = `
|
|
741
|
+
<div>
|
|
742
|
+
<h3 style="margin: 0 0 8px 0; font-size: 14px; color: #e8eaf0;">Was ist passiert? <span style="color:#ef4444">*</span></h3>
|
|
743
|
+
<p style="margin: 0 0 8px 0; font-size: 12px; color: #9096a8;">Beschreibe kurz das Problem. (z.B. "Beim Klick auf Speichern passiert nichts")</p>
|
|
744
|
+
<textarea id="br-input-actual" placeholder="Ist-Zustand..." style="width: 100%; box-sizing: border-box; height: 100px; background: #0f1117; border: 1px solid #2d3348; border-radius: 8px; color: #e8eaf0; padding: 12px; font-family: inherit; font-size: 13px; resize: vertical;"></textarea>
|
|
745
|
+
</div>
|
|
746
|
+
<div>
|
|
747
|
+
<h3 style="margin: 0 0 8px 0; font-size: 14px; color: #e8eaf0;">Was hättest du erwartet? <span style="color:#ef4444">*</span></h3>
|
|
748
|
+
<p style="margin: 0 0 8px 0; font-size: 12px; color: #9096a8;">Beschreibe das gewünschte Verhalten. (z.B. "Die Daten sollten gespeichert werden")</p>
|
|
749
|
+
<textarea id="br-input-expected" placeholder="Soll-Zustand..." style="width: 100%; box-sizing: border-box; height: 100px; background: #0f1117; border: 1px solid #2d3348; border-radius: 8px; color: #e8eaf0; padding: 12px; font-family: inherit; font-size: 13px; resize: vertical;"></textarea>
|
|
750
|
+
</div>
|
|
751
|
+
<div style="background: rgba(99, 102, 241, 0.1); border: 1px solid rgba(99, 102, 241, 0.2); border-radius: 8px; padding: 16px;">
|
|
752
|
+
<h3 style="margin: 0 0 8px 0; font-size: 13px; color: #818cf8; display: flex; align-items: center; gap: 6px;">🖍 Screenshot markieren</h3>
|
|
753
|
+
<p style="margin: 0; font-size: 12px; color: #9096a8; line-height: 1.5;">Du kannst mit der Maus auf dem Screenshot zeichnen, um das Problem genauer zu zeigen.</p>
|
|
754
|
+
</div>
|
|
755
|
+
`;
|
|
756
|
+
|
|
757
|
+
const img = new Image();
|
|
758
|
+
img.onload = () => {
|
|
759
|
+
canvas.width = img.width;
|
|
760
|
+
canvas.height = img.height;
|
|
761
|
+
const ctx = canvas.getContext('2d');
|
|
762
|
+
ctx.drawImage(img, 0, 0);
|
|
763
|
+
|
|
764
|
+
let isDrawing = false;
|
|
765
|
+
|
|
766
|
+
function getPos(e) {
|
|
767
|
+
const rect = canvas.getBoundingClientRect();
|
|
768
|
+
const scaleX = canvas.width / rect.width;
|
|
769
|
+
const scaleY = canvas.height / rect.height;
|
|
770
|
+
const clientX = e.touches && e.touches.length > 0 ? e.touches[0].clientX : e.clientX;
|
|
771
|
+
const clientY = e.touches && e.touches.length > 0 ? e.touches[0].clientY : e.clientY;
|
|
772
|
+
return {
|
|
773
|
+
x: (clientX - rect.left) * scaleX,
|
|
774
|
+
y: (clientY - rect.top) * scaleY
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const startDrawing = (e) => {
|
|
779
|
+
if (e.type.startsWith('touch')) e.preventDefault();
|
|
780
|
+
isDrawing = true;
|
|
781
|
+
const pos = getPos(e);
|
|
782
|
+
ctx.beginPath();
|
|
783
|
+
ctx.moveTo(pos.x, pos.y);
|
|
784
|
+
ctx.strokeStyle = '#ef4444';
|
|
785
|
+
ctx.lineWidth = Math.max(4, img.width / 250);
|
|
786
|
+
ctx.lineCap = 'round';
|
|
787
|
+
ctx.lineJoin = 'round';
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
const draw = (e) => {
|
|
791
|
+
if (e.type.startsWith('touch')) e.preventDefault();
|
|
792
|
+
if (!isDrawing) return;
|
|
793
|
+
const pos = getPos(e);
|
|
794
|
+
ctx.lineTo(pos.x, pos.y);
|
|
795
|
+
ctx.stroke();
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
const stopDrawing = (e) => {
|
|
799
|
+
if (e && e.type && e.type.startsWith('touch') && e.cancelable) e.preventDefault();
|
|
800
|
+
isDrawing = false;
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
canvas.addEventListener('mousedown', startDrawing);
|
|
804
|
+
canvas.addEventListener('touchstart', startDrawing, { passive: false });
|
|
805
|
+
|
|
806
|
+
canvas.addEventListener('mousemove', draw);
|
|
807
|
+
canvas.addEventListener('touchmove', draw, { passive: false });
|
|
808
|
+
|
|
809
|
+
canvas.addEventListener('mouseup', stopDrawing);
|
|
810
|
+
canvas.addEventListener('mouseleave', stopDrawing);
|
|
811
|
+
canvas.addEventListener('touchend', stopDrawing, { passive: false });
|
|
812
|
+
canvas.addEventListener('touchcancel', stopDrawing, { passive: false });
|
|
813
|
+
};
|
|
814
|
+
img.src = dataUrl;
|
|
815
|
+
|
|
816
|
+
canvasContainer.appendChild(canvas);
|
|
817
|
+
main.appendChild(canvasContainer);
|
|
818
|
+
main.appendChild(formContainer);
|
|
819
|
+
overlay.appendChild(header);
|
|
820
|
+
overlay.appendChild(main);
|
|
821
|
+
document.body.appendChild(overlay);
|
|
822
|
+
|
|
823
|
+
const actualInput = document.getElementById('br-input-actual');
|
|
824
|
+
const expectedInput = document.getElementById('br-input-expected');
|
|
825
|
+
const saveBtn = document.getElementById('br-editor-save');
|
|
826
|
+
|
|
827
|
+
const validateInputs = () => {
|
|
828
|
+
if (actualInput.value.trim().length > 0 && expectedInput.value.trim().length > 0) {
|
|
829
|
+
saveBtn.disabled = false;
|
|
830
|
+
saveBtn.style.cursor = 'pointer';
|
|
831
|
+
saveBtn.style.opacity = '1';
|
|
832
|
+
} else {
|
|
833
|
+
saveBtn.disabled = true;
|
|
834
|
+
saveBtn.style.cursor = 'not-allowed';
|
|
835
|
+
saveBtn.style.opacity = '0.5';
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
actualInput.addEventListener('input', validateInputs);
|
|
840
|
+
expectedInput.addEventListener('input', validateInputs);
|
|
841
|
+
|
|
842
|
+
document.getElementById('br-editor-cancel').addEventListener('click', () => {
|
|
843
|
+
document.body.removeChild(overlay);
|
|
844
|
+
resolve(null);
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
saveBtn.addEventListener('click', () => {
|
|
848
|
+
const newDataUrl = canvas.toDataURL('image/png');
|
|
849
|
+
document.body.removeChild(overlay);
|
|
850
|
+
resolve({
|
|
851
|
+
screenshotBase64: newDataUrl,
|
|
852
|
+
actual: actualInput.value.trim(),
|
|
853
|
+
expected: expectedInput.value.trim()
|
|
854
|
+
});
|
|
855
|
+
});
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
860
|
+
// MESSAGING — Respond to background.js requests
|
|
861
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
862
|
+
|
|
863
|
+
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|
864
|
+
if (message.type === 'GET_BUG_REPORT_DATA') {
|
|
865
|
+
// 1. Capture visual state synchronously
|
|
866
|
+
let rawScreenshot = null;
|
|
867
|
+
try {
|
|
868
|
+
rawScreenshot = captureVisualState();
|
|
869
|
+
} catch (e) {
|
|
870
|
+
console.error('Visual capture failed', e);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// 2. Async flow: show unified form, wait for user, then respond
|
|
874
|
+
showReportForm(rawScreenshot).then((formData) => {
|
|
875
|
+
if (!formData) {
|
|
876
|
+
sendResponse({ cancelled: true });
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
sendResponse({
|
|
880
|
+
pageMetadata: collectPageMetadata(),
|
|
881
|
+
interactions: InteractionBuffer.getAll(),
|
|
882
|
+
consoleLogs: ConsoleBuffer.getAll(),
|
|
883
|
+
jsErrors: ErrorBuffer.getAll(),
|
|
884
|
+
screenshotBase64: formData.screenshotBase64,
|
|
885
|
+
userDescription: {
|
|
886
|
+
actual: formData.actual,
|
|
887
|
+
expected: formData.expected
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
return true; // Keep the message channel open for async response
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
if (message.type === 'GET_PREVIEW_COUNTS') {
|
|
896
|
+
sendResponse({
|
|
897
|
+
url: window.location.href,
|
|
898
|
+
interactionCount: InteractionBuffer.getAll().length,
|
|
899
|
+
consoleLogCount: ConsoleBuffer.getAll().length,
|
|
900
|
+
jsErrorCount: ErrorBuffer.getAll().length,
|
|
901
|
+
});
|
|
902
|
+
return true;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
});
|