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