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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sebastian Striebig
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,144 @@
1
+ # Bug Report Capture — Chrome Extension
2
+
3
+ A Chrome extension for structured bug reporting. Captures user interactions, console logs, JavaScript errors, and network request metadata, then exports them as a sanitized, downloadable HTML report.
4
+
5
+ > **Looking for the website widget / NPM package?** See [`website/README.md`](website/README.md).
6
+
7
+ ---
8
+
9
+ ## Installation
10
+
11
+ 1. Open Chrome → `chrome://extensions`
12
+ 2. Enable **Developer mode** (toggle top-right)
13
+ 3. Click **Load unpacked** and select this folder
14
+ 4. The 🐛 icon appears in the toolbar — pin it via the puzzle-piece menu if needed
15
+
16
+ ---
17
+
18
+ ## Usage
19
+
20
+ 1. Navigate to any page
21
+ 2. Interact normally (clicks, scrolls, forms, etc.)
22
+ 3. Click the 🐛 toolbar icon
23
+ 4. Review the capture summary
24
+ 5. Click **Download Report** — a `bug_report_<timestamp>.json` file is saved
25
+
26
+ ---
27
+
28
+ ## Testing
29
+
30
+ ### Interaction Capture
31
+
32
+ Perform clicks, form changes, keyboard events, and scrolls, then generate a report. In the `interactions` array:
33
+ - Each entry has `timestamp`, `eventType`, `url`, `selector`, `tagName`
34
+ - Click events include `viewportCoordinates`
35
+ - Keyboard events show only the key name (`"Enter"`) — **no typed characters**
36
+ - **No form field values** appear
37
+
38
+ ### Console Log Capture
39
+
40
+ ```js
41
+ console.log("Test log message")
42
+ console.warn("Test warning")
43
+ console.error("Test error")
44
+ ```
45
+
46
+ Check the `consoleLogs` array for entries with `timestamp`, `level`, and `message`. Max 100 entries.
47
+
48
+ ### JS Error Capture
49
+
50
+ ```js
51
+ undefinedFunction() // runtime error
52
+ Promise.reject(new Error("test rejection")) // unhandled rejection
53
+ ```
54
+
55
+ Check `jsErrors` for entries with `timestamp`, `message`, `errorType`. Max 50 entries.
56
+
57
+ ### Network Request Capture
58
+
59
+ ```js
60
+ fetch('https://httpbin.org/get')
61
+ fetch('https://httpbin.org/status/500')
62
+ ```
63
+
64
+ Check `networkRequests` for `timestamp`, `method`, `url`, `statusCode`, `duration`. Max 200 entries. No cookies, auth headers, or bodies are included.
65
+
66
+ ### Privacy & Sanitization
67
+
68
+ ```js
69
+ console.log("Contact: user@example.com")
70
+ console.log("Token: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ0ZXN0IjoxfQ.abc123")
71
+ console.log("API key: apikey=sk-12345678abcdef")
72
+ ```
73
+
74
+ Verify in the report:
75
+ - Emails → `[REDACTED_EMAIL]`
76
+ - Bearer tokens → `[REDACTED_TOKEN]`
77
+ - API keys → `[REDACTED_API_KEY]`
78
+
79
+ For URL params, navigate to `https://example.com/search?q=test&page=1&token=secret123` and confirm:
80
+ - `q`, `page` → kept (safe allowlist)
81
+ - `token` → removed (sensitive blocklist)
82
+
83
+ ---
84
+
85
+ ## Report Structure
86
+
87
+ ```json
88
+ {
89
+ "schemaVersion": "1.0.0",
90
+ "reportTimestamp": "...",
91
+ "extensionVersion": "1.0.0",
92
+ "pageMetadata": {
93
+ "url": "...",
94
+ "userAgent": "...",
95
+ "viewportSize": { "width": 0, "height": 0 },
96
+ "screenResolution": { "width": 0, "height": 0 },
97
+ "scrollPosition": { "x": 0, "y": 0 },
98
+ "zoomLevel": 1,
99
+ "browserName": "...",
100
+ "browserVersion": "..."
101
+ },
102
+ "interactions": [],
103
+ "consoleLogs": [],
104
+ "jsErrors": [],
105
+ "networkRequests": [],
106
+ "sanitizationSummary": {
107
+ "totalRedactions": 0,
108
+ "redactionsByType": {}
109
+ },
110
+ "captureLimitations": []
111
+ }
112
+ ```
113
+
114
+ ---
115
+
116
+ ## File Structure
117
+
118
+ ```
119
+ Bug-download-button/
120
+ ├── website/
121
+ │ ├── bug-report.js # Website widget / NPM package
122
+ │ ├── README.md # Widget documentation
123
+ │ ├── index.html # Demo page
124
+ │ └── docs.html
125
+ ├── manifest.json # Extension manifest (Manifest V3)
126
+ ├── background.js # Service worker: network capture, report assembly
127
+ ├── content.js # Content script: interactions, console, errors
128
+ ├── sanitizer.js # Sanitization module
129
+ ├── popup.html
130
+ ├── popup.css
131
+ ├── popup.js
132
+ └── icons/
133
+ ├── icon16.png
134
+ ├── icon48.png
135
+ └── icon128.png
136
+ ```
137
+
138
+ ## Known Limitations
139
+
140
+ - Data captured only while the extension is active on the current tab
141
+ - Pre-existing console logs, errors, and network requests (before extension load) are not captured
142
+ - Browser-internal pages (`chrome://`, `about:`) are not supported
143
+ - Cross-origin iframes may not be fully captured
144
+ - Network capture uses the `webRequest` API — metadata only, no request/response bodies
package/background.js ADDED
@@ -0,0 +1,279 @@
1
+ /**
2
+ * background.js — Service Worker for Bug Report Extension (v2.0.0)
3
+ *
4
+ * Responsibilities:
5
+ * - Capture network request metadata via webRequest API (per-tab, max 200)
6
+ * - Orchestrate Ist/Soll user prompts via content script
7
+ * - Assemble report from content script data + network data
8
+ * - Apply final sanitization via sanitizer.js
9
+ * - Generate HTML dashboard via report-template.js
10
+ * - Trigger HTML file download
11
+ */
12
+
13
+ importScripts('sanitizer.js', 'report-template.js');
14
+
15
+ const EXTENSION_VERSION = '2.0.0';
16
+
17
+ // ═══════════════════════════════════════════════════════════════════
18
+ // NETWORK REQUEST BUFFER (per-tab)
19
+ // ═══════════════════════════════════════════════════════════════════
20
+
21
+ const MAX_NETWORK_ENTRIES = 200;
22
+ const networkBuffers = new Map(); // tabId → { entries: [], pendingRequests: Map }
23
+
24
+ function getTabBuffer(tabId) {
25
+ if (!networkBuffers.has(tabId)) {
26
+ networkBuffers.set(tabId, {
27
+ entries: [],
28
+ pendingRequests: new Map(),
29
+ });
30
+ }
31
+ return networkBuffers.get(tabId);
32
+ }
33
+
34
+ function addNetworkEntry(tabId, entry) {
35
+ const buf = getTabBuffer(tabId);
36
+ buf.entries.push(entry);
37
+ if (buf.entries.length > MAX_NETWORK_ENTRIES) {
38
+ buf.entries.shift();
39
+ }
40
+ }
41
+
42
+ // Clean up when tab is closed
43
+ chrome.tabs.onRemoved.addListener((tabId) => {
44
+ networkBuffers.delete(tabId);
45
+ });
46
+
47
+ // Clean up when tab navigates
48
+ chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
49
+ if (changeInfo.status === 'loading') {
50
+ // Don't clear completely on navigation, keep buffer for SPA-like apps
51
+ // But do clear pending requests
52
+ const buf = networkBuffers.get(tabId);
53
+ if (buf) {
54
+ buf.pendingRequests.clear();
55
+ }
56
+ }
57
+ });
58
+
59
+ // ── Network Request Tracking ─────────────────────────────────────
60
+
61
+ chrome.webRequest.onBeforeRequest.addListener(
62
+ (details) => {
63
+ if (details.tabId < 0) return; // Skip non-tab requests
64
+
65
+ const buf = getTabBuffer(details.tabId);
66
+ buf.pendingRequests.set(details.requestId, {
67
+ startTime: details.timeStamp,
68
+ method: details.method,
69
+ url: details.url,
70
+ type: details.type,
71
+ });
72
+ },
73
+ { urls: ['<all_urls>'] }
74
+ );
75
+
76
+ chrome.webRequest.onCompleted.addListener(
77
+ (details) => {
78
+ if (details.tabId < 0) return;
79
+
80
+ const buf = getTabBuffer(details.tabId);
81
+ const pending = buf.pendingRequests.get(details.requestId);
82
+ buf.pendingRequests.delete(details.requestId);
83
+
84
+ addNetworkEntry(details.tabId, {
85
+ timestamp: new Date(details.timeStamp).toISOString(),
86
+ method: pending?.method || details.method || 'UNKNOWN',
87
+ url: details.url,
88
+ type: details.type || undefined,
89
+ statusCode: details.statusCode,
90
+ duration: pending ? Math.round(details.timeStamp - pending.startTime) : undefined,
91
+ error: false,
92
+ });
93
+ },
94
+ { urls: ['<all_urls>'] }
95
+ );
96
+
97
+ chrome.webRequest.onErrorOccurred.addListener(
98
+ (details) => {
99
+ if (details.tabId < 0) return;
100
+
101
+ const buf = getTabBuffer(details.tabId);
102
+ const pending = buf.pendingRequests.get(details.requestId);
103
+ buf.pendingRequests.delete(details.requestId);
104
+
105
+ addNetworkEntry(details.tabId, {
106
+ timestamp: new Date(details.timeStamp).toISOString(),
107
+ method: pending?.method || 'UNKNOWN',
108
+ url: details.url,
109
+ type: details.type || undefined,
110
+ statusCode: undefined,
111
+ duration: pending ? Math.round(details.timeStamp - pending.startTime) : undefined,
112
+ error: true,
113
+ errorDescription: details.error || undefined,
114
+ });
115
+ },
116
+ { urls: ['<all_urls>'] }
117
+ );
118
+
119
+ // ═══════════════════════════════════════════════════════════════════
120
+ // REPORT ASSEMBLY & DOWNLOAD
121
+ // ═══════════════════════════════════════════════════════════════════
122
+
123
+ const CAPTURE_LIMITATIONS = [
124
+ 'Only captures events while the extension was active on the current page.',
125
+ 'Cannot capture data from browser-internal pages (chrome://, about://).',
126
+ 'Cannot capture data from restricted pages or cross-origin iframes with limited extension access.',
127
+ 'Console logs, JS errors, and network requests that occurred before extension initialization are not included.',
128
+ 'Network request/response bodies and headers are not captured.',
129
+ 'Form field values and typed characters are not captured.',
130
+ 'Visual capture renders a simplified DOM geometry, not a pixel-perfect screenshot.',
131
+ ];
132
+
133
+ async function assembleReport(tabId) {
134
+ // Get data from content script (including screenshot and user description)
135
+ const contentData = await chrome.tabs.sendMessage(tabId, {
136
+ type: 'GET_BUG_REPORT_DATA',
137
+ });
138
+
139
+ if (contentData.cancelled) {
140
+ return { cancelled: true };
141
+ }
142
+
143
+ // Get network data for this tab
144
+ const tabBuffer = networkBuffers.get(tabId);
145
+ const networkRequests = tabBuffer ? [...tabBuffer.entries] : [];
146
+
147
+ // Build the raw report
148
+ const rawReport = {
149
+ schemaVersion: '2.0.0',
150
+ reportTimestamp: new Date().toISOString(),
151
+ extensionVersion: EXTENSION_VERSION,
152
+ pageMetadata: contentData.pageMetadata,
153
+ userDescription: contentData.userDescription,
154
+ screenshotBase64: contentData.screenshotBase64,
155
+ interactions: contentData.interactions,
156
+ consoleLogs: contentData.consoleLogs,
157
+ jsErrors: contentData.jsErrors,
158
+ networkRequests: networkRequests,
159
+ };
160
+
161
+ // Apply deep sanitization (Layer 4)
162
+ // Note: screenshotBase64 is a data URL, not user text — exclude from text sanitization
163
+ const screenshotBackup = rawReport.screenshotBase64;
164
+ rawReport.screenshotBase64 = '__SCREENSHOT_PLACEHOLDER__';
165
+
166
+ const redactions = {};
167
+ const sanitizedReport = Sanitizer.sanitizeDeep(rawReport, redactions);
168
+
169
+ // Restore screenshot
170
+ sanitizedReport.screenshotBase64 = screenshotBackup;
171
+
172
+ // Add sanitization summary
173
+ sanitizedReport.sanitizationSummary = Sanitizer.buildSummary(redactions);
174
+
175
+ // Add capture limitations
176
+ sanitizedReport.captureLimitations = CAPTURE_LIMITATIONS;
177
+
178
+ // Final validation (Layer 5)
179
+ // Temporarily remove screenshot for validation (it's binary data, not user text)
180
+ const reportForValidation = { ...sanitizedReport, screenshotBase64: undefined };
181
+ const validation = Sanitizer.validateFinalReport(reportForValidation);
182
+ if (!validation.passed) {
183
+ // If validation finds issues, do another sanitization pass
184
+ const reRaw = { ...sanitizedReport, screenshotBase64: '__SCREENSHOT_PLACEHOLDER__' };
185
+ const reSanitized = Sanitizer.sanitizeDeep(reRaw, redactions);
186
+ reSanitized.screenshotBase64 = screenshotBackup;
187
+ reSanitized.sanitizationSummary = Sanitizer.buildSummary(redactions);
188
+ reSanitized.sanitizationSummary.validationIssues = validation.issues;
189
+ reSanitized.captureLimitations = CAPTURE_LIMITATIONS;
190
+ return reSanitized;
191
+ }
192
+
193
+ return sanitizedReport;
194
+ }
195
+
196
+ async function downloadReport(tabId) {
197
+ // Step 1 & 2: Get data from content script and assemble the report with sanitization
198
+ const report = await assembleReport(tabId);
199
+
200
+ // Check if user cancelled the unified form
201
+ if (report.cancelled) {
202
+ return { cancelled: true };
203
+ }
204
+
205
+ // Step 3: Build HTML dashboard
206
+ const htmlStr = ReportTemplate.build(report);
207
+ const blob = new Blob([htmlStr], { type: 'text/html' });
208
+
209
+ // Convert blob to data URL for download
210
+ const reader = new FileReader();
211
+ return new Promise((resolve, reject) => {
212
+ reader.onloadend = () => {
213
+ const dataUrl = reader.result;
214
+ const filename = `bug_report_${Date.now()}.html`;
215
+
216
+ chrome.downloads.download(
217
+ {
218
+ url: dataUrl,
219
+ filename: filename,
220
+ saveAs: false,
221
+ },
222
+ (downloadId) => {
223
+ if (chrome.runtime.lastError) {
224
+ reject(chrome.runtime.lastError.message);
225
+ } else {
226
+ resolve({ downloadId, filename });
227
+ }
228
+ }
229
+ );
230
+ };
231
+ reader.readAsDataURL(blob);
232
+ });
233
+ }
234
+
235
+ // ═══════════════════════════════════════════════════════════════════
236
+ // MESSAGE HANDLING
237
+ // ═══════════════════════════════════════════════════════════════════
238
+
239
+ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
240
+ if (message.type === 'GET_REPORT_PREVIEW') {
241
+ const tabId = message.tabId;
242
+
243
+ chrome.tabs.sendMessage(tabId, { type: 'GET_PREVIEW_COUNTS' }, (counts) => {
244
+ if (chrome.runtime.lastError) {
245
+ sendResponse({ error: chrome.runtime.lastError.message });
246
+ return;
247
+ }
248
+
249
+ const tabBuffer = networkBuffers.get(tabId);
250
+ const networkCount = tabBuffer ? tabBuffer.entries.length : 0;
251
+
252
+ sendResponse({
253
+ url: counts.url,
254
+ interactionCount: counts.interactionCount,
255
+ consoleLogCount: counts.consoleLogCount,
256
+ jsErrorCount: counts.jsErrorCount,
257
+ networkRequestCount: networkCount,
258
+ });
259
+ });
260
+
261
+ return true; // async response
262
+ }
263
+
264
+ if (message.type === 'DOWNLOAD_REPORT') {
265
+ const tabId = message.tabId;
266
+
267
+ downloadReport(tabId)
268
+ .then((result) => {
269
+ if (result.cancelled) {
270
+ sendResponse({ success: false, cancelled: true });
271
+ } else {
272
+ sendResponse({ success: true, ...result });
273
+ }
274
+ })
275
+ .catch((error) => sendResponse({ success: false, error: String(error) }));
276
+
277
+ return true; // async response
278
+ }
279
+ });