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