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/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
+ ```