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/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
|
+
});
|