@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/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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
973
+ }
859
974
  var MushiWidget = class {
860
- host;
861
- shadow;
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 = typeof input === "string" ? input : input instanceof URL ? input.href : input.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
- addEntry({
1463
- method,
1464
- url: truncateUrl(url),
1465
- status: response.status,
1466
- duration: Date.now() - startTime,
1467
- timestamp: startTime
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
- addEntry({
1472
- method,
1473
- url: truncateUrl(url),
1474
- status: 0,
1475
- duration: Date.now() - startTime,
1476
- timestamp: startTime,
1477
- error: error instanceof Error ? error.message : "Network error"
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(document.documentElement)}
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 { take };
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
- if (config.apiCascade !== false) {
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
- const now = Date.now();
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
- const now = Date.now();
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 = config;
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: config.projectId,
1987
- apiKey: config.apiKey,
1988
- ...config.apiEndpoint ? { apiEndpoint: config.apiEndpoint } : {}
2723
+ projectId: bootstrapConfig.projectId,
2724
+ apiKey: bootstrapConfig.apiKey,
2725
+ ...bootstrapConfig.apiEndpoint ? { apiEndpoint: bootstrapConfig.apiEndpoint } : {}
1989
2726
  });
1990
- const preFilter = core.createPreFilter(config.preFilter);
1991
- const offlineQueue = core.createOfflineQueue(config.offline);
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
- networkCap ??= createNetworkCapture();
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
- screenshotCap = activeConfig.capture?.screenshot !== "off" ? screenshotCap ?? createScreenshotCapture() : null;
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
- syncCaptureModules();
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 = config.proactive;
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.runtimeConfig !== false) {
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: activeConfig.capture?.console === false ? void 0 : consoleCap?.getEntries(),
2198
- networkLogs: activeConfig.capture?.network === false ? void 0 : networkCap?.getEntries(),
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;