@mhosaic/feedback 0.12.2 → 0.14.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-TBT2OODO.mjs → chunk-A4MBOFRR.mjs} +47 -181
- package/dist/chunk-A4MBOFRR.mjs.map +1 -0
- package/dist/embed.min.js +4 -38
- package/dist/embed.min.js.map +1 -1
- package/dist/error-tracking.d.ts +1 -1
- package/dist/error-tracking.mjs +12 -1
- package/dist/error-tracking.mjs.map +1 -1
- package/dist/{index-DK9sbiTf.d.ts → index-snrtK3_E.d.ts} +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +1 -1
- package/dist/react.d.ts +2 -2
- package/dist/react.mjs +1 -1
- package/dist/replay.d.ts +1 -1
- package/dist/{types-ZCdu32FU.d.ts → types-BrsktaE1.d.ts} +11 -0
- package/dist/webvitals.d.ts +1 -1
- package/package.json +1 -1
- package/dist/chunk-TBT2OODO.mjs.map +0 -1
|
@@ -43,6 +43,13 @@ function createApiClient(options) {
|
|
|
43
43
|
if (payload.synthetic) {
|
|
44
44
|
headers["X-Mhosaic-Capture-Source"] = "error-tracking";
|
|
45
45
|
}
|
|
46
|
+
const signed = options.getSignedIdentity?.();
|
|
47
|
+
if (signed && signed.userHash && signed.exp && payload.user?.id) {
|
|
48
|
+
headers["X-Mhosaic-User"] = String(payload.user.id);
|
|
49
|
+
headers["X-Mhosaic-User-Hmac"] = signed.userHash;
|
|
50
|
+
headers["X-Mhosaic-User-Exp"] = String(signed.exp);
|
|
51
|
+
if (signed.email) headers["X-Mhosaic-User-Email"] = signed.email;
|
|
52
|
+
}
|
|
46
53
|
const response = await fetcher(`${endpoint}/api/feedback/v1/reports/`, {
|
|
47
54
|
method: "POST",
|
|
48
55
|
headers,
|
|
@@ -55,10 +62,17 @@ function createApiClient(options) {
|
|
|
55
62
|
return response.json();
|
|
56
63
|
}
|
|
57
64
|
function widgetHeaders(externalId) {
|
|
58
|
-
|
|
65
|
+
const headers = {
|
|
59
66
|
Authorization: `Bearer ${options.apiKey}`,
|
|
60
67
|
"X-Mhosaic-User": externalId
|
|
61
68
|
};
|
|
69
|
+
const signed = options.getSignedIdentity?.();
|
|
70
|
+
if (signed && signed.userHash && signed.exp) {
|
|
71
|
+
headers["X-Mhosaic-User-Hmac"] = signed.userHash;
|
|
72
|
+
headers["X-Mhosaic-User-Exp"] = String(signed.exp);
|
|
73
|
+
if (signed.email) headers["X-Mhosaic-User-Email"] = signed.email;
|
|
74
|
+
}
|
|
75
|
+
return headers;
|
|
62
76
|
}
|
|
63
77
|
async function listMine(externalId) {
|
|
64
78
|
const response = await fetcher(`${endpoint}/api/feedback/v1/reports/widget/mine/`, {
|
|
@@ -537,10 +551,6 @@ var DEFAULT_STRINGS = {
|
|
|
537
551
|
"form.screenshot.annotate": "Annotate",
|
|
538
552
|
"form.screenshot.error_type": "Unsupported file type. Use PNG, JPEG or WebP.",
|
|
539
553
|
"form.screenshot.error_size": "File too large (max {max} MB).",
|
|
540
|
-
"form.screenshot.or": "or",
|
|
541
|
-
"form.screenshot.capture_page": "Capture this page",
|
|
542
|
-
"form.screenshot.capture_page_hint": "Pick a tab or window to share",
|
|
543
|
-
"form.screenshot.capture_error": "Couldn\u2019t capture the page. Try uploading a screenshot instead.",
|
|
544
554
|
"form.context.label": "Page",
|
|
545
555
|
"type.bug": "Bug",
|
|
546
556
|
"type.feature": "Feature request",
|
|
@@ -674,10 +684,6 @@ var FRENCH_STRINGS = {
|
|
|
674
684
|
"form.screenshot.annotate": "Annoter",
|
|
675
685
|
"form.screenshot.error_type": "Format non support\xE9. Utilisez PNG, JPEG ou WebP.",
|
|
676
686
|
"form.screenshot.error_size": "Fichier trop volumineux (max {max} Mo).",
|
|
677
|
-
"form.screenshot.or": "ou",
|
|
678
|
-
"form.screenshot.capture_page": "Capturer la page",
|
|
679
|
-
"form.screenshot.capture_page_hint": "Choisissez un onglet ou une fen\xEAtre \xE0 partager",
|
|
680
|
-
"form.screenshot.capture_error": "Impossible de capturer la page. T\xE9l\xE9versez une capture \xE0 la place.",
|
|
681
687
|
"form.context.label": "Page",
|
|
682
688
|
"type.bug": "Bogue",
|
|
683
689
|
"type.feature": "Suggestion",
|
|
@@ -866,65 +872,6 @@ function truncateUrl(url, maxLength = 80) {
|
|
|
866
872
|
const keepEnd = maxLength - 1 - keepStart;
|
|
867
873
|
return `${url.slice(0, keepStart)}\u2026${url.slice(url.length - keepEnd)}`;
|
|
868
874
|
}
|
|
869
|
-
async function captureWithDisplayMedia() {
|
|
870
|
-
if (typeof navigator === "undefined" || !navigator.mediaDevices?.getDisplayMedia) {
|
|
871
|
-
return null;
|
|
872
|
-
}
|
|
873
|
-
let stream = null;
|
|
874
|
-
try {
|
|
875
|
-
stream = await navigator.mediaDevices.getDisplayMedia({
|
|
876
|
-
video: { displaySurface: "browser" },
|
|
877
|
-
audio: false
|
|
878
|
-
// Hint that audio isn't needed; not all browsers honor this but it
|
|
879
|
-
// avoids the audio-share toggle being defaulted on in some.
|
|
880
|
-
});
|
|
881
|
-
} catch {
|
|
882
|
-
return null;
|
|
883
|
-
}
|
|
884
|
-
try {
|
|
885
|
-
const track = stream.getVideoTracks()[0];
|
|
886
|
-
if (!track) return null;
|
|
887
|
-
const ImageCaptureCtor = window.ImageCapture;
|
|
888
|
-
let bitmap = null;
|
|
889
|
-
if (ImageCaptureCtor) {
|
|
890
|
-
try {
|
|
891
|
-
const ic = new ImageCaptureCtor(track);
|
|
892
|
-
bitmap = await ic.grabFrame();
|
|
893
|
-
} catch {
|
|
894
|
-
bitmap = null;
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
if (!bitmap) {
|
|
898
|
-
bitmap = await grabFrameViaVideo(stream);
|
|
899
|
-
}
|
|
900
|
-
if (!bitmap) return null;
|
|
901
|
-
const canvas = document.createElement("canvas");
|
|
902
|
-
canvas.width = bitmap.width;
|
|
903
|
-
canvas.height = bitmap.height;
|
|
904
|
-
const ctx = canvas.getContext("2d");
|
|
905
|
-
if (!ctx) return null;
|
|
906
|
-
ctx.drawImage(bitmap, 0, 0);
|
|
907
|
-
return await new Promise(
|
|
908
|
-
(resolve) => canvas.toBlob(resolve, "image/png")
|
|
909
|
-
);
|
|
910
|
-
} finally {
|
|
911
|
-
stream.getTracks().forEach((t) => t.stop());
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
async function grabFrameViaVideo(stream) {
|
|
915
|
-
const video = document.createElement("video");
|
|
916
|
-
video.muted = true;
|
|
917
|
-
video.playsInline = true;
|
|
918
|
-
video.srcObject = stream;
|
|
919
|
-
try {
|
|
920
|
-
await video.play();
|
|
921
|
-
await new Promise((r) => requestAnimationFrame(() => r()));
|
|
922
|
-
if (!video.videoWidth || !video.videoHeight) return null;
|
|
923
|
-
return await createImageBitmap(video);
|
|
924
|
-
} catch {
|
|
925
|
-
return null;
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
875
|
|
|
929
876
|
// src/widget/ReportDetailView.tsx
|
|
930
877
|
import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-runtime";
|
|
@@ -2333,10 +2280,8 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
|
|
|
2333
2280
|
const [localError, setLocalError] = useState5("");
|
|
2334
2281
|
const [screenshotBlob, setScreenshotBlob] = useState5(null);
|
|
2335
2282
|
const [screenshotPreview, setScreenshotPreview] = useState5(null);
|
|
2336
|
-
const [screenshotMethod, setScreenshotMethod] = useState5("manual");
|
|
2337
2283
|
const [isDragOver, setIsDragOver] = useState5(false);
|
|
2338
2284
|
const [annotatorOpen, setAnnotatorOpen] = useState5(false);
|
|
2339
|
-
const [capturing, setCapturing] = useState5(false);
|
|
2340
2285
|
const fileInputRef = useRef4(null);
|
|
2341
2286
|
const dropZoneRef = useRef4(null);
|
|
2342
2287
|
const submitting = status === "submitting";
|
|
@@ -2347,7 +2292,7 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
|
|
|
2347
2292
|
if (screenshotPreview) URL.revokeObjectURL(screenshotPreview);
|
|
2348
2293
|
};
|
|
2349
2294
|
}, [screenshotPreview]);
|
|
2350
|
-
const acceptFile = (file
|
|
2295
|
+
const acceptFile = (file) => {
|
|
2351
2296
|
setLocalError("");
|
|
2352
2297
|
if (file instanceof File) {
|
|
2353
2298
|
const err = validateScreenshotFile(file);
|
|
@@ -2361,41 +2306,14 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
|
|
|
2361
2306
|
if (screenshotPreview) URL.revokeObjectURL(screenshotPreview);
|
|
2362
2307
|
setScreenshotBlob(file);
|
|
2363
2308
|
setScreenshotPreview(URL.createObjectURL(file));
|
|
2364
|
-
setScreenshotMethod(method);
|
|
2365
2309
|
};
|
|
2366
2310
|
const clearScreenshot = () => {
|
|
2367
2311
|
if (screenshotPreview) URL.revokeObjectURL(screenshotPreview);
|
|
2368
2312
|
setScreenshotPreview(null);
|
|
2369
2313
|
setScreenshotBlob(null);
|
|
2370
|
-
setScreenshotMethod("manual");
|
|
2371
2314
|
setLocalError("");
|
|
2372
2315
|
if (fileInputRef.current) fileInputRef.current.value = "";
|
|
2373
2316
|
};
|
|
2374
|
-
const handleCapturePage = async () => {
|
|
2375
|
-
if (capturing) return;
|
|
2376
|
-
setLocalError("");
|
|
2377
|
-
setCapturing(true);
|
|
2378
|
-
try {
|
|
2379
|
-
const blob = await captureWithDisplayMedia();
|
|
2380
|
-
if (!blob) {
|
|
2381
|
-
if (typeof navigator === "undefined" || !navigator.mediaDevices?.getDisplayMedia) {
|
|
2382
|
-
setLocalError(strings["form.screenshot.capture_error"]);
|
|
2383
|
-
}
|
|
2384
|
-
return;
|
|
2385
|
-
}
|
|
2386
|
-
if (blob.size > 10 * 1024 * 1024) {
|
|
2387
|
-
setLocalError(
|
|
2388
|
-
strings["form.screenshot.error_size"].replace("{max}", "10")
|
|
2389
|
-
);
|
|
2390
|
-
return;
|
|
2391
|
-
}
|
|
2392
|
-
acceptFile(blob, "display_media");
|
|
2393
|
-
} catch {
|
|
2394
|
-
setLocalError(strings["form.screenshot.capture_error"]);
|
|
2395
|
-
} finally {
|
|
2396
|
-
setCapturing(false);
|
|
2397
|
-
}
|
|
2398
|
-
};
|
|
2399
2317
|
const handleFileInputChange = (e) => {
|
|
2400
2318
|
const file = e.target.files?.[0];
|
|
2401
2319
|
if (file) acceptFile(file);
|
|
@@ -2457,7 +2375,7 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
|
|
|
2457
2375
|
};
|
|
2458
2376
|
if (screenshotBlob) {
|
|
2459
2377
|
values.screenshot = screenshotBlob;
|
|
2460
|
-
values.capture_method =
|
|
2378
|
+
values.capture_method = "manual";
|
|
2461
2379
|
}
|
|
2462
2380
|
onSubmit(values);
|
|
2463
2381
|
};
|
|
@@ -2570,43 +2488,7 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
|
|
|
2570
2488
|
/* @__PURE__ */ jsx8("div", { class: "screenshot-formats", children: strings["form.screenshot.formats"] })
|
|
2571
2489
|
]
|
|
2572
2490
|
}
|
|
2573
|
-
)
|
|
2574
|
-
!screenshotPreview && /* @__PURE__ */ jsxs7("div", { class: "screenshot-alt", children: [
|
|
2575
|
-
/* @__PURE__ */ jsx8("span", { class: "screenshot-or", children: strings["form.screenshot.or"] }),
|
|
2576
|
-
/* @__PURE__ */ jsxs7(
|
|
2577
|
-
"button",
|
|
2578
|
-
{
|
|
2579
|
-
type: "button",
|
|
2580
|
-
class: "btn btn--ghost screenshot-capture-page",
|
|
2581
|
-
onClick: handleCapturePage,
|
|
2582
|
-
disabled: capturing || submitting,
|
|
2583
|
-
"aria-label": strings["form.screenshot.capture_page"],
|
|
2584
|
-
children: [
|
|
2585
|
-
/* @__PURE__ */ jsxs7(
|
|
2586
|
-
"svg",
|
|
2587
|
-
{
|
|
2588
|
-
width: "14",
|
|
2589
|
-
height: "14",
|
|
2590
|
-
viewBox: "0 0 24 24",
|
|
2591
|
-
fill: "none",
|
|
2592
|
-
stroke: "currentColor",
|
|
2593
|
-
"stroke-width": "2",
|
|
2594
|
-
"stroke-linecap": "round",
|
|
2595
|
-
"stroke-linejoin": "round",
|
|
2596
|
-
"aria-hidden": "true",
|
|
2597
|
-
children: [
|
|
2598
|
-
/* @__PURE__ */ jsx8("rect", { x: "2", y: "3", width: "20", height: "14", rx: "2", ry: "2" }),
|
|
2599
|
-
/* @__PURE__ */ jsx8("line", { x1: "8", y1: "21", x2: "16", y2: "21" }),
|
|
2600
|
-
/* @__PURE__ */ jsx8("line", { x1: "12", y1: "17", x2: "12", y2: "21" })
|
|
2601
|
-
]
|
|
2602
|
-
}
|
|
2603
|
-
),
|
|
2604
|
-
capturing ? strings["form.submitting"] : strings["form.screenshot.capture_page"]
|
|
2605
|
-
]
|
|
2606
|
-
}
|
|
2607
|
-
),
|
|
2608
|
-
/* @__PURE__ */ jsx8("span", { class: "screenshot-capture-hint", children: strings["form.screenshot.capture_page_hint"] })
|
|
2609
|
-
] })
|
|
2491
|
+
)
|
|
2610
2492
|
] }),
|
|
2611
2493
|
pageUrl && /* @__PURE__ */ jsxs7("div", { class: "page-context", title: pageUrl, children: [
|
|
2612
2494
|
/* @__PURE__ */ jsx8("span", { class: "page-context-label", children: strings["form.context.label"] }),
|
|
@@ -3247,40 +3129,6 @@ var WIDGET_STYLES = `
|
|
|
3247
3129
|
.screenshot-cta strong { color: var(--mfb-accent); font-weight: 600; }
|
|
3248
3130
|
.screenshot-formats { font-size: var(--mfb-text-xs); color: var(--mfb-text-muted); }
|
|
3249
3131
|
|
|
3250
|
-
/* "or \u2014 Capture this page" row sits below the dropzone. The thin "or"
|
|
3251
|
-
* separator borrows the pattern from auth forms so the second option
|
|
3252
|
-
* reads as an alternative, not a follow-up step. v0.12. */
|
|
3253
|
-
.screenshot-alt {
|
|
3254
|
-
display: flex;
|
|
3255
|
-
align-items: center;
|
|
3256
|
-
gap: 12px;
|
|
3257
|
-
margin-top: 8px;
|
|
3258
|
-
flex-wrap: wrap;
|
|
3259
|
-
}
|
|
3260
|
-
.screenshot-or {
|
|
3261
|
-
position: relative;
|
|
3262
|
-
font-size: var(--mfb-text-xs);
|
|
3263
|
-
color: var(--mfb-text-muted);
|
|
3264
|
-
text-transform: uppercase;
|
|
3265
|
-
letter-spacing: 0.08em;
|
|
3266
|
-
padding: 0 4px;
|
|
3267
|
-
}
|
|
3268
|
-
.screenshot-capture-page {
|
|
3269
|
-
display: inline-flex;
|
|
3270
|
-
align-items: center;
|
|
3271
|
-
gap: 6px;
|
|
3272
|
-
}
|
|
3273
|
-
.screenshot-capture-page:disabled {
|
|
3274
|
-
opacity: 0.55;
|
|
3275
|
-
cursor: progress;
|
|
3276
|
-
}
|
|
3277
|
-
.screenshot-capture-hint {
|
|
3278
|
-
font-size: var(--mfb-text-xs);
|
|
3279
|
-
color: var(--mfb-text-muted);
|
|
3280
|
-
flex: 1 1 100%;
|
|
3281
|
-
margin-top: 4px;
|
|
3282
|
-
}
|
|
3283
|
-
|
|
3284
3132
|
.screenshot-preview {
|
|
3285
3133
|
position: relative;
|
|
3286
3134
|
border: 1px solid var(--mfb-border);
|
|
@@ -4516,6 +4364,7 @@ function mountWidget(options) {
|
|
|
4516
4364
|
const mountPoint = document.createElement("div");
|
|
4517
4365
|
shadow.appendChild(mountPoint);
|
|
4518
4366
|
let currentState = { open: false, status: "idle", tab: "send" };
|
|
4367
|
+
let postSubmitTimer = null;
|
|
4519
4368
|
function rerender(state) {
|
|
4520
4369
|
currentState = state;
|
|
4521
4370
|
render(h(Root, { state }), mountPoint);
|
|
@@ -4531,8 +4380,10 @@ function mountWidget(options) {
|
|
|
4531
4380
|
try {
|
|
4532
4381
|
await options.onSubmit(values);
|
|
4533
4382
|
rerender({ ...currentState, status: "success" });
|
|
4534
|
-
|
|
4535
|
-
|
|
4383
|
+
if (postSubmitTimer !== null) clearTimeout(postSubmitTimer);
|
|
4384
|
+
postSubmitTimer = setTimeout(() => {
|
|
4385
|
+
postSubmitTimer = null;
|
|
4386
|
+
rerender({
|
|
4536
4387
|
...currentState,
|
|
4537
4388
|
open: false,
|
|
4538
4389
|
status: "idle",
|
|
@@ -4540,9 +4391,8 @@ function mountWidget(options) {
|
|
|
4540
4391
|
// they immediately see their just-filed report in the list
|
|
4541
4392
|
// (and the conversation that's about to start).
|
|
4542
4393
|
tab: options.api ? "mine" : "send"
|
|
4543
|
-
})
|
|
4544
|
-
|
|
4545
|
-
);
|
|
4394
|
+
});
|
|
4395
|
+
}, 1200);
|
|
4546
4396
|
} catch (err) {
|
|
4547
4397
|
rerender({
|
|
4548
4398
|
...currentState,
|
|
@@ -4677,6 +4527,10 @@ function mountWidget(options) {
|
|
|
4677
4527
|
rerender({ ...currentState, open: false, status: "idle" });
|
|
4678
4528
|
},
|
|
4679
4529
|
dispose() {
|
|
4530
|
+
if (postSubmitTimer !== null) {
|
|
4531
|
+
clearTimeout(postSubmitTimer);
|
|
4532
|
+
postSubmitTimer = null;
|
|
4533
|
+
}
|
|
4680
4534
|
render(null, mountPoint);
|
|
4681
4535
|
options.host.innerHTML = "";
|
|
4682
4536
|
},
|
|
@@ -4697,14 +4551,26 @@ function createFeedback(config) {
|
|
|
4697
4551
|
const capture = installCapture({
|
|
4698
4552
|
...config.sanitizeUrl !== void 0 && { sanitizeUrl: config.sanitizeUrl }
|
|
4699
4553
|
});
|
|
4554
|
+
let user = config.user;
|
|
4555
|
+
let metadata = config.metadata ?? {};
|
|
4700
4556
|
const api = createApiClient({
|
|
4701
4557
|
apiKey: config.apiKey,
|
|
4702
4558
|
endpoint: config.endpoint,
|
|
4703
4559
|
...config.fetchImpl !== void 0 && { fetch: config.fetchImpl },
|
|
4704
|
-
...config.beforeSend !== void 0 && { beforeSend: config.beforeSend }
|
|
4560
|
+
...config.beforeSend !== void 0 && { beforeSend: config.beforeSend },
|
|
4561
|
+
// v0.13: the API client reads this on every request so a late
|
|
4562
|
+
// `identify({userHash, exp, ...})` lights up signed headers
|
|
4563
|
+
// immediately on subsequent calls. Returns null when the host
|
|
4564
|
+
// hasn't supplied a signature (legacy unsigned path).
|
|
4565
|
+
getSignedIdentity: () => {
|
|
4566
|
+
if (!user || !user.userHash || !user.exp || !user.email) return null;
|
|
4567
|
+
return {
|
|
4568
|
+
userHash: user.userHash,
|
|
4569
|
+
exp: user.exp,
|
|
4570
|
+
email: user.email
|
|
4571
|
+
};
|
|
4572
|
+
}
|
|
4705
4573
|
});
|
|
4706
|
-
let user = config.user;
|
|
4707
|
-
let metadata = config.metadata ?? {};
|
|
4708
4574
|
const transformers = [];
|
|
4709
4575
|
const host = document.createElement("div");
|
|
4710
4576
|
host.className = "mhosaic-feedback";
|
|
@@ -4732,8 +4598,8 @@ function createFeedback(config) {
|
|
|
4732
4598
|
capture_method: captureMethod,
|
|
4733
4599
|
technical_context
|
|
4734
4600
|
};
|
|
4735
|
-
if ("0.
|
|
4736
|
-
payload.widget_version = "0.
|
|
4601
|
+
if ("0.14.0") {
|
|
4602
|
+
payload.widget_version = "0.14.0";
|
|
4737
4603
|
}
|
|
4738
4604
|
if (manualScreenshot) payload.screenshot = manualScreenshot;
|
|
4739
4605
|
if (values.synthetic) payload.synthetic = true;
|
|
@@ -4819,4 +4685,4 @@ function createFeedback(config) {
|
|
|
4819
4685
|
export {
|
|
4820
4686
|
createFeedback
|
|
4821
4687
|
};
|
|
4822
|
-
//# sourceMappingURL=chunk-
|
|
4688
|
+
//# sourceMappingURL=chunk-A4MBOFRR.mjs.map
|