@mushi-mushi/web 0.7.0 → 0.9.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 +170 -5
- package/dist/index.cjs +1119 -93
- 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 +1120 -95
- 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
|
|
@@ -1640,6 +2052,17 @@ function createElementSelector() {
|
|
|
1640
2052
|
let active = false;
|
|
1641
2053
|
let overlay = null;
|
|
1642
2054
|
let resolvePromise = null;
|
|
2055
|
+
function findNearestTestid(el) {
|
|
2056
|
+
let cur = el;
|
|
2057
|
+
let hops = 0;
|
|
2058
|
+
while (cur && hops < 20) {
|
|
2059
|
+
const tid = cur.getAttribute?.("data-testid");
|
|
2060
|
+
if (tid) return tid;
|
|
2061
|
+
cur = cur.parentElement;
|
|
2062
|
+
hops++;
|
|
2063
|
+
}
|
|
2064
|
+
return null;
|
|
2065
|
+
}
|
|
1643
2066
|
function getXPath(el) {
|
|
1644
2067
|
const parts = [];
|
|
1645
2068
|
let current = el;
|
|
@@ -1669,7 +2092,13 @@ function createElementSelector() {
|
|
|
1669
2092
|
y: Math.round(rect.y),
|
|
1670
2093
|
width: Math.round(rect.width),
|
|
1671
2094
|
height: Math.round(rect.height)
|
|
1672
|
-
}
|
|
2095
|
+
},
|
|
2096
|
+
// v2 (whitepaper §4.7): the closest ancestor's `data-testid` lets the
|
|
2097
|
+
// server map this report → an Action node in the inventory graph
|
|
2098
|
+
// without a fuzzy NLP guess. We walk to the body so a deeply nested
|
|
2099
|
+
// span inside a button-with-testid still resolves correctly.
|
|
2100
|
+
nearestTestid: findNearestTestid(el) || void 0,
|
|
2101
|
+
route: typeof window !== "undefined" ? window.location.pathname : void 0
|
|
1673
2102
|
};
|
|
1674
2103
|
}
|
|
1675
2104
|
function createOverlay() {
|
|
@@ -1746,6 +2175,296 @@ function createElementSelector() {
|
|
|
1746
2175
|
return { activate, deactivate, isActive: () => active };
|
|
1747
2176
|
}
|
|
1748
2177
|
|
|
2178
|
+
// src/capture/timeline.ts
|
|
2179
|
+
var MAX_TIMELINE_ENTRIES = 120;
|
|
2180
|
+
function createTimelineCapture() {
|
|
2181
|
+
const entries = [];
|
|
2182
|
+
const originalPushState = history.pushState;
|
|
2183
|
+
const originalReplaceState = history.replaceState;
|
|
2184
|
+
const handlePopState = () => recordRoute("popstate");
|
|
2185
|
+
const handleHashChange = () => recordRoute("hashchange");
|
|
2186
|
+
recordRoute("initial");
|
|
2187
|
+
function record(entry) {
|
|
2188
|
+
entries.push(entry);
|
|
2189
|
+
if (entries.length > MAX_TIMELINE_ENTRIES) entries.shift();
|
|
2190
|
+
}
|
|
2191
|
+
function recordRoute(source) {
|
|
2192
|
+
if (typeof location === "undefined") return;
|
|
2193
|
+
record({
|
|
2194
|
+
ts: Date.now(),
|
|
2195
|
+
kind: "route",
|
|
2196
|
+
payload: {
|
|
2197
|
+
source,
|
|
2198
|
+
route: `${location.pathname}${location.search}${location.hash}`,
|
|
2199
|
+
href: location.href
|
|
2200
|
+
}
|
|
2201
|
+
});
|
|
2202
|
+
}
|
|
2203
|
+
function handleClick(event) {
|
|
2204
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
2205
|
+
if (!target) return;
|
|
2206
|
+
const el = target.closest('button,a,[role="button"],input,textarea,select,[data-mushi-track]') ?? target;
|
|
2207
|
+
record({
|
|
2208
|
+
ts: Date.now(),
|
|
2209
|
+
kind: "click",
|
|
2210
|
+
payload: {
|
|
2211
|
+
tag: el.tagName.toLowerCase(),
|
|
2212
|
+
id: el.id || void 0,
|
|
2213
|
+
text: textSnippet(el)
|
|
2214
|
+
}
|
|
2215
|
+
});
|
|
2216
|
+
}
|
|
2217
|
+
history.pushState = function mushiPushState(...args) {
|
|
2218
|
+
const result = originalPushState.apply(this, args);
|
|
2219
|
+
recordRoute("pushState");
|
|
2220
|
+
return result;
|
|
2221
|
+
};
|
|
2222
|
+
history.replaceState = function mushiReplaceState(...args) {
|
|
2223
|
+
const result = originalReplaceState.apply(this, args);
|
|
2224
|
+
recordRoute("replaceState");
|
|
2225
|
+
return result;
|
|
2226
|
+
};
|
|
2227
|
+
window.addEventListener("popstate", handlePopState);
|
|
2228
|
+
window.addEventListener("hashchange", handleHashChange);
|
|
2229
|
+
document.addEventListener("click", handleClick, true);
|
|
2230
|
+
return {
|
|
2231
|
+
setScreen(screen) {
|
|
2232
|
+
record({
|
|
2233
|
+
ts: Date.now(),
|
|
2234
|
+
kind: "screen",
|
|
2235
|
+
payload: screen
|
|
2236
|
+
});
|
|
2237
|
+
},
|
|
2238
|
+
getEntries(input = {}) {
|
|
2239
|
+
const merged = [
|
|
2240
|
+
...entries,
|
|
2241
|
+
...(input.consoleLogs ?? []).map((log) => ({
|
|
2242
|
+
ts: log.timestamp,
|
|
2243
|
+
kind: "log",
|
|
2244
|
+
payload: {
|
|
2245
|
+
level: log.level,
|
|
2246
|
+
message: log.message
|
|
2247
|
+
}
|
|
2248
|
+
})),
|
|
2249
|
+
...(input.networkLogs ?? []).map((network) => ({
|
|
2250
|
+
ts: network.timestamp,
|
|
2251
|
+
kind: "request",
|
|
2252
|
+
payload: {
|
|
2253
|
+
method: network.method,
|
|
2254
|
+
url: network.url,
|
|
2255
|
+
status: network.status,
|
|
2256
|
+
duration: network.duration,
|
|
2257
|
+
error: network.error
|
|
2258
|
+
}
|
|
2259
|
+
}))
|
|
2260
|
+
].sort((a, b) => a.ts - b.ts);
|
|
2261
|
+
return merged.slice(-MAX_TIMELINE_ENTRIES);
|
|
2262
|
+
},
|
|
2263
|
+
clear() {
|
|
2264
|
+
entries.length = 0;
|
|
2265
|
+
},
|
|
2266
|
+
destroy() {
|
|
2267
|
+
history.pushState = originalPushState;
|
|
2268
|
+
history.replaceState = originalReplaceState;
|
|
2269
|
+
window.removeEventListener("popstate", handlePopState);
|
|
2270
|
+
window.removeEventListener("hashchange", handleHashChange);
|
|
2271
|
+
document.removeEventListener("click", handleClick, true);
|
|
2272
|
+
}
|
|
2273
|
+
};
|
|
2274
|
+
}
|
|
2275
|
+
function textSnippet(el) {
|
|
2276
|
+
const text = (el.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
2277
|
+
return text ? text.slice(0, 80) : void 0;
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
// src/capture/discovery.ts
|
|
2281
|
+
var DEFAULT_THROTTLE_MS = 6e4;
|
|
2282
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
2283
|
+
var HEX24_RE = /^[0-9a-f]{20,}$/i;
|
|
2284
|
+
var NUMERIC_RE = /^\d+$/;
|
|
2285
|
+
var SLUG_HASHY_RE = /^[a-z0-9]{16,}$/i;
|
|
2286
|
+
function normalizeSegment(seg) {
|
|
2287
|
+
if (seg.length === 0) return seg;
|
|
2288
|
+
if (UUID_RE.test(seg)) return "[id]";
|
|
2289
|
+
if (HEX24_RE.test(seg)) return "[id]";
|
|
2290
|
+
if (NUMERIC_RE.test(seg)) return "[id]";
|
|
2291
|
+
if (SLUG_HASHY_RE.test(seg) && /\d/.test(seg)) return "[id]";
|
|
2292
|
+
return seg;
|
|
2293
|
+
}
|
|
2294
|
+
function normalizeRoute(pathname, templates) {
|
|
2295
|
+
const clean = pathname.length > 1 && pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
|
|
2296
|
+
if (templates?.length) {
|
|
2297
|
+
const matched = matchTemplate(clean, templates);
|
|
2298
|
+
if (matched) return matched;
|
|
2299
|
+
}
|
|
2300
|
+
return "/" + clean.split("/").filter((s) => s.length > 0).map(normalizeSegment).join("/");
|
|
2301
|
+
}
|
|
2302
|
+
function matchTemplate(pathname, templates) {
|
|
2303
|
+
const segs = pathname.split("/").filter((s) => s.length > 0);
|
|
2304
|
+
const sorted = [...templates].sort(
|
|
2305
|
+
(a, b) => b.split("/").length - a.split("/").length
|
|
2306
|
+
);
|
|
2307
|
+
for (const tpl of sorted) {
|
|
2308
|
+
const tplSegs = tpl.split("/").filter((s) => s.length > 0);
|
|
2309
|
+
if (tplSegs.length !== segs.length) continue;
|
|
2310
|
+
let ok = true;
|
|
2311
|
+
for (let i = 0; i < tplSegs.length; i++) {
|
|
2312
|
+
const t = tplSegs[i];
|
|
2313
|
+
const s = segs[i];
|
|
2314
|
+
if (t.startsWith("[") && t.endsWith("]")) continue;
|
|
2315
|
+
if (t.startsWith(":")) continue;
|
|
2316
|
+
if (t === s) continue;
|
|
2317
|
+
ok = false;
|
|
2318
|
+
break;
|
|
2319
|
+
}
|
|
2320
|
+
if (ok) return "/" + tplSegs.join("/");
|
|
2321
|
+
}
|
|
2322
|
+
return null;
|
|
2323
|
+
}
|
|
2324
|
+
function readTestids() {
|
|
2325
|
+
if (typeof document === "undefined") return [];
|
|
2326
|
+
const out = /* @__PURE__ */ new Set();
|
|
2327
|
+
const els = document.querySelectorAll("[data-testid]");
|
|
2328
|
+
for (const el of Array.from(els)) {
|
|
2329
|
+
const v = el.getAttribute("data-testid");
|
|
2330
|
+
if (v && v.length > 0 && v.length < 120) out.add(v);
|
|
2331
|
+
}
|
|
2332
|
+
return Array.from(out).sort();
|
|
2333
|
+
}
|
|
2334
|
+
function readQueryParamKeys() {
|
|
2335
|
+
if (typeof window === "undefined") return [];
|
|
2336
|
+
try {
|
|
2337
|
+
const params = new URLSearchParams(window.location.search);
|
|
2338
|
+
const out = /* @__PURE__ */ new Set();
|
|
2339
|
+
params.forEach((_, key) => out.add(key));
|
|
2340
|
+
return Array.from(out).sort();
|
|
2341
|
+
} catch {
|
|
2342
|
+
return [];
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
function readDomSummary() {
|
|
2346
|
+
if (typeof document === "undefined") return null;
|
|
2347
|
+
const trim = (s) => (s ?? "").replace(/\s+/g, " ").trim().slice(0, 200);
|
|
2348
|
+
const h1 = trim(document.querySelector("h1")?.textContent);
|
|
2349
|
+
if (h1) return h1;
|
|
2350
|
+
const title = trim(document.title);
|
|
2351
|
+
if (title) return title;
|
|
2352
|
+
const main = trim(document.querySelector("main")?.textContent);
|
|
2353
|
+
return main || null;
|
|
2354
|
+
}
|
|
2355
|
+
async function hashUserId(input) {
|
|
2356
|
+
if (!input || typeof crypto === "undefined" || !crypto.subtle) return null;
|
|
2357
|
+
try {
|
|
2358
|
+
const data = new TextEncoder().encode(input);
|
|
2359
|
+
const buf = await crypto.subtle.digest("SHA-256", data);
|
|
2360
|
+
const bytes = new Uint8Array(buf);
|
|
2361
|
+
let hex = "";
|
|
2362
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
2363
|
+
hex += bytes[i].toString(16).padStart(2, "0");
|
|
2364
|
+
}
|
|
2365
|
+
return hex;
|
|
2366
|
+
} catch {
|
|
2367
|
+
return null;
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
function createDiscoveryCapture(opts) {
|
|
2371
|
+
const {
|
|
2372
|
+
config,
|
|
2373
|
+
getRecentNetworkPaths,
|
|
2374
|
+
getUserId,
|
|
2375
|
+
getSessionId: getSessionId2,
|
|
2376
|
+
onEvent
|
|
2377
|
+
} = opts;
|
|
2378
|
+
const throttleMs = config.throttleMs ?? DEFAULT_THROTTLE_MS;
|
|
2379
|
+
const captureSummary = config.captureDomSummary !== false;
|
|
2380
|
+
const userIdSource = config.userIdSource ?? "auto";
|
|
2381
|
+
const lastEmittedAt = /* @__PURE__ */ new Map();
|
|
2382
|
+
let lastPath = null;
|
|
2383
|
+
let pendingTimer = null;
|
|
2384
|
+
async function emitForCurrent() {
|
|
2385
|
+
if (typeof window === "undefined") return;
|
|
2386
|
+
const route = normalizeRoute(window.location.pathname, config.routeTemplates);
|
|
2387
|
+
const now = Date.now();
|
|
2388
|
+
const last = lastEmittedAt.get(route) ?? 0;
|
|
2389
|
+
if (now - last < throttleMs) return;
|
|
2390
|
+
lastEmittedAt.set(route, now);
|
|
2391
|
+
let userIdInput = null;
|
|
2392
|
+
if (userIdSource === "auto") {
|
|
2393
|
+
userIdInput = getUserId() ?? getSessionId2();
|
|
2394
|
+
} else if (userIdSource === "session-only") {
|
|
2395
|
+
userIdInput = getSessionId2();
|
|
2396
|
+
}
|
|
2397
|
+
const event = {
|
|
2398
|
+
route,
|
|
2399
|
+
page_title: typeof document !== "undefined" ? (document.title || "").slice(0, 300) || null : null,
|
|
2400
|
+
dom_summary: captureSummary ? readDomSummary() : null,
|
|
2401
|
+
testids: readTestids(),
|
|
2402
|
+
network_paths: getRecentNetworkPaths().slice(-50),
|
|
2403
|
+
query_param_keys: readQueryParamKeys(),
|
|
2404
|
+
user_id_hash: await hashUserId(userIdInput),
|
|
2405
|
+
observed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2406
|
+
};
|
|
2407
|
+
onEvent(event);
|
|
2408
|
+
}
|
|
2409
|
+
function scheduleEmit() {
|
|
2410
|
+
if (pendingTimer) return;
|
|
2411
|
+
pendingTimer = setTimeout(() => {
|
|
2412
|
+
pendingTimer = null;
|
|
2413
|
+
void emitForCurrent();
|
|
2414
|
+
}, 100);
|
|
2415
|
+
}
|
|
2416
|
+
function onMaybeNavigation() {
|
|
2417
|
+
if (typeof window === "undefined") return;
|
|
2418
|
+
const path = window.location.pathname + window.location.search;
|
|
2419
|
+
if (path === lastPath) return;
|
|
2420
|
+
lastPath = path;
|
|
2421
|
+
scheduleEmit();
|
|
2422
|
+
}
|
|
2423
|
+
if (typeof window === "undefined") {
|
|
2424
|
+
return {
|
|
2425
|
+
destroy: () => void 0,
|
|
2426
|
+
flushNow: () => void 0
|
|
2427
|
+
};
|
|
2428
|
+
}
|
|
2429
|
+
const originalPush = window.history.pushState.bind(window.history);
|
|
2430
|
+
const originalReplace = window.history.replaceState.bind(window.history);
|
|
2431
|
+
const patchedPush = function patched(...args) {
|
|
2432
|
+
const out = originalPush(...args);
|
|
2433
|
+
onMaybeNavigation();
|
|
2434
|
+
return out;
|
|
2435
|
+
};
|
|
2436
|
+
const patchedReplace = function patched(...args) {
|
|
2437
|
+
const out = originalReplace(...args);
|
|
2438
|
+
onMaybeNavigation();
|
|
2439
|
+
return out;
|
|
2440
|
+
};
|
|
2441
|
+
window.history.pushState = patchedPush;
|
|
2442
|
+
window.history.replaceState = patchedReplace;
|
|
2443
|
+
const onPop = () => onMaybeNavigation();
|
|
2444
|
+
window.addEventListener("popstate", onPop);
|
|
2445
|
+
scheduleEmit();
|
|
2446
|
+
return {
|
|
2447
|
+
destroy() {
|
|
2448
|
+
window.removeEventListener("popstate", onPop);
|
|
2449
|
+
if (window.history.pushState === patchedPush) {
|
|
2450
|
+
window.history.pushState = originalPush;
|
|
2451
|
+
}
|
|
2452
|
+
if (window.history.replaceState === patchedReplace) {
|
|
2453
|
+
window.history.replaceState = originalReplace;
|
|
2454
|
+
}
|
|
2455
|
+
if (pendingTimer) {
|
|
2456
|
+
clearTimeout(pendingTimer);
|
|
2457
|
+
pendingTimer = null;
|
|
2458
|
+
}
|
|
2459
|
+
lastEmittedAt.clear();
|
|
2460
|
+
},
|
|
2461
|
+
flushNow() {
|
|
2462
|
+
lastEmittedAt.clear();
|
|
2463
|
+
void emitForCurrent();
|
|
2464
|
+
}
|
|
2465
|
+
};
|
|
2466
|
+
}
|
|
2467
|
+
|
|
1749
2468
|
// src/sentry.ts
|
|
1750
2469
|
function getSentryGlobal() {
|
|
1751
2470
|
try {
|
|
@@ -1832,36 +2551,25 @@ function setupProactiveTriggers(callbacks, config = {}) {
|
|
|
1832
2551
|
} catch {
|
|
1833
2552
|
}
|
|
1834
2553
|
}
|
|
1835
|
-
|
|
2554
|
+
const apiCascade = normalizeApiCascadeConfig(config.apiCascade);
|
|
2555
|
+
if (apiCascade.enabled) {
|
|
1836
2556
|
const failedRequests = [];
|
|
1837
2557
|
const origFetch = globalThis.fetch;
|
|
1838
2558
|
globalThis.fetch = async function(...args) {
|
|
2559
|
+
const [input, init] = args;
|
|
2560
|
+
const url = getRequestUrl(input);
|
|
2561
|
+
const ignoreFailure = Boolean(getInternalRequestKind(input, init)) || shouldIgnoreMushiUrl(url, {
|
|
2562
|
+
apiEndpoint: config.apiEndpoint,
|
|
2563
|
+
ignoreUrls: apiCascade.ignoreUrls
|
|
2564
|
+
});
|
|
1839
2565
|
try {
|
|
1840
2566
|
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
|
-
}
|
|
2567
|
+
if (!ignoreFailure && !res.ok && res.status >= 400) {
|
|
2568
|
+
recordApiFailure(failedRequests, callbacks);
|
|
1852
2569
|
}
|
|
1853
2570
|
return res;
|
|
1854
2571
|
} 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
|
-
}
|
|
2572
|
+
if (!ignoreFailure) recordApiFailure(failedRequests, callbacks);
|
|
1865
2573
|
throw err;
|
|
1866
2574
|
}
|
|
1867
2575
|
};
|
|
@@ -1896,6 +2604,28 @@ function setupProactiveTriggers(callbacks, config = {}) {
|
|
|
1896
2604
|
}
|
|
1897
2605
|
};
|
|
1898
2606
|
}
|
|
2607
|
+
function normalizeApiCascadeConfig(config) {
|
|
2608
|
+
if (config === false) return { enabled: false, ignoreUrls: [] };
|
|
2609
|
+
if (config && typeof config === "object") {
|
|
2610
|
+
return {
|
|
2611
|
+
enabled: config.enabled !== false,
|
|
2612
|
+
ignoreUrls: config.ignoreUrls ?? []
|
|
2613
|
+
};
|
|
2614
|
+
}
|
|
2615
|
+
return { enabled: true, ignoreUrls: [] };
|
|
2616
|
+
}
|
|
2617
|
+
function recordApiFailure(failedRequests, callbacks) {
|
|
2618
|
+
const now = Date.now();
|
|
2619
|
+
failedRequests.push(now);
|
|
2620
|
+
const recentFailures = failedRequests.filter((t) => now - t < 1e4);
|
|
2621
|
+
if (recentFailures.length >= 3) {
|
|
2622
|
+
callbacks.onTrigger("api_cascade", {
|
|
2623
|
+
failureCount: recentFailures.length,
|
|
2624
|
+
windowMs: 1e4
|
|
2625
|
+
});
|
|
2626
|
+
failedRequests.length = 0;
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
1899
2629
|
|
|
1900
2630
|
// src/proactive-manager.ts
|
|
1901
2631
|
var STORAGE_KEY_LAST_DISMISS = "mushi:lastDismiss";
|
|
@@ -1948,6 +2678,10 @@ function createProactiveManager(config = {}) {
|
|
|
1948
2678
|
return { shouldShow, recordDismissal, recordSubmission, reset };
|
|
1949
2679
|
}
|
|
1950
2680
|
|
|
2681
|
+
// src/version.ts
|
|
2682
|
+
var MUSHI_SDK_PACKAGE = "@mushi-mushi/web";
|
|
2683
|
+
var MUSHI_SDK_VERSION = "0.9.0" ;
|
|
2684
|
+
|
|
1951
2685
|
// src/mushi.ts
|
|
1952
2686
|
var instance = null;
|
|
1953
2687
|
var Mushi = class {
|
|
@@ -1977,18 +2711,21 @@ var Mushi = class {
|
|
|
1977
2711
|
instance?.destroy();
|
|
1978
2712
|
instance = null;
|
|
1979
2713
|
}
|
|
2714
|
+
static diagnose() {
|
|
2715
|
+
return instance?.diagnose() ?? diagnoseWithoutInstance();
|
|
2716
|
+
}
|
|
1980
2717
|
};
|
|
1981
2718
|
function createInstance(config) {
|
|
1982
|
-
const bootstrapConfig = config;
|
|
1983
|
-
let activeConfig =
|
|
2719
|
+
const bootstrapConfig = applyPresetConfig(config);
|
|
2720
|
+
let activeConfig = bootstrapConfig;
|
|
1984
2721
|
const log = config.debug ?? false ? core.createLogger({ scope: "mushi", level: "debug", format: "pretty" }) : core.noopLogger;
|
|
1985
2722
|
const apiClient = core.createApiClient({
|
|
1986
|
-
projectId:
|
|
1987
|
-
apiKey:
|
|
1988
|
-
...
|
|
2723
|
+
projectId: bootstrapConfig.projectId,
|
|
2724
|
+
apiKey: bootstrapConfig.apiKey,
|
|
2725
|
+
...bootstrapConfig.apiEndpoint ? { apiEndpoint: bootstrapConfig.apiEndpoint } : {}
|
|
1989
2726
|
});
|
|
1990
|
-
const preFilter = core.createPreFilter(
|
|
1991
|
-
const offlineQueue = core.createOfflineQueue(
|
|
2727
|
+
const preFilter = core.createPreFilter(bootstrapConfig.preFilter);
|
|
2728
|
+
const offlineQueue = core.createOfflineQueue(bootstrapConfig.offline);
|
|
1992
2729
|
const rateLimiter = core.createRateLimiter({ maxBurst: 10, refillRate: 1, refillIntervalMs: 5e3 });
|
|
1993
2730
|
const piiScrubber = core.createPiiScrubber();
|
|
1994
2731
|
let consoleCap = null;
|
|
@@ -1996,6 +2733,9 @@ function createInstance(config) {
|
|
|
1996
2733
|
let perfCap = null;
|
|
1997
2734
|
let screenshotCap = null;
|
|
1998
2735
|
let elementSelector = null;
|
|
2736
|
+
let discoveryCap = null;
|
|
2737
|
+
const timelineCap = createTimelineCapture();
|
|
2738
|
+
let widget;
|
|
1999
2739
|
function syncCaptureModules() {
|
|
2000
2740
|
if (activeConfig.capture?.console !== false) {
|
|
2001
2741
|
consoleCap ??= createConsoleCapture();
|
|
@@ -2004,7 +2744,15 @@ function createInstance(config) {
|
|
|
2004
2744
|
consoleCap = null;
|
|
2005
2745
|
}
|
|
2006
2746
|
if (activeConfig.capture?.network !== false) {
|
|
2007
|
-
|
|
2747
|
+
const networkOptions = {
|
|
2748
|
+
apiEndpoint: resolveApiEndpoint(activeConfig),
|
|
2749
|
+
ignoreUrls: activeConfig.capture?.ignoreUrls
|
|
2750
|
+
};
|
|
2751
|
+
if (networkCap) {
|
|
2752
|
+
networkCap.updateOptions(networkOptions);
|
|
2753
|
+
} else {
|
|
2754
|
+
networkCap = createNetworkCapture(networkOptions);
|
|
2755
|
+
}
|
|
2008
2756
|
} else {
|
|
2009
2757
|
networkCap?.destroy();
|
|
2010
2758
|
networkCap = null;
|
|
@@ -2015,8 +2763,18 @@ function createInstance(config) {
|
|
|
2015
2763
|
perfCap?.destroy();
|
|
2016
2764
|
perfCap = null;
|
|
2017
2765
|
}
|
|
2018
|
-
|
|
2766
|
+
if (activeConfig.capture?.screenshot !== "off") {
|
|
2767
|
+
const screenshotOptions = { privacy: activeConfig.privacy };
|
|
2768
|
+
if (screenshotCap) {
|
|
2769
|
+
screenshotCap.updateOptions(screenshotOptions);
|
|
2770
|
+
} else {
|
|
2771
|
+
screenshotCap = createScreenshotCapture(screenshotOptions);
|
|
2772
|
+
}
|
|
2773
|
+
} else {
|
|
2774
|
+
screenshotCap = null;
|
|
2775
|
+
}
|
|
2019
2776
|
if (!screenshotCap) pendingScreenshot = null;
|
|
2777
|
+
widget.setAllowScreenshotRemove(activeConfig.privacy?.allowUserRemoveScreenshot !== false);
|
|
2020
2778
|
if (activeConfig.capture?.elementSelector !== false) {
|
|
2021
2779
|
elementSelector ??= createElementSelector();
|
|
2022
2780
|
} else {
|
|
@@ -2024,6 +2782,40 @@ function createInstance(config) {
|
|
|
2024
2782
|
elementSelector = null;
|
|
2025
2783
|
pendingElement = null;
|
|
2026
2784
|
}
|
|
2785
|
+
const discoveryRaw = activeConfig.capture?.discoverInventory;
|
|
2786
|
+
const discoveryConfig = discoveryRaw === true ? {} : discoveryRaw && typeof discoveryRaw === "object" ? discoveryRaw : null;
|
|
2787
|
+
const discoveryEnabled = discoveryConfig != null && discoveryConfig.enabled !== false;
|
|
2788
|
+
if (discoveryEnabled) {
|
|
2789
|
+
discoveryCap?.destroy();
|
|
2790
|
+
discoveryCap = createDiscoveryCapture({
|
|
2791
|
+
config: discoveryConfig,
|
|
2792
|
+
getRecentNetworkPaths: () => {
|
|
2793
|
+
if (!networkCap) return [];
|
|
2794
|
+
return networkCap.getEntries().map((e) => {
|
|
2795
|
+
try {
|
|
2796
|
+
const u = new URL(e.url, typeof window !== "undefined" ? window.location.href : "http://localhost");
|
|
2797
|
+
if (u.host && typeof window !== "undefined" && u.host !== window.location.host) return null;
|
|
2798
|
+
return u.pathname;
|
|
2799
|
+
} catch {
|
|
2800
|
+
return null;
|
|
2801
|
+
}
|
|
2802
|
+
}).filter((p) => p != null && p.length > 0 && p.length < 200);
|
|
2803
|
+
},
|
|
2804
|
+
getUserId: () => userInfo?.id ?? null,
|
|
2805
|
+
getSessionId: core.getSessionId,
|
|
2806
|
+
onEvent: (event) => {
|
|
2807
|
+
void apiClient.postDiscoveryEvent({
|
|
2808
|
+
...event,
|
|
2809
|
+
sdk_version: MUSHI_SDK_VERSION
|
|
2810
|
+
}).catch((err) => {
|
|
2811
|
+
log.debug("discovery emit failed", { err: String(err) });
|
|
2812
|
+
});
|
|
2813
|
+
}
|
|
2814
|
+
});
|
|
2815
|
+
} else {
|
|
2816
|
+
discoveryCap?.destroy();
|
|
2817
|
+
discoveryCap = null;
|
|
2818
|
+
}
|
|
2027
2819
|
}
|
|
2028
2820
|
const listeners = /* @__PURE__ */ new Map();
|
|
2029
2821
|
function emit(type, data) {
|
|
@@ -2032,10 +2824,10 @@ function createInstance(config) {
|
|
|
2032
2824
|
let pendingScreenshot = null;
|
|
2033
2825
|
let pendingElement = null;
|
|
2034
2826
|
let pendingProactiveTrigger = null;
|
|
2827
|
+
let runtimeConfigLoaded = false;
|
|
2035
2828
|
let userInfo = null;
|
|
2036
2829
|
const customMetadata = {};
|
|
2037
|
-
|
|
2038
|
-
const widget = new MushiWidget(config.widget, {
|
|
2830
|
+
widget = new MushiWidget(bootstrapConfig.widget, {
|
|
2039
2831
|
onSubmit: async ({ category, description, intent }) => {
|
|
2040
2832
|
log.info("Report submitted", { category, intent });
|
|
2041
2833
|
proactiveManager?.recordSubmission();
|
|
@@ -2062,6 +2854,11 @@ function createInstance(config) {
|
|
|
2062
2854
|
pendingScreenshot = await screenshotCap.take();
|
|
2063
2855
|
widget.setScreenshotAttached(pendingScreenshot !== null);
|
|
2064
2856
|
},
|
|
2857
|
+
onScreenshotRemove: () => {
|
|
2858
|
+
log.debug("Screenshot attachment removed");
|
|
2859
|
+
pendingScreenshot = null;
|
|
2860
|
+
widget.setScreenshotAttached(false);
|
|
2861
|
+
},
|
|
2065
2862
|
onElementSelectorRequest: async () => {
|
|
2066
2863
|
if (!elementSelector || activeConfig.capture?.elementSelector === false) return;
|
|
2067
2864
|
log.debug("Element selector activated");
|
|
@@ -2071,8 +2868,23 @@ function createInstance(config) {
|
|
|
2071
2868
|
widget.setElementSelected(true);
|
|
2072
2869
|
log.debug("Element selected", { tagName: el.tagName, xpath: el.xpath });
|
|
2073
2870
|
}
|
|
2871
|
+
},
|
|
2872
|
+
async onReporterReportsRequest() {
|
|
2873
|
+
const result = await apiClient.listReporterReports(core.getReporterToken());
|
|
2874
|
+
if (!result.ok) throw new Error(result.error?.message ?? "Could not load reports");
|
|
2875
|
+
return result.data?.reports ?? [];
|
|
2876
|
+
},
|
|
2877
|
+
async onReporterCommentsRequest(reportId) {
|
|
2878
|
+
const result = await apiClient.listReporterComments(reportId, core.getReporterToken());
|
|
2879
|
+
if (!result.ok) throw new Error(result.error?.message ?? "Could not load thread");
|
|
2880
|
+
return result.data?.comments ?? [];
|
|
2881
|
+
},
|
|
2882
|
+
async onReporterReply(reportId, body) {
|
|
2883
|
+
const result = await apiClient.replyToReporterReport(reportId, core.getReporterToken(), body);
|
|
2884
|
+
if (!result.ok) throw new Error(result.error?.message ?? "Could not send reply");
|
|
2074
2885
|
}
|
|
2075
|
-
});
|
|
2886
|
+
}, MUSHI_SDK_VERSION);
|
|
2887
|
+
syncCaptureModules();
|
|
2076
2888
|
if (typeof document !== "undefined") {
|
|
2077
2889
|
if (document.readyState === "loading") {
|
|
2078
2890
|
document.addEventListener("DOMContentLoaded", () => widget.mount());
|
|
@@ -2082,7 +2894,7 @@ function createInstance(config) {
|
|
|
2082
2894
|
}
|
|
2083
2895
|
let proactiveTriggers = null;
|
|
2084
2896
|
let proactiveManager = null;
|
|
2085
|
-
const proactiveCfg =
|
|
2897
|
+
const proactiveCfg = activeConfig.proactive;
|
|
2086
2898
|
const hasAnyProactive = proactiveCfg && (proactiveCfg.rageClick !== false || proactiveCfg.longTask !== false || proactiveCfg.apiCascade !== false || proactiveCfg.errorBoundary === true);
|
|
2087
2899
|
if (hasAnyProactive && typeof document !== "undefined") {
|
|
2088
2900
|
proactiveManager = createProactiveManager(proactiveCfg?.cooldown);
|
|
@@ -2103,6 +2915,7 @@ function createInstance(config) {
|
|
|
2103
2915
|
rageClick: proactiveCfg?.rageClick,
|
|
2104
2916
|
longTask: proactiveCfg?.longTask,
|
|
2105
2917
|
apiCascade: proactiveCfg?.apiCascade,
|
|
2918
|
+
apiEndpoint: resolveApiEndpoint(activeConfig),
|
|
2106
2919
|
errorBoundary: proactiveCfg?.errorBoundary
|
|
2107
2920
|
}
|
|
2108
2921
|
);
|
|
@@ -2118,6 +2931,7 @@ function createInstance(config) {
|
|
|
2118
2931
|
if (result.sent > 0) log.info("Synced offline reports", { sent: result.sent });
|
|
2119
2932
|
});
|
|
2120
2933
|
function applyRuntimeConfig(runtime) {
|
|
2934
|
+
runtimeConfigLoaded = true;
|
|
2121
2935
|
if (runtime.enabled === false) {
|
|
2122
2936
|
activeConfig = bootstrapConfig;
|
|
2123
2937
|
clearCachedRuntimeConfig(config.projectId);
|
|
@@ -2131,7 +2945,7 @@ function createInstance(config) {
|
|
|
2131
2945
|
if (runtime.widget) widget.updateConfig(activeConfig.widget);
|
|
2132
2946
|
log.debug("Applied runtime SDK config", { version: runtime.version });
|
|
2133
2947
|
}
|
|
2134
|
-
if (config
|
|
2948
|
+
if (shouldUseRuntimeConfig(config)) {
|
|
2135
2949
|
const cached = readCachedRuntimeConfig(config.projectId);
|
|
2136
2950
|
if (cached) applyRuntimeConfig(cached);
|
|
2137
2951
|
apiClient.getSdkConfig().then((result) => {
|
|
@@ -2144,8 +2958,41 @@ function createInstance(config) {
|
|
|
2144
2958
|
}).catch((err) => {
|
|
2145
2959
|
log.debug("Runtime SDK config fetch failed", { error: err instanceof Error ? err.message : String(err) });
|
|
2146
2960
|
});
|
|
2961
|
+
} else if (config.runtimeConfig !== false && isLocalhostEndpoint(resolveApiEndpoint(config))) {
|
|
2962
|
+
log.debug("Runtime SDK config skipped for localhost apiEndpoint; set runtimeConfig: true to force it");
|
|
2147
2963
|
}
|
|
2964
|
+
void checkSdkFreshness();
|
|
2148
2965
|
log.info("Initialized", { projectId: config.projectId });
|
|
2966
|
+
async function checkSdkFreshness() {
|
|
2967
|
+
if (activeConfig.widget?.outdatedBanner === "off") return;
|
|
2968
|
+
const cached = readCachedSdkVersion(MUSHI_SDK_PACKAGE);
|
|
2969
|
+
if (cached) applySdkFreshness(cached);
|
|
2970
|
+
const result = await apiClient.getLatestSdkVersion(MUSHI_SDK_PACKAGE);
|
|
2971
|
+
if (!result.ok || !result.data) return;
|
|
2972
|
+
cacheSdkVersion(MUSHI_SDK_PACKAGE, result.data);
|
|
2973
|
+
applySdkFreshness(result.data);
|
|
2974
|
+
}
|
|
2975
|
+
function applySdkFreshness(info) {
|
|
2976
|
+
const latest = info.latest;
|
|
2977
|
+
const outdated = Boolean(latest && isVersionOlder(MUSHI_SDK_VERSION, latest));
|
|
2978
|
+
if (!outdated && !info.deprecated) return;
|
|
2979
|
+
const message = info.deprecationMessage ?? (outdated ? `Update ${MUSHI_SDK_PACKAGE} to ${latest}.` : null);
|
|
2980
|
+
log.warn("Mushi SDK is outdated", {
|
|
2981
|
+
package: MUSHI_SDK_PACKAGE,
|
|
2982
|
+
current: MUSHI_SDK_VERSION,
|
|
2983
|
+
latest,
|
|
2984
|
+
deprecated: info.deprecated,
|
|
2985
|
+
message
|
|
2986
|
+
});
|
|
2987
|
+
if (activeConfig.widget?.outdatedBanner !== "console-only") {
|
|
2988
|
+
widget.setSdkFreshness({
|
|
2989
|
+
latest,
|
|
2990
|
+
current: MUSHI_SDK_VERSION,
|
|
2991
|
+
deprecated: info.deprecated,
|
|
2992
|
+
message
|
|
2993
|
+
});
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2149
2996
|
async function submitReport(category, description, intent) {
|
|
2150
2997
|
const filterResult = preFilter.check(description);
|
|
2151
2998
|
if (!filterResult.passed) {
|
|
@@ -2187,6 +3034,8 @@ function createInstance(config) {
|
|
|
2187
3034
|
const scrubbedDescription = piiScrubber.scrub(preFilter.truncate(description));
|
|
2188
3035
|
const sentryCtx = config.sentry ? captureSentryContext(config.sentry) : void 0;
|
|
2189
3036
|
const fingerprintHash = await core.getDeviceFingerprintHash().catch(() => null);
|
|
3037
|
+
const consoleLogs = activeConfig.capture?.console === false ? void 0 : consoleCap?.getEntries();
|
|
3038
|
+
const networkLogs = activeConfig.capture?.network === false ? void 0 : networkCap?.getEntries();
|
|
2190
3039
|
const report = {
|
|
2191
3040
|
id: crypto.randomUUID?.() ?? `mushi_${Date.now()}_${Math.random().toString(36).slice(2)}`,
|
|
2192
3041
|
projectId: config.projectId,
|
|
@@ -2194,9 +3043,10 @@ function createInstance(config) {
|
|
|
2194
3043
|
description: scrubbedDescription,
|
|
2195
3044
|
userIntent: intent,
|
|
2196
3045
|
environment: core.captureEnvironment(),
|
|
2197
|
-
consoleLogs
|
|
2198
|
-
networkLogs
|
|
3046
|
+
consoleLogs,
|
|
3047
|
+
networkLogs,
|
|
2199
3048
|
performanceMetrics: activeConfig.capture?.performance === false ? void 0 : perfCap?.getMetrics(),
|
|
3049
|
+
timeline: timelineCap.getEntries({ consoleLogs, networkLogs }),
|
|
2200
3050
|
screenshotDataUrl: pendingScreenshot ?? void 0,
|
|
2201
3051
|
selectedElement: pendingElement ?? void 0,
|
|
2202
3052
|
metadata: {
|
|
@@ -2208,6 +3058,8 @@ function createInstance(config) {
|
|
|
2208
3058
|
reporterToken: core.getReporterToken(),
|
|
2209
3059
|
...fingerprintHash ? { fingerprintHash } : {},
|
|
2210
3060
|
appVersion: config.integrations?.vercel?.analyticsId,
|
|
3061
|
+
sdkPackage: MUSHI_SDK_PACKAGE,
|
|
3062
|
+
sdkVersion: MUSHI_SDK_VERSION,
|
|
2211
3063
|
proactiveTrigger: pendingProactiveTrigger ?? void 0,
|
|
2212
3064
|
sentryEventId: sentryCtx?.eventId,
|
|
2213
3065
|
sentryReplayId: sentryCtx?.replayId,
|
|
@@ -2262,6 +3114,9 @@ function createInstance(config) {
|
|
|
2262
3114
|
setMetadata(key, value) {
|
|
2263
3115
|
customMetadata[key] = value;
|
|
2264
3116
|
},
|
|
3117
|
+
setScreen(screen) {
|
|
3118
|
+
timelineCap.setScreen(screen);
|
|
3119
|
+
},
|
|
2265
3120
|
isOpen() {
|
|
2266
3121
|
return widget.getIsOpen();
|
|
2267
3122
|
},
|
|
@@ -2289,6 +3144,15 @@ function createInstance(config) {
|
|
|
2289
3144
|
updateConfig(runtimeConfig) {
|
|
2290
3145
|
applyRuntimeConfig(runtimeConfig);
|
|
2291
3146
|
},
|
|
3147
|
+
diagnose() {
|
|
3148
|
+
return runDiagnostics({
|
|
3149
|
+
apiEndpoint: resolveApiEndpoint(activeConfig),
|
|
3150
|
+
widgetMounted: widget.getIsMounted(),
|
|
3151
|
+
runtimeConfigLoaded,
|
|
3152
|
+
captureScreenshotAvailable: screenshotCap !== null,
|
|
3153
|
+
captureNetworkIntercepting: networkCap !== null
|
|
3154
|
+
});
|
|
3155
|
+
},
|
|
2292
3156
|
destroy() {
|
|
2293
3157
|
proactiveTriggers?.destroy();
|
|
2294
3158
|
proactiveManager?.reset();
|
|
@@ -2297,6 +3161,9 @@ function createInstance(config) {
|
|
|
2297
3161
|
networkCap?.destroy();
|
|
2298
3162
|
perfCap?.destroy();
|
|
2299
3163
|
elementSelector?.deactivate();
|
|
3164
|
+
timelineCap.destroy();
|
|
3165
|
+
discoveryCap?.destroy();
|
|
3166
|
+
discoveryCap = null;
|
|
2300
3167
|
offlineQueue.stopAutoSync();
|
|
2301
3168
|
listeners.clear();
|
|
2302
3169
|
instance = null;
|
|
@@ -2318,6 +3185,7 @@ function createInstance(config) {
|
|
|
2318
3185
|
category,
|
|
2319
3186
|
description,
|
|
2320
3187
|
environment: core.captureEnvironment(),
|
|
3188
|
+
timeline: timelineCap.getEntries(),
|
|
2321
3189
|
metadata: {
|
|
2322
3190
|
...input.metadata ?? {},
|
|
2323
3191
|
...userInfo ? { user: userInfo } : {},
|
|
@@ -2329,6 +3197,8 @@ function createInstance(config) {
|
|
|
2329
3197
|
},
|
|
2330
3198
|
sessionId: core.getSessionId(),
|
|
2331
3199
|
reporterToken: core.getReporterToken(),
|
|
3200
|
+
sdkPackage: MUSHI_SDK_PACKAGE,
|
|
3201
|
+
sdkVersion: MUSHI_SDK_VERSION,
|
|
2332
3202
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2333
3203
|
};
|
|
2334
3204
|
emit("report:submitted", { reportId: report.id });
|
|
@@ -2370,12 +3240,128 @@ function mergeRuntimeConfig(config, runtime) {
|
|
|
2370
3240
|
capture: {
|
|
2371
3241
|
...config.capture,
|
|
2372
3242
|
...runtime.capture
|
|
3243
|
+
},
|
|
3244
|
+
privacy: {
|
|
3245
|
+
...config.privacy
|
|
2373
3246
|
}
|
|
2374
3247
|
};
|
|
2375
3248
|
}
|
|
3249
|
+
function applyPresetConfig(config) {
|
|
3250
|
+
if (!config.preset) return config;
|
|
3251
|
+
const preset = presetDefaults(config.preset);
|
|
3252
|
+
return {
|
|
3253
|
+
...config,
|
|
3254
|
+
widget: {
|
|
3255
|
+
...preset.widget,
|
|
3256
|
+
...config.widget
|
|
3257
|
+
},
|
|
3258
|
+
capture: {
|
|
3259
|
+
...preset.capture,
|
|
3260
|
+
...config.capture
|
|
3261
|
+
},
|
|
3262
|
+
proactive: {
|
|
3263
|
+
...preset.proactive,
|
|
3264
|
+
...config.proactive,
|
|
3265
|
+
cooldown: {
|
|
3266
|
+
...preset.proactive?.cooldown,
|
|
3267
|
+
...config.proactive?.cooldown
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
};
|
|
3271
|
+
}
|
|
3272
|
+
function presetDefaults(preset) {
|
|
3273
|
+
switch (preset) {
|
|
3274
|
+
case "manual-only":
|
|
3275
|
+
return {
|
|
3276
|
+
widget: { trigger: "manual", outdatedBanner: "console-only" },
|
|
3277
|
+
capture: { console: true, network: true, performance: false, screenshot: "on-report", elementSelector: false },
|
|
3278
|
+
proactive: { rageClick: false, longTask: false, apiCascade: false, errorBoundary: false }
|
|
3279
|
+
};
|
|
3280
|
+
case "beta-loud":
|
|
3281
|
+
return {
|
|
3282
|
+
widget: { trigger: "auto", outdatedBanner: "banner" },
|
|
3283
|
+
capture: { console: true, network: true, performance: true, screenshot: "auto", elementSelector: true },
|
|
3284
|
+
proactive: { rageClick: true, longTask: true, apiCascade: true, errorBoundary: true }
|
|
3285
|
+
};
|
|
3286
|
+
case "internal-debug":
|
|
3287
|
+
return {
|
|
3288
|
+
widget: { trigger: "auto", outdatedBanner: "banner", brandFooter: true },
|
|
3289
|
+
capture: { console: true, network: true, performance: true, screenshot: "auto", elementSelector: true },
|
|
3290
|
+
proactive: {
|
|
3291
|
+
rageClick: true,
|
|
3292
|
+
longTask: true,
|
|
3293
|
+
apiCascade: true,
|
|
3294
|
+
errorBoundary: true,
|
|
3295
|
+
cooldown: { maxProactivePerSession: 10, dismissCooldownHours: 0, suppressAfterDismissals: 99 }
|
|
3296
|
+
}
|
|
3297
|
+
};
|
|
3298
|
+
case "production-calm":
|
|
3299
|
+
return {
|
|
3300
|
+
widget: { trigger: "auto", outdatedBanner: "console-only" },
|
|
3301
|
+
capture: { console: true, network: true, performance: false, screenshot: "on-report", elementSelector: false },
|
|
3302
|
+
proactive: { rageClick: false, longTask: false, apiCascade: false, errorBoundary: false }
|
|
3303
|
+
};
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
function resolveApiEndpoint(config) {
|
|
3307
|
+
return config.apiEndpoint ?? core.DEFAULT_API_ENDPOINT;
|
|
3308
|
+
}
|
|
3309
|
+
function shouldUseRuntimeConfig(config) {
|
|
3310
|
+
if (config.runtimeConfig === false) return false;
|
|
3311
|
+
if (config.runtimeConfig === true) return true;
|
|
3312
|
+
return !isLocalhostEndpoint(resolveApiEndpoint(config));
|
|
3313
|
+
}
|
|
3314
|
+
async function runDiagnostics(options) {
|
|
3315
|
+
const endpoint = await probeApiEndpoint(options.apiEndpoint);
|
|
3316
|
+
return {
|
|
3317
|
+
apiEndpointReachable: endpoint.reachable,
|
|
3318
|
+
cspAllowsEndpoint: endpoint.cspAllowed,
|
|
3319
|
+
widgetMounted: options.widgetMounted,
|
|
3320
|
+
shadowDomAvailable: typeof HTMLElement !== "undefined" && typeof HTMLElement.prototype.attachShadow === "function",
|
|
3321
|
+
dialogSupported: typeof HTMLDialogElement !== "undefined",
|
|
3322
|
+
runtimeConfigLoaded: options.runtimeConfigLoaded,
|
|
3323
|
+
captureScreenshotAvailable: options.captureScreenshotAvailable,
|
|
3324
|
+
captureNetworkIntercepting: options.captureNetworkIntercepting,
|
|
3325
|
+
sdkVersion: MUSHI_SDK_VERSION
|
|
3326
|
+
};
|
|
3327
|
+
}
|
|
3328
|
+
async function diagnoseWithoutInstance() {
|
|
3329
|
+
return {
|
|
3330
|
+
apiEndpointReachable: false,
|
|
3331
|
+
cspAllowsEndpoint: false,
|
|
3332
|
+
widgetMounted: false,
|
|
3333
|
+
shadowDomAvailable: typeof HTMLElement !== "undefined" && typeof HTMLElement.prototype.attachShadow === "function",
|
|
3334
|
+
dialogSupported: typeof HTMLDialogElement !== "undefined",
|
|
3335
|
+
runtimeConfigLoaded: false,
|
|
3336
|
+
captureScreenshotAvailable: false,
|
|
3337
|
+
captureNetworkIntercepting: false,
|
|
3338
|
+
sdkVersion: MUSHI_SDK_VERSION
|
|
3339
|
+
};
|
|
3340
|
+
}
|
|
3341
|
+
async function probeApiEndpoint(apiEndpoint) {
|
|
3342
|
+
if (typeof fetch === "undefined") return { reachable: false, cspAllowed: false };
|
|
3343
|
+
const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
3344
|
+
const timer = controller ? setTimeout(() => controller.abort(), 3e3) : null;
|
|
3345
|
+
try {
|
|
3346
|
+
const response = await fetch(`${apiEndpoint.replace(/\/$/, "")}/health`, {
|
|
3347
|
+
method: "GET",
|
|
3348
|
+
cache: "no-store",
|
|
3349
|
+
...controller ? { signal: controller.signal } : {},
|
|
3350
|
+
[core.MUSHI_INTERNAL_INIT_MARKER]: "diagnose"
|
|
3351
|
+
});
|
|
3352
|
+
return { reachable: response.ok, cspAllowed: true };
|
|
3353
|
+
} catch {
|
|
3354
|
+
return { reachable: false, cspAllowed: false };
|
|
3355
|
+
} finally {
|
|
3356
|
+
if (timer) clearTimeout(timer);
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
2376
3359
|
function runtimeConfigCacheKey(projectId) {
|
|
2377
3360
|
return `mushi:sdk-config:${projectId}`;
|
|
2378
3361
|
}
|
|
3362
|
+
function sdkVersionCacheKey(packageName) {
|
|
3363
|
+
return `mushi:sdk-version:${packageName}`;
|
|
3364
|
+
}
|
|
2379
3365
|
function readCachedRuntimeConfig(projectId) {
|
|
2380
3366
|
if (typeof localStorage === "undefined") return null;
|
|
2381
3367
|
try {
|
|
@@ -2404,6 +3390,42 @@ function clearCachedRuntimeConfig(projectId) {
|
|
|
2404
3390
|
} catch {
|
|
2405
3391
|
}
|
|
2406
3392
|
}
|
|
3393
|
+
function readCachedSdkVersion(packageName) {
|
|
3394
|
+
if (typeof localStorage === "undefined") return null;
|
|
3395
|
+
try {
|
|
3396
|
+
const raw = localStorage.getItem(sdkVersionCacheKey(packageName));
|
|
3397
|
+
if (!raw) return null;
|
|
3398
|
+
const parsed = JSON.parse(raw);
|
|
3399
|
+
if (!parsed.data || !parsed.cachedAt || Date.now() - parsed.cachedAt > 864e5) return null;
|
|
3400
|
+
return parsed.data;
|
|
3401
|
+
} catch {
|
|
3402
|
+
return null;
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
function cacheSdkVersion(packageName, data) {
|
|
3406
|
+
if (typeof localStorage === "undefined") return;
|
|
3407
|
+
try {
|
|
3408
|
+
localStorage.setItem(sdkVersionCacheKey(packageName), JSON.stringify({
|
|
3409
|
+
cachedAt: Date.now(),
|
|
3410
|
+
data
|
|
3411
|
+
}));
|
|
3412
|
+
} catch {
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
function isVersionOlder(current, latest) {
|
|
3416
|
+
const currentParts = parseVersion(current);
|
|
3417
|
+
const latestParts = parseVersion(latest);
|
|
3418
|
+
for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
|
|
3419
|
+
const cur = currentParts[i] ?? 0;
|
|
3420
|
+
const next = latestParts[i] ?? 0;
|
|
3421
|
+
if (cur < next) return true;
|
|
3422
|
+
if (cur > next) return false;
|
|
3423
|
+
}
|
|
3424
|
+
return false;
|
|
3425
|
+
}
|
|
3426
|
+
function parseVersion(version) {
|
|
3427
|
+
return version.split(/[.-]/).map((part) => Number.parseInt(part, 10)).filter((part) => Number.isFinite(part));
|
|
3428
|
+
}
|
|
2407
3429
|
function createNoopInstance() {
|
|
2408
3430
|
return {
|
|
2409
3431
|
report: () => {
|
|
@@ -2414,6 +3436,8 @@ function createNoopInstance() {
|
|
|
2414
3436
|
},
|
|
2415
3437
|
setMetadata: () => {
|
|
2416
3438
|
},
|
|
3439
|
+
setScreen: () => {
|
|
3440
|
+
},
|
|
2417
3441
|
isOpen: () => false,
|
|
2418
3442
|
open: () => {
|
|
2419
3443
|
},
|
|
@@ -2421,6 +3445,7 @@ function createNoopInstance() {
|
|
|
2421
3445
|
},
|
|
2422
3446
|
updateConfig: () => {
|
|
2423
3447
|
},
|
|
3448
|
+
diagnose: diagnoseWithoutInstance,
|
|
2424
3449
|
openWith: () => {
|
|
2425
3450
|
},
|
|
2426
3451
|
show: () => {
|
|
@@ -2448,6 +3473,7 @@ exports.createNetworkCapture = createNetworkCapture;
|
|
|
2448
3473
|
exports.createPerformanceCapture = createPerformanceCapture;
|
|
2449
3474
|
exports.createProactiveManager = createProactiveManager;
|
|
2450
3475
|
exports.createScreenshotCapture = createScreenshotCapture;
|
|
3476
|
+
exports.createTimelineCapture = createTimelineCapture;
|
|
2451
3477
|
exports.getAvailableLocales = getAvailableLocales;
|
|
2452
3478
|
exports.getLocale = getLocale;
|
|
2453
3479
|
exports.setupProactiveTriggers = setupProactiveTriggers;
|