@mushi-mushi/web 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
@@ -1746,6 +2158,108 @@ function createElementSelector() {
1746
2158
  return { activate, deactivate, isActive: () => active };
1747
2159
  }
1748
2160
 
2161
+ // src/capture/timeline.ts
2162
+ var MAX_TIMELINE_ENTRIES = 120;
2163
+ function createTimelineCapture() {
2164
+ const entries = [];
2165
+ const originalPushState = history.pushState;
2166
+ const originalReplaceState = history.replaceState;
2167
+ const handlePopState = () => recordRoute("popstate");
2168
+ const handleHashChange = () => recordRoute("hashchange");
2169
+ recordRoute("initial");
2170
+ function record(entry) {
2171
+ entries.push(entry);
2172
+ if (entries.length > MAX_TIMELINE_ENTRIES) entries.shift();
2173
+ }
2174
+ function recordRoute(source) {
2175
+ if (typeof location === "undefined") return;
2176
+ record({
2177
+ ts: Date.now(),
2178
+ kind: "route",
2179
+ payload: {
2180
+ source,
2181
+ route: `${location.pathname}${location.search}${location.hash}`,
2182
+ href: location.href
2183
+ }
2184
+ });
2185
+ }
2186
+ function handleClick(event) {
2187
+ const target = event.target instanceof Element ? event.target : null;
2188
+ if (!target) return;
2189
+ const el = target.closest('button,a,[role="button"],input,textarea,select,[data-mushi-track]') ?? target;
2190
+ record({
2191
+ ts: Date.now(),
2192
+ kind: "click",
2193
+ payload: {
2194
+ tag: el.tagName.toLowerCase(),
2195
+ id: el.id || void 0,
2196
+ text: textSnippet(el)
2197
+ }
2198
+ });
2199
+ }
2200
+ history.pushState = function mushiPushState(...args) {
2201
+ const result = originalPushState.apply(this, args);
2202
+ recordRoute("pushState");
2203
+ return result;
2204
+ };
2205
+ history.replaceState = function mushiReplaceState(...args) {
2206
+ const result = originalReplaceState.apply(this, args);
2207
+ recordRoute("replaceState");
2208
+ return result;
2209
+ };
2210
+ window.addEventListener("popstate", handlePopState);
2211
+ window.addEventListener("hashchange", handleHashChange);
2212
+ document.addEventListener("click", handleClick, true);
2213
+ return {
2214
+ setScreen(screen) {
2215
+ record({
2216
+ ts: Date.now(),
2217
+ kind: "screen",
2218
+ payload: screen
2219
+ });
2220
+ },
2221
+ getEntries(input = {}) {
2222
+ const merged = [
2223
+ ...entries,
2224
+ ...(input.consoleLogs ?? []).map((log) => ({
2225
+ ts: log.timestamp,
2226
+ kind: "log",
2227
+ payload: {
2228
+ level: log.level,
2229
+ message: log.message
2230
+ }
2231
+ })),
2232
+ ...(input.networkLogs ?? []).map((network) => ({
2233
+ ts: network.timestamp,
2234
+ kind: "request",
2235
+ payload: {
2236
+ method: network.method,
2237
+ url: network.url,
2238
+ status: network.status,
2239
+ duration: network.duration,
2240
+ error: network.error
2241
+ }
2242
+ }))
2243
+ ].sort((a, b) => a.ts - b.ts);
2244
+ return merged.slice(-MAX_TIMELINE_ENTRIES);
2245
+ },
2246
+ clear() {
2247
+ entries.length = 0;
2248
+ },
2249
+ destroy() {
2250
+ history.pushState = originalPushState;
2251
+ history.replaceState = originalReplaceState;
2252
+ window.removeEventListener("popstate", handlePopState);
2253
+ window.removeEventListener("hashchange", handleHashChange);
2254
+ document.removeEventListener("click", handleClick, true);
2255
+ }
2256
+ };
2257
+ }
2258
+ function textSnippet(el) {
2259
+ const text = (el.textContent ?? "").replace(/\s+/g, " ").trim();
2260
+ return text ? text.slice(0, 80) : void 0;
2261
+ }
2262
+
1749
2263
  // src/sentry.ts
1750
2264
  function getSentryGlobal() {
1751
2265
  try {
@@ -1832,36 +2346,25 @@ function setupProactiveTriggers(callbacks, config = {}) {
1832
2346
  } catch {
1833
2347
  }
1834
2348
  }
1835
- if (config.apiCascade !== false) {
2349
+ const apiCascade = normalizeApiCascadeConfig(config.apiCascade);
2350
+ if (apiCascade.enabled) {
1836
2351
  const failedRequests = [];
1837
2352
  const origFetch = globalThis.fetch;
1838
2353
  globalThis.fetch = async function(...args) {
2354
+ const [input, init] = args;
2355
+ const url = getRequestUrl(input);
2356
+ const ignoreFailure = Boolean(getInternalRequestKind(input, init)) || shouldIgnoreMushiUrl(url, {
2357
+ apiEndpoint: config.apiEndpoint,
2358
+ ignoreUrls: apiCascade.ignoreUrls
2359
+ });
1839
2360
  try {
1840
2361
  const res = await origFetch.apply(this, args);
1841
- if (!res.ok && res.status >= 400) {
1842
- 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
- }
2362
+ if (!ignoreFailure && !res.ok && res.status >= 400) {
2363
+ recordApiFailure(failedRequests, callbacks);
1852
2364
  }
1853
2365
  return res;
1854
2366
  } 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
- }
2367
+ if (!ignoreFailure) recordApiFailure(failedRequests, callbacks);
1865
2368
  throw err;
1866
2369
  }
1867
2370
  };
@@ -1896,6 +2399,28 @@ function setupProactiveTriggers(callbacks, config = {}) {
1896
2399
  }
1897
2400
  };
1898
2401
  }
2402
+ function normalizeApiCascadeConfig(config) {
2403
+ if (config === false) return { enabled: false, ignoreUrls: [] };
2404
+ if (config && typeof config === "object") {
2405
+ return {
2406
+ enabled: config.enabled !== false,
2407
+ ignoreUrls: config.ignoreUrls ?? []
2408
+ };
2409
+ }
2410
+ return { enabled: true, ignoreUrls: [] };
2411
+ }
2412
+ function recordApiFailure(failedRequests, callbacks) {
2413
+ const now = Date.now();
2414
+ failedRequests.push(now);
2415
+ const recentFailures = failedRequests.filter((t) => now - t < 1e4);
2416
+ if (recentFailures.length >= 3) {
2417
+ callbacks.onTrigger("api_cascade", {
2418
+ failureCount: recentFailures.length,
2419
+ windowMs: 1e4
2420
+ });
2421
+ failedRequests.length = 0;
2422
+ }
2423
+ }
1899
2424
 
1900
2425
  // src/proactive-manager.ts
1901
2426
  var STORAGE_KEY_LAST_DISMISS = "mushi:lastDismiss";
@@ -1948,6 +2473,10 @@ function createProactiveManager(config = {}) {
1948
2473
  return { shouldShow, recordDismissal, recordSubmission, reset };
1949
2474
  }
1950
2475
 
2476
+ // src/version.ts
2477
+ var MUSHI_SDK_PACKAGE = "@mushi-mushi/web";
2478
+ var MUSHI_SDK_VERSION = "0.8.0" ;
2479
+
1951
2480
  // src/mushi.ts
1952
2481
  var instance = null;
1953
2482
  var Mushi = class {
@@ -1977,18 +2506,21 @@ var Mushi = class {
1977
2506
  instance?.destroy();
1978
2507
  instance = null;
1979
2508
  }
2509
+ static diagnose() {
2510
+ return instance?.diagnose() ?? diagnoseWithoutInstance();
2511
+ }
1980
2512
  };
1981
2513
  function createInstance(config) {
1982
- const bootstrapConfig = config;
1983
- let activeConfig = config;
2514
+ const bootstrapConfig = applyPresetConfig(config);
2515
+ let activeConfig = bootstrapConfig;
1984
2516
  const log = config.debug ?? false ? core.createLogger({ scope: "mushi", level: "debug", format: "pretty" }) : core.noopLogger;
1985
2517
  const apiClient = core.createApiClient({
1986
- projectId: config.projectId,
1987
- apiKey: config.apiKey,
1988
- ...config.apiEndpoint ? { apiEndpoint: config.apiEndpoint } : {}
2518
+ projectId: bootstrapConfig.projectId,
2519
+ apiKey: bootstrapConfig.apiKey,
2520
+ ...bootstrapConfig.apiEndpoint ? { apiEndpoint: bootstrapConfig.apiEndpoint } : {}
1989
2521
  });
1990
- const preFilter = core.createPreFilter(config.preFilter);
1991
- const offlineQueue = core.createOfflineQueue(config.offline);
2522
+ const preFilter = core.createPreFilter(bootstrapConfig.preFilter);
2523
+ const offlineQueue = core.createOfflineQueue(bootstrapConfig.offline);
1992
2524
  const rateLimiter = core.createRateLimiter({ maxBurst: 10, refillRate: 1, refillIntervalMs: 5e3 });
1993
2525
  const piiScrubber = core.createPiiScrubber();
1994
2526
  let consoleCap = null;
@@ -1996,6 +2528,8 @@ function createInstance(config) {
1996
2528
  let perfCap = null;
1997
2529
  let screenshotCap = null;
1998
2530
  let elementSelector = null;
2531
+ const timelineCap = createTimelineCapture();
2532
+ let widget;
1999
2533
  function syncCaptureModules() {
2000
2534
  if (activeConfig.capture?.console !== false) {
2001
2535
  consoleCap ??= createConsoleCapture();
@@ -2004,7 +2538,15 @@ function createInstance(config) {
2004
2538
  consoleCap = null;
2005
2539
  }
2006
2540
  if (activeConfig.capture?.network !== false) {
2007
- networkCap ??= createNetworkCapture();
2541
+ const networkOptions = {
2542
+ apiEndpoint: resolveApiEndpoint(activeConfig),
2543
+ ignoreUrls: activeConfig.capture?.ignoreUrls
2544
+ };
2545
+ if (networkCap) {
2546
+ networkCap.updateOptions(networkOptions);
2547
+ } else {
2548
+ networkCap = createNetworkCapture(networkOptions);
2549
+ }
2008
2550
  } else {
2009
2551
  networkCap?.destroy();
2010
2552
  networkCap = null;
@@ -2015,8 +2557,18 @@ function createInstance(config) {
2015
2557
  perfCap?.destroy();
2016
2558
  perfCap = null;
2017
2559
  }
2018
- screenshotCap = activeConfig.capture?.screenshot !== "off" ? screenshotCap ?? createScreenshotCapture() : null;
2560
+ if (activeConfig.capture?.screenshot !== "off") {
2561
+ const screenshotOptions = { privacy: activeConfig.privacy };
2562
+ if (screenshotCap) {
2563
+ screenshotCap.updateOptions(screenshotOptions);
2564
+ } else {
2565
+ screenshotCap = createScreenshotCapture(screenshotOptions);
2566
+ }
2567
+ } else {
2568
+ screenshotCap = null;
2569
+ }
2019
2570
  if (!screenshotCap) pendingScreenshot = null;
2571
+ widget.setAllowScreenshotRemove(activeConfig.privacy?.allowUserRemoveScreenshot !== false);
2020
2572
  if (activeConfig.capture?.elementSelector !== false) {
2021
2573
  elementSelector ??= createElementSelector();
2022
2574
  } else {
@@ -2032,10 +2584,10 @@ function createInstance(config) {
2032
2584
  let pendingScreenshot = null;
2033
2585
  let pendingElement = null;
2034
2586
  let pendingProactiveTrigger = null;
2587
+ let runtimeConfigLoaded = false;
2035
2588
  let userInfo = null;
2036
2589
  const customMetadata = {};
2037
- syncCaptureModules();
2038
- const widget = new MushiWidget(config.widget, {
2590
+ widget = new MushiWidget(bootstrapConfig.widget, {
2039
2591
  onSubmit: async ({ category, description, intent }) => {
2040
2592
  log.info("Report submitted", { category, intent });
2041
2593
  proactiveManager?.recordSubmission();
@@ -2062,6 +2614,11 @@ function createInstance(config) {
2062
2614
  pendingScreenshot = await screenshotCap.take();
2063
2615
  widget.setScreenshotAttached(pendingScreenshot !== null);
2064
2616
  },
2617
+ onScreenshotRemove: () => {
2618
+ log.debug("Screenshot attachment removed");
2619
+ pendingScreenshot = null;
2620
+ widget.setScreenshotAttached(false);
2621
+ },
2065
2622
  onElementSelectorRequest: async () => {
2066
2623
  if (!elementSelector || activeConfig.capture?.elementSelector === false) return;
2067
2624
  log.debug("Element selector activated");
@@ -2071,8 +2628,23 @@ function createInstance(config) {
2071
2628
  widget.setElementSelected(true);
2072
2629
  log.debug("Element selected", { tagName: el.tagName, xpath: el.xpath });
2073
2630
  }
2631
+ },
2632
+ async onReporterReportsRequest() {
2633
+ const result = await apiClient.listReporterReports(core.getReporterToken());
2634
+ if (!result.ok) throw new Error(result.error?.message ?? "Could not load reports");
2635
+ return result.data?.reports ?? [];
2636
+ },
2637
+ async onReporterCommentsRequest(reportId) {
2638
+ const result = await apiClient.listReporterComments(reportId, core.getReporterToken());
2639
+ if (!result.ok) throw new Error(result.error?.message ?? "Could not load thread");
2640
+ return result.data?.comments ?? [];
2641
+ },
2642
+ async onReporterReply(reportId, body) {
2643
+ const result = await apiClient.replyToReporterReport(reportId, core.getReporterToken(), body);
2644
+ if (!result.ok) throw new Error(result.error?.message ?? "Could not send reply");
2074
2645
  }
2075
- });
2646
+ }, MUSHI_SDK_VERSION);
2647
+ syncCaptureModules();
2076
2648
  if (typeof document !== "undefined") {
2077
2649
  if (document.readyState === "loading") {
2078
2650
  document.addEventListener("DOMContentLoaded", () => widget.mount());
@@ -2082,7 +2654,7 @@ function createInstance(config) {
2082
2654
  }
2083
2655
  let proactiveTriggers = null;
2084
2656
  let proactiveManager = null;
2085
- const proactiveCfg = config.proactive;
2657
+ const proactiveCfg = activeConfig.proactive;
2086
2658
  const hasAnyProactive = proactiveCfg && (proactiveCfg.rageClick !== false || proactiveCfg.longTask !== false || proactiveCfg.apiCascade !== false || proactiveCfg.errorBoundary === true);
2087
2659
  if (hasAnyProactive && typeof document !== "undefined") {
2088
2660
  proactiveManager = createProactiveManager(proactiveCfg?.cooldown);
@@ -2103,6 +2675,7 @@ function createInstance(config) {
2103
2675
  rageClick: proactiveCfg?.rageClick,
2104
2676
  longTask: proactiveCfg?.longTask,
2105
2677
  apiCascade: proactiveCfg?.apiCascade,
2678
+ apiEndpoint: resolveApiEndpoint(activeConfig),
2106
2679
  errorBoundary: proactiveCfg?.errorBoundary
2107
2680
  }
2108
2681
  );
@@ -2118,6 +2691,7 @@ function createInstance(config) {
2118
2691
  if (result.sent > 0) log.info("Synced offline reports", { sent: result.sent });
2119
2692
  });
2120
2693
  function applyRuntimeConfig(runtime) {
2694
+ runtimeConfigLoaded = true;
2121
2695
  if (runtime.enabled === false) {
2122
2696
  activeConfig = bootstrapConfig;
2123
2697
  clearCachedRuntimeConfig(config.projectId);
@@ -2131,7 +2705,7 @@ function createInstance(config) {
2131
2705
  if (runtime.widget) widget.updateConfig(activeConfig.widget);
2132
2706
  log.debug("Applied runtime SDK config", { version: runtime.version });
2133
2707
  }
2134
- if (config.runtimeConfig !== false) {
2708
+ if (shouldUseRuntimeConfig(config)) {
2135
2709
  const cached = readCachedRuntimeConfig(config.projectId);
2136
2710
  if (cached) applyRuntimeConfig(cached);
2137
2711
  apiClient.getSdkConfig().then((result) => {
@@ -2144,8 +2718,41 @@ function createInstance(config) {
2144
2718
  }).catch((err) => {
2145
2719
  log.debug("Runtime SDK config fetch failed", { error: err instanceof Error ? err.message : String(err) });
2146
2720
  });
2721
+ } else if (config.runtimeConfig !== false && isLocalhostEndpoint(resolveApiEndpoint(config))) {
2722
+ log.debug("Runtime SDK config skipped for localhost apiEndpoint; set runtimeConfig: true to force it");
2147
2723
  }
2724
+ void checkSdkFreshness();
2148
2725
  log.info("Initialized", { projectId: config.projectId });
2726
+ async function checkSdkFreshness() {
2727
+ if (activeConfig.widget?.outdatedBanner === "off") return;
2728
+ const cached = readCachedSdkVersion(MUSHI_SDK_PACKAGE);
2729
+ if (cached) applySdkFreshness(cached);
2730
+ const result = await apiClient.getLatestSdkVersion(MUSHI_SDK_PACKAGE);
2731
+ if (!result.ok || !result.data) return;
2732
+ cacheSdkVersion(MUSHI_SDK_PACKAGE, result.data);
2733
+ applySdkFreshness(result.data);
2734
+ }
2735
+ function applySdkFreshness(info) {
2736
+ const latest = info.latest;
2737
+ const outdated = Boolean(latest && isVersionOlder(MUSHI_SDK_VERSION, latest));
2738
+ if (!outdated && !info.deprecated) return;
2739
+ const message = info.deprecationMessage ?? (outdated ? `Update ${MUSHI_SDK_PACKAGE} to ${latest}.` : null);
2740
+ log.warn("Mushi SDK is outdated", {
2741
+ package: MUSHI_SDK_PACKAGE,
2742
+ current: MUSHI_SDK_VERSION,
2743
+ latest,
2744
+ deprecated: info.deprecated,
2745
+ message
2746
+ });
2747
+ if (activeConfig.widget?.outdatedBanner !== "console-only") {
2748
+ widget.setSdkFreshness({
2749
+ latest,
2750
+ current: MUSHI_SDK_VERSION,
2751
+ deprecated: info.deprecated,
2752
+ message
2753
+ });
2754
+ }
2755
+ }
2149
2756
  async function submitReport(category, description, intent) {
2150
2757
  const filterResult = preFilter.check(description);
2151
2758
  if (!filterResult.passed) {
@@ -2187,6 +2794,8 @@ function createInstance(config) {
2187
2794
  const scrubbedDescription = piiScrubber.scrub(preFilter.truncate(description));
2188
2795
  const sentryCtx = config.sentry ? captureSentryContext(config.sentry) : void 0;
2189
2796
  const fingerprintHash = await core.getDeviceFingerprintHash().catch(() => null);
2797
+ const consoleLogs = activeConfig.capture?.console === false ? void 0 : consoleCap?.getEntries();
2798
+ const networkLogs = activeConfig.capture?.network === false ? void 0 : networkCap?.getEntries();
2190
2799
  const report = {
2191
2800
  id: crypto.randomUUID?.() ?? `mushi_${Date.now()}_${Math.random().toString(36).slice(2)}`,
2192
2801
  projectId: config.projectId,
@@ -2194,9 +2803,10 @@ function createInstance(config) {
2194
2803
  description: scrubbedDescription,
2195
2804
  userIntent: intent,
2196
2805
  environment: core.captureEnvironment(),
2197
- consoleLogs: activeConfig.capture?.console === false ? void 0 : consoleCap?.getEntries(),
2198
- networkLogs: activeConfig.capture?.network === false ? void 0 : networkCap?.getEntries(),
2806
+ consoleLogs,
2807
+ networkLogs,
2199
2808
  performanceMetrics: activeConfig.capture?.performance === false ? void 0 : perfCap?.getMetrics(),
2809
+ timeline: timelineCap.getEntries({ consoleLogs, networkLogs }),
2200
2810
  screenshotDataUrl: pendingScreenshot ?? void 0,
2201
2811
  selectedElement: pendingElement ?? void 0,
2202
2812
  metadata: {
@@ -2208,6 +2818,8 @@ function createInstance(config) {
2208
2818
  reporterToken: core.getReporterToken(),
2209
2819
  ...fingerprintHash ? { fingerprintHash } : {},
2210
2820
  appVersion: config.integrations?.vercel?.analyticsId,
2821
+ sdkPackage: MUSHI_SDK_PACKAGE,
2822
+ sdkVersion: MUSHI_SDK_VERSION,
2211
2823
  proactiveTrigger: pendingProactiveTrigger ?? void 0,
2212
2824
  sentryEventId: sentryCtx?.eventId,
2213
2825
  sentryReplayId: sentryCtx?.replayId,
@@ -2262,6 +2874,9 @@ function createInstance(config) {
2262
2874
  setMetadata(key, value) {
2263
2875
  customMetadata[key] = value;
2264
2876
  },
2877
+ setScreen(screen) {
2878
+ timelineCap.setScreen(screen);
2879
+ },
2265
2880
  isOpen() {
2266
2881
  return widget.getIsOpen();
2267
2882
  },
@@ -2289,6 +2904,15 @@ function createInstance(config) {
2289
2904
  updateConfig(runtimeConfig) {
2290
2905
  applyRuntimeConfig(runtimeConfig);
2291
2906
  },
2907
+ diagnose() {
2908
+ return runDiagnostics({
2909
+ apiEndpoint: resolveApiEndpoint(activeConfig),
2910
+ widgetMounted: widget.getIsMounted(),
2911
+ runtimeConfigLoaded,
2912
+ captureScreenshotAvailable: screenshotCap !== null,
2913
+ captureNetworkIntercepting: networkCap !== null
2914
+ });
2915
+ },
2292
2916
  destroy() {
2293
2917
  proactiveTriggers?.destroy();
2294
2918
  proactiveManager?.reset();
@@ -2297,6 +2921,7 @@ function createInstance(config) {
2297
2921
  networkCap?.destroy();
2298
2922
  perfCap?.destroy();
2299
2923
  elementSelector?.deactivate();
2924
+ timelineCap.destroy();
2300
2925
  offlineQueue.stopAutoSync();
2301
2926
  listeners.clear();
2302
2927
  instance = null;
@@ -2318,6 +2943,7 @@ function createInstance(config) {
2318
2943
  category,
2319
2944
  description,
2320
2945
  environment: core.captureEnvironment(),
2946
+ timeline: timelineCap.getEntries(),
2321
2947
  metadata: {
2322
2948
  ...input.metadata ?? {},
2323
2949
  ...userInfo ? { user: userInfo } : {},
@@ -2329,6 +2955,8 @@ function createInstance(config) {
2329
2955
  },
2330
2956
  sessionId: core.getSessionId(),
2331
2957
  reporterToken: core.getReporterToken(),
2958
+ sdkPackage: MUSHI_SDK_PACKAGE,
2959
+ sdkVersion: MUSHI_SDK_VERSION,
2332
2960
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
2333
2961
  };
2334
2962
  emit("report:submitted", { reportId: report.id });
@@ -2370,12 +2998,128 @@ function mergeRuntimeConfig(config, runtime) {
2370
2998
  capture: {
2371
2999
  ...config.capture,
2372
3000
  ...runtime.capture
3001
+ },
3002
+ privacy: {
3003
+ ...config.privacy
3004
+ }
3005
+ };
3006
+ }
3007
+ function applyPresetConfig(config) {
3008
+ if (!config.preset) return config;
3009
+ const preset = presetDefaults(config.preset);
3010
+ return {
3011
+ ...config,
3012
+ widget: {
3013
+ ...preset.widget,
3014
+ ...config.widget
3015
+ },
3016
+ capture: {
3017
+ ...preset.capture,
3018
+ ...config.capture
3019
+ },
3020
+ proactive: {
3021
+ ...preset.proactive,
3022
+ ...config.proactive,
3023
+ cooldown: {
3024
+ ...preset.proactive?.cooldown,
3025
+ ...config.proactive?.cooldown
3026
+ }
2373
3027
  }
2374
3028
  };
2375
3029
  }
3030
+ function presetDefaults(preset) {
3031
+ switch (preset) {
3032
+ case "manual-only":
3033
+ return {
3034
+ widget: { trigger: "manual", outdatedBanner: "console-only" },
3035
+ capture: { console: true, network: true, performance: false, screenshot: "on-report", elementSelector: false },
3036
+ proactive: { rageClick: false, longTask: false, apiCascade: false, errorBoundary: false }
3037
+ };
3038
+ case "beta-loud":
3039
+ return {
3040
+ widget: { trigger: "auto", outdatedBanner: "banner" },
3041
+ capture: { console: true, network: true, performance: true, screenshot: "auto", elementSelector: true },
3042
+ proactive: { rageClick: true, longTask: true, apiCascade: true, errorBoundary: true }
3043
+ };
3044
+ case "internal-debug":
3045
+ return {
3046
+ widget: { trigger: "auto", outdatedBanner: "banner", brandFooter: true },
3047
+ capture: { console: true, network: true, performance: true, screenshot: "auto", elementSelector: true },
3048
+ proactive: {
3049
+ rageClick: true,
3050
+ longTask: true,
3051
+ apiCascade: true,
3052
+ errorBoundary: true,
3053
+ cooldown: { maxProactivePerSession: 10, dismissCooldownHours: 0, suppressAfterDismissals: 99 }
3054
+ }
3055
+ };
3056
+ case "production-calm":
3057
+ return {
3058
+ widget: { trigger: "auto", outdatedBanner: "console-only" },
3059
+ capture: { console: true, network: true, performance: false, screenshot: "on-report", elementSelector: false },
3060
+ proactive: { rageClick: false, longTask: false, apiCascade: false, errorBoundary: false }
3061
+ };
3062
+ }
3063
+ }
3064
+ function resolveApiEndpoint(config) {
3065
+ return config.apiEndpoint ?? core.DEFAULT_API_ENDPOINT;
3066
+ }
3067
+ function shouldUseRuntimeConfig(config) {
3068
+ if (config.runtimeConfig === false) return false;
3069
+ if (config.runtimeConfig === true) return true;
3070
+ return !isLocalhostEndpoint(resolveApiEndpoint(config));
3071
+ }
3072
+ async function runDiagnostics(options) {
3073
+ const endpoint = await probeApiEndpoint(options.apiEndpoint);
3074
+ return {
3075
+ apiEndpointReachable: endpoint.reachable,
3076
+ cspAllowsEndpoint: endpoint.cspAllowed,
3077
+ widgetMounted: options.widgetMounted,
3078
+ shadowDomAvailable: typeof HTMLElement !== "undefined" && typeof HTMLElement.prototype.attachShadow === "function",
3079
+ dialogSupported: typeof HTMLDialogElement !== "undefined",
3080
+ runtimeConfigLoaded: options.runtimeConfigLoaded,
3081
+ captureScreenshotAvailable: options.captureScreenshotAvailable,
3082
+ captureNetworkIntercepting: options.captureNetworkIntercepting,
3083
+ sdkVersion: MUSHI_SDK_VERSION
3084
+ };
3085
+ }
3086
+ async function diagnoseWithoutInstance() {
3087
+ return {
3088
+ apiEndpointReachable: false,
3089
+ cspAllowsEndpoint: false,
3090
+ widgetMounted: false,
3091
+ shadowDomAvailable: typeof HTMLElement !== "undefined" && typeof HTMLElement.prototype.attachShadow === "function",
3092
+ dialogSupported: typeof HTMLDialogElement !== "undefined",
3093
+ runtimeConfigLoaded: false,
3094
+ captureScreenshotAvailable: false,
3095
+ captureNetworkIntercepting: false,
3096
+ sdkVersion: MUSHI_SDK_VERSION
3097
+ };
3098
+ }
3099
+ async function probeApiEndpoint(apiEndpoint) {
3100
+ if (typeof fetch === "undefined") return { reachable: false, cspAllowed: false };
3101
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
3102
+ const timer = controller ? setTimeout(() => controller.abort(), 3e3) : null;
3103
+ try {
3104
+ const response = await fetch(`${apiEndpoint.replace(/\/$/, "")}/health`, {
3105
+ method: "GET",
3106
+ cache: "no-store",
3107
+ ...controller ? { signal: controller.signal } : {},
3108
+ [core.MUSHI_INTERNAL_INIT_MARKER]: "diagnose"
3109
+ });
3110
+ return { reachable: response.ok, cspAllowed: true };
3111
+ } catch {
3112
+ return { reachable: false, cspAllowed: false };
3113
+ } finally {
3114
+ if (timer) clearTimeout(timer);
3115
+ }
3116
+ }
2376
3117
  function runtimeConfigCacheKey(projectId) {
2377
3118
  return `mushi:sdk-config:${projectId}`;
2378
3119
  }
3120
+ function sdkVersionCacheKey(packageName) {
3121
+ return `mushi:sdk-version:${packageName}`;
3122
+ }
2379
3123
  function readCachedRuntimeConfig(projectId) {
2380
3124
  if (typeof localStorage === "undefined") return null;
2381
3125
  try {
@@ -2404,6 +3148,42 @@ function clearCachedRuntimeConfig(projectId) {
2404
3148
  } catch {
2405
3149
  }
2406
3150
  }
3151
+ function readCachedSdkVersion(packageName) {
3152
+ if (typeof localStorage === "undefined") return null;
3153
+ try {
3154
+ const raw = localStorage.getItem(sdkVersionCacheKey(packageName));
3155
+ if (!raw) return null;
3156
+ const parsed = JSON.parse(raw);
3157
+ if (!parsed.data || !parsed.cachedAt || Date.now() - parsed.cachedAt > 864e5) return null;
3158
+ return parsed.data;
3159
+ } catch {
3160
+ return null;
3161
+ }
3162
+ }
3163
+ function cacheSdkVersion(packageName, data) {
3164
+ if (typeof localStorage === "undefined") return;
3165
+ try {
3166
+ localStorage.setItem(sdkVersionCacheKey(packageName), JSON.stringify({
3167
+ cachedAt: Date.now(),
3168
+ data
3169
+ }));
3170
+ } catch {
3171
+ }
3172
+ }
3173
+ function isVersionOlder(current, latest) {
3174
+ const currentParts = parseVersion(current);
3175
+ const latestParts = parseVersion(latest);
3176
+ for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
3177
+ const cur = currentParts[i] ?? 0;
3178
+ const next = latestParts[i] ?? 0;
3179
+ if (cur < next) return true;
3180
+ if (cur > next) return false;
3181
+ }
3182
+ return false;
3183
+ }
3184
+ function parseVersion(version) {
3185
+ return version.split(/[.-]/).map((part) => Number.parseInt(part, 10)).filter((part) => Number.isFinite(part));
3186
+ }
2407
3187
  function createNoopInstance() {
2408
3188
  return {
2409
3189
  report: () => {
@@ -2414,6 +3194,8 @@ function createNoopInstance() {
2414
3194
  },
2415
3195
  setMetadata: () => {
2416
3196
  },
3197
+ setScreen: () => {
3198
+ },
2417
3199
  isOpen: () => false,
2418
3200
  open: () => {
2419
3201
  },
@@ -2421,6 +3203,7 @@ function createNoopInstance() {
2421
3203
  },
2422
3204
  updateConfig: () => {
2423
3205
  },
3206
+ diagnose: diagnoseWithoutInstance,
2424
3207
  openWith: () => {
2425
3208
  },
2426
3209
  show: () => {
@@ -2448,6 +3231,7 @@ exports.createNetworkCapture = createNetworkCapture;
2448
3231
  exports.createPerformanceCapture = createPerformanceCapture;
2449
3232
  exports.createProactiveManager = createProactiveManager;
2450
3233
  exports.createScreenshotCapture = createScreenshotCapture;
3234
+ exports.createTimelineCapture = createTimelineCapture;
2451
3235
  exports.getAvailableLocales = getAvailableLocales;
2452
3236
  exports.getLocale = getLocale;
2453
3237
  exports.setupProactiveTriggers = setupProactiveTriggers;