@mhosaic/feedback 0.10.0 → 0.11.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/dist/{chunk-XSAUJEJN.mjs → chunk-AO3BXEG5.mjs} +129 -35
- package/dist/chunk-AO3BXEG5.mjs.map +1 -0
- package/dist/chunk-DKE6ELJ4.mjs +30 -0
- package/dist/chunk-DKE6ELJ4.mjs.map +1 -0
- package/dist/embed.min.js +6 -6
- package/dist/embed.min.js.map +1 -1
- package/dist/index.mjs +2 -1
- package/dist/react.mjs +2 -1
- package/dist/react.mjs.map +1 -1
- package/dist/replay.mjs +7 -2
- package/dist/replay.mjs.map +1 -1
- package/dist/webvitals.mjs +18 -1
- package/dist/webvitals.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-XSAUJEJN.mjs.map +0 -1
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
scrubCredentials
|
|
3
|
+
} from "./chunk-DKE6ELJ4.mjs";
|
|
4
|
+
|
|
1
5
|
// src/api/client.ts
|
|
2
6
|
var SCALAR_FIELDS = [
|
|
3
7
|
"description",
|
|
@@ -33,9 +37,15 @@ function createApiClient(options) {
|
|
|
33
37
|
if (payload.widget_version) {
|
|
34
38
|
form.append("widget_version", payload.widget_version);
|
|
35
39
|
}
|
|
40
|
+
const headers = {
|
|
41
|
+
Authorization: `Bearer ${options.apiKey}`
|
|
42
|
+
};
|
|
43
|
+
if (payload.synthetic) {
|
|
44
|
+
headers["X-Mhosaic-Capture-Source"] = "error-tracking";
|
|
45
|
+
}
|
|
36
46
|
const response = await fetcher(`${endpoint}/api/feedback/v1/reports/`, {
|
|
37
47
|
method: "POST",
|
|
38
|
-
headers
|
|
48
|
+
headers,
|
|
39
49
|
body: form
|
|
40
50
|
});
|
|
41
51
|
if (!response.ok) {
|
|
@@ -128,7 +138,28 @@ function createApiClient(options) {
|
|
|
128
138
|
}
|
|
129
139
|
|
|
130
140
|
// src/capture/urlSanitizer.ts
|
|
131
|
-
var
|
|
141
|
+
var SENSITIVE_NAME = /token|key|password|secret|auth|session|sig|credential|bearer|cookie/i;
|
|
142
|
+
var SENSITIVE_VALUE_PATTERNS = [
|
|
143
|
+
// JWT: three base64url segments separated by dots, header begins with eyJ.
|
|
144
|
+
/^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/,
|
|
145
|
+
// Mhosaic feedback keys (the project's own format).
|
|
146
|
+
/^(?:sk|pk)_proj_[A-Za-z0-9_-]{16,}$/,
|
|
147
|
+
// GitHub PATs (ghp/gho/ghu/ghs/ghr prefixes, 36+ chars).
|
|
148
|
+
/^gh[pousr]_[A-Za-z0-9]{30,}$/,
|
|
149
|
+
// Stripe live/test secret keys.
|
|
150
|
+
/^(?:sk|pk|rk|whsec)_(?:live|test)_[A-Za-z0-9]{16,}$/,
|
|
151
|
+
// AWS access key id.
|
|
152
|
+
/^AKIA[0-9A-Z]{12,}$/,
|
|
153
|
+
// Slack bot tokens.
|
|
154
|
+
/^xox[abprso]-[A-Za-z0-9-]{12,}$/,
|
|
155
|
+
// Generic high-entropy: hex 40+ chars (SHA-1, SHA-256) or long alnum 32+.
|
|
156
|
+
/^[a-f0-9]{40,}$/i,
|
|
157
|
+
// Long opaque base64-ish string (common for OAuth codes & session ids).
|
|
158
|
+
/^[A-Za-z0-9_-]{40,}$/
|
|
159
|
+
];
|
|
160
|
+
function looksLikeCredential(value) {
|
|
161
|
+
return SENSITIVE_VALUE_PATTERNS.some((re) => re.test(value));
|
|
162
|
+
}
|
|
132
163
|
function sanitizeUrl(url) {
|
|
133
164
|
try {
|
|
134
165
|
const isAbsolute = /^https?:\/\//i.test(url);
|
|
@@ -138,9 +169,14 @@ function sanitizeUrl(url) {
|
|
|
138
169
|
const u = new URL(url, base);
|
|
139
170
|
const clean = new URLSearchParams();
|
|
140
171
|
u.searchParams.forEach((value, name) => {
|
|
141
|
-
|
|
172
|
+
if (SENSITIVE_NAME.test(name) || looksLikeCredential(value)) {
|
|
173
|
+
clean.set(name, "[redacted]");
|
|
174
|
+
} else {
|
|
175
|
+
clean.set(name, value);
|
|
176
|
+
}
|
|
142
177
|
});
|
|
143
178
|
u.search = clean.toString();
|
|
179
|
+
u.hash = u.hash ? "#[redacted]" : "";
|
|
144
180
|
return u.toString();
|
|
145
181
|
} catch {
|
|
146
182
|
return url;
|
|
@@ -152,7 +188,16 @@ function collectDevice() {
|
|
|
152
188
|
const nav = navigator;
|
|
153
189
|
const connection = nav.connection?.effectiveType;
|
|
154
190
|
const deviceMemory = nav.deviceMemory;
|
|
155
|
-
const
|
|
191
|
+
const rawReferrer = document.referrer || void 0;
|
|
192
|
+
const referrer = rawReferrer !== void 0 ? sanitizeUrl(rawReferrer) : void 0;
|
|
193
|
+
const sanitizedHere = sanitizeUrl(window.location.pathname + window.location.search);
|
|
194
|
+
let pathname;
|
|
195
|
+
try {
|
|
196
|
+
const parsed = new URL(sanitizedHere, window.location.origin);
|
|
197
|
+
pathname = parsed.pathname + parsed.search;
|
|
198
|
+
} catch {
|
|
199
|
+
pathname = window.location.pathname;
|
|
200
|
+
}
|
|
156
201
|
return {
|
|
157
202
|
viewport: { w: window.innerWidth, h: window.innerHeight, dpr: window.devicePixelRatio || 1 },
|
|
158
203
|
screen: { w: window.screen.width, h: window.screen.height },
|
|
@@ -165,8 +210,12 @@ function collectDevice() {
|
|
|
165
210
|
...deviceMemory !== void 0 && { deviceMemory },
|
|
166
211
|
hardwareConcurrency: nav.hardwareConcurrency,
|
|
167
212
|
...referrer !== void 0 && { referrer },
|
|
168
|
-
|
|
169
|
-
|
|
213
|
+
// Hosts commonly stuff credentials / PII into page titles
|
|
214
|
+
// ("Inbox (3) — bob@x.com", "Reset password — token=abc"). Cap to a
|
|
215
|
+
// sane length and run through the same scrubber the console/error
|
|
216
|
+
// paths use.
|
|
217
|
+
title: scrubCredentials(document.title || "").slice(0, 200),
|
|
218
|
+
pathname
|
|
170
219
|
};
|
|
171
220
|
}
|
|
172
221
|
|
|
@@ -196,7 +245,8 @@ function installConsolePatch(buffer) {
|
|
|
196
245
|
originals[level] = original;
|
|
197
246
|
console[level] = function patched(...args) {
|
|
198
247
|
try {
|
|
199
|
-
const
|
|
248
|
+
const rawMessage = args.map(safeStringify).join(" ");
|
|
249
|
+
const message = scrubCredentials(rawMessage).slice(0, 2e3);
|
|
200
250
|
const entry = { level, message, ts: Date.now() };
|
|
201
251
|
if (level === "error") {
|
|
202
252
|
const stack = new Error().stack;
|
|
@@ -229,13 +279,14 @@ function installFetchPatch(buffer, sanitize) {
|
|
|
229
279
|
buffer.push({ url: sanitize(url), method, status: response.status, durationMs: Math.round(performance.now() - start), ts: Date.now() });
|
|
230
280
|
return response;
|
|
231
281
|
} catch (err) {
|
|
282
|
+
const rawErr = err instanceof Error ? err.message : String(err);
|
|
232
283
|
buffer.push({
|
|
233
284
|
url: sanitize(url),
|
|
234
285
|
method,
|
|
235
286
|
status: 0,
|
|
236
287
|
durationMs: Math.round(performance.now() - start),
|
|
237
288
|
ts: Date.now(),
|
|
238
|
-
error:
|
|
289
|
+
error: scrubCredentials(rawErr)
|
|
239
290
|
});
|
|
240
291
|
throw err;
|
|
241
292
|
}
|
|
@@ -282,9 +333,10 @@ function installErrorHandlers(buffer) {
|
|
|
282
333
|
if (typeof window === "undefined") return () => {
|
|
283
334
|
};
|
|
284
335
|
const onError = (e) => {
|
|
285
|
-
const
|
|
336
|
+
const rawStack = e.error instanceof Error ? e.error.stack : void 0;
|
|
337
|
+
const stack = rawStack !== void 0 ? scrubCredentials(rawStack) : void 0;
|
|
286
338
|
buffer.push({
|
|
287
|
-
message: e.message || "Unknown error",
|
|
339
|
+
message: scrubCredentials(e.message || "Unknown error"),
|
|
288
340
|
...stack !== void 0 && { stack },
|
|
289
341
|
ts: Date.now(),
|
|
290
342
|
source: "window.error"
|
|
@@ -292,16 +344,17 @@ function installErrorHandlers(buffer) {
|
|
|
292
344
|
};
|
|
293
345
|
const onRejection = (e) => {
|
|
294
346
|
const reason = e.reason;
|
|
295
|
-
const
|
|
347
|
+
const rawMessage = reason instanceof Error ? reason.message : typeof reason === "string" ? reason : (() => {
|
|
296
348
|
try {
|
|
297
349
|
return JSON.stringify(reason);
|
|
298
350
|
} catch {
|
|
299
351
|
return String(reason);
|
|
300
352
|
}
|
|
301
353
|
})();
|
|
302
|
-
const
|
|
354
|
+
const rawStack = reason instanceof Error ? reason.stack : void 0;
|
|
355
|
+
const stack = rawStack !== void 0 ? scrubCredentials(rawStack) : void 0;
|
|
303
356
|
buffer.push({
|
|
304
|
-
message,
|
|
357
|
+
message: scrubCredentials(rawMessage),
|
|
305
358
|
...stack !== void 0 && { stack },
|
|
306
359
|
ts: Date.now(),
|
|
307
360
|
source: "unhandledrejection"
|
|
@@ -330,7 +383,7 @@ function createPerformanceCollector(slowResourceMs = 1e3) {
|
|
|
330
383
|
} else if (entry.entryType === "resource") {
|
|
331
384
|
const e = entry;
|
|
332
385
|
if (e.duration > slowResourceMs) {
|
|
333
|
-
slowResources.push({ name: e.name, duration: e.duration, initiatorType: e.initiatorType });
|
|
386
|
+
slowResources.push({ name: sanitizeUrl(e.name), duration: e.duration, initiatorType: e.initiatorType });
|
|
334
387
|
while (slowResources.length > 20) slowResources.shift();
|
|
335
388
|
}
|
|
336
389
|
}
|
|
@@ -413,6 +466,19 @@ function installCapture(options = {}) {
|
|
|
413
466
|
}
|
|
414
467
|
|
|
415
468
|
// src/screenshot/index.ts
|
|
469
|
+
var DEFAULT_REDACTED_INPUT_SELECTORS = [
|
|
470
|
+
'input[type="password"]',
|
|
471
|
+
'input[type="tel"]',
|
|
472
|
+
'input[autocomplete*="cc-"]',
|
|
473
|
+
'input[autocomplete*="current-password"]',
|
|
474
|
+
'input[autocomplete*="new-password"]',
|
|
475
|
+
'input[autocomplete*="one-time-code"]',
|
|
476
|
+
'input[name*="cvv" i]',
|
|
477
|
+
'input[name*="ccv" i]',
|
|
478
|
+
'input[name*="cardnum" i]',
|
|
479
|
+
'input[name*="card-num" i]'
|
|
480
|
+
];
|
|
481
|
+
var REDACTION_MASK = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
|
|
416
482
|
function isMaskedElement(el) {
|
|
417
483
|
return el.hasAttribute("data-mfb-mask") || el.classList.contains("mfb-mask");
|
|
418
484
|
}
|
|
@@ -421,13 +487,33 @@ function prepareMaskMatcher(selectors) {
|
|
|
421
487
|
if (!joined) return isMaskedElement;
|
|
422
488
|
return (el) => isMaskedElement(el) || el.matches(joined);
|
|
423
489
|
}
|
|
490
|
+
function redactSensitiveInputs(clonedDoc, extraSelectors) {
|
|
491
|
+
const selectors = [
|
|
492
|
+
...DEFAULT_REDACTED_INPUT_SELECTORS,
|
|
493
|
+
...extraSelectors,
|
|
494
|
+
"[data-mfb-mask]",
|
|
495
|
+
".mfb-mask"
|
|
496
|
+
].join(",");
|
|
497
|
+
const elements = clonedDoc.querySelectorAll(selectors);
|
|
498
|
+
elements.forEach((el) => {
|
|
499
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
500
|
+
const len = Math.min(Math.max(el.value.length || 8, 4), 16);
|
|
501
|
+
el.value = REDACTION_MASK.slice(0, len);
|
|
502
|
+
el.setAttribute("value", el.value);
|
|
503
|
+
} else if (el instanceof HTMLElement) {
|
|
504
|
+
el.textContent = REDACTION_MASK;
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
}
|
|
424
508
|
async function takeScreenshot(target, options = {}) {
|
|
425
509
|
if (typeof document === "undefined") return null;
|
|
426
510
|
try {
|
|
427
511
|
const html2canvas = (await import("html2canvas-pro")).default;
|
|
428
|
-
const
|
|
512
|
+
const extraSelectors = options.mask ?? [];
|
|
513
|
+
const matcher = prepareMaskMatcher(extraSelectors);
|
|
429
514
|
const canvas = await html2canvas(target, {
|
|
430
515
|
ignoreElements: (el) => matcher(el),
|
|
516
|
+
onclone: (clonedDoc) => redactSensitiveInputs(clonedDoc, extraSelectors),
|
|
431
517
|
useCORS: true,
|
|
432
518
|
logging: false,
|
|
433
519
|
backgroundColor: null
|
|
@@ -1181,28 +1267,36 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
|
|
|
1181
1267
|
}
|
|
1182
1268
|
};
|
|
1183
1269
|
const handleMouseMove = (e) => {
|
|
1184
|
-
if (!isDrawingRef.current
|
|
1270
|
+
if (!isDrawingRef.current) return;
|
|
1185
1271
|
const { x, y } = getCanvasCoords(e);
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1272
|
+
setDraftShape((current) => {
|
|
1273
|
+
if (!current) return current;
|
|
1274
|
+
if (current.kind === "rectangle" || current.kind === "highlight" || current.kind === "blur") {
|
|
1275
|
+
return { ...current, w: x - current.x, h: y - current.y };
|
|
1276
|
+
}
|
|
1277
|
+
if (current.kind === "arrow") {
|
|
1278
|
+
return { ...current, x2: x, y2: y };
|
|
1279
|
+
}
|
|
1280
|
+
if (current.kind === "freehand") {
|
|
1281
|
+
return { ...current, points: [...current.points, { x, y }] };
|
|
1282
|
+
}
|
|
1283
|
+
return current;
|
|
1284
|
+
});
|
|
1193
1285
|
};
|
|
1194
1286
|
const handleMouseUp = () => {
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1287
|
+
setDraftShape((current) => {
|
|
1288
|
+
if (isDrawingRef.current && current) {
|
|
1289
|
+
const isTiny = (current.kind === "rectangle" || current.kind === "highlight" || current.kind === "blur") && Math.abs(current.w) < 4 && Math.abs(current.h) < 4 || current.kind === "arrow" && Math.hypot(
|
|
1290
|
+
current.x2 - current.x1,
|
|
1291
|
+
current.y2 - current.y1
|
|
1292
|
+
) < 4 || current.kind === "freehand" && current.points.length < 3;
|
|
1293
|
+
if (!isTiny) {
|
|
1294
|
+
addShape(current);
|
|
1295
|
+
}
|
|
1202
1296
|
}
|
|
1203
|
-
|
|
1297
|
+
return null;
|
|
1298
|
+
});
|
|
1204
1299
|
isDrawingRef.current = false;
|
|
1205
|
-
setDraftShape(null);
|
|
1206
1300
|
};
|
|
1207
1301
|
const handleSave = async () => {
|
|
1208
1302
|
const canvas = canvasRef.current;
|
|
@@ -3483,8 +3577,8 @@ function createFeedback(config) {
|
|
|
3483
3577
|
capture_method: screenshot ? manualScreenshot ? "manual" : "html2canvas" : "none",
|
|
3484
3578
|
technical_context
|
|
3485
3579
|
};
|
|
3486
|
-
if ("0.
|
|
3487
|
-
payload.widget_version = "0.
|
|
3580
|
+
if ("0.11.0") {
|
|
3581
|
+
payload.widget_version = "0.11.0";
|
|
3488
3582
|
}
|
|
3489
3583
|
if (screenshot) payload.screenshot = screenshot;
|
|
3490
3584
|
if (values.synthetic) payload.synthetic = true;
|
|
@@ -3570,4 +3664,4 @@ function createFeedback(config) {
|
|
|
3570
3664
|
export {
|
|
3571
3665
|
createFeedback
|
|
3572
3666
|
};
|
|
3573
|
-
//# sourceMappingURL=chunk-
|
|
3667
|
+
//# sourceMappingURL=chunk-AO3BXEG5.mjs.map
|