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/sanitizer.js
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sanitizer.js — Shared sanitization module for Bug Report Extension
|
|
3
|
+
*
|
|
4
|
+
* Implements multi-layer privacy protection:
|
|
5
|
+
* Layer 1: Data minimization (handled at collection sites)
|
|
6
|
+
* Layer 2: Allowlist-based collection (handled at collection sites)
|
|
7
|
+
* Layer 3: Field-level exclusion (handled at collection sites)
|
|
8
|
+
* Layer 4: Pattern sanitization (this module)
|
|
9
|
+
* Layer 5: Final JSON validation (this module)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const Sanitizer = (() => {
|
|
13
|
+
// ── Query Parameter Allowlist & Blocklist ──────────────────────────
|
|
14
|
+
|
|
15
|
+
const SAFE_QUERY_PARAMS = new Set([
|
|
16
|
+
// Pagination & Navigation
|
|
17
|
+
'q','query','search','keyword','term','id','pid','post_id','item_id','article_id','slug','path','route',
|
|
18
|
+
'page','p','limit','offset','skip','take','cursor','start','end','per_page','size','index','first','last','next','prev','before','after',
|
|
19
|
+
// Sorting & Filtering
|
|
20
|
+
'sort','order','orderby','sortby','dir','direction','filter','max','min','category','tag','type','status','state','date','year','month','day',
|
|
21
|
+
// UI & Display
|
|
22
|
+
'view','mode','display','format','layout','theme','tab','panel','step','section','anchor','eventorigin',
|
|
23
|
+
// App State & Routing
|
|
24
|
+
'action','method','module','component','feature','flag','variant','experiment','version','v',
|
|
25
|
+
// Localization
|
|
26
|
+
'lang','locale','language','hl','gl','country','region','currency',
|
|
27
|
+
// Marketing & Analytics
|
|
28
|
+
'ref','source','utm_source','utm_medium','utm_campaign','utm_term','utm_content','gclid','fbclid','msclkid','mc_cid','mc_eid'
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
const SENSITIVE_QUERY_PARAMS = new Set([
|
|
32
|
+
'token', 'access_token', 'refresh_token', 'id_token', 'auth_token',
|
|
33
|
+
'session', 'sessionid', 'session_id', 'sid',
|
|
34
|
+
'apikey', 'api_key', 'key', 'client_secret', 'secret',
|
|
35
|
+
'password', 'passwd', 'pwd',
|
|
36
|
+
'email', 'mail', 'e-mail',
|
|
37
|
+
'userid', 'user_id', 'uid', 'customerid', 'customer_id',
|
|
38
|
+
'orderid', 'order_id',
|
|
39
|
+
'ssn', 'credit_card', 'cc', 'cvv',
|
|
40
|
+
'auth', 'authorization', 'bearer',
|
|
41
|
+
'code', 'otp', 'verification', 'reset_token',
|
|
42
|
+
'nonce', 'csrf', 'xsrf',
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
// ── Regex Patterns for Sensitive Data ──────────────────────────────
|
|
46
|
+
|
|
47
|
+
const SANITIZATION_PATTERNS = [
|
|
48
|
+
{
|
|
49
|
+
name: 'email',
|
|
50
|
+
pattern: /[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}/g,
|
|
51
|
+
replacement: '[REDACTED_EMAIL]',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'phone',
|
|
55
|
+
pattern: /(?<![a-zA-Z0-9])(?:\+?\d{1,4}[\s\-.]?)?\(?\d{2,4}\)?[\s\-.]?\d{3,4}[\s\-.]?\d{3,5}(?![a-zA-Z0-9])/g,
|
|
56
|
+
replacement: '[REDACTED_PHONE]',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: 'bearer_token',
|
|
60
|
+
pattern: /Bearer\s+[A-Za-z0-9\-._~+/]+=*/gi,
|
|
61
|
+
replacement: 'Bearer [REDACTED_TOKEN]',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'jwt',
|
|
65
|
+
pattern: /eyJ[A-Za-z0-9\-_]+\.eyJ[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_.+/=]*/g,
|
|
66
|
+
replacement: '[REDACTED_JWT]',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'api_key',
|
|
70
|
+
pattern: /(?:api[_\-]?key|apikey)\s*[:=]\s*["']?[A-Za-z0-9\-._~+/]{8,}["']?/gi,
|
|
71
|
+
replacement: '[REDACTED_API_KEY]',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'generic_secret',
|
|
75
|
+
pattern: /(?:secret|private[_\-]?key|client[_\-]?secret)\s*[:=]\s*["']?[A-Za-z0-9\-._~+/]{8,}["']?/gi,
|
|
76
|
+
replacement: '[REDACTED_SECRET]',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'password_field',
|
|
80
|
+
pattern: /(?:password|passwd|pwd)\s*[:=]\s*["']?[^\s"',}{]{1,}["']?/gi,
|
|
81
|
+
replacement: '[REDACTED_PASSWORD]',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'session_id',
|
|
85
|
+
pattern: /(?:session[_\-]?id|sid|jsessionid|phpsessid)\s*[:=]\s*["']?[A-Za-z0-9\-._]{8,}["']?/gi,
|
|
86
|
+
replacement: '[REDACTED_SESSION]',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: 'cookie',
|
|
90
|
+
pattern: /(?:cookie|set-cookie)\s*[:=]\s*["']?[^\n"']{8,}["']?/gi,
|
|
91
|
+
replacement: '[REDACTED_COOKIE]',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: 'authorization',
|
|
95
|
+
pattern: /(?:authorization)\s*[:=]\s*["']?[^\n"']{8,}["']?/gi,
|
|
96
|
+
replacement: '[REDACTED_AUTH]',
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
// ── URL Sanitization ───────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Sanitize a URL: preserve path, sanitize query parameters.
|
|
104
|
+
* - Safe params: kept as-is
|
|
105
|
+
* - Sensitive params: removed entirely
|
|
106
|
+
* - Unknown params: value replaced with [PARAM_REMOVED]
|
|
107
|
+
*/
|
|
108
|
+
function sanitizeUrl(urlString) {
|
|
109
|
+
if (!urlString || typeof urlString !== 'string') return urlString;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const url = new URL(urlString);
|
|
113
|
+
const sanitizedParams = new URLSearchParams();
|
|
114
|
+
let hadParams = false;
|
|
115
|
+
|
|
116
|
+
for (const [key, value] of url.searchParams) {
|
|
117
|
+
hadParams = true;
|
|
118
|
+
const keyLower = key.toLowerCase();
|
|
119
|
+
|
|
120
|
+
if (SENSITIVE_QUERY_PARAMS.has(keyLower)) {
|
|
121
|
+
// Sensitive: remove entirely (don't even include the key)
|
|
122
|
+
continue;
|
|
123
|
+
} else if (SAFE_QUERY_PARAMS.has(keyLower)) {
|
|
124
|
+
// Safe: keep as-is
|
|
125
|
+
sanitizedParams.set(key, value);
|
|
126
|
+
} else {
|
|
127
|
+
// Unknown: keep key, mask value
|
|
128
|
+
sanitizedParams.set(key, '[PARAM_REMOVED]');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
url.search = sanitizedParams.toString();
|
|
133
|
+
|
|
134
|
+
// Also strip hash if it looks like it contains sensitive data
|
|
135
|
+
if (url.hash && url.hash.length > 100) {
|
|
136
|
+
url.hash = '';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return url.toString();
|
|
140
|
+
} catch {
|
|
141
|
+
// Not a valid URL — apply text sanitization instead
|
|
142
|
+
return sanitizeText(urlString);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── Text Sanitization ──────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Sanitize a text string by applying all regex patterns.
|
|
150
|
+
* Returns { text, redactions } where redactions counts per pattern.
|
|
151
|
+
*/
|
|
152
|
+
function sanitizeText(text, redactions = null) {
|
|
153
|
+
if (!text || typeof text !== 'string') return text;
|
|
154
|
+
|
|
155
|
+
let result = text;
|
|
156
|
+
for (const { name, pattern, replacement } of SANITIZATION_PATTERNS) {
|
|
157
|
+
const before = result;
|
|
158
|
+
// Reset regex lastIndex for global patterns
|
|
159
|
+
pattern.lastIndex = 0;
|
|
160
|
+
result = result.replace(pattern, replacement);
|
|
161
|
+
if (redactions && result !== before) {
|
|
162
|
+
const count = (before.match(pattern) || []).length;
|
|
163
|
+
pattern.lastIndex = 0;
|
|
164
|
+
redactions[name] = (redactions[name] || 0) + count;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── Deep Sanitization ──────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Recursively sanitize all string values in an object/array.
|
|
175
|
+
* URL fields get URL-specific sanitization.
|
|
176
|
+
*/
|
|
177
|
+
function sanitizeDeep(obj, redactions = {}) {
|
|
178
|
+
if (obj === null || obj === undefined) return obj;
|
|
179
|
+
|
|
180
|
+
if (typeof obj === 'string') {
|
|
181
|
+
return sanitizeText(obj, redactions);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (Array.isArray(obj)) {
|
|
185
|
+
return obj.map(item => sanitizeDeep(item, redactions));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (typeof obj === 'object') {
|
|
189
|
+
const result = {};
|
|
190
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
191
|
+
const keyLower = key.toLowerCase();
|
|
192
|
+
|
|
193
|
+
// URL fields get URL-specific sanitization
|
|
194
|
+
if (keyLower === 'url' || keyLower.endsWith('url') || keyLower === 'href') {
|
|
195
|
+
result[key] = typeof value === 'string' ? sanitizeUrl(value) : sanitizeDeep(value, redactions);
|
|
196
|
+
} else {
|
|
197
|
+
result[key] = sanitizeDeep(value, redactions);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return obj;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── Final Validation (Layer 5) ─────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Walk the entire JSON tree and flag any remaining suspicious patterns.
|
|
210
|
+
* Returns true if the data passes validation.
|
|
211
|
+
*/
|
|
212
|
+
function validateFinalReport(report) {
|
|
213
|
+
const issues = [];
|
|
214
|
+
const jsonStr = JSON.stringify(report);
|
|
215
|
+
|
|
216
|
+
// Check for any remaining email-like patterns
|
|
217
|
+
const emailCheck = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
|
|
218
|
+
const emailMatches = jsonStr.match(emailCheck);
|
|
219
|
+
if (emailMatches) {
|
|
220
|
+
issues.push(`Found ${emailMatches.length} potential email address(es)`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Check for any remaining JWT-like patterns
|
|
224
|
+
const jwtCheck = /eyJ[A-Za-z0-9\-_]{10,}/g;
|
|
225
|
+
const jwtMatches = jsonStr.match(jwtCheck);
|
|
226
|
+
if (jwtMatches) {
|
|
227
|
+
issues.push(`Found ${jwtMatches.length} potential JWT token(s)`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
passed: issues.length === 0,
|
|
232
|
+
issues,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── Build Sanitization Summary ─────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
function buildSummary(redactions) {
|
|
239
|
+
const totalRedactions = Object.values(redactions).reduce((a, b) => a + b, 0);
|
|
240
|
+
return {
|
|
241
|
+
totalRedactions,
|
|
242
|
+
redactionsByType: { ...redactions },
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ── Public API ─────────────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
sanitizeUrl,
|
|
250
|
+
sanitizeText,
|
|
251
|
+
sanitizeDeep,
|
|
252
|
+
validateFinalReport,
|
|
253
|
+
buildSummary,
|
|
254
|
+
SAFE_QUERY_PARAMS,
|
|
255
|
+
SENSITIVE_QUERY_PARAMS,
|
|
256
|
+
};
|
|
257
|
+
})();
|
|
258
|
+
|
|
259
|
+
// Make available in different contexts
|
|
260
|
+
if (typeof globalThis !== 'undefined') {
|
|
261
|
+
globalThis.Sanitizer = Sanitizer;
|
|
262
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# Bug Report Widget
|
|
2
|
+
|
|
3
|
+
A drop-in JavaScript widget for structured bug reporting. Users click a floating button, describe what happened, and download a self-contained HTML report with an annotated screenshot, interactions, console logs, JS errors, and network requests — all sanitized.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install bug-report-js
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
**Via script tag:**
|
|
18
|
+
|
|
19
|
+
```html
|
|
20
|
+
<script src="node_modules/bug-report-js/website/bug-report.js"></script>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The widget auto-initializes and auto-detects the browser language. A floating button appears bottom-right.
|
|
24
|
+
|
|
25
|
+
**Via import (bundler):**
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
import BugReportWidget from 'bug-report-js';
|
|
29
|
+
|
|
30
|
+
BugReportWidget.init({
|
|
31
|
+
language: 'en',
|
|
32
|
+
primaryColor: '#6366f1',
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Branding / Corporate Identity
|
|
39
|
+
|
|
40
|
+
### Colors
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
BugReportWidget.init({
|
|
44
|
+
primaryColor: '#E63946', // buttons, hover borders, scrollbars, report accent
|
|
45
|
+
primaryColorHover: '#C1121F', // hover state (defaults to primaryColor if omitted)
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Icon
|
|
50
|
+
|
|
51
|
+
The `icon` value is rendered as `innerHTML` — use an emoji or an inline SVG:
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
// Emoji
|
|
55
|
+
BugReportWidget.init({ icon: '🚨' });
|
|
56
|
+
|
|
57
|
+
// Inline SVG (e.g. your logo)
|
|
58
|
+
BugReportWidget.init({
|
|
59
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="white">
|
|
60
|
+
<path d="M12 2L2 7l10 5 10-5-10-5z"/>
|
|
61
|
+
</svg>`
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Onboarding Tooltip
|
|
66
|
+
|
|
67
|
+
An optional speech-bubble tooltip above the button, auto-hides after 8 seconds. Use `{icon}` as a placeholder for the configured icon. Supports HTML:
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
BugReportWidget.init({
|
|
71
|
+
tooltipMessage: '<strong>Found a bug?</strong><br>Click {icon} to report it.',
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Full Branding Example
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
BugReportWidget.init({
|
|
79
|
+
icon: '🚨',
|
|
80
|
+
primaryColor: '#0052CC',
|
|
81
|
+
primaryColorHover: '#0747A6',
|
|
82
|
+
tooltipMessage: '<strong>Found a bug?</strong><br>Click {icon} to send a report.',
|
|
83
|
+
language: 'en',
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Language & i18n
|
|
90
|
+
|
|
91
|
+
The widget auto-detects the browser language (`navigator.language`). Built-in translations: `en`, `de`.
|
|
92
|
+
|
|
93
|
+
**Force a language:**
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
BugReportWidget.init({ language: 'en' });
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Add a new language:**
|
|
100
|
+
|
|
101
|
+
```js
|
|
102
|
+
BugReportWidget.init({
|
|
103
|
+
language: 'fr',
|
|
104
|
+
translations: {
|
|
105
|
+
fr: {
|
|
106
|
+
btnTitle: 'Signaler un bug',
|
|
107
|
+
modalTitle: 'Rapport de bug',
|
|
108
|
+
actualPrompt: 'Que s\'est-il passé ?',
|
|
109
|
+
actualPlaceholder: 'État actuel…',
|
|
110
|
+
expectedPrompt: 'Qu\'est-ce que vous attendiez ?',
|
|
111
|
+
expectedPlaceholder: 'État attendu…',
|
|
112
|
+
cancel: 'Annuler',
|
|
113
|
+
download: 'Télécharger le rapport',
|
|
114
|
+
// see Translation Keys table for all keys
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Override individual strings in an existing language:**
|
|
121
|
+
|
|
122
|
+
```js
|
|
123
|
+
BugReportWidget.init({
|
|
124
|
+
translations: {
|
|
125
|
+
en: { btnTitle: 'Report an Issue' }
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Translation Keys
|
|
131
|
+
|
|
132
|
+
| Key | Default (en) | What it controls |
|
|
133
|
+
|-----|-------------|-----------------|
|
|
134
|
+
| `btnTitle` | `Create Bug Report` | Floating button tooltip |
|
|
135
|
+
| `modalTitle` | `Bug Report` | Report title |
|
|
136
|
+
| `actualPrompt` | `What happened?` | "Actual state" field heading |
|
|
137
|
+
| `actualDesc` | `Briefly describe the problem…` | "Actual state" field description |
|
|
138
|
+
| `actualPlaceholder` | `Actual state…` | "Actual state" textarea placeholder |
|
|
139
|
+
| `expectedPrompt` | `What did you expect?` | "Expected state" field heading |
|
|
140
|
+
| `expectedDesc` | `Describe the expected behavior…` | "Expected state" field description |
|
|
141
|
+
| `expectedPlaceholder` | `Expected state…` | "Expected state" textarea placeholder |
|
|
142
|
+
| `drawTitle` | `Mark Screenshot` | Screenshot annotation modal title |
|
|
143
|
+
| `drawDesc` | `You can draw on the screenshot…` | Screenshot annotation modal description |
|
|
144
|
+
| `drawInstruction` | `Draw on the screenshot…` | Instruction inside the annotation view |
|
|
145
|
+
| `cancel` | `Cancel` | Cancel button |
|
|
146
|
+
| `download` | `Download Report` | Download button |
|
|
147
|
+
| `actualLabel` | `Actual state` | Label in the downloaded report |
|
|
148
|
+
| `expectedLabel` | `Expected state` | Label in the downloaded report |
|
|
149
|
+
| `screenshotTitle` | `Annotated Screenshot…` | Screenshot section heading in report |
|
|
150
|
+
| `screenshotNA` | `Screenshot not available` | Shown when capture fails |
|
|
151
|
+
| `interactions` | `User Interactions` | Section heading in report |
|
|
152
|
+
| `consoleLogs` | `Console Logs` | Section heading in report |
|
|
153
|
+
| `jsErrors` | `JavaScript Errors` | Section heading in report |
|
|
154
|
+
| `networkRequests` | `Network Requests` | Section heading in report |
|
|
155
|
+
| `sanitizationSummary` | `Sanitization Summary` | Section heading in report |
|
|
156
|
+
| `redactions` | `redactions` | Counter label ("3 redactions") |
|
|
157
|
+
| `noRedactions` | `No redactions were necessary.` | Shown when nothing was redacted |
|
|
158
|
+
| `limitations` | `Capture Limitations` | Section heading in report |
|
|
159
|
+
| `reportTitle` | `Bug Report` | `<title>` and heading of the HTML report file |
|
|
160
|
+
| `createdAt` | `Created on` | Date prefix in report header |
|
|
161
|
+
| `metaUrl` | `URL` | Metadata card label |
|
|
162
|
+
| `metaBrowser` | `Browser` | Metadata card label |
|
|
163
|
+
| `metaViewport` | `Viewport` | Metadata card label |
|
|
164
|
+
| `metaScreenResolution` | `Screen Resolution` | Metadata card label |
|
|
165
|
+
| `metaScrollPosition` | `Scroll Position` | Metadata card label |
|
|
166
|
+
| `metaZoomLevel` | `Zoom Level` | Metadata card label |
|
|
167
|
+
| `metaUserAgent` | `User Agent` | Metadata card label |
|
|
168
|
+
| `noInfo` | `No information provided` | Fallback when user skips a field |
|
|
169
|
+
| `unknown` | `Unknown` | Fallback for undetected browser name |
|
|
170
|
+
| `generatedAt` | `Generated` | Footer prefix |
|
|
171
|
+
| `reportFooter` | `Bug Report Dashboard` | Footer label |
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Capture Limits
|
|
176
|
+
|
|
177
|
+
```js
|
|
178
|
+
BugReportWidget.init({
|
|
179
|
+
limits: {
|
|
180
|
+
interactions: 50, // default: 50
|
|
181
|
+
console: 100, // default: 100
|
|
182
|
+
errors: 50, // default: 50
|
|
183
|
+
network: 200, // default: 200
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Sanitization
|
|
191
|
+
|
|
192
|
+
Sensitive data is automatically redacted from URLs and console logs. You can extend or replace the built-in rules.
|
|
193
|
+
|
|
194
|
+
> Setting any of these fields **replaces** the built-in list for that field.
|
|
195
|
+
|
|
196
|
+
**Extend the URL parameter allowlist** (values kept as-is):
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
BugReportWidget.init({
|
|
200
|
+
sanitization: {
|
|
201
|
+
safeParams: ['q', 'page', 'sort', 'my_custom_param'],
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**Extend the sensitive parameter blocklist** (removed entirely from URLs):
|
|
207
|
+
|
|
208
|
+
```js
|
|
209
|
+
BugReportWidget.init({
|
|
210
|
+
sanitization: {
|
|
211
|
+
sensitiveParams: ['token', 'api_key', 'my_internal_id'],
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**Add custom regex redaction patterns:**
|
|
217
|
+
|
|
218
|
+
```js
|
|
219
|
+
BugReportWidget.init({
|
|
220
|
+
sanitization: {
|
|
221
|
+
patterns: [
|
|
222
|
+
{ name: 'invoice_id', p: /INV-\d{6}/g, r: '[REDACTED_INVOICE]' },
|
|
223
|
+
]
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Full `init()` Reference
|
|
231
|
+
|
|
232
|
+
```js
|
|
233
|
+
BugReportWidget.init({
|
|
234
|
+
// Appearance
|
|
235
|
+
icon: '🐛', // emoji or inline SVG string
|
|
236
|
+
primaryColor: '#6366f1', // hex color
|
|
237
|
+
primaryColorHover: '#7577f5', // hex color (defaults to primaryColor)
|
|
238
|
+
tooltipMessage: null, // HTML string, or null to disable
|
|
239
|
+
|
|
240
|
+
// Language
|
|
241
|
+
language: 'en', // 'en' | 'de' | any key added via translations
|
|
242
|
+
translations: { // add languages or override individual strings
|
|
243
|
+
en: { btnTitle: 'Report Issue' }
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
// Capture limits (ring buffer sizes)
|
|
247
|
+
limits: {
|
|
248
|
+
interactions: 50,
|
|
249
|
+
console: 100,
|
|
250
|
+
errors: 50,
|
|
251
|
+
network: 200,
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
// Sanitization
|
|
255
|
+
sanitization: {
|
|
256
|
+
safeParams: [...], // URL params whose values are kept
|
|
257
|
+
sensitiveParams: [...], // URL params removed entirely
|
|
258
|
+
patterns: [...], // { name, p: RegExp, r: string }
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## Extension vs. Widget
|
|
266
|
+
|
|
267
|
+
| Feature | Chrome Extension | Website Widget |
|
|
268
|
+
|---------|-----------------|----------------|
|
|
269
|
+
| Network capture | All requests via `webRequest` API | `fetch()` + `XHR` only |
|
|
270
|
+
| Installation | Chrome Extensions page | Single `<script>` tag or NPM |
|
|
271
|
+
| Scope | Any website | Only the site that includes it |
|
|
272
|
+
| Script/image requests | ✅ Captured | ❌ Not captured |
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Local Demo
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
cd website
|
|
280
|
+
python3 -m http.server 8080
|
|
281
|
+
# open http://localhost:8080
|
|
282
|
+
```
|