@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.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createLogger, noopLogger, createApiClient, createPreFilter, createOfflineQueue, createRateLimiter, createPiiScrubber, getDeviceFingerprintHash, getReporterToken, getSessionId, captureEnvironment } from '@mushi-mushi/core';
1
+ import { createLogger, noopLogger, createApiClient, createPreFilter, createOfflineQueue, createRateLimiter, createPiiScrubber, getReporterToken, getDeviceFingerprintHash, getSessionId, captureEnvironment, DEFAULT_API_ENDPOINT, MUSHI_INTERNAL_INIT_MARKER, MUSHI_INTERNAL_HEADER } from '@mushi-mushi/core';
2
2
 
3
3
  // src/mushi.ts
4
4
 
@@ -389,6 +389,29 @@ function getWidgetStyles(theme) {
389
389
  left: var(--mushi-left, calc(24px + env(safe-area-inset-left, 0px)));
390
390
  --mushi-origin: top left;
391
391
  }
392
+ .mushi-outdated {
393
+ margin: 12px 14px 0;
394
+ padding: 10px 12px;
395
+ border: 1px solid ${vermillionWash};
396
+ background: ${vermillionWash};
397
+ color: ${vermillionInk};
398
+ font-family: ${fontBody};
399
+ font-size: 12px;
400
+ line-height: 1.4;
401
+ }
402
+ .mushi-outdated strong {
403
+ display: block;
404
+ font-family: ${fontMono};
405
+ font-size: 10px;
406
+ letter-spacing: 0.12em;
407
+ text-transform: uppercase;
408
+ margin-bottom: 2px;
409
+ }
410
+ .mushi-outdated span {
411
+ display: block;
412
+ margin-top: 3px;
413
+ color: ${inkMuted};
414
+ }
392
415
 
393
416
  @keyframes mushi-stamp-in {
394
417
  0% { opacity: 0; transform: scale(0.94) translateY(6px); }
@@ -543,6 +566,75 @@ function getWidgetStyles(theme) {
543
566
  transform: translateX(-4px);
544
567
  transition: opacity 220ms ${easeStamp}, transform 220ms ${easeStamp}, color 220ms ${easeStamp};
545
568
  }
569
+ .mushi-report-row {
570
+ width: 100%;
571
+ display: grid;
572
+ grid-template-columns: auto 1fr auto;
573
+ gap: 8px;
574
+ align-items: center;
575
+ padding: 10px 0;
576
+ border: 0;
577
+ border-bottom: 1px solid ${rule};
578
+ background: transparent;
579
+ color: ${ink};
580
+ cursor: pointer;
581
+ text-align: left;
582
+ }
583
+ .mushi-report-status {
584
+ font-family: ${fontMono};
585
+ font-size: 10px;
586
+ color: ${vermillion};
587
+ text-transform: uppercase;
588
+ }
589
+ .mushi-report-title {
590
+ font-size: 13px;
591
+ overflow: hidden;
592
+ text-overflow: ellipsis;
593
+ white-space: nowrap;
594
+ }
595
+ .mushi-thread-summary {
596
+ border-bottom: 1px solid ${rule};
597
+ padding-bottom: 10px;
598
+ margin-bottom: 10px;
599
+ }
600
+ .mushi-thread-summary span {
601
+ font-family: ${fontMono};
602
+ font-size: 10px;
603
+ color: ${vermillion};
604
+ text-transform: uppercase;
605
+ }
606
+ .mushi-thread {
607
+ display: grid;
608
+ gap: 8px;
609
+ max-height: 180px;
610
+ overflow: auto;
611
+ margin-bottom: 12px;
612
+ }
613
+ .mushi-thread-comment {
614
+ padding: 8px 10px;
615
+ border: 1px solid ${rule};
616
+ background: ${isDark ? "rgba(242,235,221,0.04)" : "rgba(14,13,11,0.03)"};
617
+ }
618
+ .mushi-thread-comment.reporter {
619
+ border-color: ${vermillionWash};
620
+ background: ${vermillionWash};
621
+ }
622
+ .mushi-thread-comment strong {
623
+ display: block;
624
+ font-family: ${fontMono};
625
+ font-size: 10px;
626
+ letter-spacing: 0.08em;
627
+ text-transform: uppercase;
628
+ margin-bottom: 3px;
629
+ }
630
+ .mushi-thread-comment p,
631
+ .mushi-muted,
632
+ .mushi-error-inline {
633
+ font-size: 12px;
634
+ color: ${inkMuted};
635
+ line-height: 1.45;
636
+ }
637
+ .mushi-error-inline { color: ${vermillion}; }
546
638
 
547
639
  .mushi-selected-category {
548
640
  display: inline-flex;
@@ -651,6 +743,16 @@ function getWidgetStyles(theme) {
651
743
  border-color: ${vermillion};
652
744
  background: ${vermillionWash};
653
745
  }
746
+ .mushi-attach-btn.danger {
747
+ color: ${vermillionInk};
748
+ border-color: ${vermillionWash};
749
+ background: transparent;
750
+ }
751
+ .mushi-attach-btn.danger:hover {
752
+ color: ${vermillion};
753
+ border-color: ${vermillion};
754
+ background: ${vermillionWash};
755
+ }
654
756
  .mushi-attach-btn:focus-visible {
655
757
  outline: 2px solid ${vermillion};
656
758
  outline-offset: 2px;
@@ -720,6 +822,17 @@ function getWidgetStyles(theme) {
720
822
  }
721
823
  .mushi-submit:hover .mushi-submit-arrow { transform: translateX(3px); }
722
824
 
825
+ .mushi-brand-footer {
826
+ padding: 9px 14px 11px;
827
+ border-top: 1px solid ${rule};
828
+ color: ${inkFaint};
829
+ font-family: ${fontMono};
830
+ font-size: 9px;
831
+ letter-spacing: 0.16em;
832
+ text-align: center;
833
+ text-transform: uppercase;
834
+ }
835
+
723
836
  .mushi-step-indicator {
724
837
  display: flex;
725
838
  align-items: center;
@@ -849,43 +962,19 @@ var TOTAL_STEPS = 3;
849
962
  var STEP_NUMBER = {
850
963
  category: 1,
851
964
  intent: 2,
852
- details: 3
853
- };
965
+ details: 3};
854
966
  function isSubmitShortcut(e) {
855
967
  return (e.metaKey || e.ctrlKey) && e.key === "Enter";
856
968
  }
969
+ function escapeHtml(value) {
970
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
971
+ }
857
972
  var MushiWidget = class {
858
- host;
859
- shadow;
860
- config;
861
- callbacks;
862
- locale;
863
- isOpen = false;
864
- step = "category";
865
- selectedCategory = null;
866
- selectedIntent = null;
867
- screenshotAttached = false;
868
- elementSelected = false;
869
- submitting = false;
870
- triggerVisible = true;
871
- triggerShrunk = false;
872
- triggerHiddenByScroll = false;
873
- attachedLaunchers = [];
874
- smartHideCleanup = null;
875
- smartHideTimer = null;
876
- /** Captured at the moment of submit so the success ledger metadata
877
- * ("REPORT · 14:23:07 JST") doesn't drift while the success step
878
- * is on screen. */
879
- submittedAt = null;
880
- /** Pending success-state + auto-close timers. Tracked so destroy()
881
- * can clear them — otherwise a host that unmounts mid-submit leaks
882
- * this MushiWidget reference (and re-renders into a detached shadow
883
- * root) for up to ~3.3s after destroy. */
884
- successTimer = null;
885
- autoCloseTimer = null;
886
- constructor(config = {}, callbacks) {
973
+ constructor(config = {}, callbacks, sdkVersion = "0.7.0") {
974
+ this.sdkVersion = sdkVersion;
887
975
  this.config = {
888
976
  position: config.position ?? "bottom-right",
977
+ anchor: config.anchor ?? {},
889
978
  theme: config.theme ?? "auto",
890
979
  // Falsy-OR (NOT `??`) on purpose: `triggerText: ''` is semantically
891
980
  // nonsense — it would render a labelless, glyphless trigger button
@@ -907,7 +996,9 @@ var MushiWidget = class {
907
996
  hideOnRoutes: config.hideOnRoutes ?? [],
908
997
  environments: config.environments ?? {},
909
998
  smartHide: config.smartHide ?? false,
910
- draggable: config.draggable ?? false
999
+ draggable: config.draggable ?? false,
1000
+ brandFooter: config.brandFooter ?? true,
1001
+ outdatedBanner: config.outdatedBanner ?? "auto"
911
1002
  };
912
1003
  this.callbacks = callbacks;
913
1004
  this.locale = getLocale(this.config.locale === "auto" ? void 0 : this.config.locale);
@@ -915,16 +1006,57 @@ var MushiWidget = class {
915
1006
  this.host.id = "mushi-mushi-widget";
916
1007
  this.shadow = this.host.attachShadow({ mode: "closed" });
917
1008
  }
1009
+ sdkVersion;
1010
+ host;
1011
+ shadow;
1012
+ config;
1013
+ callbacks;
1014
+ locale;
1015
+ isOpen = false;
1016
+ step = "category";
1017
+ selectedCategory = null;
1018
+ selectedIntent = null;
1019
+ screenshotAttached = false;
1020
+ allowScreenshotRemove = true;
1021
+ elementSelected = false;
1022
+ submitting = false;
1023
+ triggerVisible = true;
1024
+ triggerShrunk = false;
1025
+ triggerHiddenByScroll = false;
1026
+ sdkFreshness = null;
1027
+ reporterReports = [];
1028
+ reporterComments = [];
1029
+ selectedReportId = null;
1030
+ reporterLoading = false;
1031
+ reporterError = null;
1032
+ attachedLaunchers = [];
1033
+ smartHideCleanup = null;
1034
+ smartHideTimer = null;
1035
+ /** Captured at the moment of submit so the success ledger metadata
1036
+ * ("REPORT · 14:23:07 JST") doesn't drift while the success step
1037
+ * is on screen. */
1038
+ submittedAt = null;
1039
+ /** Pending success-state + auto-close timers. Tracked so destroy()
1040
+ * can clear them — otherwise a host that unmounts mid-submit leaks
1041
+ * this MushiWidget reference (and re-renders into a detached shadow
1042
+ * root) for up to ~3.3s after destroy. */
1043
+ successTimer = null;
1044
+ autoCloseTimer = null;
918
1045
  mount() {
1046
+ if (this.host.isConnected) return;
919
1047
  document.body.appendChild(this.host);
920
1048
  this.syncAttachedLaunchers();
921
1049
  this.syncSmartHide();
922
1050
  this.render();
923
1051
  }
1052
+ getIsMounted() {
1053
+ return this.host.isConnected;
1054
+ }
924
1055
  updateConfig(config = {}) {
925
1056
  this.config = {
926
1057
  ...this.config,
927
1058
  ...config.position ? { position: config.position } : {},
1059
+ ...config.anchor !== void 0 ? { anchor: config.anchor } : {},
928
1060
  ...config.theme ? { theme: config.theme } : {},
929
1061
  ...config.triggerText !== void 0 ? { triggerText: config.triggerText || "\u{1F41B}" } : {},
930
1062
  ...config.expandedTitle !== void 0 ? { expandedTitle: config.expandedTitle } : {},
@@ -939,7 +1071,9 @@ var MushiWidget = class {
939
1071
  ...config.hideOnRoutes !== void 0 ? { hideOnRoutes: config.hideOnRoutes } : {},
940
1072
  ...config.environments !== void 0 ? { environments: config.environments } : {},
941
1073
  ...config.smartHide !== void 0 ? { smartHide: config.smartHide } : {},
942
- ...config.draggable !== void 0 ? { draggable: config.draggable } : {}
1074
+ ...config.draggable !== void 0 ? { draggable: config.draggable } : {},
1075
+ ...config.brandFooter !== void 0 ? { brandFooter: config.brandFooter } : {},
1076
+ ...config.outdatedBanner !== void 0 ? { outdatedBanner: config.outdatedBanner } : {}
943
1077
  };
944
1078
  this.locale = getLocale(this.config.locale === "auto" ? void 0 : this.config.locale);
945
1079
  this.syncAttachedLaunchers();
@@ -1002,10 +1136,18 @@ var MushiWidget = class {
1002
1136
  this.screenshotAttached = attached;
1003
1137
  if (this.isOpen) this.render();
1004
1138
  }
1139
+ setAllowScreenshotRemove(allow) {
1140
+ this.allowScreenshotRemove = allow;
1141
+ if (this.isOpen) this.render();
1142
+ }
1005
1143
  setElementSelected(selected) {
1006
1144
  this.elementSelected = selected;
1007
1145
  if (this.isOpen) this.render();
1008
1146
  }
1147
+ setSdkFreshness(info) {
1148
+ this.sdkFreshness = info;
1149
+ if (this.isOpen) this.render();
1150
+ }
1009
1151
  destroy() {
1010
1152
  if (this.successTimer !== null) {
1011
1153
  clearTimeout(this.successTimer);
@@ -1131,13 +1273,22 @@ var MushiWidget = class {
1131
1273
  panel.style.zIndex = String(this.config.zIndex + 1);
1132
1274
  this.applyInsetVars(panel);
1133
1275
  if (this.isOpen) {
1134
- panel.innerHTML = this.renderStep();
1276
+ panel.innerHTML = `${this.renderOutdatedBanner()}${this.renderStep()}${this.renderBrandFooter()}`;
1135
1277
  this.shadow.appendChild(panel);
1136
1278
  this.attachHandlers(panel);
1137
1279
  this.trapFocus(panel);
1138
1280
  }
1139
1281
  }
1140
1282
  applyInsetVars(el) {
1283
+ const { anchor } = this.config;
1284
+ if (anchor && Object.keys(anchor).length > 0) {
1285
+ ["top", "right", "bottom", "left"].forEach((edge) => {
1286
+ const value = anchor[edge];
1287
+ if (value !== void 0) el.style.setProperty(`--mushi-${edge}`, value);
1288
+ });
1289
+ el.style.setProperty("--mushi-safe-area", this.config.respectSafeArea ? "1" : "0");
1290
+ return;
1291
+ }
1141
1292
  const { inset } = this.config;
1142
1293
  if (!this.config.respectSafeArea) {
1143
1294
  ["top", "right", "bottom", "left"].forEach((edge) => {
@@ -1161,8 +1312,29 @@ var MushiWidget = class {
1161
1312
  return this.renderDetailsStep();
1162
1313
  case "success":
1163
1314
  return this.renderSuccessStep();
1315
+ case "reports":
1316
+ return this.renderReportsStep();
1317
+ case "report-detail":
1318
+ return this.renderReportDetailStep();
1164
1319
  }
1165
1320
  }
1321
+ renderOutdatedBanner() {
1322
+ if (!this.sdkFreshness) return "";
1323
+ if (this.config.outdatedBanner === "off" || this.config.outdatedBanner === "console-only") return "";
1324
+ const { latest, current, deprecated, message } = this.sdkFreshness;
1325
+ if (!latest && !deprecated) return "";
1326
+ return `
1327
+ <div class="mushi-outdated" role="status">
1328
+ <strong>Mushi SDK ${escapeHtml(current)}</strong>
1329
+ ${latest ? `latest is ${escapeHtml(latest)}.` : "needs attention."}
1330
+ ${message ? `<span>${escapeHtml(message)}</span>` : ""}
1331
+ </div>
1332
+ `;
1333
+ }
1334
+ renderBrandFooter() {
1335
+ if (this.config.brandFooter === false) return "";
1336
+ return `<div class="mushi-brand-footer">Powered by Mushi v${escapeHtml(this.sdkVersion)}</div>`;
1337
+ }
1166
1338
  /**
1167
1339
  * Editorial masthead. Always carries:
1168
1340
  * • the brand mark (虫 kanji on vermillion, "MUSHI" in mono above)
@@ -1221,11 +1393,61 @@ var MushiWidget = class {
1221
1393
  return `
1222
1394
  ${this.renderHeader({ title: t.step1.heading, step: STEP_NUMBER.category })}
1223
1395
  <div class="mushi-body" role="radiogroup" aria-label="${t.step1.heading}">
1396
+ <button type="button" class="mushi-option-btn mushi-reports-entry" data-action="reports">
1397
+ <span class="mushi-option-icon" aria-hidden="true">\u{1F4EC}</span>
1398
+ <div class="mushi-option-text">
1399
+ <span class="mushi-option-label">Your reports${this.unreadCount() ? ` (${this.unreadCount()} new)` : ""}</span>
1400
+ <span class="mushi-option-desc">See status, developer replies, and respond</span>
1401
+ </div>
1402
+ <span class="mushi-option-arrow" aria-hidden="true">\u2192</span>
1403
+ </button>
1224
1404
  ${categories}
1225
1405
  </div>
1226
1406
  ${this.renderStepIndicator(STEP_NUMBER.category)}
1227
1407
  `;
1228
1408
  }
1409
+ renderReportsStep() {
1410
+ const reports = this.reporterReports.map((report) => `
1411
+ <button type="button" class="mushi-report-row" data-report-id="${escapeHtml(report.id)}">
1412
+ <span class="mushi-report-status">${escapeHtml(report.status)}</span>
1413
+ <span class="mushi-report-title">${escapeHtml(report.summary ?? report.description ?? `Report ${report.id.slice(0, 8)}`)}</span>
1414
+ ${report.unread_count ? `<b>${report.unread_count}</b>` : ""}
1415
+ </button>
1416
+ `).join("");
1417
+ return `
1418
+ ${this.renderHeader({ title: "Your reports", showBack: true, eyebrow: "Mushi \xB7 Inbox" })}
1419
+ <div class="mushi-body">
1420
+ ${this.reporterLoading ? '<p class="mushi-muted">Loading reports\u2026</p>' : ""}
1421
+ ${this.reporterError ? `<p class="mushi-error-inline">${escapeHtml(this.reporterError)}</p>` : ""}
1422
+ ${reports || (!this.reporterLoading ? '<p class="mushi-muted">No reports from this browser yet.</p>' : "")}
1423
+ </div>
1424
+ `;
1425
+ }
1426
+ renderReportDetailStep() {
1427
+ const report = this.reporterReports.find((r) => r.id === this.selectedReportId);
1428
+ const comments = this.reporterComments.map((comment) => `
1429
+ <div class="mushi-thread-comment ${comment.author_kind}">
1430
+ <strong>${escapeHtml(comment.author_kind === "reporter" ? "You" : comment.author_name ?? "Developer")}</strong>
1431
+ <p>${escapeHtml(comment.body)}</p>
1432
+ </div>
1433
+ `).join("");
1434
+ return `
1435
+ ${this.renderHeader({ title: "Report thread", showBack: true, eyebrow: "Mushi \xB7 Inbox" })}
1436
+ <div class="mushi-body">
1437
+ <div class="mushi-thread-summary">
1438
+ <span>${escapeHtml(report?.status ?? "unknown")}</span>
1439
+ <p>${escapeHtml(report?.summary ?? report?.description ?? "Report details")}</p>
1440
+ </div>
1441
+ <div class="mushi-thread">
1442
+ ${this.reporterLoading ? '<p class="mushi-muted">Loading thread\u2026</p>' : comments || '<p class="mushi-muted">No developer replies yet.</p>'}
1443
+ </div>
1444
+ <textarea class="mushi-textarea" data-role="reporter-reply" rows="3" placeholder="Reply to the developer\u2026"></textarea>
1445
+ <button type="button" class="mushi-submit" data-action="reporter-reply">
1446
+ <span>Reply</span><span class="mushi-submit-arrow" aria-hidden="true">\u2192</span>
1447
+ </button>
1448
+ </div>
1449
+ `;
1450
+ }
1229
1451
  renderIntentStep() {
1230
1452
  const t = this.locale;
1231
1453
  const cat = this.selectedCategory;
@@ -1265,6 +1487,7 @@ var MushiWidget = class {
1265
1487
  <button type="button" class="mushi-attach-btn${this.screenshotAttached ? " active" : ""}" data-action="screenshot">
1266
1488
  \u{1F4F8} ${this.screenshotAttached ? t.step3.screenshotAttached : t.step3.screenshotButton}
1267
1489
  </button>
1490
+ ${this.screenshotAttached && this.allowScreenshotRemove ? '<button type="button" class="mushi-attach-btn danger" data-action="remove-screenshot">\u2715 Remove screenshot</button>' : ""}
1268
1491
  <button type="button" class="mushi-attach-btn${this.elementSelected ? " active" : ""}" data-action="element">
1269
1492
  \u{1F3AF} ${this.elementSelected ? t.step3.elementSelected : t.step3.elementButton}
1270
1493
  </button>
@@ -1317,9 +1540,26 @@ var MushiWidget = class {
1317
1540
  } else if (this.step === "details") {
1318
1541
  this.step = "intent";
1319
1542
  this.selectedIntent = null;
1543
+ } else if (this.step === "reports") {
1544
+ this.step = "category";
1545
+ } else if (this.step === "report-detail") {
1546
+ this.step = "reports";
1547
+ this.selectedReportId = null;
1320
1548
  }
1321
1549
  this.render();
1322
1550
  });
1551
+ panel.querySelector('[data-action="reports"]')?.addEventListener("click", () => {
1552
+ void this.loadReporterReports();
1553
+ });
1554
+ panel.querySelectorAll("[data-report-id]").forEach((btn) => {
1555
+ btn.addEventListener("click", () => {
1556
+ const reportId = btn.dataset.reportId;
1557
+ if (reportId) void this.loadReporterComments(reportId);
1558
+ });
1559
+ });
1560
+ panel.querySelector('[data-action="reporter-reply"]')?.addEventListener("click", () => {
1561
+ void this.submitReporterReply(panel);
1562
+ });
1323
1563
  panel.querySelectorAll("[data-category]").forEach((btn) => {
1324
1564
  btn.addEventListener("click", () => {
1325
1565
  this.selectedCategory = btn.dataset.category;
@@ -1337,6 +1577,9 @@ var MushiWidget = class {
1337
1577
  panel.querySelector('[data-action="screenshot"]')?.addEventListener("click", () => {
1338
1578
  this.callbacks.onScreenshotRequest();
1339
1579
  });
1580
+ panel.querySelector('[data-action="remove-screenshot"]')?.addEventListener("click", () => {
1581
+ this.callbacks.onScreenshotRemove?.();
1582
+ });
1340
1583
  panel.querySelector('[data-action="element"]')?.addEventListener("click", () => {
1341
1584
  this.callbacks.onElementSelectorRequest?.();
1342
1585
  });
@@ -1394,6 +1637,57 @@ var MushiWidget = class {
1394
1637
  if (focusable.length > 0) focusable[0].focus();
1395
1638
  });
1396
1639
  }
1640
+ unreadCount() {
1641
+ return this.reporterReports.reduce((sum, report) => sum + (report.unread_count ?? 0), 0);
1642
+ }
1643
+ async loadReporterReports() {
1644
+ this.step = "reports";
1645
+ this.reporterLoading = true;
1646
+ this.reporterError = null;
1647
+ this.render();
1648
+ try {
1649
+ this.reporterReports = await this.callbacks.onReporterReportsRequest?.() ?? [];
1650
+ } catch (err) {
1651
+ this.reporterError = err instanceof Error ? err.message : "Could not load reports.";
1652
+ } finally {
1653
+ this.reporterLoading = false;
1654
+ this.render();
1655
+ }
1656
+ }
1657
+ async loadReporterComments(reportId) {
1658
+ this.selectedReportId = reportId;
1659
+ this.step = "report-detail";
1660
+ this.reporterLoading = true;
1661
+ this.reporterError = null;
1662
+ this.render();
1663
+ try {
1664
+ this.reporterComments = await this.callbacks.onReporterCommentsRequest?.(reportId) ?? [];
1665
+ } catch (err) {
1666
+ this.reporterError = err instanceof Error ? err.message : "Could not load thread.";
1667
+ } finally {
1668
+ this.reporterLoading = false;
1669
+ this.render();
1670
+ }
1671
+ }
1672
+ async submitReporterReply(panel) {
1673
+ const reportId = this.selectedReportId;
1674
+ const textarea = panel.querySelector('[data-role="reporter-reply"]');
1675
+ const replyButton = panel.querySelector('[data-action="reporter-reply"]');
1676
+ const body = textarea?.value.trim() ?? "";
1677
+ if (!reportId || !body || this.reporterLoading) return;
1678
+ this.reporterLoading = true;
1679
+ if (replyButton) replyButton.disabled = true;
1680
+ this.render();
1681
+ try {
1682
+ await this.callbacks.onReporterReply?.(reportId, body);
1683
+ if (textarea) textarea.value = "";
1684
+ await this.loadReporterComments(reportId);
1685
+ } catch (err) {
1686
+ this.reporterError = err instanceof Error ? err.message : "Could not send reply.";
1687
+ this.reporterLoading = false;
1688
+ this.render();
1689
+ }
1690
+ }
1397
1691
  };
1398
1692
 
1399
1693
  // src/capture/console.ts
@@ -1445,35 +1739,109 @@ function createConsoleCapture() {
1445
1739
  }
1446
1740
  };
1447
1741
  }
1742
+ var DEFAULT_INTERNAL_URL_MATCHERS = [
1743
+ /\/v1\/sdk(?:\/|$)/,
1744
+ /\/v1\/reports(?:\/|$)/,
1745
+ /\/v1\/notifications(?:\/|$)/,
1746
+ /\/v1\/reputation(?:\/|$)/
1747
+ ];
1748
+ function getRequestUrl(input) {
1749
+ if (typeof input === "string") return input;
1750
+ if (input instanceof URL) return input.href;
1751
+ return input.url;
1752
+ }
1753
+ function getInternalRequestKind(input, init) {
1754
+ const marker = init?.[MUSHI_INTERNAL_INIT_MARKER];
1755
+ if (marker) return marker;
1756
+ const initHeader = readHeader(init?.headers, MUSHI_INTERNAL_HEADER);
1757
+ if (initHeader) return initHeader;
1758
+ if (typeof Request !== "undefined" && input instanceof Request) {
1759
+ const requestHeader = input.headers.get(MUSHI_INTERNAL_HEADER);
1760
+ if (requestHeader) return requestHeader;
1761
+ }
1762
+ return null;
1763
+ }
1764
+ function shouldIgnoreMushiUrl(url, options = {}) {
1765
+ const matchers = [...DEFAULT_INTERNAL_URL_MATCHERS, ...options.ignoreUrls ?? []];
1766
+ if (matchers.some((matcher) => matchesUrl(url, matcher))) return true;
1767
+ const endpoint = normalizeUrlPrefix(options.apiEndpoint);
1768
+ return endpoint ? normalizeComparableUrl(url).startsWith(endpoint) : false;
1769
+ }
1770
+ function matchesUrl(url, matcher) {
1771
+ if (typeof matcher === "string") {
1772
+ return normalizeComparableUrl(url).includes(matcher);
1773
+ }
1774
+ matcher.lastIndex = 0;
1775
+ return matcher.test(url);
1776
+ }
1777
+ function normalizeUrlPrefix(url) {
1778
+ if (!url) return null;
1779
+ return normalizeComparableUrl(url).replace(/\/+$/, "");
1780
+ }
1781
+ function isLocalhostEndpoint(url) {
1782
+ if (!url) return false;
1783
+ try {
1784
+ const parsed = new URL(url);
1785
+ return parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "::1" || parsed.hostname.endsWith(".localhost");
1786
+ } catch {
1787
+ return /\blocalhost\b|127\.0\.0\.1/.test(url);
1788
+ }
1789
+ }
1790
+ function normalizeComparableUrl(url) {
1791
+ try {
1792
+ return new URL(url, typeof location !== "undefined" ? location.href : "http://localhost").href;
1793
+ } catch {
1794
+ return url;
1795
+ }
1796
+ }
1797
+ function readHeader(headers, name) {
1798
+ if (!headers) return null;
1799
+ if (typeof Headers !== "undefined" && headers instanceof Headers) {
1800
+ return headers.get(name);
1801
+ }
1802
+ if (Array.isArray(headers)) {
1803
+ const found = headers.find(([key]) => key.toLowerCase() === name.toLowerCase());
1804
+ return found?.[1] ?? null;
1805
+ }
1806
+ const record = headers;
1807
+ return record[name] ?? record[name.toLowerCase()] ?? null;
1808
+ }
1448
1809
 
1449
1810
  // src/capture/network.ts
1450
1811
  var MAX_ENTRIES2 = 30;
1451
- function createNetworkCapture() {
1812
+ function createNetworkCapture(options = {}) {
1452
1813
  const entries = [];
1453
1814
  const originalFetch = globalThis.fetch;
1815
+ let activeOptions = options;
1454
1816
  globalThis.fetch = async function mushiFetchInterceptor(input, init) {
1455
1817
  const startTime = Date.now();
1456
1818
  const method = init?.method?.toUpperCase() ?? "GET";
1457
- const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
1819
+ const url = getRequestUrl(input);
1820
+ const internalKind = getInternalRequestKind(input, init);
1821
+ const shouldRecord = !internalKind && !shouldIgnoreMushiUrl(url, activeOptions);
1458
1822
  try {
1459
1823
  const response = await originalFetch.call(globalThis, input, init);
1460
- addEntry({
1461
- method,
1462
- url: truncateUrl(url),
1463
- status: response.status,
1464
- duration: Date.now() - startTime,
1465
- timestamp: startTime
1466
- });
1824
+ if (shouldRecord) {
1825
+ addEntry({
1826
+ method,
1827
+ url: truncateUrl(url),
1828
+ status: response.status,
1829
+ duration: Date.now() - startTime,
1830
+ timestamp: startTime
1831
+ });
1832
+ }
1467
1833
  return response;
1468
1834
  } catch (error) {
1469
- addEntry({
1470
- method,
1471
- url: truncateUrl(url),
1472
- status: 0,
1473
- duration: Date.now() - startTime,
1474
- timestamp: startTime,
1475
- error: error instanceof Error ? error.message : "Network error"
1476
- });
1835
+ if (shouldRecord) {
1836
+ addEntry({
1837
+ method,
1838
+ url: truncateUrl(url),
1839
+ status: 0,
1840
+ duration: Date.now() - startTime,
1841
+ timestamp: startTime,
1842
+ error: error instanceof Error ? error.message : "Network error"
1843
+ });
1844
+ }
1477
1845
  throw error;
1478
1846
  }
1479
1847
  };
@@ -1490,6 +1858,9 @@ function createNetworkCapture() {
1490
1858
  clear() {
1491
1859
  entries.length = 0;
1492
1860
  },
1861
+ updateOptions(nextOptions) {
1862
+ activeOptions = nextOptions;
1863
+ },
1493
1864
  destroy() {
1494
1865
  globalThis.fetch = originalFetch;
1495
1866
  }
@@ -1509,7 +1880,8 @@ function truncateUrl(url) {
1509
1880
  }
1510
1881
 
1511
1882
  // src/capture/screenshot.ts
1512
- function createScreenshotCapture() {
1883
+ function createScreenshotCapture(options = {}) {
1884
+ let activeOptions = options;
1513
1885
  async function take() {
1514
1886
  try {
1515
1887
  if (typeof document === "undefined") return null;
@@ -1522,11 +1894,12 @@ function createScreenshotCapture() {
1522
1894
  canvas.width = width * dpr;
1523
1895
  canvas.height = height * dpr;
1524
1896
  ctx.scale(dpr, dpr);
1897
+ const safeDocument = buildPrivacySafeDocument(activeOptions.privacy);
1525
1898
  const svgData = `
1526
1899
  <svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
1527
1900
  <foreignObject width="100%" height="100%">
1528
1901
  <div xmlns="http://www.w3.org/1999/xhtml">
1529
- ${new XMLSerializer().serializeToString(document.documentElement)}
1902
+ ${new XMLSerializer().serializeToString(safeDocument)}
1530
1903
  </div>
1531
1904
  </foreignObject>
1532
1905
  </svg>
@@ -1555,7 +1928,46 @@ function createScreenshotCapture() {
1555
1928
  return null;
1556
1929
  }
1557
1930
  }
1558
- return { take };
1931
+ return {
1932
+ take,
1933
+ updateOptions(nextOptions) {
1934
+ activeOptions = nextOptions;
1935
+ }
1936
+ };
1937
+ }
1938
+ function buildPrivacySafeDocument(privacy) {
1939
+ const clone = document.documentElement.cloneNode(true);
1940
+ for (const selector of privacy?.blockSelectors ?? []) {
1941
+ for (const el of safeQueryAll(clone, selector)) {
1942
+ el.remove();
1943
+ }
1944
+ }
1945
+ for (const selector of privacy?.maskSelectors ?? []) {
1946
+ for (const el of safeQueryAll(clone, selector)) {
1947
+ maskElement(el);
1948
+ }
1949
+ }
1950
+ return clone;
1951
+ }
1952
+ function safeQueryAll(root, selector) {
1953
+ try {
1954
+ return Array.from(root.querySelectorAll(selector));
1955
+ } catch {
1956
+ return [];
1957
+ }
1958
+ }
1959
+ function maskElement(el) {
1960
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
1961
+ el.value = "";
1962
+ el.setAttribute("value", "");
1963
+ el.setAttribute("placeholder", "\u2022\u2022\u2022\u2022");
1964
+ }
1965
+ el.textContent = el.children.length === 0 ? "\u2022\u2022\u2022\u2022" : el.textContent;
1966
+ el.setAttribute(
1967
+ "style",
1968
+ `${el.getAttribute("style") ?? ""};background:#8f8f8f!important;color:transparent!important;text-shadow:none!important;`
1969
+ );
1970
+ el.setAttribute("data-mushi-masked", "true");
1559
1971
  }
1560
1972
 
1561
1973
  // src/capture/performance.ts
@@ -1638,6 +2050,17 @@ function createElementSelector() {
1638
2050
  let active = false;
1639
2051
  let overlay = null;
1640
2052
  let resolvePromise = null;
2053
+ function findNearestTestid(el) {
2054
+ let cur = el;
2055
+ let hops = 0;
2056
+ while (cur && hops < 20) {
2057
+ const tid = cur.getAttribute?.("data-testid");
2058
+ if (tid) return tid;
2059
+ cur = cur.parentElement;
2060
+ hops++;
2061
+ }
2062
+ return null;
2063
+ }
1641
2064
  function getXPath(el) {
1642
2065
  const parts = [];
1643
2066
  let current = el;
@@ -1667,7 +2090,13 @@ function createElementSelector() {
1667
2090
  y: Math.round(rect.y),
1668
2091
  width: Math.round(rect.width),
1669
2092
  height: Math.round(rect.height)
1670
- }
2093
+ },
2094
+ // v2 (whitepaper §4.7): the closest ancestor's `data-testid` lets the
2095
+ // server map this report → an Action node in the inventory graph
2096
+ // without a fuzzy NLP guess. We walk to the body so a deeply nested
2097
+ // span inside a button-with-testid still resolves correctly.
2098
+ nearestTestid: findNearestTestid(el) || void 0,
2099
+ route: typeof window !== "undefined" ? window.location.pathname : void 0
1671
2100
  };
1672
2101
  }
1673
2102
  function createOverlay() {
@@ -1744,6 +2173,296 @@ function createElementSelector() {
1744
2173
  return { activate, deactivate, isActive: () => active };
1745
2174
  }
1746
2175
 
2176
+ // src/capture/timeline.ts
2177
+ var MAX_TIMELINE_ENTRIES = 120;
2178
+ function createTimelineCapture() {
2179
+ const entries = [];
2180
+ const originalPushState = history.pushState;
2181
+ const originalReplaceState = history.replaceState;
2182
+ const handlePopState = () => recordRoute("popstate");
2183
+ const handleHashChange = () => recordRoute("hashchange");
2184
+ recordRoute("initial");
2185
+ function record(entry) {
2186
+ entries.push(entry);
2187
+ if (entries.length > MAX_TIMELINE_ENTRIES) entries.shift();
2188
+ }
2189
+ function recordRoute(source) {
2190
+ if (typeof location === "undefined") return;
2191
+ record({
2192
+ ts: Date.now(),
2193
+ kind: "route",
2194
+ payload: {
2195
+ source,
2196
+ route: `${location.pathname}${location.search}${location.hash}`,
2197
+ href: location.href
2198
+ }
2199
+ });
2200
+ }
2201
+ function handleClick(event) {
2202
+ const target = event.target instanceof Element ? event.target : null;
2203
+ if (!target) return;
2204
+ const el = target.closest('button,a,[role="button"],input,textarea,select,[data-mushi-track]') ?? target;
2205
+ record({
2206
+ ts: Date.now(),
2207
+ kind: "click",
2208
+ payload: {
2209
+ tag: el.tagName.toLowerCase(),
2210
+ id: el.id || void 0,
2211
+ text: textSnippet(el)
2212
+ }
2213
+ });
2214
+ }
2215
+ history.pushState = function mushiPushState(...args) {
2216
+ const result = originalPushState.apply(this, args);
2217
+ recordRoute("pushState");
2218
+ return result;
2219
+ };
2220
+ history.replaceState = function mushiReplaceState(...args) {
2221
+ const result = originalReplaceState.apply(this, args);
2222
+ recordRoute("replaceState");
2223
+ return result;
2224
+ };
2225
+ window.addEventListener("popstate", handlePopState);
2226
+ window.addEventListener("hashchange", handleHashChange);
2227
+ document.addEventListener("click", handleClick, true);
2228
+ return {
2229
+ setScreen(screen) {
2230
+ record({
2231
+ ts: Date.now(),
2232
+ kind: "screen",
2233
+ payload: screen
2234
+ });
2235
+ },
2236
+ getEntries(input = {}) {
2237
+ const merged = [
2238
+ ...entries,
2239
+ ...(input.consoleLogs ?? []).map((log) => ({
2240
+ ts: log.timestamp,
2241
+ kind: "log",
2242
+ payload: {
2243
+ level: log.level,
2244
+ message: log.message
2245
+ }
2246
+ })),
2247
+ ...(input.networkLogs ?? []).map((network) => ({
2248
+ ts: network.timestamp,
2249
+ kind: "request",
2250
+ payload: {
2251
+ method: network.method,
2252
+ url: network.url,
2253
+ status: network.status,
2254
+ duration: network.duration,
2255
+ error: network.error
2256
+ }
2257
+ }))
2258
+ ].sort((a, b) => a.ts - b.ts);
2259
+ return merged.slice(-MAX_TIMELINE_ENTRIES);
2260
+ },
2261
+ clear() {
2262
+ entries.length = 0;
2263
+ },
2264
+ destroy() {
2265
+ history.pushState = originalPushState;
2266
+ history.replaceState = originalReplaceState;
2267
+ window.removeEventListener("popstate", handlePopState);
2268
+ window.removeEventListener("hashchange", handleHashChange);
2269
+ document.removeEventListener("click", handleClick, true);
2270
+ }
2271
+ };
2272
+ }
2273
+ function textSnippet(el) {
2274
+ const text = (el.textContent ?? "").replace(/\s+/g, " ").trim();
2275
+ return text ? text.slice(0, 80) : void 0;
2276
+ }
2277
+
2278
+ // src/capture/discovery.ts
2279
+ var DEFAULT_THROTTLE_MS = 6e4;
2280
+ 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;
2281
+ var HEX24_RE = /^[0-9a-f]{20,}$/i;
2282
+ var NUMERIC_RE = /^\d+$/;
2283
+ var SLUG_HASHY_RE = /^[a-z0-9]{16,}$/i;
2284
+ function normalizeSegment(seg) {
2285
+ if (seg.length === 0) return seg;
2286
+ if (UUID_RE.test(seg)) return "[id]";
2287
+ if (HEX24_RE.test(seg)) return "[id]";
2288
+ if (NUMERIC_RE.test(seg)) return "[id]";
2289
+ if (SLUG_HASHY_RE.test(seg) && /\d/.test(seg)) return "[id]";
2290
+ return seg;
2291
+ }
2292
+ function normalizeRoute(pathname, templates) {
2293
+ const clean = pathname.length > 1 && pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
2294
+ if (templates?.length) {
2295
+ const matched = matchTemplate(clean, templates);
2296
+ if (matched) return matched;
2297
+ }
2298
+ return "/" + clean.split("/").filter((s) => s.length > 0).map(normalizeSegment).join("/");
2299
+ }
2300
+ function matchTemplate(pathname, templates) {
2301
+ const segs = pathname.split("/").filter((s) => s.length > 0);
2302
+ const sorted = [...templates].sort(
2303
+ (a, b) => b.split("/").length - a.split("/").length
2304
+ );
2305
+ for (const tpl of sorted) {
2306
+ const tplSegs = tpl.split("/").filter((s) => s.length > 0);
2307
+ if (tplSegs.length !== segs.length) continue;
2308
+ let ok = true;
2309
+ for (let i = 0; i < tplSegs.length; i++) {
2310
+ const t = tplSegs[i];
2311
+ const s = segs[i];
2312
+ if (t.startsWith("[") && t.endsWith("]")) continue;
2313
+ if (t.startsWith(":")) continue;
2314
+ if (t === s) continue;
2315
+ ok = false;
2316
+ break;
2317
+ }
2318
+ if (ok) return "/" + tplSegs.join("/");
2319
+ }
2320
+ return null;
2321
+ }
2322
+ function readTestids() {
2323
+ if (typeof document === "undefined") return [];
2324
+ const out = /* @__PURE__ */ new Set();
2325
+ const els = document.querySelectorAll("[data-testid]");
2326
+ for (const el of Array.from(els)) {
2327
+ const v = el.getAttribute("data-testid");
2328
+ if (v && v.length > 0 && v.length < 120) out.add(v);
2329
+ }
2330
+ return Array.from(out).sort();
2331
+ }
2332
+ function readQueryParamKeys() {
2333
+ if (typeof window === "undefined") return [];
2334
+ try {
2335
+ const params = new URLSearchParams(window.location.search);
2336
+ const out = /* @__PURE__ */ new Set();
2337
+ params.forEach((_, key) => out.add(key));
2338
+ return Array.from(out).sort();
2339
+ } catch {
2340
+ return [];
2341
+ }
2342
+ }
2343
+ function readDomSummary() {
2344
+ if (typeof document === "undefined") return null;
2345
+ const trim = (s) => (s ?? "").replace(/\s+/g, " ").trim().slice(0, 200);
2346
+ const h1 = trim(document.querySelector("h1")?.textContent);
2347
+ if (h1) return h1;
2348
+ const title = trim(document.title);
2349
+ if (title) return title;
2350
+ const main = trim(document.querySelector("main")?.textContent);
2351
+ return main || null;
2352
+ }
2353
+ async function hashUserId(input) {
2354
+ if (!input || typeof crypto === "undefined" || !crypto.subtle) return null;
2355
+ try {
2356
+ const data = new TextEncoder().encode(input);
2357
+ const buf = await crypto.subtle.digest("SHA-256", data);
2358
+ const bytes = new Uint8Array(buf);
2359
+ let hex = "";
2360
+ for (let i = 0; i < bytes.length; i++) {
2361
+ hex += bytes[i].toString(16).padStart(2, "0");
2362
+ }
2363
+ return hex;
2364
+ } catch {
2365
+ return null;
2366
+ }
2367
+ }
2368
+ function createDiscoveryCapture(opts) {
2369
+ const {
2370
+ config,
2371
+ getRecentNetworkPaths,
2372
+ getUserId,
2373
+ getSessionId: getSessionId2,
2374
+ onEvent
2375
+ } = opts;
2376
+ const throttleMs = config.throttleMs ?? DEFAULT_THROTTLE_MS;
2377
+ const captureSummary = config.captureDomSummary !== false;
2378
+ const userIdSource = config.userIdSource ?? "auto";
2379
+ const lastEmittedAt = /* @__PURE__ */ new Map();
2380
+ let lastPath = null;
2381
+ let pendingTimer = null;
2382
+ async function emitForCurrent() {
2383
+ if (typeof window === "undefined") return;
2384
+ const route = normalizeRoute(window.location.pathname, config.routeTemplates);
2385
+ const now = Date.now();
2386
+ const last = lastEmittedAt.get(route) ?? 0;
2387
+ if (now - last < throttleMs) return;
2388
+ lastEmittedAt.set(route, now);
2389
+ let userIdInput = null;
2390
+ if (userIdSource === "auto") {
2391
+ userIdInput = getUserId() ?? getSessionId2();
2392
+ } else if (userIdSource === "session-only") {
2393
+ userIdInput = getSessionId2();
2394
+ }
2395
+ const event = {
2396
+ route,
2397
+ page_title: typeof document !== "undefined" ? (document.title || "").slice(0, 300) || null : null,
2398
+ dom_summary: captureSummary ? readDomSummary() : null,
2399
+ testids: readTestids(),
2400
+ network_paths: getRecentNetworkPaths().slice(-50),
2401
+ query_param_keys: readQueryParamKeys(),
2402
+ user_id_hash: await hashUserId(userIdInput),
2403
+ observed_at: (/* @__PURE__ */ new Date()).toISOString()
2404
+ };
2405
+ onEvent(event);
2406
+ }
2407
+ function scheduleEmit() {
2408
+ if (pendingTimer) return;
2409
+ pendingTimer = setTimeout(() => {
2410
+ pendingTimer = null;
2411
+ void emitForCurrent();
2412
+ }, 100);
2413
+ }
2414
+ function onMaybeNavigation() {
2415
+ if (typeof window === "undefined") return;
2416
+ const path = window.location.pathname + window.location.search;
2417
+ if (path === lastPath) return;
2418
+ lastPath = path;
2419
+ scheduleEmit();
2420
+ }
2421
+ if (typeof window === "undefined") {
2422
+ return {
2423
+ destroy: () => void 0,
2424
+ flushNow: () => void 0
2425
+ };
2426
+ }
2427
+ const originalPush = window.history.pushState.bind(window.history);
2428
+ const originalReplace = window.history.replaceState.bind(window.history);
2429
+ const patchedPush = function patched(...args) {
2430
+ const out = originalPush(...args);
2431
+ onMaybeNavigation();
2432
+ return out;
2433
+ };
2434
+ const patchedReplace = function patched(...args) {
2435
+ const out = originalReplace(...args);
2436
+ onMaybeNavigation();
2437
+ return out;
2438
+ };
2439
+ window.history.pushState = patchedPush;
2440
+ window.history.replaceState = patchedReplace;
2441
+ const onPop = () => onMaybeNavigation();
2442
+ window.addEventListener("popstate", onPop);
2443
+ scheduleEmit();
2444
+ return {
2445
+ destroy() {
2446
+ window.removeEventListener("popstate", onPop);
2447
+ if (window.history.pushState === patchedPush) {
2448
+ window.history.pushState = originalPush;
2449
+ }
2450
+ if (window.history.replaceState === patchedReplace) {
2451
+ window.history.replaceState = originalReplace;
2452
+ }
2453
+ if (pendingTimer) {
2454
+ clearTimeout(pendingTimer);
2455
+ pendingTimer = null;
2456
+ }
2457
+ lastEmittedAt.clear();
2458
+ },
2459
+ flushNow() {
2460
+ lastEmittedAt.clear();
2461
+ void emitForCurrent();
2462
+ }
2463
+ };
2464
+ }
2465
+
1747
2466
  // src/sentry.ts
1748
2467
  function getSentryGlobal() {
1749
2468
  try {
@@ -1830,36 +2549,25 @@ function setupProactiveTriggers(callbacks, config = {}) {
1830
2549
  } catch {
1831
2550
  }
1832
2551
  }
1833
- if (config.apiCascade !== false) {
2552
+ const apiCascade = normalizeApiCascadeConfig(config.apiCascade);
2553
+ if (apiCascade.enabled) {
1834
2554
  const failedRequests = [];
1835
2555
  const origFetch = globalThis.fetch;
1836
2556
  globalThis.fetch = async function(...args) {
2557
+ const [input, init] = args;
2558
+ const url = getRequestUrl(input);
2559
+ const ignoreFailure = Boolean(getInternalRequestKind(input, init)) || shouldIgnoreMushiUrl(url, {
2560
+ apiEndpoint: config.apiEndpoint,
2561
+ ignoreUrls: apiCascade.ignoreUrls
2562
+ });
1837
2563
  try {
1838
2564
  const res = await origFetch.apply(this, args);
1839
- if (!res.ok && res.status >= 400) {
1840
- const now = Date.now();
1841
- failedRequests.push(now);
1842
- const recentFailures = failedRequests.filter((t) => now - t < 1e4);
1843
- if (recentFailures.length >= 3) {
1844
- callbacks.onTrigger("api_cascade", {
1845
- failureCount: recentFailures.length,
1846
- windowMs: 1e4
1847
- });
1848
- failedRequests.length = 0;
1849
- }
2565
+ if (!ignoreFailure && !res.ok && res.status >= 400) {
2566
+ recordApiFailure(failedRequests, callbacks);
1850
2567
  }
1851
2568
  return res;
1852
2569
  } catch (err) {
1853
- const now = Date.now();
1854
- failedRequests.push(now);
1855
- const recentFailures = failedRequests.filter((t) => now - t < 1e4);
1856
- if (recentFailures.length >= 3) {
1857
- callbacks.onTrigger("api_cascade", {
1858
- failureCount: recentFailures.length,
1859
- windowMs: 1e4
1860
- });
1861
- failedRequests.length = 0;
1862
- }
2570
+ if (!ignoreFailure) recordApiFailure(failedRequests, callbacks);
1863
2571
  throw err;
1864
2572
  }
1865
2573
  };
@@ -1894,6 +2602,28 @@ function setupProactiveTriggers(callbacks, config = {}) {
1894
2602
  }
1895
2603
  };
1896
2604
  }
2605
+ function normalizeApiCascadeConfig(config) {
2606
+ if (config === false) return { enabled: false, ignoreUrls: [] };
2607
+ if (config && typeof config === "object") {
2608
+ return {
2609
+ enabled: config.enabled !== false,
2610
+ ignoreUrls: config.ignoreUrls ?? []
2611
+ };
2612
+ }
2613
+ return { enabled: true, ignoreUrls: [] };
2614
+ }
2615
+ function recordApiFailure(failedRequests, callbacks) {
2616
+ const now = Date.now();
2617
+ failedRequests.push(now);
2618
+ const recentFailures = failedRequests.filter((t) => now - t < 1e4);
2619
+ if (recentFailures.length >= 3) {
2620
+ callbacks.onTrigger("api_cascade", {
2621
+ failureCount: recentFailures.length,
2622
+ windowMs: 1e4
2623
+ });
2624
+ failedRequests.length = 0;
2625
+ }
2626
+ }
1897
2627
 
1898
2628
  // src/proactive-manager.ts
1899
2629
  var STORAGE_KEY_LAST_DISMISS = "mushi:lastDismiss";
@@ -1946,6 +2676,10 @@ function createProactiveManager(config = {}) {
1946
2676
  return { shouldShow, recordDismissal, recordSubmission, reset };
1947
2677
  }
1948
2678
 
2679
+ // src/version.ts
2680
+ var MUSHI_SDK_PACKAGE = "@mushi-mushi/web";
2681
+ var MUSHI_SDK_VERSION = "0.9.0" ;
2682
+
1949
2683
  // src/mushi.ts
1950
2684
  var instance = null;
1951
2685
  var Mushi = class {
@@ -1975,18 +2709,21 @@ var Mushi = class {
1975
2709
  instance?.destroy();
1976
2710
  instance = null;
1977
2711
  }
2712
+ static diagnose() {
2713
+ return instance?.diagnose() ?? diagnoseWithoutInstance();
2714
+ }
1978
2715
  };
1979
2716
  function createInstance(config) {
1980
- const bootstrapConfig = config;
1981
- let activeConfig = config;
2717
+ const bootstrapConfig = applyPresetConfig(config);
2718
+ let activeConfig = bootstrapConfig;
1982
2719
  const log = config.debug ?? false ? createLogger({ scope: "mushi", level: "debug", format: "pretty" }) : noopLogger;
1983
2720
  const apiClient = createApiClient({
1984
- projectId: config.projectId,
1985
- apiKey: config.apiKey,
1986
- ...config.apiEndpoint ? { apiEndpoint: config.apiEndpoint } : {}
2721
+ projectId: bootstrapConfig.projectId,
2722
+ apiKey: bootstrapConfig.apiKey,
2723
+ ...bootstrapConfig.apiEndpoint ? { apiEndpoint: bootstrapConfig.apiEndpoint } : {}
1987
2724
  });
1988
- const preFilter = createPreFilter(config.preFilter);
1989
- const offlineQueue = createOfflineQueue(config.offline);
2725
+ const preFilter = createPreFilter(bootstrapConfig.preFilter);
2726
+ const offlineQueue = createOfflineQueue(bootstrapConfig.offline);
1990
2727
  const rateLimiter = createRateLimiter({ maxBurst: 10, refillRate: 1, refillIntervalMs: 5e3 });
1991
2728
  const piiScrubber = createPiiScrubber();
1992
2729
  let consoleCap = null;
@@ -1994,6 +2731,9 @@ function createInstance(config) {
1994
2731
  let perfCap = null;
1995
2732
  let screenshotCap = null;
1996
2733
  let elementSelector = null;
2734
+ let discoveryCap = null;
2735
+ const timelineCap = createTimelineCapture();
2736
+ let widget;
1997
2737
  function syncCaptureModules() {
1998
2738
  if (activeConfig.capture?.console !== false) {
1999
2739
  consoleCap ??= createConsoleCapture();
@@ -2002,7 +2742,15 @@ function createInstance(config) {
2002
2742
  consoleCap = null;
2003
2743
  }
2004
2744
  if (activeConfig.capture?.network !== false) {
2005
- networkCap ??= createNetworkCapture();
2745
+ const networkOptions = {
2746
+ apiEndpoint: resolveApiEndpoint(activeConfig),
2747
+ ignoreUrls: activeConfig.capture?.ignoreUrls
2748
+ };
2749
+ if (networkCap) {
2750
+ networkCap.updateOptions(networkOptions);
2751
+ } else {
2752
+ networkCap = createNetworkCapture(networkOptions);
2753
+ }
2006
2754
  } else {
2007
2755
  networkCap?.destroy();
2008
2756
  networkCap = null;
@@ -2013,8 +2761,18 @@ function createInstance(config) {
2013
2761
  perfCap?.destroy();
2014
2762
  perfCap = null;
2015
2763
  }
2016
- screenshotCap = activeConfig.capture?.screenshot !== "off" ? screenshotCap ?? createScreenshotCapture() : null;
2764
+ if (activeConfig.capture?.screenshot !== "off") {
2765
+ const screenshotOptions = { privacy: activeConfig.privacy };
2766
+ if (screenshotCap) {
2767
+ screenshotCap.updateOptions(screenshotOptions);
2768
+ } else {
2769
+ screenshotCap = createScreenshotCapture(screenshotOptions);
2770
+ }
2771
+ } else {
2772
+ screenshotCap = null;
2773
+ }
2017
2774
  if (!screenshotCap) pendingScreenshot = null;
2775
+ widget.setAllowScreenshotRemove(activeConfig.privacy?.allowUserRemoveScreenshot !== false);
2018
2776
  if (activeConfig.capture?.elementSelector !== false) {
2019
2777
  elementSelector ??= createElementSelector();
2020
2778
  } else {
@@ -2022,6 +2780,40 @@ function createInstance(config) {
2022
2780
  elementSelector = null;
2023
2781
  pendingElement = null;
2024
2782
  }
2783
+ const discoveryRaw = activeConfig.capture?.discoverInventory;
2784
+ const discoveryConfig = discoveryRaw === true ? {} : discoveryRaw && typeof discoveryRaw === "object" ? discoveryRaw : null;
2785
+ const discoveryEnabled = discoveryConfig != null && discoveryConfig.enabled !== false;
2786
+ if (discoveryEnabled) {
2787
+ discoveryCap?.destroy();
2788
+ discoveryCap = createDiscoveryCapture({
2789
+ config: discoveryConfig,
2790
+ getRecentNetworkPaths: () => {
2791
+ if (!networkCap) return [];
2792
+ return networkCap.getEntries().map((e) => {
2793
+ try {
2794
+ const u = new URL(e.url, typeof window !== "undefined" ? window.location.href : "http://localhost");
2795
+ if (u.host && typeof window !== "undefined" && u.host !== window.location.host) return null;
2796
+ return u.pathname;
2797
+ } catch {
2798
+ return null;
2799
+ }
2800
+ }).filter((p) => p != null && p.length > 0 && p.length < 200);
2801
+ },
2802
+ getUserId: () => userInfo?.id ?? null,
2803
+ getSessionId,
2804
+ onEvent: (event) => {
2805
+ void apiClient.postDiscoveryEvent({
2806
+ ...event,
2807
+ sdk_version: MUSHI_SDK_VERSION
2808
+ }).catch((err) => {
2809
+ log.debug("discovery emit failed", { err: String(err) });
2810
+ });
2811
+ }
2812
+ });
2813
+ } else {
2814
+ discoveryCap?.destroy();
2815
+ discoveryCap = null;
2816
+ }
2025
2817
  }
2026
2818
  const listeners = /* @__PURE__ */ new Map();
2027
2819
  function emit(type, data) {
@@ -2030,10 +2822,10 @@ function createInstance(config) {
2030
2822
  let pendingScreenshot = null;
2031
2823
  let pendingElement = null;
2032
2824
  let pendingProactiveTrigger = null;
2825
+ let runtimeConfigLoaded = false;
2033
2826
  let userInfo = null;
2034
2827
  const customMetadata = {};
2035
- syncCaptureModules();
2036
- const widget = new MushiWidget(config.widget, {
2828
+ widget = new MushiWidget(bootstrapConfig.widget, {
2037
2829
  onSubmit: async ({ category, description, intent }) => {
2038
2830
  log.info("Report submitted", { category, intent });
2039
2831
  proactiveManager?.recordSubmission();
@@ -2060,6 +2852,11 @@ function createInstance(config) {
2060
2852
  pendingScreenshot = await screenshotCap.take();
2061
2853
  widget.setScreenshotAttached(pendingScreenshot !== null);
2062
2854
  },
2855
+ onScreenshotRemove: () => {
2856
+ log.debug("Screenshot attachment removed");
2857
+ pendingScreenshot = null;
2858
+ widget.setScreenshotAttached(false);
2859
+ },
2063
2860
  onElementSelectorRequest: async () => {
2064
2861
  if (!elementSelector || activeConfig.capture?.elementSelector === false) return;
2065
2862
  log.debug("Element selector activated");
@@ -2069,8 +2866,23 @@ function createInstance(config) {
2069
2866
  widget.setElementSelected(true);
2070
2867
  log.debug("Element selected", { tagName: el.tagName, xpath: el.xpath });
2071
2868
  }
2869
+ },
2870
+ async onReporterReportsRequest() {
2871
+ const result = await apiClient.listReporterReports(getReporterToken());
2872
+ if (!result.ok) throw new Error(result.error?.message ?? "Could not load reports");
2873
+ return result.data?.reports ?? [];
2874
+ },
2875
+ async onReporterCommentsRequest(reportId) {
2876
+ const result = await apiClient.listReporterComments(reportId, getReporterToken());
2877
+ if (!result.ok) throw new Error(result.error?.message ?? "Could not load thread");
2878
+ return result.data?.comments ?? [];
2879
+ },
2880
+ async onReporterReply(reportId, body) {
2881
+ const result = await apiClient.replyToReporterReport(reportId, getReporterToken(), body);
2882
+ if (!result.ok) throw new Error(result.error?.message ?? "Could not send reply");
2072
2883
  }
2073
- });
2884
+ }, MUSHI_SDK_VERSION);
2885
+ syncCaptureModules();
2074
2886
  if (typeof document !== "undefined") {
2075
2887
  if (document.readyState === "loading") {
2076
2888
  document.addEventListener("DOMContentLoaded", () => widget.mount());
@@ -2080,7 +2892,7 @@ function createInstance(config) {
2080
2892
  }
2081
2893
  let proactiveTriggers = null;
2082
2894
  let proactiveManager = null;
2083
- const proactiveCfg = config.proactive;
2895
+ const proactiveCfg = activeConfig.proactive;
2084
2896
  const hasAnyProactive = proactiveCfg && (proactiveCfg.rageClick !== false || proactiveCfg.longTask !== false || proactiveCfg.apiCascade !== false || proactiveCfg.errorBoundary === true);
2085
2897
  if (hasAnyProactive && typeof document !== "undefined") {
2086
2898
  proactiveManager = createProactiveManager(proactiveCfg?.cooldown);
@@ -2101,6 +2913,7 @@ function createInstance(config) {
2101
2913
  rageClick: proactiveCfg?.rageClick,
2102
2914
  longTask: proactiveCfg?.longTask,
2103
2915
  apiCascade: proactiveCfg?.apiCascade,
2916
+ apiEndpoint: resolveApiEndpoint(activeConfig),
2104
2917
  errorBoundary: proactiveCfg?.errorBoundary
2105
2918
  }
2106
2919
  );
@@ -2116,6 +2929,7 @@ function createInstance(config) {
2116
2929
  if (result.sent > 0) log.info("Synced offline reports", { sent: result.sent });
2117
2930
  });
2118
2931
  function applyRuntimeConfig(runtime) {
2932
+ runtimeConfigLoaded = true;
2119
2933
  if (runtime.enabled === false) {
2120
2934
  activeConfig = bootstrapConfig;
2121
2935
  clearCachedRuntimeConfig(config.projectId);
@@ -2129,7 +2943,7 @@ function createInstance(config) {
2129
2943
  if (runtime.widget) widget.updateConfig(activeConfig.widget);
2130
2944
  log.debug("Applied runtime SDK config", { version: runtime.version });
2131
2945
  }
2132
- if (config.runtimeConfig !== false) {
2946
+ if (shouldUseRuntimeConfig(config)) {
2133
2947
  const cached = readCachedRuntimeConfig(config.projectId);
2134
2948
  if (cached) applyRuntimeConfig(cached);
2135
2949
  apiClient.getSdkConfig().then((result) => {
@@ -2142,8 +2956,41 @@ function createInstance(config) {
2142
2956
  }).catch((err) => {
2143
2957
  log.debug("Runtime SDK config fetch failed", { error: err instanceof Error ? err.message : String(err) });
2144
2958
  });
2959
+ } else if (config.runtimeConfig !== false && isLocalhostEndpoint(resolveApiEndpoint(config))) {
2960
+ log.debug("Runtime SDK config skipped for localhost apiEndpoint; set runtimeConfig: true to force it");
2145
2961
  }
2962
+ void checkSdkFreshness();
2146
2963
  log.info("Initialized", { projectId: config.projectId });
2964
+ async function checkSdkFreshness() {
2965
+ if (activeConfig.widget?.outdatedBanner === "off") return;
2966
+ const cached = readCachedSdkVersion(MUSHI_SDK_PACKAGE);
2967
+ if (cached) applySdkFreshness(cached);
2968
+ const result = await apiClient.getLatestSdkVersion(MUSHI_SDK_PACKAGE);
2969
+ if (!result.ok || !result.data) return;
2970
+ cacheSdkVersion(MUSHI_SDK_PACKAGE, result.data);
2971
+ applySdkFreshness(result.data);
2972
+ }
2973
+ function applySdkFreshness(info) {
2974
+ const latest = info.latest;
2975
+ const outdated = Boolean(latest && isVersionOlder(MUSHI_SDK_VERSION, latest));
2976
+ if (!outdated && !info.deprecated) return;
2977
+ const message = info.deprecationMessage ?? (outdated ? `Update ${MUSHI_SDK_PACKAGE} to ${latest}.` : null);
2978
+ log.warn("Mushi SDK is outdated", {
2979
+ package: MUSHI_SDK_PACKAGE,
2980
+ current: MUSHI_SDK_VERSION,
2981
+ latest,
2982
+ deprecated: info.deprecated,
2983
+ message
2984
+ });
2985
+ if (activeConfig.widget?.outdatedBanner !== "console-only") {
2986
+ widget.setSdkFreshness({
2987
+ latest,
2988
+ current: MUSHI_SDK_VERSION,
2989
+ deprecated: info.deprecated,
2990
+ message
2991
+ });
2992
+ }
2993
+ }
2147
2994
  async function submitReport(category, description, intent) {
2148
2995
  const filterResult = preFilter.check(description);
2149
2996
  if (!filterResult.passed) {
@@ -2185,6 +3032,8 @@ function createInstance(config) {
2185
3032
  const scrubbedDescription = piiScrubber.scrub(preFilter.truncate(description));
2186
3033
  const sentryCtx = config.sentry ? captureSentryContext(config.sentry) : void 0;
2187
3034
  const fingerprintHash = await getDeviceFingerprintHash().catch(() => null);
3035
+ const consoleLogs = activeConfig.capture?.console === false ? void 0 : consoleCap?.getEntries();
3036
+ const networkLogs = activeConfig.capture?.network === false ? void 0 : networkCap?.getEntries();
2188
3037
  const report = {
2189
3038
  id: crypto.randomUUID?.() ?? `mushi_${Date.now()}_${Math.random().toString(36).slice(2)}`,
2190
3039
  projectId: config.projectId,
@@ -2192,9 +3041,10 @@ function createInstance(config) {
2192
3041
  description: scrubbedDescription,
2193
3042
  userIntent: intent,
2194
3043
  environment: captureEnvironment(),
2195
- consoleLogs: activeConfig.capture?.console === false ? void 0 : consoleCap?.getEntries(),
2196
- networkLogs: activeConfig.capture?.network === false ? void 0 : networkCap?.getEntries(),
3044
+ consoleLogs,
3045
+ networkLogs,
2197
3046
  performanceMetrics: activeConfig.capture?.performance === false ? void 0 : perfCap?.getMetrics(),
3047
+ timeline: timelineCap.getEntries({ consoleLogs, networkLogs }),
2198
3048
  screenshotDataUrl: pendingScreenshot ?? void 0,
2199
3049
  selectedElement: pendingElement ?? void 0,
2200
3050
  metadata: {
@@ -2206,6 +3056,8 @@ function createInstance(config) {
2206
3056
  reporterToken: getReporterToken(),
2207
3057
  ...fingerprintHash ? { fingerprintHash } : {},
2208
3058
  appVersion: config.integrations?.vercel?.analyticsId,
3059
+ sdkPackage: MUSHI_SDK_PACKAGE,
3060
+ sdkVersion: MUSHI_SDK_VERSION,
2209
3061
  proactiveTrigger: pendingProactiveTrigger ?? void 0,
2210
3062
  sentryEventId: sentryCtx?.eventId,
2211
3063
  sentryReplayId: sentryCtx?.replayId,
@@ -2260,6 +3112,9 @@ function createInstance(config) {
2260
3112
  setMetadata(key, value) {
2261
3113
  customMetadata[key] = value;
2262
3114
  },
3115
+ setScreen(screen) {
3116
+ timelineCap.setScreen(screen);
3117
+ },
2263
3118
  isOpen() {
2264
3119
  return widget.getIsOpen();
2265
3120
  },
@@ -2287,6 +3142,15 @@ function createInstance(config) {
2287
3142
  updateConfig(runtimeConfig) {
2288
3143
  applyRuntimeConfig(runtimeConfig);
2289
3144
  },
3145
+ diagnose() {
3146
+ return runDiagnostics({
3147
+ apiEndpoint: resolveApiEndpoint(activeConfig),
3148
+ widgetMounted: widget.getIsMounted(),
3149
+ runtimeConfigLoaded,
3150
+ captureScreenshotAvailable: screenshotCap !== null,
3151
+ captureNetworkIntercepting: networkCap !== null
3152
+ });
3153
+ },
2290
3154
  destroy() {
2291
3155
  proactiveTriggers?.destroy();
2292
3156
  proactiveManager?.reset();
@@ -2295,6 +3159,9 @@ function createInstance(config) {
2295
3159
  networkCap?.destroy();
2296
3160
  perfCap?.destroy();
2297
3161
  elementSelector?.deactivate();
3162
+ timelineCap.destroy();
3163
+ discoveryCap?.destroy();
3164
+ discoveryCap = null;
2298
3165
  offlineQueue.stopAutoSync();
2299
3166
  listeners.clear();
2300
3167
  instance = null;
@@ -2316,6 +3183,7 @@ function createInstance(config) {
2316
3183
  category,
2317
3184
  description,
2318
3185
  environment: captureEnvironment(),
3186
+ timeline: timelineCap.getEntries(),
2319
3187
  metadata: {
2320
3188
  ...input.metadata ?? {},
2321
3189
  ...userInfo ? { user: userInfo } : {},
@@ -2327,6 +3195,8 @@ function createInstance(config) {
2327
3195
  },
2328
3196
  sessionId: getSessionId(),
2329
3197
  reporterToken: getReporterToken(),
3198
+ sdkPackage: MUSHI_SDK_PACKAGE,
3199
+ sdkVersion: MUSHI_SDK_VERSION,
2330
3200
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
2331
3201
  };
2332
3202
  emit("report:submitted", { reportId: report.id });
@@ -2368,12 +3238,128 @@ function mergeRuntimeConfig(config, runtime) {
2368
3238
  capture: {
2369
3239
  ...config.capture,
2370
3240
  ...runtime.capture
3241
+ },
3242
+ privacy: {
3243
+ ...config.privacy
2371
3244
  }
2372
3245
  };
2373
3246
  }
3247
+ function applyPresetConfig(config) {
3248
+ if (!config.preset) return config;
3249
+ const preset = presetDefaults(config.preset);
3250
+ return {
3251
+ ...config,
3252
+ widget: {
3253
+ ...preset.widget,
3254
+ ...config.widget
3255
+ },
3256
+ capture: {
3257
+ ...preset.capture,
3258
+ ...config.capture
3259
+ },
3260
+ proactive: {
3261
+ ...preset.proactive,
3262
+ ...config.proactive,
3263
+ cooldown: {
3264
+ ...preset.proactive?.cooldown,
3265
+ ...config.proactive?.cooldown
3266
+ }
3267
+ }
3268
+ };
3269
+ }
3270
+ function presetDefaults(preset) {
3271
+ switch (preset) {
3272
+ case "manual-only":
3273
+ return {
3274
+ widget: { trigger: "manual", outdatedBanner: "console-only" },
3275
+ capture: { console: true, network: true, performance: false, screenshot: "on-report", elementSelector: false },
3276
+ proactive: { rageClick: false, longTask: false, apiCascade: false, errorBoundary: false }
3277
+ };
3278
+ case "beta-loud":
3279
+ return {
3280
+ widget: { trigger: "auto", outdatedBanner: "banner" },
3281
+ capture: { console: true, network: true, performance: true, screenshot: "auto", elementSelector: true },
3282
+ proactive: { rageClick: true, longTask: true, apiCascade: true, errorBoundary: true }
3283
+ };
3284
+ case "internal-debug":
3285
+ return {
3286
+ widget: { trigger: "auto", outdatedBanner: "banner", brandFooter: true },
3287
+ capture: { console: true, network: true, performance: true, screenshot: "auto", elementSelector: true },
3288
+ proactive: {
3289
+ rageClick: true,
3290
+ longTask: true,
3291
+ apiCascade: true,
3292
+ errorBoundary: true,
3293
+ cooldown: { maxProactivePerSession: 10, dismissCooldownHours: 0, suppressAfterDismissals: 99 }
3294
+ }
3295
+ };
3296
+ case "production-calm":
3297
+ return {
3298
+ widget: { trigger: "auto", outdatedBanner: "console-only" },
3299
+ capture: { console: true, network: true, performance: false, screenshot: "on-report", elementSelector: false },
3300
+ proactive: { rageClick: false, longTask: false, apiCascade: false, errorBoundary: false }
3301
+ };
3302
+ }
3303
+ }
3304
+ function resolveApiEndpoint(config) {
3305
+ return config.apiEndpoint ?? DEFAULT_API_ENDPOINT;
3306
+ }
3307
+ function shouldUseRuntimeConfig(config) {
3308
+ if (config.runtimeConfig === false) return false;
3309
+ if (config.runtimeConfig === true) return true;
3310
+ return !isLocalhostEndpoint(resolveApiEndpoint(config));
3311
+ }
3312
+ async function runDiagnostics(options) {
3313
+ const endpoint = await probeApiEndpoint(options.apiEndpoint);
3314
+ return {
3315
+ apiEndpointReachable: endpoint.reachable,
3316
+ cspAllowsEndpoint: endpoint.cspAllowed,
3317
+ widgetMounted: options.widgetMounted,
3318
+ shadowDomAvailable: typeof HTMLElement !== "undefined" && typeof HTMLElement.prototype.attachShadow === "function",
3319
+ dialogSupported: typeof HTMLDialogElement !== "undefined",
3320
+ runtimeConfigLoaded: options.runtimeConfigLoaded,
3321
+ captureScreenshotAvailable: options.captureScreenshotAvailable,
3322
+ captureNetworkIntercepting: options.captureNetworkIntercepting,
3323
+ sdkVersion: MUSHI_SDK_VERSION
3324
+ };
3325
+ }
3326
+ async function diagnoseWithoutInstance() {
3327
+ return {
3328
+ apiEndpointReachable: false,
3329
+ cspAllowsEndpoint: false,
3330
+ widgetMounted: false,
3331
+ shadowDomAvailable: typeof HTMLElement !== "undefined" && typeof HTMLElement.prototype.attachShadow === "function",
3332
+ dialogSupported: typeof HTMLDialogElement !== "undefined",
3333
+ runtimeConfigLoaded: false,
3334
+ captureScreenshotAvailable: false,
3335
+ captureNetworkIntercepting: false,
3336
+ sdkVersion: MUSHI_SDK_VERSION
3337
+ };
3338
+ }
3339
+ async function probeApiEndpoint(apiEndpoint) {
3340
+ if (typeof fetch === "undefined") return { reachable: false, cspAllowed: false };
3341
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
3342
+ const timer = controller ? setTimeout(() => controller.abort(), 3e3) : null;
3343
+ try {
3344
+ const response = await fetch(`${apiEndpoint.replace(/\/$/, "")}/health`, {
3345
+ method: "GET",
3346
+ cache: "no-store",
3347
+ ...controller ? { signal: controller.signal } : {},
3348
+ [MUSHI_INTERNAL_INIT_MARKER]: "diagnose"
3349
+ });
3350
+ return { reachable: response.ok, cspAllowed: true };
3351
+ } catch {
3352
+ return { reachable: false, cspAllowed: false };
3353
+ } finally {
3354
+ if (timer) clearTimeout(timer);
3355
+ }
3356
+ }
2374
3357
  function runtimeConfigCacheKey(projectId) {
2375
3358
  return `mushi:sdk-config:${projectId}`;
2376
3359
  }
3360
+ function sdkVersionCacheKey(packageName) {
3361
+ return `mushi:sdk-version:${packageName}`;
3362
+ }
2377
3363
  function readCachedRuntimeConfig(projectId) {
2378
3364
  if (typeof localStorage === "undefined") return null;
2379
3365
  try {
@@ -2402,6 +3388,42 @@ function clearCachedRuntimeConfig(projectId) {
2402
3388
  } catch {
2403
3389
  }
2404
3390
  }
3391
+ function readCachedSdkVersion(packageName) {
3392
+ if (typeof localStorage === "undefined") return null;
3393
+ try {
3394
+ const raw = localStorage.getItem(sdkVersionCacheKey(packageName));
3395
+ if (!raw) return null;
3396
+ const parsed = JSON.parse(raw);
3397
+ if (!parsed.data || !parsed.cachedAt || Date.now() - parsed.cachedAt > 864e5) return null;
3398
+ return parsed.data;
3399
+ } catch {
3400
+ return null;
3401
+ }
3402
+ }
3403
+ function cacheSdkVersion(packageName, data) {
3404
+ if (typeof localStorage === "undefined") return;
3405
+ try {
3406
+ localStorage.setItem(sdkVersionCacheKey(packageName), JSON.stringify({
3407
+ cachedAt: Date.now(),
3408
+ data
3409
+ }));
3410
+ } catch {
3411
+ }
3412
+ }
3413
+ function isVersionOlder(current, latest) {
3414
+ const currentParts = parseVersion(current);
3415
+ const latestParts = parseVersion(latest);
3416
+ for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
3417
+ const cur = currentParts[i] ?? 0;
3418
+ const next = latestParts[i] ?? 0;
3419
+ if (cur < next) return true;
3420
+ if (cur > next) return false;
3421
+ }
3422
+ return false;
3423
+ }
3424
+ function parseVersion(version) {
3425
+ return version.split(/[.-]/).map((part) => Number.parseInt(part, 10)).filter((part) => Number.isFinite(part));
3426
+ }
2405
3427
  function createNoopInstance() {
2406
3428
  return {
2407
3429
  report: () => {
@@ -2412,6 +3434,8 @@ function createNoopInstance() {
2412
3434
  },
2413
3435
  setMetadata: () => {
2414
3436
  },
3437
+ setScreen: () => {
3438
+ },
2415
3439
  isOpen: () => false,
2416
3440
  open: () => {
2417
3441
  },
@@ -2419,6 +3443,7 @@ function createNoopInstance() {
2419
3443
  },
2420
3444
  updateConfig: () => {
2421
3445
  },
3446
+ diagnose: diagnoseWithoutInstance,
2422
3447
  openWith: () => {
2423
3448
  },
2424
3449
  show: () => {
@@ -2438,6 +3463,6 @@ function createNoopInstance() {
2438
3463
  };
2439
3464
  }
2440
3465
 
2441
- export { Mushi, MushiWidget, createConsoleCapture, createElementSelector, createNetworkCapture, createPerformanceCapture, createProactiveManager, createScreenshotCapture, getAvailableLocales, getLocale, setupProactiveTriggers };
3466
+ export { Mushi, MushiWidget, createConsoleCapture, createElementSelector, createNetworkCapture, createPerformanceCapture, createProactiveManager, createScreenshotCapture, createTimelineCapture, getAvailableLocales, getLocale, setupProactiveTriggers };
2442
3467
  //# sourceMappingURL=index.js.map
2443
3468
  //# sourceMappingURL=index.js.map