@mushi-mushi/web 0.9.0 → 1.1.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, getReporterToken, getDeviceFingerprintHash, getSessionId, captureEnvironment, DEFAULT_API_ENDPOINT, MUSHI_INTERNAL_INIT_MARKER, MUSHI_INTERNAL_HEADER } from '@mushi-mushi/core';
1
+ import { createLogger, noopLogger, createApiClient, createPreFilter, createOfflineQueue, createRateLimiter, createPiiScrubber, createBreadcrumbBuffer, getReporterToken, getDeviceFingerprintHash, getSessionId, captureEnvironment, DEFAULT_API_ENDPOINT, MUSHI_INTERNAL_INIT_MARKER, MUSHI_INTERNAL_HEADER, normaliseThrown } from '@mushi-mushi/core';
2
2
 
3
3
  // src/mushi.ts
4
4
 
@@ -933,6 +933,236 @@ function getWidgetStyles(theme) {
933
933
  letter-spacing: 0.02em;
934
934
  }
935
935
 
936
+ /* \u2500\u2500 Rewards nudge (category step) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
937
+ .mushi-rewards-nudge {
938
+ border-top: 1px solid ${rule};
939
+ padding: 10px 0 4px;
940
+ margin-top: 6px;
941
+ }
942
+ .mushi-rewards-row {
943
+ display: flex;
944
+ align-items: center;
945
+ gap: 6px;
946
+ margin-bottom: 8px;
947
+ }
948
+ .mushi-tier-pip {
949
+ width: 7px;
950
+ height: 7px;
951
+ border-radius: 50%;
952
+ flex-shrink: 0;
953
+ }
954
+ .mushi-rewards-tier-name {
955
+ font-family: ${fontMono};
956
+ font-size: 11px;
957
+ letter-spacing: 0.08em;
958
+ text-transform: uppercase;
959
+ color: ${ink};
960
+ }
961
+ .mushi-rewards-pts-count {
962
+ font-family: ${fontMono};
963
+ font-size: 11px;
964
+ color: ${inkMuted};
965
+ margin-right: auto;
966
+ }
967
+ .mushi-rewards-pts-earn {
968
+ font-family: ${fontMono};
969
+ font-size: 10px;
970
+ color: ${vermillion};
971
+ letter-spacing: 0.04em;
972
+ white-space: nowrap;
973
+ }
974
+ .mushi-tier-bar-track {
975
+ height: 3px;
976
+ background: ${ruleStrong};
977
+ border-radius: 2px;
978
+ overflow: hidden;
979
+ margin-bottom: 5px;
980
+ }
981
+ .mushi-tier-bar-fill {
982
+ height: 100%;
983
+ background: ${vermillion};
984
+ border-radius: 2px;
985
+ transition: width 600ms ${easeStamp};
986
+ }
987
+ .mushi-rewards-next-label {
988
+ font-family: ${fontMono};
989
+ font-size: 10px;
990
+ color: ${inkMuted};
991
+ text-align: right;
992
+ letter-spacing: 0.02em;
993
+ }
994
+
995
+ /* \u2500\u2500 Rewards on success step \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
996
+ .mushi-success-rewards {
997
+ margin-top: 14px;
998
+ padding-top: 12px;
999
+ border-top: 1px solid ${rule};
1000
+ width: 100%;
1001
+ }
1002
+ .mushi-success-pts-award {
1003
+ font-family: ${fontMono};
1004
+ font-size: 22px;
1005
+ font-weight: 700;
1006
+ color: ${vermillion};
1007
+ text-align: center;
1008
+ letter-spacing: 0.06em;
1009
+ margin-bottom: 10px;
1010
+ opacity: 0;
1011
+ animation: mushi-pts-pop 420ms ${easeStamp} 900ms forwards;
1012
+ }
1013
+ .success-bar { margin: 0 0 5px; }
1014
+
1015
+ @keyframes mushi-pts-pop {
1016
+ from { opacity: 0; transform: scale(0.75) translateY(6px); }
1017
+ to { opacity: 1; transform: scale(1) translateY(0); }
1018
+ }
1019
+
1020
+ /* \u2500\u2500\u2500 Beta mode strip (category step) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1021
+
1022
+ .mushi-beta-strip {
1023
+ margin: 0 16px 2px;
1024
+ padding: 9px 12px;
1025
+ background: rgba(99, 102, 241, 0.07);
1026
+ border: 1px solid rgba(99, 102, 241, 0.18);
1027
+ border-radius: 8px;
1028
+ display: flex;
1029
+ flex-direction: column;
1030
+ gap: 4px;
1031
+ }
1032
+
1033
+ .mushi-beta-strip-row {
1034
+ display: flex;
1035
+ align-items: center;
1036
+ gap: 8px;
1037
+ }
1038
+
1039
+ .mushi-beta-tag {
1040
+ display: inline-flex;
1041
+ align-items: center;
1042
+ padding: 1px 6px;
1043
+ border-radius: 4px;
1044
+ background: rgba(245, 158, 11, 0.15);
1045
+ border: 1px solid rgba(245, 158, 11, 0.35);
1046
+ color: #b45309;
1047
+ font-family: var(--mushi-font-mono);
1048
+ font-size: 9px;
1049
+ font-weight: 700;
1050
+ letter-spacing: 0.08em;
1051
+ line-height: 1.6;
1052
+ white-space: nowrap;
1053
+ flex-shrink: 0;
1054
+ }
1055
+
1056
+ @media (prefers-color-scheme: dark) {
1057
+ .mushi-beta-tag {
1058
+ background: rgba(245, 158, 11, 0.12);
1059
+ border-color: rgba(245, 158, 11, 0.28);
1060
+ color: #fbbf24;
1061
+ }
1062
+ }
1063
+
1064
+ .mushi-beta-msg {
1065
+ font-size: 11px;
1066
+ color: var(--mushi-text-dim);
1067
+ line-height: 1.45;
1068
+ }
1069
+
1070
+ .mushi-beta-contact-hint {
1071
+ font-size: 10px;
1072
+ color: var(--mushi-text-dim);
1073
+ opacity: 0.72;
1074
+ font-family: var(--mushi-font-mono);
1075
+ }
1076
+
1077
+ .mushi-beta-perks {
1078
+ list-style: none;
1079
+ margin: 2px 0 0;
1080
+ padding: 0;
1081
+ display: flex;
1082
+ flex-direction: column;
1083
+ gap: 2px;
1084
+ }
1085
+
1086
+ .mushi-beta-perks li {
1087
+ font-size: 10.5px;
1088
+ color: #4f46e5;
1089
+ font-weight: 500;
1090
+ }
1091
+
1092
+ @media (prefers-color-scheme: dark) {
1093
+ .mushi-beta-perks li {
1094
+ color: #818cf8;
1095
+ }
1096
+ }
1097
+
1098
+ /* \u2500\u2500\u2500 Beta changelog (collapsible What's new) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1099
+
1100
+ .mushi-changelog {
1101
+ margin-top: 5px;
1102
+ }
1103
+
1104
+ .mushi-changelog-summary {
1105
+ font-size: 10.5px;
1106
+ color: var(--mushi-text-dim);
1107
+ cursor: pointer;
1108
+ list-style: none;
1109
+ display: flex;
1110
+ align-items: center;
1111
+ gap: 4px;
1112
+ user-select: none;
1113
+ }
1114
+
1115
+ .mushi-changelog-summary::before {
1116
+ content: '\u25B6';
1117
+ font-size: 7px;
1118
+ opacity: 0.6;
1119
+ transition: transform 0.15s ease;
1120
+ }
1121
+
1122
+ .mushi-changelog[open] .mushi-changelog-summary::before {
1123
+ transform: rotate(90deg);
1124
+ }
1125
+
1126
+ .mushi-changelog-list {
1127
+ margin: 5px 0 0 4px;
1128
+ padding: 0;
1129
+ list-style: none;
1130
+ display: flex;
1131
+ flex-direction: column;
1132
+ gap: 2px;
1133
+ }
1134
+
1135
+ .mushi-changelog-list li {
1136
+ font-size: 10.5px;
1137
+ color: var(--mushi-text-dim);
1138
+ line-height: 1.5;
1139
+ }
1140
+
1141
+ /* \u2500\u2500\u2500 Beta success footer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1142
+
1143
+ .mushi-beta-success-footer {
1144
+ margin-top: 14px;
1145
+ padding: 10px 14px;
1146
+ background: rgba(99, 102, 241, 0.06);
1147
+ border: 1px solid rgba(99, 102, 241, 0.14);
1148
+ border-radius: 8px;
1149
+ display: flex;
1150
+ flex-direction: column;
1151
+ gap: 3px;
1152
+ text-align: left;
1153
+ }
1154
+
1155
+ .mushi-beta-success-line {
1156
+ font-size: 11px;
1157
+ color: var(--mushi-text-dim);
1158
+ line-height: 1.5;
1159
+ }
1160
+
1161
+ .mushi-beta-success-dim {
1162
+ opacity: 0.65;
1163
+ font-size: 10.5px;
1164
+ }
1165
+
936
1166
  @media (prefers-reduced-motion: reduce) {
937
1167
  *,
938
1168
  *::before,
@@ -943,6 +1173,7 @@ function getWidgetStyles(theme) {
943
1173
  }
944
1174
  .mushi-success-stamp circle { stroke-dashoffset: 0; }
945
1175
  .mushi-success-stamp-label { opacity: 1; }
1176
+ .mushi-success-pts-award { opacity: 1; }
946
1177
  }
947
1178
  `;
948
1179
  }
@@ -998,7 +1229,8 @@ var MushiWidget = class {
998
1229
  smartHide: config.smartHide ?? false,
999
1230
  draggable: config.draggable ?? false,
1000
1231
  brandFooter: config.brandFooter ?? true,
1001
- outdatedBanner: config.outdatedBanner ?? "auto"
1232
+ outdatedBanner: config.outdatedBanner ?? "auto",
1233
+ betaMode: config.betaMode ?? {}
1002
1234
  };
1003
1235
  this.callbacks = callbacks;
1004
1236
  this.locale = getLocale(this.config.locale === "auto" ? void 0 : this.config.locale);
@@ -1042,6 +1274,7 @@ var MushiWidget = class {
1042
1274
  * root) for up to ~3.3s after destroy. */
1043
1275
  successTimer = null;
1044
1276
  autoCloseTimer = null;
1277
+ rewardsState = null;
1045
1278
  mount() {
1046
1279
  if (this.host.isConnected) return;
1047
1280
  document.body.appendChild(this.host);
@@ -1073,7 +1306,8 @@ var MushiWidget = class {
1073
1306
  ...config.smartHide !== void 0 ? { smartHide: config.smartHide } : {},
1074
1307
  ...config.draggable !== void 0 ? { draggable: config.draggable } : {},
1075
1308
  ...config.brandFooter !== void 0 ? { brandFooter: config.brandFooter } : {},
1076
- ...config.outdatedBanner !== void 0 ? { outdatedBanner: config.outdatedBanner } : {}
1309
+ ...config.outdatedBanner !== void 0 ? { outdatedBanner: config.outdatedBanner } : {},
1310
+ ...config.betaMode !== void 0 ? { betaMode: config.betaMode } : {}
1077
1311
  };
1078
1312
  this.locale = getLocale(this.config.locale === "auto" ? void 0 : this.config.locale);
1079
1313
  this.syncAttachedLaunchers();
@@ -1148,6 +1382,10 @@ var MushiWidget = class {
1148
1382
  this.sdkFreshness = info;
1149
1383
  if (this.isOpen) this.render();
1150
1384
  }
1385
+ setRewardsState(state) {
1386
+ this.rewardsState = state;
1387
+ if (this.isOpen) this.render();
1388
+ }
1151
1389
  destroy() {
1152
1390
  if (this.successTimer !== null) {
1153
1391
  clearTimeout(this.successTimer);
@@ -1392,6 +1630,7 @@ var MushiWidget = class {
1392
1630
  `).join("");
1393
1631
  return `
1394
1632
  ${this.renderHeader({ title: t.step1.heading, step: STEP_NUMBER.category })}
1633
+ ${this.config.betaMode?.enabled ? this.renderBetaStrip() : ""}
1395
1634
  <div class="mushi-body" role="radiogroup" aria-label="${t.step1.heading}">
1396
1635
  <button type="button" class="mushi-option-btn mushi-reports-entry" data-action="reports">
1397
1636
  <span class="mushi-option-icon" aria-hidden="true">\u{1F4EC}</span>
@@ -1402,10 +1641,52 @@ var MushiWidget = class {
1402
1641
  <span class="mushi-option-arrow" aria-hidden="true">\u2192</span>
1403
1642
  </button>
1404
1643
  ${categories}
1644
+ ${this.rewardsState ? this.renderRewardsNudge() : ""}
1405
1645
  </div>
1406
1646
  ${this.renderStepIndicator(STEP_NUMBER.category)}
1407
1647
  `;
1408
1648
  }
1649
+ /** Collapsible "What's new" changelog row. Closes the reporter feedback loop. */
1650
+ renderBetaChangelog() {
1651
+ const entries = this.config.betaMode?.changelogItems;
1652
+ if (!entries?.length) return "";
1653
+ const latest = entries[0];
1654
+ const items = latest.items.map((item) => `<li>\u2022 ${escapeHtml(item)}</li>`).join("");
1655
+ const label = latest.date ? `What\u2019s new in ${escapeHtml(latest.version)} \xB7 ${escapeHtml(latest.date)}` : `What\u2019s new in ${escapeHtml(latest.version)}`;
1656
+ return `
1657
+ <details class="mushi-changelog">
1658
+ <summary class="mushi-changelog-summary">${label}</summary>
1659
+ <ul class="mushi-changelog-list">${items}</ul>
1660
+ </details>
1661
+ `;
1662
+ }
1663
+ /**
1664
+ * Discreet beta status strip: communicates "work in progress", invites
1665
+ * feedback, and sets expectations — reducing user frustration while
1666
+ * nudging the reciprocity instinct ("your reports help us build this").
1667
+ */
1668
+ renderBetaStrip() {
1669
+ const beta = this.config.betaMode;
1670
+ const appName = escapeHtml(beta.appName ?? "This app");
1671
+ const message = beta.message ? escapeHtml(beta.message) : `${appName} is in early development \u2014 updates ship weekly`;
1672
+ const email = beta.contactEmail ? escapeHtml(beta.contactEmail) : null;
1673
+ const perks = beta.perks ?? [];
1674
+ return `
1675
+ <div class="mushi-beta-strip" role="note" aria-label="Beta status">
1676
+ <div class="mushi-beta-strip-row">
1677
+ <span class="mushi-beta-tag" aria-hidden="true">BETA</span>
1678
+ <span class="mushi-beta-msg">${message}</span>
1679
+ </div>
1680
+ ${email ? `<div class="mushi-beta-contact-hint">Reports go to ${email} \xB7 reviewed by the team</div>` : ""}
1681
+ ${perks.length > 0 ? `
1682
+ <ul class="mushi-beta-perks" aria-label="Beta tester perks">
1683
+ ${perks.map((p) => `<li>\u2713 ${escapeHtml(p)}</li>`).join("")}
1684
+ </ul>
1685
+ ` : ""}
1686
+ ${this.renderBetaChangelog()}
1687
+ </div>
1688
+ `;
1689
+ }
1409
1690
  renderReportsStep() {
1410
1691
  const reports = this.reporterReports.map((report) => `
1411
1692
  <button type="button" class="mushi-report-row" data-report-id="${escapeHtml(report.id)}">
@@ -1526,7 +1807,92 @@ var MushiWidget = class {
1526
1807
  </div>
1527
1808
  <div class="mushi-success-headline">${t.widget.submitted}</div>
1528
1809
  <div class="mushi-success-meta">REPORT \xB7 ${time}</div>
1810
+ ${this.rewardsState ? this.renderSuccessRewards() : ""}
1811
+ ${this.config.betaMode?.enabled ? this.renderBetaSuccessFooter() : ""}
1812
+ </div>
1813
+ </div>
1814
+ `;
1815
+ }
1816
+ /**
1817
+ * Reciprocity footer on the success step: closes the feedback loop by
1818
+ * attributing where the report goes, sets a response expectation, and
1819
+ * reinforces the "beta tester" identity (Peak-End Rule — the last thing
1820
+ * the user sees shapes their entire impression of the interaction).
1821
+ */
1822
+ renderBetaSuccessFooter() {
1823
+ const beta = this.config.betaMode;
1824
+ const email = beta.contactEmail ? escapeHtml(beta.contactEmail) : null;
1825
+ const appName = escapeHtml(beta.appName ?? "the team");
1826
+ return `
1827
+ <div class="mushi-beta-success-footer" role="note" aria-label="Beta feedback acknowledgement">
1828
+ ${email ? `<div class="mushi-beta-success-line">\u{1F4EC} Sent to ${email}</div>` : `<div class="mushi-beta-success-line">\u{1F4EC} Sent to ${appName}</div>`}
1829
+ <div class="mushi-beta-success-line mushi-beta-success-dim">We aim to review within 48h \xB7 thank you for helping build this</div>
1830
+ </div>
1831
+ `;
1832
+ }
1833
+ tierColor(slug) {
1834
+ const colors = {
1835
+ free: "#6b7280",
1836
+ explorer: "#3b82f6",
1837
+ contributor: "#8b5cf6",
1838
+ champion: "#f59e0b"
1839
+ };
1840
+ return colors[slug] ?? "#6c47ff";
1841
+ }
1842
+ /** Compact rewards nudge rendered at the bottom of the category-step body. */
1843
+ renderRewardsNudge() {
1844
+ const { tier, nextTier, totalPoints, pointsForReport } = this.rewardsState;
1845
+ const tierName = tier?.displayName ?? "Free";
1846
+ const tierSlug = tier?.slug ?? "free";
1847
+ const color = this.tierColor(tierSlug);
1848
+ let pct = 100;
1849
+ let nextLabel = "";
1850
+ if (nextTier) {
1851
+ const base = tier?.pointsThreshold ?? 0;
1852
+ const ceiling = nextTier.pointsThreshold;
1853
+ pct = ceiling > base ? Math.round(Math.min(1, (totalPoints - base) / (ceiling - base)) * 100) : 100;
1854
+ const remaining = Math.max(0, ceiling - totalPoints);
1855
+ nextLabel = `${remaining.toLocaleString()} pts to ${escapeHtml(nextTier.displayName)}`;
1856
+ }
1857
+ return `
1858
+ <div class="mushi-rewards-nudge" aria-label="Rewards progress">
1859
+ <div class="mushi-rewards-row">
1860
+ <span class="mushi-tier-pip" style="background:${color}" aria-hidden="true"></span>
1861
+ <span class="mushi-rewards-tier-name">${escapeHtml(tierName)}</span>
1862
+ <span class="mushi-rewards-pts-count">${totalPoints.toLocaleString()} pts</span>
1863
+ <span class="mushi-rewards-pts-earn">+${pointsForReport} pts for a report</span>
1529
1864
  </div>
1865
+ ${nextTier ? `
1866
+ <div class="mushi-tier-bar-track" role="progressbar" aria-valuenow="${pct}" aria-valuemin="0" aria-valuemax="100" aria-label="Progress to ${escapeHtml(nextTier.displayName)}">
1867
+ <div class="mushi-tier-bar-fill" style="width:${pct}%"></div>
1868
+ </div>
1869
+ <div class="mushi-rewards-next-label">${nextLabel}</div>
1870
+ ` : ""}
1871
+ </div>
1872
+ `;
1873
+ }
1874
+ /** Points earned + tier progress shown on the success step. */
1875
+ renderSuccessRewards() {
1876
+ const { tier, nextTier, totalPoints, pointsForReport } = this.rewardsState;
1877
+ const projected = totalPoints + pointsForReport;
1878
+ let pctAfter = 100;
1879
+ let nextLabel = "";
1880
+ if (nextTier) {
1881
+ const base = tier?.pointsThreshold ?? 0;
1882
+ const ceiling = nextTier.pointsThreshold;
1883
+ pctAfter = ceiling > base ? Math.round(Math.min(1, (projected - base) / (ceiling - base)) * 100) : 100;
1884
+ const remaining = Math.max(0, ceiling - projected);
1885
+ nextLabel = remaining > 0 ? `${remaining.toLocaleString()} pts to ${escapeHtml(nextTier.displayName)}` : `\u{1F389} ${escapeHtml(nextTier.displayName)} reached!`;
1886
+ }
1887
+ return `
1888
+ <div class="mushi-success-rewards">
1889
+ <div class="mushi-success-pts-award">+${pointsForReport} pts</div>
1890
+ ${nextTier ? `
1891
+ <div class="mushi-tier-bar-track success-bar" role="progressbar" aria-valuenow="${pctAfter}" aria-valuemin="0" aria-valuemax="100" aria-label="Progress to ${escapeHtml(nextTier.displayName)}">
1892
+ <div class="mushi-tier-bar-fill" style="width:${pctAfter}%"></div>
1893
+ </div>
1894
+ <div class="mushi-rewards-next-label">${nextLabel}</div>
1895
+ ` : ""}
1530
1896
  </div>
1531
1897
  `;
1532
1898
  }
@@ -1690,6 +2056,304 @@ var MushiWidget = class {
1690
2056
  }
1691
2057
  };
1692
2058
 
2059
+ // src/rewards.ts
2060
+ var MIN_FLUSH_INTERVAL = 3e4;
2061
+ var DEFAULT_FLUSH_INTERVAL = 3e5;
2062
+ var DWELL_SAMPLE_INTERVAL = 6e4;
2063
+ var MAX_SESSION_MINUTES_PER_DAY = 60;
2064
+ var DAILY_RESET_KEY_PREFIX = "mushi_session_min_day_";
2065
+ var pendingEvents = [];
2066
+ var flushTimer = null;
2067
+ var dwellTimer = null;
2068
+ var currentUserId = null;
2069
+ var currentUserTraits = null;
2070
+ var reporterTokenHash = null;
2071
+ var apiClient = null;
2072
+ var optedIn = false;
2073
+ var tierCache = null;
2074
+ var tierCacheTime = 0;
2075
+ var TIER_CACHE_TTL = 5 * 60 * 1e3;
2076
+ var seenRoutes = /* @__PURE__ */ new Set();
2077
+ function getConsentKey(projectId) {
2078
+ return `mushi_rewards_consent_${projectId}`;
2079
+ }
2080
+ function isConsentGranted(projectId) {
2081
+ try {
2082
+ return localStorage.getItem(getConsentKey(projectId)) === "1";
2083
+ } catch {
2084
+ return false;
2085
+ }
2086
+ }
2087
+ function setConsentGranted(projectId, granted) {
2088
+ try {
2089
+ if (granted) {
2090
+ localStorage.setItem(getConsentKey(projectId), "1");
2091
+ } else {
2092
+ localStorage.removeItem(getConsentKey(projectId));
2093
+ }
2094
+ optedIn = granted;
2095
+ apiClient?.submitActivity(currentUserId ?? "", [], { optedIn: granted }).catch(() => {
2096
+ });
2097
+ } catch {
2098
+ }
2099
+ }
2100
+ function getTodayKey() {
2101
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2102
+ }
2103
+ function getSessionMinutesToday(projectId) {
2104
+ try {
2105
+ const val = sessionStorage.getItem(`${DAILY_RESET_KEY_PREFIX}${projectId}_${getTodayKey()}`);
2106
+ return val ? parseInt(val, 10) : 0;
2107
+ } catch {
2108
+ return 0;
2109
+ }
2110
+ }
2111
+ function incrementSessionMinutes(projectId) {
2112
+ const today = getTodayKey();
2113
+ const key = `${DAILY_RESET_KEY_PREFIX}${projectId}_${today}`;
2114
+ try {
2115
+ const next = getSessionMinutesToday(projectId) + 1;
2116
+ sessionStorage.setItem(key, String(next));
2117
+ return next;
2118
+ } catch {
2119
+ return 99;
2120
+ }
2121
+ }
2122
+ function initRewards(ctx) {
2123
+ apiClient = ctx.client;
2124
+ currentUserId = ctx.userId;
2125
+ currentUserTraits = ctx.traits ?? null;
2126
+ reporterTokenHash = ctx.reporterToken ?? null;
2127
+ const { projectId } = ctx;
2128
+ const flushMs = Math.max(
2129
+ MIN_FLUSH_INTERVAL,
2130
+ ctx.config.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL
2131
+ );
2132
+ if (ctx.config.consentMode === "auto") {
2133
+ optedIn = true;
2134
+ setConsentGranted(projectId, true);
2135
+ } else {
2136
+ optedIn = isConsentGranted(projectId);
2137
+ }
2138
+ if (ctx.config.trackActivity) {
2139
+ installActivityListeners(projectId);
2140
+ }
2141
+ if (flushTimer) clearInterval(flushTimer);
2142
+ flushTimer = setInterval(() => flush(ctx), flushMs);
2143
+ if (dwellTimer) clearInterval(dwellTimer);
2144
+ dwellTimer = setInterval(() => {
2145
+ if (!optedIn || !currentUserId) return;
2146
+ const minutes = getSessionMinutesToday(projectId);
2147
+ if (minutes < MAX_SESSION_MINUTES_PER_DAY) {
2148
+ incrementSessionMinutes(projectId);
2149
+ enqueue({ action: "session_minute", metadata: { minutes_today: minutes + 1 } });
2150
+ }
2151
+ }, DWELL_SAMPLE_INTERVAL);
2152
+ if (ctx.config.showInWidget) {
2153
+ fetchAndCacheTier(currentUserId).then((tier) => {
2154
+ if (tier) renderTierBadge(tier, ctx.config);
2155
+ });
2156
+ }
2157
+ if (ctx.config.consentMode !== "auto" && !optedIn) {
2158
+ renderConsentBanner(projectId, ctx.config);
2159
+ }
2160
+ }
2161
+ function updateRewardsUser(userId, traits) {
2162
+ currentUserId = userId;
2163
+ currentUserTraits = traits ?? null;
2164
+ tierCache = null;
2165
+ tierCacheTime = 0;
2166
+ }
2167
+ function enqueue(event) {
2168
+ if (!optedIn || !currentUserId) return;
2169
+ pendingEvents.push({ ...event, queuedAt: Date.now() });
2170
+ }
2171
+ async function flush(ctx) {
2172
+ if (!optedIn || !currentUserId || pendingEvents.length === 0) return;
2173
+ let hostJwt = null;
2174
+ if (ctx.config.verifyUserToken) {
2175
+ try {
2176
+ hostJwt = await ctx.config.verifyUserToken();
2177
+ } catch {
2178
+ }
2179
+ }
2180
+ const batch = pendingEvents.splice(0, 100);
2181
+ try {
2182
+ await ctx.client.submitActivity(currentUserId, batch, {
2183
+ userTraits: currentUserTraits ?? void 0,
2184
+ reporterTokenHash: reporterTokenHash ?? void 0,
2185
+ optedIn: true,
2186
+ hostJwt: hostJwt ?? void 0
2187
+ });
2188
+ } catch {
2189
+ pendingEvents.unshift(...batch.slice(0, 50));
2190
+ }
2191
+ }
2192
+ async function getTier(userId) {
2193
+ const now = Date.now();
2194
+ if (tierCache && now - tierCacheTime < TIER_CACHE_TTL) return tierCache;
2195
+ return fetchAndCacheTier(userId);
2196
+ }
2197
+ async function fetchAndCacheTier(userId) {
2198
+ if (!apiClient) return null;
2199
+ const res = await apiClient.getMyTier(userId);
2200
+ if (res.ok && res.data) {
2201
+ tierCache = res.data;
2202
+ tierCacheTime = Date.now();
2203
+ return tierCache;
2204
+ }
2205
+ return null;
2206
+ }
2207
+ var routeObserver = null;
2208
+ var clickHandler = null;
2209
+ var origPushState = null;
2210
+ var lastRoute = "";
2211
+ function installActivityListeners(projectId) {
2212
+ const emitRoute = () => {
2213
+ const route = location.pathname;
2214
+ if (route === lastRoute) return;
2215
+ lastRoute = route;
2216
+ const isNewToday = !seenRoutes.has(`${projectId}:${route}`);
2217
+ if (isNewToday) {
2218
+ seenRoutes.add(`${projectId}:${route}`);
2219
+ enqueue({ action: "screen_view_unique_per_day", metadata: { route } });
2220
+ }
2221
+ };
2222
+ origPushState = history.pushState.bind(history);
2223
+ history.pushState = function(...args) {
2224
+ origPushState(...args);
2225
+ emitRoute();
2226
+ };
2227
+ window.addEventListener("popstate", emitRoute);
2228
+ emitRoute();
2229
+ routeObserver = new MutationObserver(() => emitRoute());
2230
+ const main = document.querySelector("main") ?? document.body;
2231
+ routeObserver.observe(main, { childList: true, subtree: false });
2232
+ clickHandler = (e) => {
2233
+ const target = e.target.closest("[data-testid]");
2234
+ if (!target) return;
2235
+ const testid = target.dataset.testid;
2236
+ if (!testid) return;
2237
+ enqueue({ action: "element_selected", metadata: { testid, route: location.pathname } });
2238
+ };
2239
+ document.addEventListener("click", clickHandler, { capture: true, passive: true });
2240
+ }
2241
+ var badgeHost = null;
2242
+ function renderTierBadge(tier, config) {
2243
+ if (!config.showInWidget) return;
2244
+ if (badgeHost) badgeHost.remove();
2245
+ badgeHost = document.createElement("div");
2246
+ badgeHost.id = "mushi-tier-badge";
2247
+ Object.assign(badgeHost.style, {
2248
+ position: "fixed",
2249
+ bottom: "56px",
2250
+ // above the widget button
2251
+ right: "16px",
2252
+ zIndex: "2147483645",
2253
+ fontFamily: "system-ui, sans-serif"
2254
+ });
2255
+ const shadow = badgeHost.attachShadow({ mode: "closed" });
2256
+ shadow.innerHTML = `
2257
+ <style>
2258
+ :host { display: block; }
2259
+ .badge {
2260
+ display: inline-flex;
2261
+ align-items: center;
2262
+ gap: 6px;
2263
+ padding: 4px 10px;
2264
+ border-radius: 999px;
2265
+ background: rgba(0,0,0,0.75);
2266
+ color: #fff;
2267
+ font-size: 11px;
2268
+ font-weight: 600;
2269
+ letter-spacing: 0.02em;
2270
+ backdrop-filter: blur(6px);
2271
+ cursor: default;
2272
+ user-select: none;
2273
+ }
2274
+ .dot {
2275
+ width: 6px;
2276
+ height: 6px;
2277
+ border-radius: 50%;
2278
+ background: #6c47ff;
2279
+ flex-shrink: 0;
2280
+ }
2281
+ </style>
2282
+ <div class="badge">
2283
+ <span class="dot"></span>
2284
+ <span>${tier.displayName}</span>
2285
+ </div>
2286
+ `;
2287
+ document.body.appendChild(badgeHost);
2288
+ }
2289
+ var consentHost = null;
2290
+ function renderConsentBanner(projectId, config) {
2291
+ if (consentHost) return;
2292
+ consentHost = document.createElement("div");
2293
+ consentHost.id = "mushi-consent-banner";
2294
+ Object.assign(consentHost.style, {
2295
+ position: "fixed",
2296
+ bottom: "80px",
2297
+ right: "16px",
2298
+ zIndex: "2147483646",
2299
+ maxWidth: "280px",
2300
+ fontFamily: "system-ui, sans-serif"
2301
+ });
2302
+ const shadow = consentHost.attachShadow({ mode: "closed" });
2303
+ shadow.innerHTML = `
2304
+ <style>
2305
+ :host { display: block; }
2306
+ .banner {
2307
+ background: #fff;
2308
+ border: 1px solid #e5e7eb;
2309
+ border-radius: 12px;
2310
+ box-shadow: 0 8px 24px rgba(0,0,0,0.12);
2311
+ padding: 14px 16px;
2312
+ font-size: 13px;
2313
+ line-height: 1.5;
2314
+ color: #374151;
2315
+ }
2316
+ .title { font-weight: 700; margin-bottom: 6px; color: #111827; }
2317
+ .actions { display: flex; gap: 8px; margin-top: 10px; }
2318
+ button {
2319
+ flex: 1;
2320
+ padding: 6px 10px;
2321
+ border-radius: 6px;
2322
+ border: none;
2323
+ cursor: pointer;
2324
+ font-size: 12px;
2325
+ font-weight: 600;
2326
+ }
2327
+ .accept { background: #6c47ff; color: #fff; }
2328
+ .decline { background: #f3f4f6; color: #374151; }
2329
+ </style>
2330
+ <div class="banner">
2331
+ <div class="title">\u{1F3AF} Earn rewards</div>
2332
+ <div>Help improve this app and earn points, badges, and perks for your contributions.</div>
2333
+ <div class="actions">
2334
+ <button class="accept" id="accept">Enable</button>
2335
+ <button class="decline" id="decline">No thanks</button>
2336
+ </div>
2337
+ </div>
2338
+ `;
2339
+ shadow.getElementById("accept")?.addEventListener("click", () => {
2340
+ setConsentGranted(projectId, true);
2341
+ consentHost?.remove();
2342
+ consentHost = null;
2343
+ if (config.showInWidget && currentUserId) {
2344
+ fetchAndCacheTier(currentUserId).then((tier) => {
2345
+ if (tier) renderTierBadge(tier, config);
2346
+ });
2347
+ }
2348
+ });
2349
+ shadow.getElementById("decline")?.addEventListener("click", () => {
2350
+ setConsentGranted(projectId, false);
2351
+ consentHost?.remove();
2352
+ consentHost = null;
2353
+ });
2354
+ document.body.appendChild(consentHost);
2355
+ }
2356
+
1693
2357
  // src/capture/console.ts
1694
2358
  var MAX_ENTRIES = 50;
1695
2359
  var MAX_MESSAGE_LENGTH = 500;
@@ -2466,42 +3130,211 @@ function createDiscoveryCapture(opts) {
2466
3130
  // src/sentry.ts
2467
3131
  function getSentryGlobal() {
2468
3132
  try {
2469
- const win = globalThis;
2470
- if (win.__SENTRY__) {
2471
- const sentry = win.__SENTRY__;
2472
- const hub = sentry.hub;
2473
- return hub;
3133
+ const w = globalThis;
3134
+ if (w.Sentry) return w.Sentry;
3135
+ return void 0;
3136
+ } catch {
3137
+ return void 0;
3138
+ }
3139
+ }
3140
+ function getSentryReplayGlobal() {
3141
+ try {
3142
+ const w = globalThis;
3143
+ return w.__SENTRY_REPLAY__;
3144
+ } catch {
3145
+ return void 0;
3146
+ }
3147
+ }
3148
+ function detectSentrySdkFamily() {
3149
+ try {
3150
+ const w = globalThis;
3151
+ const meta = w.__SENTRY__;
3152
+ const sentry = w.Sentry;
3153
+ if (meta?.version === "9" || sentry && typeof sentry.lastEventId === "function") {
3154
+ return meta?.version === "9" ? "v9" : "v8";
3155
+ }
3156
+ if (meta?.version === "8") return "v8";
3157
+ if (sentry && typeof sentry.getCurrentHub === "function") return "v7";
3158
+ return "unknown";
3159
+ } catch {
3160
+ return "unknown";
3161
+ }
3162
+ }
3163
+ function captureSentryContext(_config, options = {}) {
3164
+ const limit = Math.max(0, options.breadcrumbsLimit ?? 30);
3165
+ const out = {};
3166
+ const sentry = getSentryGlobal();
3167
+ if (!sentry) return out;
3168
+ out.sdk = detectSentrySdkFamily();
3169
+ try {
3170
+ const v8 = sentry;
3171
+ if (typeof v8.lastEventId === "function") {
3172
+ out.eventId = v8.lastEventId() ?? void 0;
3173
+ } else {
3174
+ const v7 = sentry;
3175
+ const scope2 = v7.getCurrentHub?.()?.getScope?.();
3176
+ out.eventId = scope2?.getLastEventId?.() ?? void 0;
3177
+ }
3178
+ } catch {
3179
+ }
3180
+ let scope;
3181
+ try {
3182
+ const v8 = sentry;
3183
+ if (typeof v8.getCurrentScope === "function") {
3184
+ scope = v8.getCurrentScope();
3185
+ } else {
3186
+ const v7 = sentry;
3187
+ scope = v7.getCurrentHub?.()?.getScope?.();
3188
+ }
3189
+ } catch {
3190
+ }
3191
+ if (scope) {
3192
+ try {
3193
+ const user = scope.getUser?.();
3194
+ if (user) {
3195
+ out.user = {
3196
+ id: typeof user.id === "string" ? user.id : void 0,
3197
+ email: typeof user.email === "string" ? user.email : void 0,
3198
+ username: typeof user.username === "string" ? user.username : void 0,
3199
+ ip_address: typeof user.ip_address === "string" ? user.ip_address : void 0
3200
+ };
3201
+ }
3202
+ } catch {
3203
+ }
3204
+ try {
3205
+ const tags = scope.getTags?.();
3206
+ if (tags && typeof tags === "object") {
3207
+ const pruned = {};
3208
+ for (const [k, v] of Object.entries(tags)) {
3209
+ if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
3210
+ pruned[k] = v;
3211
+ }
3212
+ }
3213
+ if (Object.keys(pruned).length > 0) out.tags = pruned;
3214
+ }
3215
+ } catch {
3216
+ }
3217
+ try {
3218
+ out.transactionName = scope.getTransactionName?.() ?? scope.getTransaction?.()?.name ?? void 0;
3219
+ } catch {
3220
+ }
3221
+ try {
3222
+ out.sessionId = scope.getSession?.()?.sid ?? void 0;
3223
+ } catch {
3224
+ }
3225
+ try {
3226
+ const raw = scope.getBreadcrumbs?.() ?? scope._breadcrumbs ?? [];
3227
+ if (Array.isArray(raw) && raw.length > 0) {
3228
+ const sliced = raw.slice(-limit);
3229
+ out.breadcrumbs = sliced.map((b) => {
3230
+ const r = b;
3231
+ return {
3232
+ timestamp: typeof r.timestamp === "number" ? (
3233
+ // Sentry stores breadcrumb timestamps in seconds; convert
3234
+ // to ms so the field is comparable to Mushi's own.
3235
+ r.timestamp < 1e12 ? Math.round(r.timestamp * 1e3) : r.timestamp
3236
+ ) : void 0,
3237
+ category: typeof r.category === "string" ? r.category : void 0,
3238
+ level: typeof r.level === "string" ? r.level : void 0,
3239
+ message: typeof r.message === "string" ? r.message : void 0,
3240
+ type: typeof r.type === "string" ? r.type : void 0,
3241
+ data: r.data && typeof r.data === "object" ? r.data : void 0
3242
+ };
3243
+ });
3244
+ }
3245
+ } catch {
3246
+ }
3247
+ }
3248
+ try {
3249
+ const v8 = sentry;
3250
+ let span;
3251
+ if (typeof v8.getActiveSpan === "function") {
3252
+ span = v8.getActiveSpan();
3253
+ } else if (scope?.getSpan) {
3254
+ span = scope.getSpan();
3255
+ }
3256
+ if (span) {
3257
+ const ctx = span.spanContext?.();
3258
+ out.traceId = ctx?.traceId ?? span.traceId ?? void 0;
3259
+ out.spanId = ctx?.spanId ?? span.spanId ?? void 0;
2474
3260
  }
2475
- if (win.Sentry) {
2476
- return win.Sentry;
3261
+ } catch {
3262
+ }
3263
+ let client;
3264
+ try {
3265
+ const v8 = sentry;
3266
+ if (typeof v8.getClient === "function") {
3267
+ client = v8.getClient();
3268
+ } else {
3269
+ const v7 = sentry;
3270
+ client = v7.getCurrentHub?.()?.getClient?.();
3271
+ }
3272
+ } catch {
3273
+ }
3274
+ if (client) {
3275
+ try {
3276
+ const opts = client.getOptions?.();
3277
+ if (opts?.release) out.release = opts.release;
3278
+ if (opts?.environment) out.environment = opts.environment;
3279
+ } catch {
2477
3280
  }
3281
+ try {
3282
+ const dsn = client.getDsn?.();
3283
+ if (dsn?.host && dsn?.projectId && out.eventId) {
3284
+ const orgHost = dsn.host.replace(/^o\d+\./, "");
3285
+ out.issueUrl = `https://${orgHost}/issues/?query=${encodeURIComponent(out.eventId)}`;
3286
+ }
3287
+ } catch {
3288
+ }
3289
+ }
3290
+ try {
3291
+ const v8 = sentry;
3292
+ const replay = v8.getReplay?.() ?? getSentryReplayGlobal();
3293
+ out.replayId = replay?.getReplayId?.() ?? void 0;
2478
3294
  } catch {
2479
3295
  }
2480
- return void 0;
3296
+ return out;
2481
3297
  }
2482
- function captureSentryContext(_config) {
2483
- const context = {};
3298
+ function tagSentryScope(reportId, options = {}) {
3299
+ const sentry = getSentryGlobal();
3300
+ if (!sentry) return;
2484
3301
  try {
2485
- const hub = getSentryGlobal();
2486
- if (!hub) return context;
2487
- const scope = hub.getScope?.();
2488
- if (scope) {
2489
- context.eventId = scope.getLastEventId?.() ?? void 0;
3302
+ const v8 = sentry;
3303
+ if (typeof v8.setTag === "function") {
3304
+ v8.setTag("mushi.report_id", reportId);
3305
+ if (options.reportUrl) v8.setTag("mushi.report_url", options.reportUrl);
3306
+ }
3307
+ if (typeof v8.setContext === "function") {
3308
+ v8.setContext("mushi_report", {
3309
+ id: reportId,
3310
+ ...options.reportUrl ? { url: options.reportUrl } : {},
3311
+ captured_at: (/* @__PURE__ */ new Date()).toISOString()
3312
+ });
2490
3313
  }
2491
- const client = hub.getClient?.();
2492
- if (client) {
2493
- const options = client.getOptions?.();
2494
- context.release = options?.release;
2495
- context.environment = options?.environment;
3314
+ if (typeof v8.addBreadcrumb === "function") {
3315
+ v8.addBreadcrumb({
3316
+ category: "mushi",
3317
+ type: "info",
3318
+ level: "info",
3319
+ message: `Mushi report submitted (${reportId})`,
3320
+ data: { report_id: reportId, ...options.reportUrl ? { url: options.reportUrl } : {} }
3321
+ });
2496
3322
  }
2497
- const win = globalThis;
2498
- if (win.__SENTRY_REPLAY__) {
2499
- const replay = win.__SENTRY_REPLAY__;
2500
- context.replayId = replay.getReplayId?.() ?? void 0;
3323
+ } catch {
3324
+ }
3325
+ try {
3326
+ const v7 = sentry;
3327
+ const scope = v7.getCurrentHub?.()?.getScope?.();
3328
+ if (scope) {
3329
+ scope.setTag?.("mushi.report_id", reportId);
3330
+ if (options.reportUrl) scope.setTag?.("mushi.report_url", options.reportUrl);
3331
+ scope.setContext?.("mushi_report", {
3332
+ id: reportId,
3333
+ ...options.reportUrl ? { url: options.reportUrl } : {}
3334
+ });
2501
3335
  }
2502
3336
  } catch {
2503
3337
  }
2504
- return context;
2505
3338
  }
2506
3339
 
2507
3340
  // src/proactive-triggers.ts
@@ -2678,7 +3511,7 @@ function createProactiveManager(config = {}) {
2678
3511
 
2679
3512
  // src/version.ts
2680
3513
  var MUSHI_SDK_PACKAGE = "@mushi-mushi/web";
2681
- var MUSHI_SDK_VERSION = "0.9.0" ;
3514
+ var MUSHI_SDK_VERSION = "1.1.0" ;
2682
3515
 
2683
3516
  // src/mushi.ts
2684
3517
  var instance = null;
@@ -2717,7 +3550,7 @@ function createInstance(config) {
2717
3550
  const bootstrapConfig = applyPresetConfig(config);
2718
3551
  let activeConfig = bootstrapConfig;
2719
3552
  const log = config.debug ?? false ? createLogger({ scope: "mushi", level: "debug", format: "pretty" }) : noopLogger;
2720
- const apiClient = createApiClient({
3553
+ const apiClient2 = createApiClient({
2721
3554
  projectId: bootstrapConfig.projectId,
2722
3555
  apiKey: bootstrapConfig.apiKey,
2723
3556
  ...bootstrapConfig.apiEndpoint ? { apiEndpoint: bootstrapConfig.apiEndpoint } : {}
@@ -2726,6 +3559,30 @@ function createInstance(config) {
2726
3559
  const offlineQueue = createOfflineQueue(bootstrapConfig.offline);
2727
3560
  const rateLimiter = createRateLimiter({ maxBurst: 10, refillRate: 1, refillIntervalMs: 5e3 });
2728
3561
  const piiScrubber = createPiiScrubber();
3562
+ function scrubBreadcrumbsForWire(crumbs) {
3563
+ return crumbs.map((c) => {
3564
+ const next = { ...c };
3565
+ if (typeof c.message === "string") {
3566
+ next.message = piiScrubber.scrub(c.message);
3567
+ }
3568
+ if (c.data && typeof c.data === "object") {
3569
+ const cleaned = {};
3570
+ for (const [k, v] of Object.entries(c.data)) {
3571
+ cleaned[k] = typeof v === "string" ? piiScrubber.scrub(v) : v;
3572
+ }
3573
+ next.data = cleaned;
3574
+ }
3575
+ return next;
3576
+ });
3577
+ }
3578
+ function scrubTagsForWire(tags) {
3579
+ if (!tags) return void 0;
3580
+ const out = {};
3581
+ for (const [k, v] of Object.entries(tags)) {
3582
+ out[k] = typeof v === "string" ? piiScrubber.scrub(v) : v;
3583
+ }
3584
+ return out;
3585
+ }
2729
3586
  let consoleCap = null;
2730
3587
  let networkCap = null;
2731
3588
  let perfCap = null;
@@ -2802,7 +3659,7 @@ function createInstance(config) {
2802
3659
  getUserId: () => userInfo?.id ?? null,
2803
3660
  getSessionId,
2804
3661
  onEvent: (event) => {
2805
- void apiClient.postDiscoveryEvent({
3662
+ void apiClient2.postDiscoveryEvent({
2806
3663
  ...event,
2807
3664
  sdk_version: MUSHI_SDK_VERSION
2808
3665
  }).catch((err) => {
@@ -2825,6 +3682,16 @@ function createInstance(config) {
2825
3682
  let runtimeConfigLoaded = false;
2826
3683
  let userInfo = null;
2827
3684
  const customMetadata = {};
3685
+ const stickyTags = {};
3686
+ const breadcrumbs = createBreadcrumbBuffer({ max: 50 });
3687
+ breadcrumbs.add({
3688
+ category: "lifecycle",
3689
+ level: "info",
3690
+ message: "Mushi SDK init",
3691
+ data: { projectId: bootstrapConfig.projectId, sdkVersion: MUSHI_SDK_VERSION }
3692
+ });
3693
+ let detachAutoBreadcrumbs = null;
3694
+ detachAutoBreadcrumbs = installAutoBreadcrumbs(breadcrumbs);
2828
3695
  widget = new MushiWidget(bootstrapConfig.widget, {
2829
3696
  onSubmit: async ({ category, description, intent }) => {
2830
3697
  log.info("Report submitted", { category, intent });
@@ -2868,17 +3735,17 @@ function createInstance(config) {
2868
3735
  }
2869
3736
  },
2870
3737
  async onReporterReportsRequest() {
2871
- const result = await apiClient.listReporterReports(getReporterToken());
3738
+ const result = await apiClient2.listReporterReports(getReporterToken());
2872
3739
  if (!result.ok) throw new Error(result.error?.message ?? "Could not load reports");
2873
3740
  return result.data?.reports ?? [];
2874
3741
  },
2875
3742
  async onReporterCommentsRequest(reportId) {
2876
- const result = await apiClient.listReporterComments(reportId, getReporterToken());
3743
+ const result = await apiClient2.listReporterComments(reportId, getReporterToken());
2877
3744
  if (!result.ok) throw new Error(result.error?.message ?? "Could not load thread");
2878
3745
  return result.data?.comments ?? [];
2879
3746
  },
2880
3747
  async onReporterReply(reportId, body) {
2881
- const result = await apiClient.replyToReporterReport(reportId, getReporterToken(), body);
3748
+ const result = await apiClient2.replyToReporterReport(reportId, getReporterToken(), body);
2882
3749
  if (!result.ok) throw new Error(result.error?.message ?? "Could not send reply");
2883
3750
  }
2884
3751
  }, MUSHI_SDK_VERSION);
@@ -2924,8 +3791,8 @@ function createInstance(config) {
2924
3791
  errorBoundary: proactiveCfg?.errorBoundary === true
2925
3792
  });
2926
3793
  }
2927
- offlineQueue.startAutoSync(apiClient);
2928
- offlineQueue.flush(apiClient).then((result) => {
3794
+ offlineQueue.startAutoSync(apiClient2);
3795
+ offlineQueue.flush(apiClient2).then((result) => {
2929
3796
  if (result.sent > 0) log.info("Synced offline reports", { sent: result.sent });
2930
3797
  });
2931
3798
  function applyRuntimeConfig(runtime) {
@@ -2946,7 +3813,7 @@ function createInstance(config) {
2946
3813
  if (shouldUseRuntimeConfig(config)) {
2947
3814
  const cached = readCachedRuntimeConfig(config.projectId);
2948
3815
  if (cached) applyRuntimeConfig(cached);
2949
- apiClient.getSdkConfig().then((result) => {
3816
+ apiClient2.getSdkConfig().then((result) => {
2950
3817
  if (result.ok && result.data) {
2951
3818
  cacheRuntimeConfig(config.projectId, result.data);
2952
3819
  applyRuntimeConfig(result.data);
@@ -2965,7 +3832,7 @@ function createInstance(config) {
2965
3832
  if (activeConfig.widget?.outdatedBanner === "off") return;
2966
3833
  const cached = readCachedSdkVersion(MUSHI_SDK_PACKAGE);
2967
3834
  if (cached) applySdkFreshness(cached);
2968
- const result = await apiClient.getLatestSdkVersion(MUSHI_SDK_PACKAGE);
3835
+ const result = await apiClient2.getLatestSdkVersion(MUSHI_SDK_PACKAGE);
2969
3836
  if (!result.ok || !result.data) return;
2970
3837
  cacheSdkVersion(MUSHI_SDK_PACKAGE, result.data);
2971
3838
  applySdkFreshness(result.data);
@@ -3034,6 +3901,15 @@ function createInstance(config) {
3034
3901
  const fingerprintHash = await getDeviceFingerprintHash().catch(() => null);
3035
3902
  const consoleLogs = activeConfig.capture?.console === false ? void 0 : consoleCap?.getEntries();
3036
3903
  const networkLogs = activeConfig.capture?.network === false ? void 0 : networkCap?.getEntries();
3904
+ const reportBreadcrumbs = scrubBreadcrumbsForWire(breadcrumbs.getAll());
3905
+ const stickyTagSnapshot = scrubTagsForWire(
3906
+ Object.keys(stickyTags).length > 0 ? { ...stickyTags } : void 0
3907
+ );
3908
+ const sentryCtxScrubbed = sentryCtx ? {
3909
+ ...sentryCtx,
3910
+ ...sentryCtx.breadcrumbs ? { breadcrumbs: scrubBreadcrumbsForWire(sentryCtx.breadcrumbs) } : {},
3911
+ ...sentryCtx.tags ? { tags: scrubTagsForWire(sentryCtx.tags) } : {}
3912
+ } : void 0;
3037
3913
  const report = {
3038
3914
  id: crypto.randomUUID?.() ?? `mushi_${Date.now()}_${Math.random().toString(36).slice(2)}`,
3039
3915
  projectId: config.projectId,
@@ -3059,10 +3935,24 @@ function createInstance(config) {
3059
3935
  sdkPackage: MUSHI_SDK_PACKAGE,
3060
3936
  sdkVersion: MUSHI_SDK_VERSION,
3061
3937
  proactiveTrigger: pendingProactiveTrigger ?? void 0,
3938
+ // Top-level Sentry-grade observability fields. Breadcrumbs are
3939
+ // surfaced separately from `consoleLogs` because they're the
3940
+ // higher-signal "what just happened" trail (vs. the high-volume
3941
+ // raw console mirror), and the admin /reports drawer shows them
3942
+ // in different panes.
3943
+ ...reportBreadcrumbs.length > 0 ? { breadcrumbs: reportBreadcrumbs } : {},
3944
+ ...stickyTagSnapshot ? { tags: stickyTagSnapshot } : {},
3945
+ ...sentryCtxScrubbed ? { sentryContext: sentryCtxScrubbed } : {},
3062
3946
  sentryEventId: sentryCtx?.eventId,
3063
3947
  sentryReplayId: sentryCtx?.replayId,
3064
3948
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
3065
3949
  };
3950
+ breadcrumbs.add({
3951
+ category: "lifecycle",
3952
+ level: "info",
3953
+ message: `Mushi report submitting (${category})`,
3954
+ data: { reportId: report.id, category }
3955
+ });
3066
3956
  if (config.integrations?.custom) {
3067
3957
  const builder = {
3068
3958
  addMetadata(key, value) {
@@ -3084,14 +3974,34 @@ function createInstance(config) {
3084
3974
  emit("report:queued", { reportId: report.id });
3085
3975
  return;
3086
3976
  }
3087
- const result = await apiClient.submitReport(report);
3977
+ const result = await apiClient2.submitReport(report);
3088
3978
  if (result.ok) {
3089
3979
  log.info("Report sent", { reportId: result.data?.reportId });
3090
3980
  emit("report:sent", { reportId: result.data?.reportId });
3981
+ breadcrumbs.add({
3982
+ category: "lifecycle",
3983
+ level: "info",
3984
+ message: `Mushi report sent (${result.data?.reportId ?? report.id})`
3985
+ });
3986
+ enqueue({
3987
+ action: "report_submit",
3988
+ metadata: { category, reportId: result.data?.reportId ?? report.id }
3989
+ });
3990
+ try {
3991
+ if (config.sentry && result.data?.reportId) {
3992
+ tagSentryScope(result.data.reportId);
3993
+ }
3994
+ } catch {
3995
+ }
3091
3996
  } else {
3092
3997
  log.warn("Report failed, queuing for retry", { reportId: report.id, error: result.error });
3093
3998
  await offlineQueue.enqueue(report);
3094
3999
  emit("report:failed", { reportId: report.id, error: result.error });
4000
+ breadcrumbs.add({
4001
+ category: "lifecycle",
4002
+ level: "warning",
4003
+ message: `Mushi report queued for retry (${report.id})`
4004
+ });
3095
4005
  }
3096
4006
  pendingScreenshot = null;
3097
4007
  pendingElement = null;
@@ -3163,6 +4073,9 @@ function createInstance(config) {
3163
4073
  discoveryCap?.destroy();
3164
4074
  discoveryCap = null;
3165
4075
  offlineQueue.stopAutoSync();
4076
+ detachAutoBreadcrumbs?.();
4077
+ detachAutoBreadcrumbs = null;
4078
+ breadcrumbs.clear();
3166
4079
  listeners.clear();
3167
4080
  instance = null;
3168
4081
  log.debug("Destroyed");
@@ -3177,6 +4090,16 @@ function createInstance(config) {
3177
4090
  }
3178
4091
  const description = piiScrubber.scrub(preFilter.truncate(input.description));
3179
4092
  const category = input.category ?? "bug";
4093
+ const sentryCtx = config.sentry ? captureSentryContext(config.sentry) : void 0;
4094
+ const captureBreadcrumbs = scrubBreadcrumbsForWire(breadcrumbs.getAll());
4095
+ const mergedTags = scrubTagsForWire(
4096
+ Object.keys(stickyTags).length === 0 && !input.tags ? void 0 : { ...stickyTags, ...input.tags ?? {} }
4097
+ );
4098
+ const sentryCtxScrubbed = sentryCtx ? {
4099
+ ...sentryCtx,
4100
+ ...sentryCtx.breadcrumbs ? { breadcrumbs: scrubBreadcrumbsForWire(sentryCtx.breadcrumbs) } : {},
4101
+ ...sentryCtx.tags ? { tags: scrubTagsForWire(sentryCtx.tags) } : {}
4102
+ } : void 0;
3180
4103
  const report = {
3181
4104
  id: crypto.randomUUID?.() ?? `mushi_${Date.now()}_${Math.random().toString(36).slice(2)}`,
3182
4105
  projectId: config.projectId,
@@ -3187,16 +4110,20 @@ function createInstance(config) {
3187
4110
  metadata: {
3188
4111
  ...input.metadata ?? {},
3189
4112
  ...userInfo ? { user: userInfo } : {},
3190
- ...input.tags ? { tags: input.tags } : {},
3191
4113
  ...input.error ? { error: input.error } : {},
3192
4114
  ...input.severity ? { severity: input.severity } : {},
3193
4115
  ...input.component ? { component: input.component } : {},
3194
4116
  ...input.source ? { source: input.source } : { source: "captureEvent" }
3195
4117
  },
4118
+ ...captureBreadcrumbs.length > 0 ? { breadcrumbs: captureBreadcrumbs } : {},
4119
+ ...mergedTags && Object.keys(mergedTags).length > 0 ? { tags: mergedTags } : {},
4120
+ ...sentryCtxScrubbed ? { sentryContext: sentryCtxScrubbed } : {},
3196
4121
  sessionId: getSessionId(),
3197
4122
  reporterToken: getReporterToken(),
3198
4123
  sdkPackage: MUSHI_SDK_PACKAGE,
3199
4124
  sdkVersion: MUSHI_SDK_VERSION,
4125
+ sentryEventId: sentryCtx?.eventId,
4126
+ sentryReplayId: sentryCtx?.replayId,
3200
4127
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
3201
4128
  };
3202
4129
  emit("report:submitted", { reportId: report.id });
@@ -3205,15 +4132,46 @@ function createInstance(config) {
3205
4132
  emit("report:queued", { reportId: report.id });
3206
4133
  return null;
3207
4134
  }
3208
- const res = await apiClient.submitReport(report);
4135
+ const res = await apiClient2.submitReport(report);
3209
4136
  if (res.ok) {
3210
4137
  emit("report:sent", { reportId: res.data?.reportId });
4138
+ try {
4139
+ if (config.sentry && res.data?.reportId) tagSentryScope(res.data.reportId);
4140
+ } catch {
4141
+ }
3211
4142
  return res.data?.reportId ?? null;
3212
4143
  }
3213
4144
  await offlineQueue.enqueue(report);
3214
4145
  emit("report:failed", { reportId: report.id, error: res.error });
3215
4146
  return null;
3216
4147
  },
4148
+ async captureException(error, options) {
4149
+ const normalised = normaliseThrown(error);
4150
+ breadcrumbs.add({
4151
+ category: "lifecycle",
4152
+ level: "error",
4153
+ message: `Mushi.captureException(${normalised.name}): ${normalised.message}`,
4154
+ ...normalised.stack ? { data: { stack: normalised.stack.slice(0, 500) } } : {}
4155
+ });
4156
+ const description = options?.description?.trim() || `${normalised.name}: ${normalised.message}` || "Uncaught exception";
4157
+ return sdk.captureEvent({
4158
+ description,
4159
+ category: options?.category ?? "bug",
4160
+ severity: options?.severity ?? "high",
4161
+ ...options?.component ? { component: options.component } : {},
4162
+ ...options?.tags ? { tags: options.tags } : {},
4163
+ source: options?.source ?? "captureException",
4164
+ error: {
4165
+ name: normalised.name,
4166
+ message: normalised.message,
4167
+ ...normalised.stack ? { stack: normalised.stack } : {}
4168
+ },
4169
+ metadata: {
4170
+ ...options?.metadata ?? {},
4171
+ ...normalised.cause ? { cause: normalised.cause } : {}
4172
+ }
4173
+ });
4174
+ },
3217
4175
  identify(userId, traits) {
3218
4176
  userInfo = { id: userId, ...traits?.email ? { email: traits.email } : {}, ...traits?.name ? { name: traits.name } : {} };
3219
4177
  if (traits) {
@@ -3221,6 +4179,84 @@ function createInstance(config) {
3221
4179
  if (k !== "email" && k !== "name") customMetadata[`user.${k}`] = v;
3222
4180
  }
3223
4181
  }
4182
+ breadcrumbs.add({
4183
+ category: "lifecycle",
4184
+ level: "info",
4185
+ message: `Mushi.identify(${userId})`
4186
+ });
4187
+ if (activeConfig.rewards?.enabled) {
4188
+ const rewardsCtx = {
4189
+ client: apiClient2,
4190
+ config: activeConfig.rewards,
4191
+ projectId: bootstrapConfig.projectId,
4192
+ userId,
4193
+ traits: traits ? { email: traits.email, name: traits.name, provider: traits.provider } : void 0
4194
+ };
4195
+ if (userInfo.id === userId) {
4196
+ initRewards(rewardsCtx);
4197
+ } else {
4198
+ updateRewardsUser(userId, rewardsCtx.traits);
4199
+ }
4200
+ if (activeConfig.rewards.showInWidget !== false) {
4201
+ void apiClient2.getMyPoints(userId).then((res) => {
4202
+ if (!res.ok) return;
4203
+ const d = res.data;
4204
+ widget.setRewardsState({
4205
+ tier: d.tier ? { slug: d.tier.slug ?? "free", displayName: d.tier.display_name ?? "Free", pointsThreshold: d.tier.points_threshold ?? 0 } : null,
4206
+ nextTier: d.next_tier ? { displayName: d.next_tier.display_name ?? "", pointsThreshold: d.next_tier.points_threshold ?? 0 } : null,
4207
+ totalPoints: d.total_points ?? 0,
4208
+ pointsForReport: d.report_submit_pts ?? 50
4209
+ });
4210
+ }).catch(() => {
4211
+ });
4212
+ }
4213
+ }
4214
+ },
4215
+ addBreadcrumb(crumb) {
4216
+ breadcrumbs.add(crumb);
4217
+ },
4218
+ getBreadcrumbs() {
4219
+ return breadcrumbs.getAll();
4220
+ },
4221
+ setTag(key, value) {
4222
+ if (typeof key !== "string" || key.length === 0) return;
4223
+ stickyTags[key] = value;
4224
+ },
4225
+ setTags(tags) {
4226
+ if (!tags || typeof tags !== "object") return;
4227
+ for (const [k, v] of Object.entries(tags)) {
4228
+ if (typeof k === "string" && k.length > 0) {
4229
+ stickyTags[k] = v;
4230
+ }
4231
+ }
4232
+ },
4233
+ clearTag(key) {
4234
+ if (typeof key === "string" && key.length > 0) {
4235
+ delete stickyTags[key];
4236
+ return;
4237
+ }
4238
+ for (const k of Object.keys(stickyTags)) delete stickyTags[k];
4239
+ },
4240
+ // ─── Rewards program (P1) ──────────────────────────────────
4241
+ async getReputation() {
4242
+ if (!userInfo?.id) return null;
4243
+ const res = await apiClient2.getMyPoints(userInfo.id);
4244
+ if (!res.ok) return null;
4245
+ return {
4246
+ totalPoints: res.data.total_points ?? 0,
4247
+ points30d: res.data.points_30d ?? 0,
4248
+ reputation: 1,
4249
+ confirmedBugs: 0,
4250
+ totalReports: 0
4251
+ };
4252
+ },
4253
+ async getTier() {
4254
+ if (!userInfo?.id) return null;
4255
+ return getTier(userInfo.id);
4256
+ },
4257
+ recordActivity(action, metadata) {
4258
+ if (!activeConfig.rewards?.enabled) return;
4259
+ enqueue({ action, metadata });
3224
4260
  }
3225
4261
  };
3226
4262
  return sdk;
@@ -3233,7 +4269,10 @@ function mergeRuntimeConfig(config, runtime) {
3233
4269
  widget: {
3234
4270
  ...config.widget,
3235
4271
  ...runtime.widget,
3236
- ...widgetTrigger ? { trigger: widgetTrigger } : {}
4272
+ ...widgetTrigger ? { trigger: widgetTrigger } : {},
4273
+ // betaMode is local-only: set by the host app, not the dashboard.
4274
+ // Restore it after the runtime spread so it is never silently cleared.
4275
+ ...config.widget?.betaMode ? { betaMode: config.widget.betaMode } : {}
3237
4276
  },
3238
4277
  capture: {
3239
4278
  ...config.capture,
@@ -3458,10 +4497,148 @@ function createNoopInstance() {
3458
4497
  instance = null;
3459
4498
  },
3460
4499
  captureEvent: async () => null,
4500
+ captureException: async () => null,
3461
4501
  identify: () => {
4502
+ },
4503
+ addBreadcrumb: () => {
4504
+ },
4505
+ getBreadcrumbs: () => [],
4506
+ setTag: () => {
4507
+ },
4508
+ setTags: () => {
4509
+ },
4510
+ clearTag: () => {
4511
+ },
4512
+ getReputation: async () => null,
4513
+ getTier: async () => null,
4514
+ recordActivity: () => {
3462
4515
  }
3463
4516
  };
3464
4517
  }
4518
+ function installAutoBreadcrumbs(buffer) {
4519
+ if (typeof window === "undefined") return () => {
4520
+ };
4521
+ const cleanups = [];
4522
+ try {
4523
+ const dispatchRouteChange = (kind) => {
4524
+ buffer.add({
4525
+ category: "navigation",
4526
+ level: "info",
4527
+ message: `${kind}: ${window.location.pathname}`,
4528
+ data: { url: window.location.href, kind }
4529
+ });
4530
+ };
4531
+ const onPop = () => dispatchRouteChange("popstate");
4532
+ window.addEventListener("popstate", onPop, { passive: true });
4533
+ cleanups.push(() => window.removeEventListener("popstate", onPop));
4534
+ const origPush = window.history.pushState;
4535
+ const origReplace = window.history.replaceState;
4536
+ window.history.pushState = function patched(...args) {
4537
+ const ret = origPush.apply(this, args);
4538
+ try {
4539
+ dispatchRouteChange("pushState");
4540
+ } catch {
4541
+ }
4542
+ return ret;
4543
+ };
4544
+ window.history.replaceState = function patched(...args) {
4545
+ const ret = origReplace.apply(this, args);
4546
+ try {
4547
+ dispatchRouteChange("replaceState");
4548
+ } catch {
4549
+ }
4550
+ return ret;
4551
+ };
4552
+ cleanups.push(() => {
4553
+ window.history.pushState = origPush;
4554
+ window.history.replaceState = origReplace;
4555
+ });
4556
+ } catch {
4557
+ }
4558
+ try {
4559
+ const origError = console.error;
4560
+ const origWarn = console.warn;
4561
+ console.error = function(...args) {
4562
+ try {
4563
+ buffer.add({
4564
+ category: "console",
4565
+ level: "error",
4566
+ message: args.map(stringifyConsoleArg).join(" ")
4567
+ });
4568
+ } catch {
4569
+ }
4570
+ return origError.apply(this, args);
4571
+ };
4572
+ console.warn = function(...args) {
4573
+ try {
4574
+ buffer.add({
4575
+ category: "console",
4576
+ level: "warning",
4577
+ message: args.map(stringifyConsoleArg).join(" ")
4578
+ });
4579
+ } catch {
4580
+ }
4581
+ return origWarn.apply(this, args);
4582
+ };
4583
+ cleanups.push(() => {
4584
+ console.error = origError;
4585
+ console.warn = origWarn;
4586
+ });
4587
+ } catch {
4588
+ }
4589
+ try {
4590
+ const onClick = (ev) => {
4591
+ try {
4592
+ const target = ev.target;
4593
+ if (!(target instanceof Element)) return;
4594
+ let cur = target;
4595
+ let hops = 0;
4596
+ while (cur && hops < 10) {
4597
+ const tid = cur.getAttribute("data-testid");
4598
+ if (tid) {
4599
+ const text = (cur.textContent ?? "").trim().slice(0, 80);
4600
+ buffer.add({
4601
+ category: "ui.click",
4602
+ level: "info",
4603
+ message: `clicked ${tid}${text ? ` \u2014 ${text}` : ""}`,
4604
+ data: { testid: tid, tag: cur.tagName.toLowerCase() }
4605
+ });
4606
+ return;
4607
+ }
4608
+ cur = cur.parentElement;
4609
+ hops++;
4610
+ }
4611
+ } catch {
4612
+ }
4613
+ };
4614
+ document.addEventListener("click", onClick, { passive: true, capture: true });
4615
+ cleanups.push(() => document.removeEventListener("click", onClick, true));
4616
+ } catch {
4617
+ }
4618
+ return () => {
4619
+ for (const c of cleanups) {
4620
+ try {
4621
+ c();
4622
+ } catch {
4623
+ }
4624
+ }
4625
+ };
4626
+ }
4627
+ function stringifyConsoleArg(arg) {
4628
+ try {
4629
+ if (arg instanceof Error) {
4630
+ return `${arg.name}: ${arg.message}`;
4631
+ }
4632
+ if (typeof arg === "object" && arg !== null) {
4633
+ const json = JSON.stringify(arg);
4634
+ return json.length > 200 ? `${json.slice(0, 200)}\u2026` : json;
4635
+ }
4636
+ const s = String(arg);
4637
+ return s.length > 200 ? `${s.slice(0, 200)}\u2026` : s;
4638
+ } catch {
4639
+ return `[${typeof arg}]`;
4640
+ }
4641
+ }
3465
4642
 
3466
4643
  export { Mushi, MushiWidget, createConsoleCapture, createElementSelector, createNetworkCapture, createPerformanceCapture, createProactiveManager, createScreenshotCapture, createTimelineCapture, getAvailableLocales, getLocale, setupProactiveTriggers };
3467
4644
  //# sourceMappingURL=index.js.map