@mushi-mushi/web 0.7.0 → 0.8.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/README.md +128 -5
- package/dist/index.cjs +876 -92
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +61 -7
- package/dist/index.d.ts +61 -7
- package/dist/index.js +877 -94
- package/dist/index.js.map +1 -1
- package/dist/test-utils.cjs +17 -0
- package/dist/test-utils.cjs.map +1 -1
- package/dist/test-utils.d.cts +21 -2
- package/dist/test-utils.d.ts +21 -2
- package/dist/test-utils.js +15 -1
- package/dist/test-utils.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -391,6 +391,29 @@ function getWidgetStyles(theme) {
|
|
|
391
391
|
left: var(--mushi-left, calc(24px + env(safe-area-inset-left, 0px)));
|
|
392
392
|
--mushi-origin: top left;
|
|
393
393
|
}
|
|
394
|
+
.mushi-outdated {
|
|
395
|
+
margin: 12px 14px 0;
|
|
396
|
+
padding: 10px 12px;
|
|
397
|
+
border: 1px solid ${vermillionWash};
|
|
398
|
+
background: ${vermillionWash};
|
|
399
|
+
color: ${vermillionInk};
|
|
400
|
+
font-family: ${fontBody};
|
|
401
|
+
font-size: 12px;
|
|
402
|
+
line-height: 1.4;
|
|
403
|
+
}
|
|
404
|
+
.mushi-outdated strong {
|
|
405
|
+
display: block;
|
|
406
|
+
font-family: ${fontMono};
|
|
407
|
+
font-size: 10px;
|
|
408
|
+
letter-spacing: 0.12em;
|
|
409
|
+
text-transform: uppercase;
|
|
410
|
+
margin-bottom: 2px;
|
|
411
|
+
}
|
|
412
|
+
.mushi-outdated span {
|
|
413
|
+
display: block;
|
|
414
|
+
margin-top: 3px;
|
|
415
|
+
color: ${inkMuted};
|
|
416
|
+
}
|
|
394
417
|
|
|
395
418
|
@keyframes mushi-stamp-in {
|
|
396
419
|
0% { opacity: 0; transform: scale(0.94) translateY(6px); }
|
|
@@ -545,6 +568,75 @@ function getWidgetStyles(theme) {
|
|
|
545
568
|
transform: translateX(-4px);
|
|
546
569
|
transition: opacity 220ms ${easeStamp}, transform 220ms ${easeStamp}, color 220ms ${easeStamp};
|
|
547
570
|
}
|
|
571
|
+
.mushi-report-row {
|
|
572
|
+
width: 100%;
|
|
573
|
+
display: grid;
|
|
574
|
+
grid-template-columns: auto 1fr auto;
|
|
575
|
+
gap: 8px;
|
|
576
|
+
align-items: center;
|
|
577
|
+
padding: 10px 0;
|
|
578
|
+
border: 0;
|
|
579
|
+
border-bottom: 1px solid ${rule};
|
|
580
|
+
background: transparent;
|
|
581
|
+
color: ${ink};
|
|
582
|
+
cursor: pointer;
|
|
583
|
+
text-align: left;
|
|
584
|
+
}
|
|
585
|
+
.mushi-report-status {
|
|
586
|
+
font-family: ${fontMono};
|
|
587
|
+
font-size: 10px;
|
|
588
|
+
color: ${vermillion};
|
|
589
|
+
text-transform: uppercase;
|
|
590
|
+
}
|
|
591
|
+
.mushi-report-title {
|
|
592
|
+
font-size: 13px;
|
|
593
|
+
overflow: hidden;
|
|
594
|
+
text-overflow: ellipsis;
|
|
595
|
+
white-space: nowrap;
|
|
596
|
+
}
|
|
597
|
+
.mushi-thread-summary {
|
|
598
|
+
border-bottom: 1px solid ${rule};
|
|
599
|
+
padding-bottom: 10px;
|
|
600
|
+
margin-bottom: 10px;
|
|
601
|
+
}
|
|
602
|
+
.mushi-thread-summary span {
|
|
603
|
+
font-family: ${fontMono};
|
|
604
|
+
font-size: 10px;
|
|
605
|
+
color: ${vermillion};
|
|
606
|
+
text-transform: uppercase;
|
|
607
|
+
}
|
|
608
|
+
.mushi-thread {
|
|
609
|
+
display: grid;
|
|
610
|
+
gap: 8px;
|
|
611
|
+
max-height: 180px;
|
|
612
|
+
overflow: auto;
|
|
613
|
+
margin-bottom: 12px;
|
|
614
|
+
}
|
|
615
|
+
.mushi-thread-comment {
|
|
616
|
+
padding: 8px 10px;
|
|
617
|
+
border: 1px solid ${rule};
|
|
618
|
+
background: ${isDark ? "rgba(242,235,221,0.04)" : "rgba(14,13,11,0.03)"};
|
|
619
|
+
}
|
|
620
|
+
.mushi-thread-comment.reporter {
|
|
621
|
+
border-color: ${vermillionWash};
|
|
622
|
+
background: ${vermillionWash};
|
|
623
|
+
}
|
|
624
|
+
.mushi-thread-comment strong {
|
|
625
|
+
display: block;
|
|
626
|
+
font-family: ${fontMono};
|
|
627
|
+
font-size: 10px;
|
|
628
|
+
letter-spacing: 0.08em;
|
|
629
|
+
text-transform: uppercase;
|
|
630
|
+
margin-bottom: 3px;
|
|
631
|
+
}
|
|
632
|
+
.mushi-thread-comment p,
|
|
633
|
+
.mushi-muted,
|
|
634
|
+
.mushi-error-inline {
|
|
635
|
+
font-size: 12px;
|
|
636
|
+
color: ${inkMuted};
|
|
637
|
+
line-height: 1.45;
|
|
638
|
+
}
|
|
639
|
+
.mushi-error-inline { color: ${vermillion}; }
|
|
548
640
|
|
|
549
641
|
.mushi-selected-category {
|
|
550
642
|
display: inline-flex;
|
|
@@ -653,6 +745,16 @@ function getWidgetStyles(theme) {
|
|
|
653
745
|
border-color: ${vermillion};
|
|
654
746
|
background: ${vermillionWash};
|
|
655
747
|
}
|
|
748
|
+
.mushi-attach-btn.danger {
|
|
749
|
+
color: ${vermillionInk};
|
|
750
|
+
border-color: ${vermillionWash};
|
|
751
|
+
background: transparent;
|
|
752
|
+
}
|
|
753
|
+
.mushi-attach-btn.danger:hover {
|
|
754
|
+
color: ${vermillion};
|
|
755
|
+
border-color: ${vermillion};
|
|
756
|
+
background: ${vermillionWash};
|
|
757
|
+
}
|
|
656
758
|
.mushi-attach-btn:focus-visible {
|
|
657
759
|
outline: 2px solid ${vermillion};
|
|
658
760
|
outline-offset: 2px;
|
|
@@ -722,6 +824,17 @@ function getWidgetStyles(theme) {
|
|
|
722
824
|
}
|
|
723
825
|
.mushi-submit:hover .mushi-submit-arrow { transform: translateX(3px); }
|
|
724
826
|
|
|
827
|
+
.mushi-brand-footer {
|
|
828
|
+
padding: 9px 14px 11px;
|
|
829
|
+
border-top: 1px solid ${rule};
|
|
830
|
+
color: ${inkFaint};
|
|
831
|
+
font-family: ${fontMono};
|
|
832
|
+
font-size: 9px;
|
|
833
|
+
letter-spacing: 0.16em;
|
|
834
|
+
text-align: center;
|
|
835
|
+
text-transform: uppercase;
|
|
836
|
+
}
|
|
837
|
+
|
|
725
838
|
.mushi-step-indicator {
|
|
726
839
|
display: flex;
|
|
727
840
|
align-items: center;
|
|
@@ -851,43 +964,19 @@ var TOTAL_STEPS = 3;
|
|
|
851
964
|
var STEP_NUMBER = {
|
|
852
965
|
category: 1,
|
|
853
966
|
intent: 2,
|
|
854
|
-
details: 3
|
|
855
|
-
};
|
|
967
|
+
details: 3};
|
|
856
968
|
function isSubmitShortcut(e) {
|
|
857
969
|
return (e.metaKey || e.ctrlKey) && e.key === "Enter";
|
|
858
970
|
}
|
|
971
|
+
function escapeHtml(value) {
|
|
972
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
973
|
+
}
|
|
859
974
|
var MushiWidget = class {
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
config;
|
|
863
|
-
callbacks;
|
|
864
|
-
locale;
|
|
865
|
-
isOpen = false;
|
|
866
|
-
step = "category";
|
|
867
|
-
selectedCategory = null;
|
|
868
|
-
selectedIntent = null;
|
|
869
|
-
screenshotAttached = false;
|
|
870
|
-
elementSelected = false;
|
|
871
|
-
submitting = false;
|
|
872
|
-
triggerVisible = true;
|
|
873
|
-
triggerShrunk = false;
|
|
874
|
-
triggerHiddenByScroll = false;
|
|
875
|
-
attachedLaunchers = [];
|
|
876
|
-
smartHideCleanup = null;
|
|
877
|
-
smartHideTimer = null;
|
|
878
|
-
/** Captured at the moment of submit so the success ledger metadata
|
|
879
|
-
* ("REPORT · 14:23:07 JST") doesn't drift while the success step
|
|
880
|
-
* is on screen. */
|
|
881
|
-
submittedAt = null;
|
|
882
|
-
/** Pending success-state + auto-close timers. Tracked so destroy()
|
|
883
|
-
* can clear them — otherwise a host that unmounts mid-submit leaks
|
|
884
|
-
* this MushiWidget reference (and re-renders into a detached shadow
|
|
885
|
-
* root) for up to ~3.3s after destroy. */
|
|
886
|
-
successTimer = null;
|
|
887
|
-
autoCloseTimer = null;
|
|
888
|
-
constructor(config = {}, callbacks) {
|
|
975
|
+
constructor(config = {}, callbacks, sdkVersion = "0.7.0") {
|
|
976
|
+
this.sdkVersion = sdkVersion;
|
|
889
977
|
this.config = {
|
|
890
978
|
position: config.position ?? "bottom-right",
|
|
979
|
+
anchor: config.anchor ?? {},
|
|
891
980
|
theme: config.theme ?? "auto",
|
|
892
981
|
// Falsy-OR (NOT `??`) on purpose: `triggerText: ''` is semantically
|
|
893
982
|
// nonsense — it would render a labelless, glyphless trigger button
|
|
@@ -909,7 +998,9 @@ var MushiWidget = class {
|
|
|
909
998
|
hideOnRoutes: config.hideOnRoutes ?? [],
|
|
910
999
|
environments: config.environments ?? {},
|
|
911
1000
|
smartHide: config.smartHide ?? false,
|
|
912
|
-
draggable: config.draggable ?? false
|
|
1001
|
+
draggable: config.draggable ?? false,
|
|
1002
|
+
brandFooter: config.brandFooter ?? true,
|
|
1003
|
+
outdatedBanner: config.outdatedBanner ?? "auto"
|
|
913
1004
|
};
|
|
914
1005
|
this.callbacks = callbacks;
|
|
915
1006
|
this.locale = getLocale(this.config.locale === "auto" ? void 0 : this.config.locale);
|
|
@@ -917,16 +1008,57 @@ var MushiWidget = class {
|
|
|
917
1008
|
this.host.id = "mushi-mushi-widget";
|
|
918
1009
|
this.shadow = this.host.attachShadow({ mode: "closed" });
|
|
919
1010
|
}
|
|
1011
|
+
sdkVersion;
|
|
1012
|
+
host;
|
|
1013
|
+
shadow;
|
|
1014
|
+
config;
|
|
1015
|
+
callbacks;
|
|
1016
|
+
locale;
|
|
1017
|
+
isOpen = false;
|
|
1018
|
+
step = "category";
|
|
1019
|
+
selectedCategory = null;
|
|
1020
|
+
selectedIntent = null;
|
|
1021
|
+
screenshotAttached = false;
|
|
1022
|
+
allowScreenshotRemove = true;
|
|
1023
|
+
elementSelected = false;
|
|
1024
|
+
submitting = false;
|
|
1025
|
+
triggerVisible = true;
|
|
1026
|
+
triggerShrunk = false;
|
|
1027
|
+
triggerHiddenByScroll = false;
|
|
1028
|
+
sdkFreshness = null;
|
|
1029
|
+
reporterReports = [];
|
|
1030
|
+
reporterComments = [];
|
|
1031
|
+
selectedReportId = null;
|
|
1032
|
+
reporterLoading = false;
|
|
1033
|
+
reporterError = null;
|
|
1034
|
+
attachedLaunchers = [];
|
|
1035
|
+
smartHideCleanup = null;
|
|
1036
|
+
smartHideTimer = null;
|
|
1037
|
+
/** Captured at the moment of submit so the success ledger metadata
|
|
1038
|
+
* ("REPORT · 14:23:07 JST") doesn't drift while the success step
|
|
1039
|
+
* is on screen. */
|
|
1040
|
+
submittedAt = null;
|
|
1041
|
+
/** Pending success-state + auto-close timers. Tracked so destroy()
|
|
1042
|
+
* can clear them — otherwise a host that unmounts mid-submit leaks
|
|
1043
|
+
* this MushiWidget reference (and re-renders into a detached shadow
|
|
1044
|
+
* root) for up to ~3.3s after destroy. */
|
|
1045
|
+
successTimer = null;
|
|
1046
|
+
autoCloseTimer = null;
|
|
920
1047
|
mount() {
|
|
1048
|
+
if (this.host.isConnected) return;
|
|
921
1049
|
document.body.appendChild(this.host);
|
|
922
1050
|
this.syncAttachedLaunchers();
|
|
923
1051
|
this.syncSmartHide();
|
|
924
1052
|
this.render();
|
|
925
1053
|
}
|
|
1054
|
+
getIsMounted() {
|
|
1055
|
+
return this.host.isConnected;
|
|
1056
|
+
}
|
|
926
1057
|
updateConfig(config = {}) {
|
|
927
1058
|
this.config = {
|
|
928
1059
|
...this.config,
|
|
929
1060
|
...config.position ? { position: config.position } : {},
|
|
1061
|
+
...config.anchor !== void 0 ? { anchor: config.anchor } : {},
|
|
930
1062
|
...config.theme ? { theme: config.theme } : {},
|
|
931
1063
|
...config.triggerText !== void 0 ? { triggerText: config.triggerText || "\u{1F41B}" } : {},
|
|
932
1064
|
...config.expandedTitle !== void 0 ? { expandedTitle: config.expandedTitle } : {},
|
|
@@ -941,7 +1073,9 @@ var MushiWidget = class {
|
|
|
941
1073
|
...config.hideOnRoutes !== void 0 ? { hideOnRoutes: config.hideOnRoutes } : {},
|
|
942
1074
|
...config.environments !== void 0 ? { environments: config.environments } : {},
|
|
943
1075
|
...config.smartHide !== void 0 ? { smartHide: config.smartHide } : {},
|
|
944
|
-
...config.draggable !== void 0 ? { draggable: config.draggable } : {}
|
|
1076
|
+
...config.draggable !== void 0 ? { draggable: config.draggable } : {},
|
|
1077
|
+
...config.brandFooter !== void 0 ? { brandFooter: config.brandFooter } : {},
|
|
1078
|
+
...config.outdatedBanner !== void 0 ? { outdatedBanner: config.outdatedBanner } : {}
|
|
945
1079
|
};
|
|
946
1080
|
this.locale = getLocale(this.config.locale === "auto" ? void 0 : this.config.locale);
|
|
947
1081
|
this.syncAttachedLaunchers();
|
|
@@ -1004,10 +1138,18 @@ var MushiWidget = class {
|
|
|
1004
1138
|
this.screenshotAttached = attached;
|
|
1005
1139
|
if (this.isOpen) this.render();
|
|
1006
1140
|
}
|
|
1141
|
+
setAllowScreenshotRemove(allow) {
|
|
1142
|
+
this.allowScreenshotRemove = allow;
|
|
1143
|
+
if (this.isOpen) this.render();
|
|
1144
|
+
}
|
|
1007
1145
|
setElementSelected(selected) {
|
|
1008
1146
|
this.elementSelected = selected;
|
|
1009
1147
|
if (this.isOpen) this.render();
|
|
1010
1148
|
}
|
|
1149
|
+
setSdkFreshness(info) {
|
|
1150
|
+
this.sdkFreshness = info;
|
|
1151
|
+
if (this.isOpen) this.render();
|
|
1152
|
+
}
|
|
1011
1153
|
destroy() {
|
|
1012
1154
|
if (this.successTimer !== null) {
|
|
1013
1155
|
clearTimeout(this.successTimer);
|
|
@@ -1133,13 +1275,22 @@ var MushiWidget = class {
|
|
|
1133
1275
|
panel.style.zIndex = String(this.config.zIndex + 1);
|
|
1134
1276
|
this.applyInsetVars(panel);
|
|
1135
1277
|
if (this.isOpen) {
|
|
1136
|
-
panel.innerHTML = this.renderStep()
|
|
1278
|
+
panel.innerHTML = `${this.renderOutdatedBanner()}${this.renderStep()}${this.renderBrandFooter()}`;
|
|
1137
1279
|
this.shadow.appendChild(panel);
|
|
1138
1280
|
this.attachHandlers(panel);
|
|
1139
1281
|
this.trapFocus(panel);
|
|
1140
1282
|
}
|
|
1141
1283
|
}
|
|
1142
1284
|
applyInsetVars(el) {
|
|
1285
|
+
const { anchor } = this.config;
|
|
1286
|
+
if (anchor && Object.keys(anchor).length > 0) {
|
|
1287
|
+
["top", "right", "bottom", "left"].forEach((edge) => {
|
|
1288
|
+
const value = anchor[edge];
|
|
1289
|
+
if (value !== void 0) el.style.setProperty(`--mushi-${edge}`, value);
|
|
1290
|
+
});
|
|
1291
|
+
el.style.setProperty("--mushi-safe-area", this.config.respectSafeArea ? "1" : "0");
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1143
1294
|
const { inset } = this.config;
|
|
1144
1295
|
if (!this.config.respectSafeArea) {
|
|
1145
1296
|
["top", "right", "bottom", "left"].forEach((edge) => {
|
|
@@ -1163,8 +1314,29 @@ var MushiWidget = class {
|
|
|
1163
1314
|
return this.renderDetailsStep();
|
|
1164
1315
|
case "success":
|
|
1165
1316
|
return this.renderSuccessStep();
|
|
1317
|
+
case "reports":
|
|
1318
|
+
return this.renderReportsStep();
|
|
1319
|
+
case "report-detail":
|
|
1320
|
+
return this.renderReportDetailStep();
|
|
1166
1321
|
}
|
|
1167
1322
|
}
|
|
1323
|
+
renderOutdatedBanner() {
|
|
1324
|
+
if (!this.sdkFreshness) return "";
|
|
1325
|
+
if (this.config.outdatedBanner === "off" || this.config.outdatedBanner === "console-only") return "";
|
|
1326
|
+
const { latest, current, deprecated, message } = this.sdkFreshness;
|
|
1327
|
+
if (!latest && !deprecated) return "";
|
|
1328
|
+
return `
|
|
1329
|
+
<div class="mushi-outdated" role="status">
|
|
1330
|
+
<strong>Mushi SDK ${escapeHtml(current)}</strong>
|
|
1331
|
+
${latest ? `latest is ${escapeHtml(latest)}.` : "needs attention."}
|
|
1332
|
+
${message ? `<span>${escapeHtml(message)}</span>` : ""}
|
|
1333
|
+
</div>
|
|
1334
|
+
`;
|
|
1335
|
+
}
|
|
1336
|
+
renderBrandFooter() {
|
|
1337
|
+
if (this.config.brandFooter === false) return "";
|
|
1338
|
+
return `<div class="mushi-brand-footer">Powered by Mushi v${escapeHtml(this.sdkVersion)}</div>`;
|
|
1339
|
+
}
|
|
1168
1340
|
/**
|
|
1169
1341
|
* Editorial masthead. Always carries:
|
|
1170
1342
|
* • the brand mark (虫 kanji on vermillion, "MUSHI" in mono above)
|
|
@@ -1223,11 +1395,61 @@ var MushiWidget = class {
|
|
|
1223
1395
|
return `
|
|
1224
1396
|
${this.renderHeader({ title: t.step1.heading, step: STEP_NUMBER.category })}
|
|
1225
1397
|
<div class="mushi-body" role="radiogroup" aria-label="${t.step1.heading}">
|
|
1398
|
+
<button type="button" class="mushi-option-btn mushi-reports-entry" data-action="reports">
|
|
1399
|
+
<span class="mushi-option-icon" aria-hidden="true">\u{1F4EC}</span>
|
|
1400
|
+
<div class="mushi-option-text">
|
|
1401
|
+
<span class="mushi-option-label">Your reports${this.unreadCount() ? ` (${this.unreadCount()} new)` : ""}</span>
|
|
1402
|
+
<span class="mushi-option-desc">See status, developer replies, and respond</span>
|
|
1403
|
+
</div>
|
|
1404
|
+
<span class="mushi-option-arrow" aria-hidden="true">\u2192</span>
|
|
1405
|
+
</button>
|
|
1226
1406
|
${categories}
|
|
1227
1407
|
</div>
|
|
1228
1408
|
${this.renderStepIndicator(STEP_NUMBER.category)}
|
|
1229
1409
|
`;
|
|
1230
1410
|
}
|
|
1411
|
+
renderReportsStep() {
|
|
1412
|
+
const reports = this.reporterReports.map((report) => `
|
|
1413
|
+
<button type="button" class="mushi-report-row" data-report-id="${escapeHtml(report.id)}">
|
|
1414
|
+
<span class="mushi-report-status">${escapeHtml(report.status)}</span>
|
|
1415
|
+
<span class="mushi-report-title">${escapeHtml(report.summary ?? report.description ?? `Report ${report.id.slice(0, 8)}`)}</span>
|
|
1416
|
+
${report.unread_count ? `<b>${report.unread_count}</b>` : ""}
|
|
1417
|
+
</button>
|
|
1418
|
+
`).join("");
|
|
1419
|
+
return `
|
|
1420
|
+
${this.renderHeader({ title: "Your reports", showBack: true, eyebrow: "Mushi \xB7 Inbox" })}
|
|
1421
|
+
<div class="mushi-body">
|
|
1422
|
+
${this.reporterLoading ? '<p class="mushi-muted">Loading reports\u2026</p>' : ""}
|
|
1423
|
+
${this.reporterError ? `<p class="mushi-error-inline">${escapeHtml(this.reporterError)}</p>` : ""}
|
|
1424
|
+
${reports || (!this.reporterLoading ? '<p class="mushi-muted">No reports from this browser yet.</p>' : "")}
|
|
1425
|
+
</div>
|
|
1426
|
+
`;
|
|
1427
|
+
}
|
|
1428
|
+
renderReportDetailStep() {
|
|
1429
|
+
const report = this.reporterReports.find((r) => r.id === this.selectedReportId);
|
|
1430
|
+
const comments = this.reporterComments.map((comment) => `
|
|
1431
|
+
<div class="mushi-thread-comment ${comment.author_kind}">
|
|
1432
|
+
<strong>${escapeHtml(comment.author_kind === "reporter" ? "You" : comment.author_name ?? "Developer")}</strong>
|
|
1433
|
+
<p>${escapeHtml(comment.body)}</p>
|
|
1434
|
+
</div>
|
|
1435
|
+
`).join("");
|
|
1436
|
+
return `
|
|
1437
|
+
${this.renderHeader({ title: "Report thread", showBack: true, eyebrow: "Mushi \xB7 Inbox" })}
|
|
1438
|
+
<div class="mushi-body">
|
|
1439
|
+
<div class="mushi-thread-summary">
|
|
1440
|
+
<span>${escapeHtml(report?.status ?? "unknown")}</span>
|
|
1441
|
+
<p>${escapeHtml(report?.summary ?? report?.description ?? "Report details")}</p>
|
|
1442
|
+
</div>
|
|
1443
|
+
<div class="mushi-thread">
|
|
1444
|
+
${this.reporterLoading ? '<p class="mushi-muted">Loading thread\u2026</p>' : comments || '<p class="mushi-muted">No developer replies yet.</p>'}
|
|
1445
|
+
</div>
|
|
1446
|
+
<textarea class="mushi-textarea" data-role="reporter-reply" rows="3" placeholder="Reply to the developer\u2026"></textarea>
|
|
1447
|
+
<button type="button" class="mushi-submit" data-action="reporter-reply">
|
|
1448
|
+
<span>Reply</span><span class="mushi-submit-arrow" aria-hidden="true">\u2192</span>
|
|
1449
|
+
</button>
|
|
1450
|
+
</div>
|
|
1451
|
+
`;
|
|
1452
|
+
}
|
|
1231
1453
|
renderIntentStep() {
|
|
1232
1454
|
const t = this.locale;
|
|
1233
1455
|
const cat = this.selectedCategory;
|
|
@@ -1267,6 +1489,7 @@ var MushiWidget = class {
|
|
|
1267
1489
|
<button type="button" class="mushi-attach-btn${this.screenshotAttached ? " active" : ""}" data-action="screenshot">
|
|
1268
1490
|
\u{1F4F8} ${this.screenshotAttached ? t.step3.screenshotAttached : t.step3.screenshotButton}
|
|
1269
1491
|
</button>
|
|
1492
|
+
${this.screenshotAttached && this.allowScreenshotRemove ? '<button type="button" class="mushi-attach-btn danger" data-action="remove-screenshot">\u2715 Remove screenshot</button>' : ""}
|
|
1270
1493
|
<button type="button" class="mushi-attach-btn${this.elementSelected ? " active" : ""}" data-action="element">
|
|
1271
1494
|
\u{1F3AF} ${this.elementSelected ? t.step3.elementSelected : t.step3.elementButton}
|
|
1272
1495
|
</button>
|
|
@@ -1319,9 +1542,26 @@ var MushiWidget = class {
|
|
|
1319
1542
|
} else if (this.step === "details") {
|
|
1320
1543
|
this.step = "intent";
|
|
1321
1544
|
this.selectedIntent = null;
|
|
1545
|
+
} else if (this.step === "reports") {
|
|
1546
|
+
this.step = "category";
|
|
1547
|
+
} else if (this.step === "report-detail") {
|
|
1548
|
+
this.step = "reports";
|
|
1549
|
+
this.selectedReportId = null;
|
|
1322
1550
|
}
|
|
1323
1551
|
this.render();
|
|
1324
1552
|
});
|
|
1553
|
+
panel.querySelector('[data-action="reports"]')?.addEventListener("click", () => {
|
|
1554
|
+
void this.loadReporterReports();
|
|
1555
|
+
});
|
|
1556
|
+
panel.querySelectorAll("[data-report-id]").forEach((btn) => {
|
|
1557
|
+
btn.addEventListener("click", () => {
|
|
1558
|
+
const reportId = btn.dataset.reportId;
|
|
1559
|
+
if (reportId) void this.loadReporterComments(reportId);
|
|
1560
|
+
});
|
|
1561
|
+
});
|
|
1562
|
+
panel.querySelector('[data-action="reporter-reply"]')?.addEventListener("click", () => {
|
|
1563
|
+
void this.submitReporterReply(panel);
|
|
1564
|
+
});
|
|
1325
1565
|
panel.querySelectorAll("[data-category]").forEach((btn) => {
|
|
1326
1566
|
btn.addEventListener("click", () => {
|
|
1327
1567
|
this.selectedCategory = btn.dataset.category;
|
|
@@ -1339,6 +1579,9 @@ var MushiWidget = class {
|
|
|
1339
1579
|
panel.querySelector('[data-action="screenshot"]')?.addEventListener("click", () => {
|
|
1340
1580
|
this.callbacks.onScreenshotRequest();
|
|
1341
1581
|
});
|
|
1582
|
+
panel.querySelector('[data-action="remove-screenshot"]')?.addEventListener("click", () => {
|
|
1583
|
+
this.callbacks.onScreenshotRemove?.();
|
|
1584
|
+
});
|
|
1342
1585
|
panel.querySelector('[data-action="element"]')?.addEventListener("click", () => {
|
|
1343
1586
|
this.callbacks.onElementSelectorRequest?.();
|
|
1344
1587
|
});
|
|
@@ -1396,6 +1639,57 @@ var MushiWidget = class {
|
|
|
1396
1639
|
if (focusable.length > 0) focusable[0].focus();
|
|
1397
1640
|
});
|
|
1398
1641
|
}
|
|
1642
|
+
unreadCount() {
|
|
1643
|
+
return this.reporterReports.reduce((sum, report) => sum + (report.unread_count ?? 0), 0);
|
|
1644
|
+
}
|
|
1645
|
+
async loadReporterReports() {
|
|
1646
|
+
this.step = "reports";
|
|
1647
|
+
this.reporterLoading = true;
|
|
1648
|
+
this.reporterError = null;
|
|
1649
|
+
this.render();
|
|
1650
|
+
try {
|
|
1651
|
+
this.reporterReports = await this.callbacks.onReporterReportsRequest?.() ?? [];
|
|
1652
|
+
} catch (err) {
|
|
1653
|
+
this.reporterError = err instanceof Error ? err.message : "Could not load reports.";
|
|
1654
|
+
} finally {
|
|
1655
|
+
this.reporterLoading = false;
|
|
1656
|
+
this.render();
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
async loadReporterComments(reportId) {
|
|
1660
|
+
this.selectedReportId = reportId;
|
|
1661
|
+
this.step = "report-detail";
|
|
1662
|
+
this.reporterLoading = true;
|
|
1663
|
+
this.reporterError = null;
|
|
1664
|
+
this.render();
|
|
1665
|
+
try {
|
|
1666
|
+
this.reporterComments = await this.callbacks.onReporterCommentsRequest?.(reportId) ?? [];
|
|
1667
|
+
} catch (err) {
|
|
1668
|
+
this.reporterError = err instanceof Error ? err.message : "Could not load thread.";
|
|
1669
|
+
} finally {
|
|
1670
|
+
this.reporterLoading = false;
|
|
1671
|
+
this.render();
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
async submitReporterReply(panel) {
|
|
1675
|
+
const reportId = this.selectedReportId;
|
|
1676
|
+
const textarea = panel.querySelector('[data-role="reporter-reply"]');
|
|
1677
|
+
const replyButton = panel.querySelector('[data-action="reporter-reply"]');
|
|
1678
|
+
const body = textarea?.value.trim() ?? "";
|
|
1679
|
+
if (!reportId || !body || this.reporterLoading) return;
|
|
1680
|
+
this.reporterLoading = true;
|
|
1681
|
+
if (replyButton) replyButton.disabled = true;
|
|
1682
|
+
this.render();
|
|
1683
|
+
try {
|
|
1684
|
+
await this.callbacks.onReporterReply?.(reportId, body);
|
|
1685
|
+
if (textarea) textarea.value = "";
|
|
1686
|
+
await this.loadReporterComments(reportId);
|
|
1687
|
+
} catch (err) {
|
|
1688
|
+
this.reporterError = err instanceof Error ? err.message : "Could not send reply.";
|
|
1689
|
+
this.reporterLoading = false;
|
|
1690
|
+
this.render();
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1399
1693
|
};
|
|
1400
1694
|
|
|
1401
1695
|
// src/capture/console.ts
|
|
@@ -1447,35 +1741,109 @@ function createConsoleCapture() {
|
|
|
1447
1741
|
}
|
|
1448
1742
|
};
|
|
1449
1743
|
}
|
|
1744
|
+
var DEFAULT_INTERNAL_URL_MATCHERS = [
|
|
1745
|
+
/\/v1\/sdk(?:\/|$)/,
|
|
1746
|
+
/\/v1\/reports(?:\/|$)/,
|
|
1747
|
+
/\/v1\/notifications(?:\/|$)/,
|
|
1748
|
+
/\/v1\/reputation(?:\/|$)/
|
|
1749
|
+
];
|
|
1750
|
+
function getRequestUrl(input) {
|
|
1751
|
+
if (typeof input === "string") return input;
|
|
1752
|
+
if (input instanceof URL) return input.href;
|
|
1753
|
+
return input.url;
|
|
1754
|
+
}
|
|
1755
|
+
function getInternalRequestKind(input, init) {
|
|
1756
|
+
const marker = init?.[core.MUSHI_INTERNAL_INIT_MARKER];
|
|
1757
|
+
if (marker) return marker;
|
|
1758
|
+
const initHeader = readHeader(init?.headers, core.MUSHI_INTERNAL_HEADER);
|
|
1759
|
+
if (initHeader) return initHeader;
|
|
1760
|
+
if (typeof Request !== "undefined" && input instanceof Request) {
|
|
1761
|
+
const requestHeader = input.headers.get(core.MUSHI_INTERNAL_HEADER);
|
|
1762
|
+
if (requestHeader) return requestHeader;
|
|
1763
|
+
}
|
|
1764
|
+
return null;
|
|
1765
|
+
}
|
|
1766
|
+
function shouldIgnoreMushiUrl(url, options = {}) {
|
|
1767
|
+
const matchers = [...DEFAULT_INTERNAL_URL_MATCHERS, ...options.ignoreUrls ?? []];
|
|
1768
|
+
if (matchers.some((matcher) => matchesUrl(url, matcher))) return true;
|
|
1769
|
+
const endpoint = normalizeUrlPrefix(options.apiEndpoint);
|
|
1770
|
+
return endpoint ? normalizeComparableUrl(url).startsWith(endpoint) : false;
|
|
1771
|
+
}
|
|
1772
|
+
function matchesUrl(url, matcher) {
|
|
1773
|
+
if (typeof matcher === "string") {
|
|
1774
|
+
return normalizeComparableUrl(url).includes(matcher);
|
|
1775
|
+
}
|
|
1776
|
+
matcher.lastIndex = 0;
|
|
1777
|
+
return matcher.test(url);
|
|
1778
|
+
}
|
|
1779
|
+
function normalizeUrlPrefix(url) {
|
|
1780
|
+
if (!url) return null;
|
|
1781
|
+
return normalizeComparableUrl(url).replace(/\/+$/, "");
|
|
1782
|
+
}
|
|
1783
|
+
function isLocalhostEndpoint(url) {
|
|
1784
|
+
if (!url) return false;
|
|
1785
|
+
try {
|
|
1786
|
+
const parsed = new URL(url);
|
|
1787
|
+
return parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "::1" || parsed.hostname.endsWith(".localhost");
|
|
1788
|
+
} catch {
|
|
1789
|
+
return /\blocalhost\b|127\.0\.0\.1/.test(url);
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
function normalizeComparableUrl(url) {
|
|
1793
|
+
try {
|
|
1794
|
+
return new URL(url, typeof location !== "undefined" ? location.href : "http://localhost").href;
|
|
1795
|
+
} catch {
|
|
1796
|
+
return url;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
function readHeader(headers, name) {
|
|
1800
|
+
if (!headers) return null;
|
|
1801
|
+
if (typeof Headers !== "undefined" && headers instanceof Headers) {
|
|
1802
|
+
return headers.get(name);
|
|
1803
|
+
}
|
|
1804
|
+
if (Array.isArray(headers)) {
|
|
1805
|
+
const found = headers.find(([key]) => key.toLowerCase() === name.toLowerCase());
|
|
1806
|
+
return found?.[1] ?? null;
|
|
1807
|
+
}
|
|
1808
|
+
const record = headers;
|
|
1809
|
+
return record[name] ?? record[name.toLowerCase()] ?? null;
|
|
1810
|
+
}
|
|
1450
1811
|
|
|
1451
1812
|
// src/capture/network.ts
|
|
1452
1813
|
var MAX_ENTRIES2 = 30;
|
|
1453
|
-
function createNetworkCapture() {
|
|
1814
|
+
function createNetworkCapture(options = {}) {
|
|
1454
1815
|
const entries = [];
|
|
1455
1816
|
const originalFetch = globalThis.fetch;
|
|
1817
|
+
let activeOptions = options;
|
|
1456
1818
|
globalThis.fetch = async function mushiFetchInterceptor(input, init) {
|
|
1457
1819
|
const startTime = Date.now();
|
|
1458
1820
|
const method = init?.method?.toUpperCase() ?? "GET";
|
|
1459
|
-
const url =
|
|
1821
|
+
const url = getRequestUrl(input);
|
|
1822
|
+
const internalKind = getInternalRequestKind(input, init);
|
|
1823
|
+
const shouldRecord = !internalKind && !shouldIgnoreMushiUrl(url, activeOptions);
|
|
1460
1824
|
try {
|
|
1461
1825
|
const response = await originalFetch.call(globalThis, input, init);
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1826
|
+
if (shouldRecord) {
|
|
1827
|
+
addEntry({
|
|
1828
|
+
method,
|
|
1829
|
+
url: truncateUrl(url),
|
|
1830
|
+
status: response.status,
|
|
1831
|
+
duration: Date.now() - startTime,
|
|
1832
|
+
timestamp: startTime
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1469
1835
|
return response;
|
|
1470
1836
|
} catch (error) {
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1837
|
+
if (shouldRecord) {
|
|
1838
|
+
addEntry({
|
|
1839
|
+
method,
|
|
1840
|
+
url: truncateUrl(url),
|
|
1841
|
+
status: 0,
|
|
1842
|
+
duration: Date.now() - startTime,
|
|
1843
|
+
timestamp: startTime,
|
|
1844
|
+
error: error instanceof Error ? error.message : "Network error"
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1479
1847
|
throw error;
|
|
1480
1848
|
}
|
|
1481
1849
|
};
|
|
@@ -1492,6 +1860,9 @@ function createNetworkCapture() {
|
|
|
1492
1860
|
clear() {
|
|
1493
1861
|
entries.length = 0;
|
|
1494
1862
|
},
|
|
1863
|
+
updateOptions(nextOptions) {
|
|
1864
|
+
activeOptions = nextOptions;
|
|
1865
|
+
},
|
|
1495
1866
|
destroy() {
|
|
1496
1867
|
globalThis.fetch = originalFetch;
|
|
1497
1868
|
}
|
|
@@ -1511,7 +1882,8 @@ function truncateUrl(url) {
|
|
|
1511
1882
|
}
|
|
1512
1883
|
|
|
1513
1884
|
// src/capture/screenshot.ts
|
|
1514
|
-
function createScreenshotCapture() {
|
|
1885
|
+
function createScreenshotCapture(options = {}) {
|
|
1886
|
+
let activeOptions = options;
|
|
1515
1887
|
async function take() {
|
|
1516
1888
|
try {
|
|
1517
1889
|
if (typeof document === "undefined") return null;
|
|
@@ -1524,11 +1896,12 @@ function createScreenshotCapture() {
|
|
|
1524
1896
|
canvas.width = width * dpr;
|
|
1525
1897
|
canvas.height = height * dpr;
|
|
1526
1898
|
ctx.scale(dpr, dpr);
|
|
1899
|
+
const safeDocument = buildPrivacySafeDocument(activeOptions.privacy);
|
|
1527
1900
|
const svgData = `
|
|
1528
1901
|
<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
|
|
1529
1902
|
<foreignObject width="100%" height="100%">
|
|
1530
1903
|
<div xmlns="http://www.w3.org/1999/xhtml">
|
|
1531
|
-
${new XMLSerializer().serializeToString(
|
|
1904
|
+
${new XMLSerializer().serializeToString(safeDocument)}
|
|
1532
1905
|
</div>
|
|
1533
1906
|
</foreignObject>
|
|
1534
1907
|
</svg>
|
|
@@ -1557,7 +1930,46 @@ function createScreenshotCapture() {
|
|
|
1557
1930
|
return null;
|
|
1558
1931
|
}
|
|
1559
1932
|
}
|
|
1560
|
-
return {
|
|
1933
|
+
return {
|
|
1934
|
+
take,
|
|
1935
|
+
updateOptions(nextOptions) {
|
|
1936
|
+
activeOptions = nextOptions;
|
|
1937
|
+
}
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
function buildPrivacySafeDocument(privacy) {
|
|
1941
|
+
const clone = document.documentElement.cloneNode(true);
|
|
1942
|
+
for (const selector of privacy?.blockSelectors ?? []) {
|
|
1943
|
+
for (const el of safeQueryAll(clone, selector)) {
|
|
1944
|
+
el.remove();
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
for (const selector of privacy?.maskSelectors ?? []) {
|
|
1948
|
+
for (const el of safeQueryAll(clone, selector)) {
|
|
1949
|
+
maskElement(el);
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
return clone;
|
|
1953
|
+
}
|
|
1954
|
+
function safeQueryAll(root, selector) {
|
|
1955
|
+
try {
|
|
1956
|
+
return Array.from(root.querySelectorAll(selector));
|
|
1957
|
+
} catch {
|
|
1958
|
+
return [];
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
function maskElement(el) {
|
|
1962
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
1963
|
+
el.value = "";
|
|
1964
|
+
el.setAttribute("value", "");
|
|
1965
|
+
el.setAttribute("placeholder", "\u2022\u2022\u2022\u2022");
|
|
1966
|
+
}
|
|
1967
|
+
el.textContent = el.children.length === 0 ? "\u2022\u2022\u2022\u2022" : el.textContent;
|
|
1968
|
+
el.setAttribute(
|
|
1969
|
+
"style",
|
|
1970
|
+
`${el.getAttribute("style") ?? ""};background:#8f8f8f!important;color:transparent!important;text-shadow:none!important;`
|
|
1971
|
+
);
|
|
1972
|
+
el.setAttribute("data-mushi-masked", "true");
|
|
1561
1973
|
}
|
|
1562
1974
|
|
|
1563
1975
|
// src/capture/performance.ts
|
|
@@ -1746,6 +2158,108 @@ function createElementSelector() {
|
|
|
1746
2158
|
return { activate, deactivate, isActive: () => active };
|
|
1747
2159
|
}
|
|
1748
2160
|
|
|
2161
|
+
// src/capture/timeline.ts
|
|
2162
|
+
var MAX_TIMELINE_ENTRIES = 120;
|
|
2163
|
+
function createTimelineCapture() {
|
|
2164
|
+
const entries = [];
|
|
2165
|
+
const originalPushState = history.pushState;
|
|
2166
|
+
const originalReplaceState = history.replaceState;
|
|
2167
|
+
const handlePopState = () => recordRoute("popstate");
|
|
2168
|
+
const handleHashChange = () => recordRoute("hashchange");
|
|
2169
|
+
recordRoute("initial");
|
|
2170
|
+
function record(entry) {
|
|
2171
|
+
entries.push(entry);
|
|
2172
|
+
if (entries.length > MAX_TIMELINE_ENTRIES) entries.shift();
|
|
2173
|
+
}
|
|
2174
|
+
function recordRoute(source) {
|
|
2175
|
+
if (typeof location === "undefined") return;
|
|
2176
|
+
record({
|
|
2177
|
+
ts: Date.now(),
|
|
2178
|
+
kind: "route",
|
|
2179
|
+
payload: {
|
|
2180
|
+
source,
|
|
2181
|
+
route: `${location.pathname}${location.search}${location.hash}`,
|
|
2182
|
+
href: location.href
|
|
2183
|
+
}
|
|
2184
|
+
});
|
|
2185
|
+
}
|
|
2186
|
+
function handleClick(event) {
|
|
2187
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
2188
|
+
if (!target) return;
|
|
2189
|
+
const el = target.closest('button,a,[role="button"],input,textarea,select,[data-mushi-track]') ?? target;
|
|
2190
|
+
record({
|
|
2191
|
+
ts: Date.now(),
|
|
2192
|
+
kind: "click",
|
|
2193
|
+
payload: {
|
|
2194
|
+
tag: el.tagName.toLowerCase(),
|
|
2195
|
+
id: el.id || void 0,
|
|
2196
|
+
text: textSnippet(el)
|
|
2197
|
+
}
|
|
2198
|
+
});
|
|
2199
|
+
}
|
|
2200
|
+
history.pushState = function mushiPushState(...args) {
|
|
2201
|
+
const result = originalPushState.apply(this, args);
|
|
2202
|
+
recordRoute("pushState");
|
|
2203
|
+
return result;
|
|
2204
|
+
};
|
|
2205
|
+
history.replaceState = function mushiReplaceState(...args) {
|
|
2206
|
+
const result = originalReplaceState.apply(this, args);
|
|
2207
|
+
recordRoute("replaceState");
|
|
2208
|
+
return result;
|
|
2209
|
+
};
|
|
2210
|
+
window.addEventListener("popstate", handlePopState);
|
|
2211
|
+
window.addEventListener("hashchange", handleHashChange);
|
|
2212
|
+
document.addEventListener("click", handleClick, true);
|
|
2213
|
+
return {
|
|
2214
|
+
setScreen(screen) {
|
|
2215
|
+
record({
|
|
2216
|
+
ts: Date.now(),
|
|
2217
|
+
kind: "screen",
|
|
2218
|
+
payload: screen
|
|
2219
|
+
});
|
|
2220
|
+
},
|
|
2221
|
+
getEntries(input = {}) {
|
|
2222
|
+
const merged = [
|
|
2223
|
+
...entries,
|
|
2224
|
+
...(input.consoleLogs ?? []).map((log) => ({
|
|
2225
|
+
ts: log.timestamp,
|
|
2226
|
+
kind: "log",
|
|
2227
|
+
payload: {
|
|
2228
|
+
level: log.level,
|
|
2229
|
+
message: log.message
|
|
2230
|
+
}
|
|
2231
|
+
})),
|
|
2232
|
+
...(input.networkLogs ?? []).map((network) => ({
|
|
2233
|
+
ts: network.timestamp,
|
|
2234
|
+
kind: "request",
|
|
2235
|
+
payload: {
|
|
2236
|
+
method: network.method,
|
|
2237
|
+
url: network.url,
|
|
2238
|
+
status: network.status,
|
|
2239
|
+
duration: network.duration,
|
|
2240
|
+
error: network.error
|
|
2241
|
+
}
|
|
2242
|
+
}))
|
|
2243
|
+
].sort((a, b) => a.ts - b.ts);
|
|
2244
|
+
return merged.slice(-MAX_TIMELINE_ENTRIES);
|
|
2245
|
+
},
|
|
2246
|
+
clear() {
|
|
2247
|
+
entries.length = 0;
|
|
2248
|
+
},
|
|
2249
|
+
destroy() {
|
|
2250
|
+
history.pushState = originalPushState;
|
|
2251
|
+
history.replaceState = originalReplaceState;
|
|
2252
|
+
window.removeEventListener("popstate", handlePopState);
|
|
2253
|
+
window.removeEventListener("hashchange", handleHashChange);
|
|
2254
|
+
document.removeEventListener("click", handleClick, true);
|
|
2255
|
+
}
|
|
2256
|
+
};
|
|
2257
|
+
}
|
|
2258
|
+
function textSnippet(el) {
|
|
2259
|
+
const text = (el.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
2260
|
+
return text ? text.slice(0, 80) : void 0;
|
|
2261
|
+
}
|
|
2262
|
+
|
|
1749
2263
|
// src/sentry.ts
|
|
1750
2264
|
function getSentryGlobal() {
|
|
1751
2265
|
try {
|
|
@@ -1832,36 +2346,25 @@ function setupProactiveTriggers(callbacks, config = {}) {
|
|
|
1832
2346
|
} catch {
|
|
1833
2347
|
}
|
|
1834
2348
|
}
|
|
1835
|
-
|
|
2349
|
+
const apiCascade = normalizeApiCascadeConfig(config.apiCascade);
|
|
2350
|
+
if (apiCascade.enabled) {
|
|
1836
2351
|
const failedRequests = [];
|
|
1837
2352
|
const origFetch = globalThis.fetch;
|
|
1838
2353
|
globalThis.fetch = async function(...args) {
|
|
2354
|
+
const [input, init] = args;
|
|
2355
|
+
const url = getRequestUrl(input);
|
|
2356
|
+
const ignoreFailure = Boolean(getInternalRequestKind(input, init)) || shouldIgnoreMushiUrl(url, {
|
|
2357
|
+
apiEndpoint: config.apiEndpoint,
|
|
2358
|
+
ignoreUrls: apiCascade.ignoreUrls
|
|
2359
|
+
});
|
|
1839
2360
|
try {
|
|
1840
2361
|
const res = await origFetch.apply(this, args);
|
|
1841
|
-
if (!res.ok && res.status >= 400) {
|
|
1842
|
-
|
|
1843
|
-
failedRequests.push(now);
|
|
1844
|
-
const recentFailures = failedRequests.filter((t) => now - t < 1e4);
|
|
1845
|
-
if (recentFailures.length >= 3) {
|
|
1846
|
-
callbacks.onTrigger("api_cascade", {
|
|
1847
|
-
failureCount: recentFailures.length,
|
|
1848
|
-
windowMs: 1e4
|
|
1849
|
-
});
|
|
1850
|
-
failedRequests.length = 0;
|
|
1851
|
-
}
|
|
2362
|
+
if (!ignoreFailure && !res.ok && res.status >= 400) {
|
|
2363
|
+
recordApiFailure(failedRequests, callbacks);
|
|
1852
2364
|
}
|
|
1853
2365
|
return res;
|
|
1854
2366
|
} catch (err) {
|
|
1855
|
-
|
|
1856
|
-
failedRequests.push(now);
|
|
1857
|
-
const recentFailures = failedRequests.filter((t) => now - t < 1e4);
|
|
1858
|
-
if (recentFailures.length >= 3) {
|
|
1859
|
-
callbacks.onTrigger("api_cascade", {
|
|
1860
|
-
failureCount: recentFailures.length,
|
|
1861
|
-
windowMs: 1e4
|
|
1862
|
-
});
|
|
1863
|
-
failedRequests.length = 0;
|
|
1864
|
-
}
|
|
2367
|
+
if (!ignoreFailure) recordApiFailure(failedRequests, callbacks);
|
|
1865
2368
|
throw err;
|
|
1866
2369
|
}
|
|
1867
2370
|
};
|
|
@@ -1896,6 +2399,28 @@ function setupProactiveTriggers(callbacks, config = {}) {
|
|
|
1896
2399
|
}
|
|
1897
2400
|
};
|
|
1898
2401
|
}
|
|
2402
|
+
function normalizeApiCascadeConfig(config) {
|
|
2403
|
+
if (config === false) return { enabled: false, ignoreUrls: [] };
|
|
2404
|
+
if (config && typeof config === "object") {
|
|
2405
|
+
return {
|
|
2406
|
+
enabled: config.enabled !== false,
|
|
2407
|
+
ignoreUrls: config.ignoreUrls ?? []
|
|
2408
|
+
};
|
|
2409
|
+
}
|
|
2410
|
+
return { enabled: true, ignoreUrls: [] };
|
|
2411
|
+
}
|
|
2412
|
+
function recordApiFailure(failedRequests, callbacks) {
|
|
2413
|
+
const now = Date.now();
|
|
2414
|
+
failedRequests.push(now);
|
|
2415
|
+
const recentFailures = failedRequests.filter((t) => now - t < 1e4);
|
|
2416
|
+
if (recentFailures.length >= 3) {
|
|
2417
|
+
callbacks.onTrigger("api_cascade", {
|
|
2418
|
+
failureCount: recentFailures.length,
|
|
2419
|
+
windowMs: 1e4
|
|
2420
|
+
});
|
|
2421
|
+
failedRequests.length = 0;
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
1899
2424
|
|
|
1900
2425
|
// src/proactive-manager.ts
|
|
1901
2426
|
var STORAGE_KEY_LAST_DISMISS = "mushi:lastDismiss";
|
|
@@ -1948,6 +2473,10 @@ function createProactiveManager(config = {}) {
|
|
|
1948
2473
|
return { shouldShow, recordDismissal, recordSubmission, reset };
|
|
1949
2474
|
}
|
|
1950
2475
|
|
|
2476
|
+
// src/version.ts
|
|
2477
|
+
var MUSHI_SDK_PACKAGE = "@mushi-mushi/web";
|
|
2478
|
+
var MUSHI_SDK_VERSION = "0.8.0" ;
|
|
2479
|
+
|
|
1951
2480
|
// src/mushi.ts
|
|
1952
2481
|
var instance = null;
|
|
1953
2482
|
var Mushi = class {
|
|
@@ -1977,18 +2506,21 @@ var Mushi = class {
|
|
|
1977
2506
|
instance?.destroy();
|
|
1978
2507
|
instance = null;
|
|
1979
2508
|
}
|
|
2509
|
+
static diagnose() {
|
|
2510
|
+
return instance?.diagnose() ?? diagnoseWithoutInstance();
|
|
2511
|
+
}
|
|
1980
2512
|
};
|
|
1981
2513
|
function createInstance(config) {
|
|
1982
|
-
const bootstrapConfig = config;
|
|
1983
|
-
let activeConfig =
|
|
2514
|
+
const bootstrapConfig = applyPresetConfig(config);
|
|
2515
|
+
let activeConfig = bootstrapConfig;
|
|
1984
2516
|
const log = config.debug ?? false ? core.createLogger({ scope: "mushi", level: "debug", format: "pretty" }) : core.noopLogger;
|
|
1985
2517
|
const apiClient = core.createApiClient({
|
|
1986
|
-
projectId:
|
|
1987
|
-
apiKey:
|
|
1988
|
-
...
|
|
2518
|
+
projectId: bootstrapConfig.projectId,
|
|
2519
|
+
apiKey: bootstrapConfig.apiKey,
|
|
2520
|
+
...bootstrapConfig.apiEndpoint ? { apiEndpoint: bootstrapConfig.apiEndpoint } : {}
|
|
1989
2521
|
});
|
|
1990
|
-
const preFilter = core.createPreFilter(
|
|
1991
|
-
const offlineQueue = core.createOfflineQueue(
|
|
2522
|
+
const preFilter = core.createPreFilter(bootstrapConfig.preFilter);
|
|
2523
|
+
const offlineQueue = core.createOfflineQueue(bootstrapConfig.offline);
|
|
1992
2524
|
const rateLimiter = core.createRateLimiter({ maxBurst: 10, refillRate: 1, refillIntervalMs: 5e3 });
|
|
1993
2525
|
const piiScrubber = core.createPiiScrubber();
|
|
1994
2526
|
let consoleCap = null;
|
|
@@ -1996,6 +2528,8 @@ function createInstance(config) {
|
|
|
1996
2528
|
let perfCap = null;
|
|
1997
2529
|
let screenshotCap = null;
|
|
1998
2530
|
let elementSelector = null;
|
|
2531
|
+
const timelineCap = createTimelineCapture();
|
|
2532
|
+
let widget;
|
|
1999
2533
|
function syncCaptureModules() {
|
|
2000
2534
|
if (activeConfig.capture?.console !== false) {
|
|
2001
2535
|
consoleCap ??= createConsoleCapture();
|
|
@@ -2004,7 +2538,15 @@ function createInstance(config) {
|
|
|
2004
2538
|
consoleCap = null;
|
|
2005
2539
|
}
|
|
2006
2540
|
if (activeConfig.capture?.network !== false) {
|
|
2007
|
-
|
|
2541
|
+
const networkOptions = {
|
|
2542
|
+
apiEndpoint: resolveApiEndpoint(activeConfig),
|
|
2543
|
+
ignoreUrls: activeConfig.capture?.ignoreUrls
|
|
2544
|
+
};
|
|
2545
|
+
if (networkCap) {
|
|
2546
|
+
networkCap.updateOptions(networkOptions);
|
|
2547
|
+
} else {
|
|
2548
|
+
networkCap = createNetworkCapture(networkOptions);
|
|
2549
|
+
}
|
|
2008
2550
|
} else {
|
|
2009
2551
|
networkCap?.destroy();
|
|
2010
2552
|
networkCap = null;
|
|
@@ -2015,8 +2557,18 @@ function createInstance(config) {
|
|
|
2015
2557
|
perfCap?.destroy();
|
|
2016
2558
|
perfCap = null;
|
|
2017
2559
|
}
|
|
2018
|
-
|
|
2560
|
+
if (activeConfig.capture?.screenshot !== "off") {
|
|
2561
|
+
const screenshotOptions = { privacy: activeConfig.privacy };
|
|
2562
|
+
if (screenshotCap) {
|
|
2563
|
+
screenshotCap.updateOptions(screenshotOptions);
|
|
2564
|
+
} else {
|
|
2565
|
+
screenshotCap = createScreenshotCapture(screenshotOptions);
|
|
2566
|
+
}
|
|
2567
|
+
} else {
|
|
2568
|
+
screenshotCap = null;
|
|
2569
|
+
}
|
|
2019
2570
|
if (!screenshotCap) pendingScreenshot = null;
|
|
2571
|
+
widget.setAllowScreenshotRemove(activeConfig.privacy?.allowUserRemoveScreenshot !== false);
|
|
2020
2572
|
if (activeConfig.capture?.elementSelector !== false) {
|
|
2021
2573
|
elementSelector ??= createElementSelector();
|
|
2022
2574
|
} else {
|
|
@@ -2032,10 +2584,10 @@ function createInstance(config) {
|
|
|
2032
2584
|
let pendingScreenshot = null;
|
|
2033
2585
|
let pendingElement = null;
|
|
2034
2586
|
let pendingProactiveTrigger = null;
|
|
2587
|
+
let runtimeConfigLoaded = false;
|
|
2035
2588
|
let userInfo = null;
|
|
2036
2589
|
const customMetadata = {};
|
|
2037
|
-
|
|
2038
|
-
const widget = new MushiWidget(config.widget, {
|
|
2590
|
+
widget = new MushiWidget(bootstrapConfig.widget, {
|
|
2039
2591
|
onSubmit: async ({ category, description, intent }) => {
|
|
2040
2592
|
log.info("Report submitted", { category, intent });
|
|
2041
2593
|
proactiveManager?.recordSubmission();
|
|
@@ -2062,6 +2614,11 @@ function createInstance(config) {
|
|
|
2062
2614
|
pendingScreenshot = await screenshotCap.take();
|
|
2063
2615
|
widget.setScreenshotAttached(pendingScreenshot !== null);
|
|
2064
2616
|
},
|
|
2617
|
+
onScreenshotRemove: () => {
|
|
2618
|
+
log.debug("Screenshot attachment removed");
|
|
2619
|
+
pendingScreenshot = null;
|
|
2620
|
+
widget.setScreenshotAttached(false);
|
|
2621
|
+
},
|
|
2065
2622
|
onElementSelectorRequest: async () => {
|
|
2066
2623
|
if (!elementSelector || activeConfig.capture?.elementSelector === false) return;
|
|
2067
2624
|
log.debug("Element selector activated");
|
|
@@ -2071,8 +2628,23 @@ function createInstance(config) {
|
|
|
2071
2628
|
widget.setElementSelected(true);
|
|
2072
2629
|
log.debug("Element selected", { tagName: el.tagName, xpath: el.xpath });
|
|
2073
2630
|
}
|
|
2631
|
+
},
|
|
2632
|
+
async onReporterReportsRequest() {
|
|
2633
|
+
const result = await apiClient.listReporterReports(core.getReporterToken());
|
|
2634
|
+
if (!result.ok) throw new Error(result.error?.message ?? "Could not load reports");
|
|
2635
|
+
return result.data?.reports ?? [];
|
|
2636
|
+
},
|
|
2637
|
+
async onReporterCommentsRequest(reportId) {
|
|
2638
|
+
const result = await apiClient.listReporterComments(reportId, core.getReporterToken());
|
|
2639
|
+
if (!result.ok) throw new Error(result.error?.message ?? "Could not load thread");
|
|
2640
|
+
return result.data?.comments ?? [];
|
|
2641
|
+
},
|
|
2642
|
+
async onReporterReply(reportId, body) {
|
|
2643
|
+
const result = await apiClient.replyToReporterReport(reportId, core.getReporterToken(), body);
|
|
2644
|
+
if (!result.ok) throw new Error(result.error?.message ?? "Could not send reply");
|
|
2074
2645
|
}
|
|
2075
|
-
});
|
|
2646
|
+
}, MUSHI_SDK_VERSION);
|
|
2647
|
+
syncCaptureModules();
|
|
2076
2648
|
if (typeof document !== "undefined") {
|
|
2077
2649
|
if (document.readyState === "loading") {
|
|
2078
2650
|
document.addEventListener("DOMContentLoaded", () => widget.mount());
|
|
@@ -2082,7 +2654,7 @@ function createInstance(config) {
|
|
|
2082
2654
|
}
|
|
2083
2655
|
let proactiveTriggers = null;
|
|
2084
2656
|
let proactiveManager = null;
|
|
2085
|
-
const proactiveCfg =
|
|
2657
|
+
const proactiveCfg = activeConfig.proactive;
|
|
2086
2658
|
const hasAnyProactive = proactiveCfg && (proactiveCfg.rageClick !== false || proactiveCfg.longTask !== false || proactiveCfg.apiCascade !== false || proactiveCfg.errorBoundary === true);
|
|
2087
2659
|
if (hasAnyProactive && typeof document !== "undefined") {
|
|
2088
2660
|
proactiveManager = createProactiveManager(proactiveCfg?.cooldown);
|
|
@@ -2103,6 +2675,7 @@ function createInstance(config) {
|
|
|
2103
2675
|
rageClick: proactiveCfg?.rageClick,
|
|
2104
2676
|
longTask: proactiveCfg?.longTask,
|
|
2105
2677
|
apiCascade: proactiveCfg?.apiCascade,
|
|
2678
|
+
apiEndpoint: resolveApiEndpoint(activeConfig),
|
|
2106
2679
|
errorBoundary: proactiveCfg?.errorBoundary
|
|
2107
2680
|
}
|
|
2108
2681
|
);
|
|
@@ -2118,6 +2691,7 @@ function createInstance(config) {
|
|
|
2118
2691
|
if (result.sent > 0) log.info("Synced offline reports", { sent: result.sent });
|
|
2119
2692
|
});
|
|
2120
2693
|
function applyRuntimeConfig(runtime) {
|
|
2694
|
+
runtimeConfigLoaded = true;
|
|
2121
2695
|
if (runtime.enabled === false) {
|
|
2122
2696
|
activeConfig = bootstrapConfig;
|
|
2123
2697
|
clearCachedRuntimeConfig(config.projectId);
|
|
@@ -2131,7 +2705,7 @@ function createInstance(config) {
|
|
|
2131
2705
|
if (runtime.widget) widget.updateConfig(activeConfig.widget);
|
|
2132
2706
|
log.debug("Applied runtime SDK config", { version: runtime.version });
|
|
2133
2707
|
}
|
|
2134
|
-
if (config
|
|
2708
|
+
if (shouldUseRuntimeConfig(config)) {
|
|
2135
2709
|
const cached = readCachedRuntimeConfig(config.projectId);
|
|
2136
2710
|
if (cached) applyRuntimeConfig(cached);
|
|
2137
2711
|
apiClient.getSdkConfig().then((result) => {
|
|
@@ -2144,8 +2718,41 @@ function createInstance(config) {
|
|
|
2144
2718
|
}).catch((err) => {
|
|
2145
2719
|
log.debug("Runtime SDK config fetch failed", { error: err instanceof Error ? err.message : String(err) });
|
|
2146
2720
|
});
|
|
2721
|
+
} else if (config.runtimeConfig !== false && isLocalhostEndpoint(resolveApiEndpoint(config))) {
|
|
2722
|
+
log.debug("Runtime SDK config skipped for localhost apiEndpoint; set runtimeConfig: true to force it");
|
|
2147
2723
|
}
|
|
2724
|
+
void checkSdkFreshness();
|
|
2148
2725
|
log.info("Initialized", { projectId: config.projectId });
|
|
2726
|
+
async function checkSdkFreshness() {
|
|
2727
|
+
if (activeConfig.widget?.outdatedBanner === "off") return;
|
|
2728
|
+
const cached = readCachedSdkVersion(MUSHI_SDK_PACKAGE);
|
|
2729
|
+
if (cached) applySdkFreshness(cached);
|
|
2730
|
+
const result = await apiClient.getLatestSdkVersion(MUSHI_SDK_PACKAGE);
|
|
2731
|
+
if (!result.ok || !result.data) return;
|
|
2732
|
+
cacheSdkVersion(MUSHI_SDK_PACKAGE, result.data);
|
|
2733
|
+
applySdkFreshness(result.data);
|
|
2734
|
+
}
|
|
2735
|
+
function applySdkFreshness(info) {
|
|
2736
|
+
const latest = info.latest;
|
|
2737
|
+
const outdated = Boolean(latest && isVersionOlder(MUSHI_SDK_VERSION, latest));
|
|
2738
|
+
if (!outdated && !info.deprecated) return;
|
|
2739
|
+
const message = info.deprecationMessage ?? (outdated ? `Update ${MUSHI_SDK_PACKAGE} to ${latest}.` : null);
|
|
2740
|
+
log.warn("Mushi SDK is outdated", {
|
|
2741
|
+
package: MUSHI_SDK_PACKAGE,
|
|
2742
|
+
current: MUSHI_SDK_VERSION,
|
|
2743
|
+
latest,
|
|
2744
|
+
deprecated: info.deprecated,
|
|
2745
|
+
message
|
|
2746
|
+
});
|
|
2747
|
+
if (activeConfig.widget?.outdatedBanner !== "console-only") {
|
|
2748
|
+
widget.setSdkFreshness({
|
|
2749
|
+
latest,
|
|
2750
|
+
current: MUSHI_SDK_VERSION,
|
|
2751
|
+
deprecated: info.deprecated,
|
|
2752
|
+
message
|
|
2753
|
+
});
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2149
2756
|
async function submitReport(category, description, intent) {
|
|
2150
2757
|
const filterResult = preFilter.check(description);
|
|
2151
2758
|
if (!filterResult.passed) {
|
|
@@ -2187,6 +2794,8 @@ function createInstance(config) {
|
|
|
2187
2794
|
const scrubbedDescription = piiScrubber.scrub(preFilter.truncate(description));
|
|
2188
2795
|
const sentryCtx = config.sentry ? captureSentryContext(config.sentry) : void 0;
|
|
2189
2796
|
const fingerprintHash = await core.getDeviceFingerprintHash().catch(() => null);
|
|
2797
|
+
const consoleLogs = activeConfig.capture?.console === false ? void 0 : consoleCap?.getEntries();
|
|
2798
|
+
const networkLogs = activeConfig.capture?.network === false ? void 0 : networkCap?.getEntries();
|
|
2190
2799
|
const report = {
|
|
2191
2800
|
id: crypto.randomUUID?.() ?? `mushi_${Date.now()}_${Math.random().toString(36).slice(2)}`,
|
|
2192
2801
|
projectId: config.projectId,
|
|
@@ -2194,9 +2803,10 @@ function createInstance(config) {
|
|
|
2194
2803
|
description: scrubbedDescription,
|
|
2195
2804
|
userIntent: intent,
|
|
2196
2805
|
environment: core.captureEnvironment(),
|
|
2197
|
-
consoleLogs
|
|
2198
|
-
networkLogs
|
|
2806
|
+
consoleLogs,
|
|
2807
|
+
networkLogs,
|
|
2199
2808
|
performanceMetrics: activeConfig.capture?.performance === false ? void 0 : perfCap?.getMetrics(),
|
|
2809
|
+
timeline: timelineCap.getEntries({ consoleLogs, networkLogs }),
|
|
2200
2810
|
screenshotDataUrl: pendingScreenshot ?? void 0,
|
|
2201
2811
|
selectedElement: pendingElement ?? void 0,
|
|
2202
2812
|
metadata: {
|
|
@@ -2208,6 +2818,8 @@ function createInstance(config) {
|
|
|
2208
2818
|
reporterToken: core.getReporterToken(),
|
|
2209
2819
|
...fingerprintHash ? { fingerprintHash } : {},
|
|
2210
2820
|
appVersion: config.integrations?.vercel?.analyticsId,
|
|
2821
|
+
sdkPackage: MUSHI_SDK_PACKAGE,
|
|
2822
|
+
sdkVersion: MUSHI_SDK_VERSION,
|
|
2211
2823
|
proactiveTrigger: pendingProactiveTrigger ?? void 0,
|
|
2212
2824
|
sentryEventId: sentryCtx?.eventId,
|
|
2213
2825
|
sentryReplayId: sentryCtx?.replayId,
|
|
@@ -2262,6 +2874,9 @@ function createInstance(config) {
|
|
|
2262
2874
|
setMetadata(key, value) {
|
|
2263
2875
|
customMetadata[key] = value;
|
|
2264
2876
|
},
|
|
2877
|
+
setScreen(screen) {
|
|
2878
|
+
timelineCap.setScreen(screen);
|
|
2879
|
+
},
|
|
2265
2880
|
isOpen() {
|
|
2266
2881
|
return widget.getIsOpen();
|
|
2267
2882
|
},
|
|
@@ -2289,6 +2904,15 @@ function createInstance(config) {
|
|
|
2289
2904
|
updateConfig(runtimeConfig) {
|
|
2290
2905
|
applyRuntimeConfig(runtimeConfig);
|
|
2291
2906
|
},
|
|
2907
|
+
diagnose() {
|
|
2908
|
+
return runDiagnostics({
|
|
2909
|
+
apiEndpoint: resolveApiEndpoint(activeConfig),
|
|
2910
|
+
widgetMounted: widget.getIsMounted(),
|
|
2911
|
+
runtimeConfigLoaded,
|
|
2912
|
+
captureScreenshotAvailable: screenshotCap !== null,
|
|
2913
|
+
captureNetworkIntercepting: networkCap !== null
|
|
2914
|
+
});
|
|
2915
|
+
},
|
|
2292
2916
|
destroy() {
|
|
2293
2917
|
proactiveTriggers?.destroy();
|
|
2294
2918
|
proactiveManager?.reset();
|
|
@@ -2297,6 +2921,7 @@ function createInstance(config) {
|
|
|
2297
2921
|
networkCap?.destroy();
|
|
2298
2922
|
perfCap?.destroy();
|
|
2299
2923
|
elementSelector?.deactivate();
|
|
2924
|
+
timelineCap.destroy();
|
|
2300
2925
|
offlineQueue.stopAutoSync();
|
|
2301
2926
|
listeners.clear();
|
|
2302
2927
|
instance = null;
|
|
@@ -2318,6 +2943,7 @@ function createInstance(config) {
|
|
|
2318
2943
|
category,
|
|
2319
2944
|
description,
|
|
2320
2945
|
environment: core.captureEnvironment(),
|
|
2946
|
+
timeline: timelineCap.getEntries(),
|
|
2321
2947
|
metadata: {
|
|
2322
2948
|
...input.metadata ?? {},
|
|
2323
2949
|
...userInfo ? { user: userInfo } : {},
|
|
@@ -2329,6 +2955,8 @@ function createInstance(config) {
|
|
|
2329
2955
|
},
|
|
2330
2956
|
sessionId: core.getSessionId(),
|
|
2331
2957
|
reporterToken: core.getReporterToken(),
|
|
2958
|
+
sdkPackage: MUSHI_SDK_PACKAGE,
|
|
2959
|
+
sdkVersion: MUSHI_SDK_VERSION,
|
|
2332
2960
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2333
2961
|
};
|
|
2334
2962
|
emit("report:submitted", { reportId: report.id });
|
|
@@ -2370,12 +2998,128 @@ function mergeRuntimeConfig(config, runtime) {
|
|
|
2370
2998
|
capture: {
|
|
2371
2999
|
...config.capture,
|
|
2372
3000
|
...runtime.capture
|
|
3001
|
+
},
|
|
3002
|
+
privacy: {
|
|
3003
|
+
...config.privacy
|
|
3004
|
+
}
|
|
3005
|
+
};
|
|
3006
|
+
}
|
|
3007
|
+
function applyPresetConfig(config) {
|
|
3008
|
+
if (!config.preset) return config;
|
|
3009
|
+
const preset = presetDefaults(config.preset);
|
|
3010
|
+
return {
|
|
3011
|
+
...config,
|
|
3012
|
+
widget: {
|
|
3013
|
+
...preset.widget,
|
|
3014
|
+
...config.widget
|
|
3015
|
+
},
|
|
3016
|
+
capture: {
|
|
3017
|
+
...preset.capture,
|
|
3018
|
+
...config.capture
|
|
3019
|
+
},
|
|
3020
|
+
proactive: {
|
|
3021
|
+
...preset.proactive,
|
|
3022
|
+
...config.proactive,
|
|
3023
|
+
cooldown: {
|
|
3024
|
+
...preset.proactive?.cooldown,
|
|
3025
|
+
...config.proactive?.cooldown
|
|
3026
|
+
}
|
|
2373
3027
|
}
|
|
2374
3028
|
};
|
|
2375
3029
|
}
|
|
3030
|
+
function presetDefaults(preset) {
|
|
3031
|
+
switch (preset) {
|
|
3032
|
+
case "manual-only":
|
|
3033
|
+
return {
|
|
3034
|
+
widget: { trigger: "manual", outdatedBanner: "console-only" },
|
|
3035
|
+
capture: { console: true, network: true, performance: false, screenshot: "on-report", elementSelector: false },
|
|
3036
|
+
proactive: { rageClick: false, longTask: false, apiCascade: false, errorBoundary: false }
|
|
3037
|
+
};
|
|
3038
|
+
case "beta-loud":
|
|
3039
|
+
return {
|
|
3040
|
+
widget: { trigger: "auto", outdatedBanner: "banner" },
|
|
3041
|
+
capture: { console: true, network: true, performance: true, screenshot: "auto", elementSelector: true },
|
|
3042
|
+
proactive: { rageClick: true, longTask: true, apiCascade: true, errorBoundary: true }
|
|
3043
|
+
};
|
|
3044
|
+
case "internal-debug":
|
|
3045
|
+
return {
|
|
3046
|
+
widget: { trigger: "auto", outdatedBanner: "banner", brandFooter: true },
|
|
3047
|
+
capture: { console: true, network: true, performance: true, screenshot: "auto", elementSelector: true },
|
|
3048
|
+
proactive: {
|
|
3049
|
+
rageClick: true,
|
|
3050
|
+
longTask: true,
|
|
3051
|
+
apiCascade: true,
|
|
3052
|
+
errorBoundary: true,
|
|
3053
|
+
cooldown: { maxProactivePerSession: 10, dismissCooldownHours: 0, suppressAfterDismissals: 99 }
|
|
3054
|
+
}
|
|
3055
|
+
};
|
|
3056
|
+
case "production-calm":
|
|
3057
|
+
return {
|
|
3058
|
+
widget: { trigger: "auto", outdatedBanner: "console-only" },
|
|
3059
|
+
capture: { console: true, network: true, performance: false, screenshot: "on-report", elementSelector: false },
|
|
3060
|
+
proactive: { rageClick: false, longTask: false, apiCascade: false, errorBoundary: false }
|
|
3061
|
+
};
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
function resolveApiEndpoint(config) {
|
|
3065
|
+
return config.apiEndpoint ?? core.DEFAULT_API_ENDPOINT;
|
|
3066
|
+
}
|
|
3067
|
+
function shouldUseRuntimeConfig(config) {
|
|
3068
|
+
if (config.runtimeConfig === false) return false;
|
|
3069
|
+
if (config.runtimeConfig === true) return true;
|
|
3070
|
+
return !isLocalhostEndpoint(resolveApiEndpoint(config));
|
|
3071
|
+
}
|
|
3072
|
+
async function runDiagnostics(options) {
|
|
3073
|
+
const endpoint = await probeApiEndpoint(options.apiEndpoint);
|
|
3074
|
+
return {
|
|
3075
|
+
apiEndpointReachable: endpoint.reachable,
|
|
3076
|
+
cspAllowsEndpoint: endpoint.cspAllowed,
|
|
3077
|
+
widgetMounted: options.widgetMounted,
|
|
3078
|
+
shadowDomAvailable: typeof HTMLElement !== "undefined" && typeof HTMLElement.prototype.attachShadow === "function",
|
|
3079
|
+
dialogSupported: typeof HTMLDialogElement !== "undefined",
|
|
3080
|
+
runtimeConfigLoaded: options.runtimeConfigLoaded,
|
|
3081
|
+
captureScreenshotAvailable: options.captureScreenshotAvailable,
|
|
3082
|
+
captureNetworkIntercepting: options.captureNetworkIntercepting,
|
|
3083
|
+
sdkVersion: MUSHI_SDK_VERSION
|
|
3084
|
+
};
|
|
3085
|
+
}
|
|
3086
|
+
async function diagnoseWithoutInstance() {
|
|
3087
|
+
return {
|
|
3088
|
+
apiEndpointReachable: false,
|
|
3089
|
+
cspAllowsEndpoint: false,
|
|
3090
|
+
widgetMounted: false,
|
|
3091
|
+
shadowDomAvailable: typeof HTMLElement !== "undefined" && typeof HTMLElement.prototype.attachShadow === "function",
|
|
3092
|
+
dialogSupported: typeof HTMLDialogElement !== "undefined",
|
|
3093
|
+
runtimeConfigLoaded: false,
|
|
3094
|
+
captureScreenshotAvailable: false,
|
|
3095
|
+
captureNetworkIntercepting: false,
|
|
3096
|
+
sdkVersion: MUSHI_SDK_VERSION
|
|
3097
|
+
};
|
|
3098
|
+
}
|
|
3099
|
+
async function probeApiEndpoint(apiEndpoint) {
|
|
3100
|
+
if (typeof fetch === "undefined") return { reachable: false, cspAllowed: false };
|
|
3101
|
+
const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
3102
|
+
const timer = controller ? setTimeout(() => controller.abort(), 3e3) : null;
|
|
3103
|
+
try {
|
|
3104
|
+
const response = await fetch(`${apiEndpoint.replace(/\/$/, "")}/health`, {
|
|
3105
|
+
method: "GET",
|
|
3106
|
+
cache: "no-store",
|
|
3107
|
+
...controller ? { signal: controller.signal } : {},
|
|
3108
|
+
[core.MUSHI_INTERNAL_INIT_MARKER]: "diagnose"
|
|
3109
|
+
});
|
|
3110
|
+
return { reachable: response.ok, cspAllowed: true };
|
|
3111
|
+
} catch {
|
|
3112
|
+
return { reachable: false, cspAllowed: false };
|
|
3113
|
+
} finally {
|
|
3114
|
+
if (timer) clearTimeout(timer);
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
2376
3117
|
function runtimeConfigCacheKey(projectId) {
|
|
2377
3118
|
return `mushi:sdk-config:${projectId}`;
|
|
2378
3119
|
}
|
|
3120
|
+
function sdkVersionCacheKey(packageName) {
|
|
3121
|
+
return `mushi:sdk-version:${packageName}`;
|
|
3122
|
+
}
|
|
2379
3123
|
function readCachedRuntimeConfig(projectId) {
|
|
2380
3124
|
if (typeof localStorage === "undefined") return null;
|
|
2381
3125
|
try {
|
|
@@ -2404,6 +3148,42 @@ function clearCachedRuntimeConfig(projectId) {
|
|
|
2404
3148
|
} catch {
|
|
2405
3149
|
}
|
|
2406
3150
|
}
|
|
3151
|
+
function readCachedSdkVersion(packageName) {
|
|
3152
|
+
if (typeof localStorage === "undefined") return null;
|
|
3153
|
+
try {
|
|
3154
|
+
const raw = localStorage.getItem(sdkVersionCacheKey(packageName));
|
|
3155
|
+
if (!raw) return null;
|
|
3156
|
+
const parsed = JSON.parse(raw);
|
|
3157
|
+
if (!parsed.data || !parsed.cachedAt || Date.now() - parsed.cachedAt > 864e5) return null;
|
|
3158
|
+
return parsed.data;
|
|
3159
|
+
} catch {
|
|
3160
|
+
return null;
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
function cacheSdkVersion(packageName, data) {
|
|
3164
|
+
if (typeof localStorage === "undefined") return;
|
|
3165
|
+
try {
|
|
3166
|
+
localStorage.setItem(sdkVersionCacheKey(packageName), JSON.stringify({
|
|
3167
|
+
cachedAt: Date.now(),
|
|
3168
|
+
data
|
|
3169
|
+
}));
|
|
3170
|
+
} catch {
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
function isVersionOlder(current, latest) {
|
|
3174
|
+
const currentParts = parseVersion(current);
|
|
3175
|
+
const latestParts = parseVersion(latest);
|
|
3176
|
+
for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
|
|
3177
|
+
const cur = currentParts[i] ?? 0;
|
|
3178
|
+
const next = latestParts[i] ?? 0;
|
|
3179
|
+
if (cur < next) return true;
|
|
3180
|
+
if (cur > next) return false;
|
|
3181
|
+
}
|
|
3182
|
+
return false;
|
|
3183
|
+
}
|
|
3184
|
+
function parseVersion(version) {
|
|
3185
|
+
return version.split(/[.-]/).map((part) => Number.parseInt(part, 10)).filter((part) => Number.isFinite(part));
|
|
3186
|
+
}
|
|
2407
3187
|
function createNoopInstance() {
|
|
2408
3188
|
return {
|
|
2409
3189
|
report: () => {
|
|
@@ -2414,6 +3194,8 @@ function createNoopInstance() {
|
|
|
2414
3194
|
},
|
|
2415
3195
|
setMetadata: () => {
|
|
2416
3196
|
},
|
|
3197
|
+
setScreen: () => {
|
|
3198
|
+
},
|
|
2417
3199
|
isOpen: () => false,
|
|
2418
3200
|
open: () => {
|
|
2419
3201
|
},
|
|
@@ -2421,6 +3203,7 @@ function createNoopInstance() {
|
|
|
2421
3203
|
},
|
|
2422
3204
|
updateConfig: () => {
|
|
2423
3205
|
},
|
|
3206
|
+
diagnose: diagnoseWithoutInstance,
|
|
2424
3207
|
openWith: () => {
|
|
2425
3208
|
},
|
|
2426
3209
|
show: () => {
|
|
@@ -2448,6 +3231,7 @@ exports.createNetworkCapture = createNetworkCapture;
|
|
|
2448
3231
|
exports.createPerformanceCapture = createPerformanceCapture;
|
|
2449
3232
|
exports.createProactiveManager = createProactiveManager;
|
|
2450
3233
|
exports.createScreenshotCapture = createScreenshotCapture;
|
|
3234
|
+
exports.createTimelineCapture = createTimelineCapture;
|
|
2451
3235
|
exports.getAvailableLocales = getAvailableLocales;
|
|
2452
3236
|
exports.getLocale = getLocale;
|
|
2453
3237
|
exports.setupProactiveTriggers = setupProactiveTriggers;
|