@mushi-mushi/web 1.3.0 → 1.6.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 +0 -11
- package/dist/index.cjs +670 -156
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +99 -10
- package/dist/index.d.ts +99 -10
- package/dist/index.js +670 -156
- package/dist/index.js.map +1 -1
- package/package.json +16 -16
package/dist/index.cjs
CHANGED
|
@@ -340,6 +340,21 @@ function getWidgetStyles(theme) {
|
|
|
340
340
|
outline: 2px solid ${vermillion};
|
|
341
341
|
outline-offset: 3px;
|
|
342
342
|
}
|
|
343
|
+
/* First-session welcome pulse. Three soft halos at 800ms each, then
|
|
344
|
+
auto-clear. Uses a box-shadow ring rather than transform/scale so it
|
|
345
|
+
can compose with the hover transform without fighting it. Respects
|
|
346
|
+
prefers-reduced-motion. */
|
|
347
|
+
@keyframes mushi-trigger-pulse {
|
|
348
|
+
0% { box-shadow: 0 0 0 0 rgba(212, 67, 50, 0.55), 0 1px 0 ${rule}, 0 10px 24px -14px rgba(14,13,11,0.45); }
|
|
349
|
+
70% { box-shadow: 0 0 0 16px rgba(212, 67, 50, 0), 0 1px 0 ${rule}, 0 10px 24px -14px rgba(14,13,11,0.45); }
|
|
350
|
+
100% { box-shadow: 0 0 0 0 rgba(212, 67, 50, 0), 0 1px 0 ${rule}, 0 10px 24px -14px rgba(14,13,11,0.45); }
|
|
351
|
+
}
|
|
352
|
+
.mushi-trigger-pulse {
|
|
353
|
+
animation: mushi-trigger-pulse 800ms ${easeStamp} 3;
|
|
354
|
+
}
|
|
355
|
+
@media (prefers-reduced-motion: reduce) {
|
|
356
|
+
.mushi-trigger-pulse { animation: none; }
|
|
357
|
+
}
|
|
343
358
|
.mushi-trigger.bottom-right {
|
|
344
359
|
bottom: var(--mushi-bottom, calc(24px + env(safe-area-inset-bottom, 0px)));
|
|
345
360
|
right: var(--mushi-right, calc(24px + env(safe-area-inset-right, 0px)));
|
|
@@ -610,6 +625,25 @@ function getWidgetStyles(theme) {
|
|
|
610
625
|
transform: translateX(-4px);
|
|
611
626
|
transition: opacity 220ms ${easeStamp}, transform 220ms ${easeStamp}, color 220ms ${easeStamp};
|
|
612
627
|
}
|
|
628
|
+
/* Feature-request and Reports-inbox entries sit above the five
|
|
629
|
+
category cards as discoverable shortcuts. We give them a subtle
|
|
630
|
+
left rule so the eye reads them as a separate group rather than
|
|
631
|
+
"another category". The shortcut group has zero hover indent
|
|
632
|
+
overshoot \u2014 we want them quiet until intent. */
|
|
633
|
+
.mushi-feature-entry,
|
|
634
|
+
.mushi-reports-entry {
|
|
635
|
+
padding-left: 10px;
|
|
636
|
+
border-left: 2px solid ${inkFaint};
|
|
637
|
+
transition: padding 220ms ${easeStamp}, color 220ms ${easeStamp}, border-color 220ms ${easeStamp};
|
|
638
|
+
}
|
|
639
|
+
.mushi-feature-entry:hover,
|
|
640
|
+
.mushi-reports-entry:hover {
|
|
641
|
+
border-left-color: ${vermillion};
|
|
642
|
+
padding-left: 14px;
|
|
643
|
+
}
|
|
644
|
+
.mushi-feature-entry .mushi-option-icon {
|
|
645
|
+
filter: none;
|
|
646
|
+
}
|
|
613
647
|
.mushi-report-row {
|
|
614
648
|
width: 100%;
|
|
615
649
|
display: grid;
|
|
@@ -1027,6 +1061,113 @@ function getWidgetStyles(theme) {
|
|
|
1027
1061
|
color: ${inkMuted};
|
|
1028
1062
|
}
|
|
1029
1063
|
|
|
1064
|
+
/* \u2500\u2500 Two-way receipt (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 */
|
|
1065
|
+
/* The receipt block sits below the stamp/meta. Three states: */
|
|
1066
|
+
/* 1. delivering... (spinner pill, while host onSubmit awaits) */
|
|
1067
|
+
/* 2. confirmed (Receipt #abc12345 + Track on Mushi link) */
|
|
1068
|
+
/* 3. queued offline (warn pill \u2014 degrade gracefully) */
|
|
1069
|
+
.mushi-success-receipt {
|
|
1070
|
+
margin-top: 14px;
|
|
1071
|
+
width: 100%;
|
|
1072
|
+
max-width: 280px;
|
|
1073
|
+
display: flex;
|
|
1074
|
+
flex-direction: column;
|
|
1075
|
+
gap: 6px;
|
|
1076
|
+
align-items: stretch;
|
|
1077
|
+
}
|
|
1078
|
+
.mushi-success-receipt-row {
|
|
1079
|
+
display: flex;
|
|
1080
|
+
align-items: center;
|
|
1081
|
+
justify-content: center;
|
|
1082
|
+
gap: 8px;
|
|
1083
|
+
font-family: ${fontMono};
|
|
1084
|
+
font-size: 11px;
|
|
1085
|
+
letter-spacing: 0.05em;
|
|
1086
|
+
color: ${inkMuted};
|
|
1087
|
+
}
|
|
1088
|
+
.mushi-success-receipt-label {
|
|
1089
|
+
text-transform: uppercase;
|
|
1090
|
+
letter-spacing: 0.10em;
|
|
1091
|
+
color: ${inkMuted};
|
|
1092
|
+
}
|
|
1093
|
+
.mushi-success-receipt-id {
|
|
1094
|
+
display: inline-flex;
|
|
1095
|
+
align-items: center;
|
|
1096
|
+
gap: 5px;
|
|
1097
|
+
padding: 3px 8px;
|
|
1098
|
+
border-radius: 4px;
|
|
1099
|
+
background: transparent;
|
|
1100
|
+
border: 1px dashed ${rule};
|
|
1101
|
+
color: inherit;
|
|
1102
|
+
font-family: ${fontMono};
|
|
1103
|
+
font-size: 12px;
|
|
1104
|
+
letter-spacing: 0.02em;
|
|
1105
|
+
cursor: pointer;
|
|
1106
|
+
transition: background 120ms ease, border-color 120ms ease;
|
|
1107
|
+
}
|
|
1108
|
+
.mushi-success-receipt-id:hover,
|
|
1109
|
+
.mushi-success-receipt-id:focus-visible {
|
|
1110
|
+
background: rgba(217, 65, 47, 0.06);
|
|
1111
|
+
border-color: ${vermillion};
|
|
1112
|
+
color: ${vermillion};
|
|
1113
|
+
outline: none;
|
|
1114
|
+
}
|
|
1115
|
+
.mushi-success-receipt-copy {
|
|
1116
|
+
font-size: 11px;
|
|
1117
|
+
opacity: 0.7;
|
|
1118
|
+
}
|
|
1119
|
+
.mushi-success-receipt-track {
|
|
1120
|
+
display: inline-flex;
|
|
1121
|
+
align-items: center;
|
|
1122
|
+
justify-content: center;
|
|
1123
|
+
gap: 4px;
|
|
1124
|
+
padding: 6px 10px;
|
|
1125
|
+
border-radius: 4px;
|
|
1126
|
+
background: ${vermillion};
|
|
1127
|
+
color: #fff;
|
|
1128
|
+
font-family: ${fontMono};
|
|
1129
|
+
font-size: 11px;
|
|
1130
|
+
letter-spacing: 0.10em;
|
|
1131
|
+
text-transform: uppercase;
|
|
1132
|
+
text-decoration: none;
|
|
1133
|
+
transition: filter 120ms ease;
|
|
1134
|
+
}
|
|
1135
|
+
.mushi-success-receipt-track:hover,
|
|
1136
|
+
.mushi-success-receipt-track:focus-visible {
|
|
1137
|
+
filter: brightness(0.95);
|
|
1138
|
+
outline: none;
|
|
1139
|
+
}
|
|
1140
|
+
.mushi-success-receipt-spinner {
|
|
1141
|
+
width: 11px;
|
|
1142
|
+
height: 11px;
|
|
1143
|
+
border-radius: 50%;
|
|
1144
|
+
border: 1.5px solid ${rule};
|
|
1145
|
+
border-top-color: ${vermillion};
|
|
1146
|
+
animation: mushi-receipt-spin 0.8s linear infinite;
|
|
1147
|
+
}
|
|
1148
|
+
@keyframes mushi-receipt-spin {
|
|
1149
|
+
to { transform: rotate(360deg); }
|
|
1150
|
+
}
|
|
1151
|
+
.mushi-success-receipt-hint {
|
|
1152
|
+
color: ${inkMuted};
|
|
1153
|
+
font-style: italic;
|
|
1154
|
+
}
|
|
1155
|
+
.mushi-success-receipt-warn {
|
|
1156
|
+
color: ${vermillion};
|
|
1157
|
+
}
|
|
1158
|
+
.mushi-success-sla {
|
|
1159
|
+
margin-top: 2px;
|
|
1160
|
+
font-family: ${fontDisplay};
|
|
1161
|
+
font-size: 12px;
|
|
1162
|
+
line-height: 1.45;
|
|
1163
|
+
text-align: center;
|
|
1164
|
+
color: ${inkMuted};
|
|
1165
|
+
max-width: 260px;
|
|
1166
|
+
}
|
|
1167
|
+
.mushi-success-sla-default {
|
|
1168
|
+
opacity: 0.85;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1030
1171
|
@keyframes mushi-stamp-ring {
|
|
1031
1172
|
to { stroke-dashoffset: 0; }
|
|
1032
1173
|
}
|
|
@@ -1299,6 +1440,7 @@ var CATEGORY_ICONS = {
|
|
|
1299
1440
|
confusing: "\u{1F615}",
|
|
1300
1441
|
other: "\u{1F4DD}"
|
|
1301
1442
|
};
|
|
1443
|
+
var FEATURE_REQUEST_INTENT = "Feature request";
|
|
1302
1444
|
function pad2(n) {
|
|
1303
1445
|
return n < 10 ? `0${n}` : String(n);
|
|
1304
1446
|
}
|
|
@@ -1344,7 +1486,12 @@ var MushiWidget = class {
|
|
|
1344
1486
|
brandFooter: config.brandFooter ?? true,
|
|
1345
1487
|
outdatedBanner: config.outdatedBanner ?? "auto",
|
|
1346
1488
|
betaMode: config.betaMode ?? {},
|
|
1347
|
-
minDescriptionLength: config.minDescriptionLength ?? 20
|
|
1489
|
+
minDescriptionLength: config.minDescriptionLength ?? 20,
|
|
1490
|
+
dashboardUrl: config.dashboardUrl ?? "",
|
|
1491
|
+
responseSlaLabel: config.responseSlaLabel ?? "",
|
|
1492
|
+
featureRequestCard: config.featureRequestCard ?? true,
|
|
1493
|
+
featureRequestLabel: config.featureRequestLabel ?? "",
|
|
1494
|
+
featureRequestDescription: config.featureRequestDescription ?? ""
|
|
1348
1495
|
};
|
|
1349
1496
|
this.callbacks = callbacks;
|
|
1350
1497
|
this.locale = getLocale(this.config.locale === "auto" ? void 0 : this.config.locale);
|
|
@@ -1362,6 +1509,13 @@ var MushiWidget = class {
|
|
|
1362
1509
|
step = "category";
|
|
1363
1510
|
selectedCategory = null;
|
|
1364
1511
|
selectedIntent = null;
|
|
1512
|
+
/**
|
|
1513
|
+
* True when the user took the "Feature request" shortcut. We track this
|
|
1514
|
+
* separately from `selectedCategory='other'` so the Back button on the
|
|
1515
|
+
* details step jumps straight back to the category picker instead of
|
|
1516
|
+
* landing on the intent picker the user explicitly skipped.
|
|
1517
|
+
*/
|
|
1518
|
+
viaFeatureRequest = false;
|
|
1365
1519
|
screenshotAttached = false;
|
|
1366
1520
|
screenshotCapturing = false;
|
|
1367
1521
|
screenshotError = false;
|
|
@@ -1399,6 +1553,17 @@ var MushiWidget = class {
|
|
|
1399
1553
|
successTimer = null;
|
|
1400
1554
|
autoCloseTimer = null;
|
|
1401
1555
|
rewardsState = null;
|
|
1556
|
+
/** Server-confirmed id for the just-submitted report. Surfaces in
|
|
1557
|
+
* the success step as a copyable receipt + optional deep link to
|
|
1558
|
+
* the Mushi console (when `dashboardUrl` is configured). Cleared
|
|
1559
|
+
* on every new `open()` so a re-opened widget never reuses a
|
|
1560
|
+
* stale id from the previous session. */
|
|
1561
|
+
lastReportId = null;
|
|
1562
|
+
/** True when the just-submitted report was queued offline (no
|
|
1563
|
+
* network, or the API errored and went into the retry queue).
|
|
1564
|
+
* Drives a different success copy so the user knows the report
|
|
1565
|
+
* hasn't actually reached the console yet. */
|
|
1566
|
+
lastSubmitQueuedOffline = false;
|
|
1402
1567
|
mount() {
|
|
1403
1568
|
if (this.host.isConnected) return;
|
|
1404
1569
|
document.body.appendChild(this.host);
|
|
@@ -1433,7 +1598,12 @@ var MushiWidget = class {
|
|
|
1433
1598
|
...config.brandFooter !== void 0 ? { brandFooter: config.brandFooter } : {},
|
|
1434
1599
|
...config.outdatedBanner !== void 0 ? { outdatedBanner: config.outdatedBanner } : {},
|
|
1435
1600
|
...config.betaMode !== void 0 ? { betaMode: config.betaMode } : {},
|
|
1436
|
-
...config.minDescriptionLength !== void 0 ? { minDescriptionLength: config.minDescriptionLength } : {}
|
|
1601
|
+
...config.minDescriptionLength !== void 0 ? { minDescriptionLength: config.minDescriptionLength } : {},
|
|
1602
|
+
...config.dashboardUrl !== void 0 ? { dashboardUrl: config.dashboardUrl } : {},
|
|
1603
|
+
...config.responseSlaLabel !== void 0 ? { responseSlaLabel: config.responseSlaLabel } : {},
|
|
1604
|
+
...config.featureRequestCard !== void 0 ? { featureRequestCard: config.featureRequestCard } : {},
|
|
1605
|
+
...config.featureRequestLabel !== void 0 ? { featureRequestLabel: config.featureRequestLabel } : {},
|
|
1606
|
+
...config.featureRequestDescription !== void 0 ? { featureRequestDescription: config.featureRequestDescription } : {}
|
|
1437
1607
|
};
|
|
1438
1608
|
this.locale = getLocale(this.config.locale === "auto" ? void 0 : this.config.locale);
|
|
1439
1609
|
this.syncAttachedLaunchers();
|
|
@@ -1451,7 +1621,15 @@ var MushiWidget = class {
|
|
|
1451
1621
|
this.submitting = false;
|
|
1452
1622
|
this.submittedAt = null;
|
|
1453
1623
|
this.removeSelectorHint();
|
|
1454
|
-
|
|
1624
|
+
this.lastReportId = null;
|
|
1625
|
+
this.lastSubmitQueuedOffline = false;
|
|
1626
|
+
this.viaFeatureRequest = false;
|
|
1627
|
+
if (options?.featureRequest) {
|
|
1628
|
+
this.selectedCategory = "other";
|
|
1629
|
+
this.selectedIntent = FEATURE_REQUEST_INTENT;
|
|
1630
|
+
this.viaFeatureRequest = true;
|
|
1631
|
+
this.step = "details";
|
|
1632
|
+
} else if (options?.category) {
|
|
1455
1633
|
this.selectedCategory = options.category;
|
|
1456
1634
|
this.selectedIntent = null;
|
|
1457
1635
|
this.step = "intent";
|
|
@@ -1469,6 +1647,22 @@ var MushiWidget = class {
|
|
|
1469
1647
|
this.render();
|
|
1470
1648
|
this.callbacks.onClose();
|
|
1471
1649
|
}
|
|
1650
|
+
/**
|
|
1651
|
+
* Briefly highlight the trigger button (a soft pulse + tooltip) without
|
|
1652
|
+
* opening the full reporter panel. Use for first-session welcome nudges
|
|
1653
|
+
* and other "by the way, this exists" prompts where forcing the panel
|
|
1654
|
+
* open would feel aggressive. Honours `position: 'none'` (no-op when
|
|
1655
|
+
* the trigger button is hidden).
|
|
1656
|
+
*/
|
|
1657
|
+
pulseTrigger() {
|
|
1658
|
+
if (this.isOpen) return;
|
|
1659
|
+
const trigger = this.shadow.querySelector(".mushi-trigger");
|
|
1660
|
+
if (!trigger) return;
|
|
1661
|
+
trigger.classList.add("mushi-trigger-pulse");
|
|
1662
|
+
window.setTimeout(() => {
|
|
1663
|
+
trigger.classList.remove("mushi-trigger-pulse");
|
|
1664
|
+
}, 2400);
|
|
1665
|
+
}
|
|
1472
1666
|
getIsOpen() {
|
|
1473
1667
|
return this.isOpen;
|
|
1474
1668
|
}
|
|
@@ -1891,12 +2085,46 @@ var MushiWidget = class {
|
|
|
1891
2085
|
</div>
|
|
1892
2086
|
<span class="mushi-option-arrow" aria-hidden="true">\u2192</span>
|
|
1893
2087
|
</button>
|
|
2088
|
+
${this.renderFeatureRequestEntry()}
|
|
1894
2089
|
${categories}
|
|
1895
2090
|
${this.rewardsState ? this.renderRewardsNudge() : ""}
|
|
1896
2091
|
</div>
|
|
1897
2092
|
${this.renderStepIndicator(STEP_NUMBER.category)}
|
|
1898
2093
|
`;
|
|
1899
2094
|
}
|
|
2095
|
+
/**
|
|
2096
|
+
* First-class "Feature request" entry rendered at the top of the
|
|
2097
|
+
* category step. Beta apps consistently get more useful signal when
|
|
2098
|
+
* the user has a no-friction path to say "I wish this did X" — burying
|
|
2099
|
+
* it as an intent under the "Other" category drops feature submissions
|
|
2100
|
+
* by ~40% in industry studies (Userpilot, Usersnap 2025).
|
|
2101
|
+
*
|
|
2102
|
+
* Wire format: still routes through the standard `other` category with
|
|
2103
|
+
* a `user_category = 'Feature request'` stamp, so we don't need a DB
|
|
2104
|
+
* migration. The admin console filters on that string to surface the
|
|
2105
|
+
* Feature-request swimlane.
|
|
2106
|
+
*/
|
|
2107
|
+
renderFeatureRequestEntry() {
|
|
2108
|
+
const enabled = this.config.featureRequestCard !== false;
|
|
2109
|
+
if (!enabled) return "";
|
|
2110
|
+
const label = this.config.featureRequestLabel ?? "Feature request";
|
|
2111
|
+
const desc = this.config.featureRequestDescription ?? "Suggest something new \u2014 even rough ideas help us prioritise";
|
|
2112
|
+
return `
|
|
2113
|
+
<button
|
|
2114
|
+
type="button"
|
|
2115
|
+
class="mushi-option-btn mushi-feature-entry"
|
|
2116
|
+
data-action="feature-request"
|
|
2117
|
+
aria-label="${escapeHtml(label)}"
|
|
2118
|
+
>
|
|
2119
|
+
<span class="mushi-option-icon" aria-hidden="true">\u2728</span>
|
|
2120
|
+
<div class="mushi-option-text">
|
|
2121
|
+
<span class="mushi-option-label">${escapeHtml(label)}</span>
|
|
2122
|
+
<span class="mushi-option-desc">${escapeHtml(desc)}</span>
|
|
2123
|
+
</div>
|
|
2124
|
+
<span class="mushi-option-arrow" aria-hidden="true">\u2192</span>
|
|
2125
|
+
</button>
|
|
2126
|
+
`;
|
|
2127
|
+
}
|
|
1900
2128
|
/** Collapsible "What's new" changelog row. Closes the reporter feedback loop. */
|
|
1901
2129
|
renderBetaChangelog() {
|
|
1902
2130
|
const entries = this.config.betaMode?.changelogItems;
|
|
@@ -2095,12 +2323,81 @@ var MushiWidget = class {
|
|
|
2095
2323
|
</div>
|
|
2096
2324
|
<div class="mushi-success-headline">${t.widget.submitted}</div>
|
|
2097
2325
|
<div class="mushi-success-meta">REPORT \xB7 ${time}</div>
|
|
2326
|
+
${this.renderSuccessReceipt()}
|
|
2098
2327
|
${this.rewardsState ? this.renderSuccessRewards() : ""}
|
|
2099
2328
|
${this.config.betaMode?.enabled ? this.renderBetaSuccessFooter() : ""}
|
|
2100
2329
|
</div>
|
|
2101
2330
|
</div>
|
|
2102
2331
|
`;
|
|
2103
2332
|
}
|
|
2333
|
+
/**
|
|
2334
|
+
* Two-way receipt block. Until the host's `onSubmit` resolves with a
|
|
2335
|
+
* server-confirmed report id, we show a discreet "delivering..." pill so
|
|
2336
|
+
* the user knows their submission is still in flight. Once we have the
|
|
2337
|
+
* id, we surface a short monospaced id + a copy button + an optional
|
|
2338
|
+
* "Track on Mushi" deep link to `dashboardUrl/reports/<id>` so the user
|
|
2339
|
+
* can watch the status walk through queued -> classified -> fixed in
|
|
2340
|
+
* real time (Peak-End rule: the last impression sticks). If we never
|
|
2341
|
+
* get an id (offline retry queue), we say so explicitly rather than
|
|
2342
|
+
* pretending everything is fine.
|
|
2343
|
+
*/
|
|
2344
|
+
renderSuccessReceipt() {
|
|
2345
|
+
if (this.lastSubmitQueuedOffline) {
|
|
2346
|
+
return `
|
|
2347
|
+
<div class="mushi-success-receipt" role="status">
|
|
2348
|
+
<div class="mushi-success-receipt-row mushi-success-receipt-warn">
|
|
2349
|
+
<span class="mushi-success-receipt-label">Queued offline</span>
|
|
2350
|
+
<span class="mushi-success-receipt-hint">We’ll send it the moment you’re back online.</span>
|
|
2351
|
+
</div>
|
|
2352
|
+
</div>
|
|
2353
|
+
`;
|
|
2354
|
+
}
|
|
2355
|
+
if (!this.lastReportId) {
|
|
2356
|
+
return `
|
|
2357
|
+
<div class="mushi-success-receipt" role="status">
|
|
2358
|
+
<div class="mushi-success-receipt-row">
|
|
2359
|
+
<span class="mushi-success-receipt-spinner" aria-hidden="true"></span>
|
|
2360
|
+
<span class="mushi-success-receipt-hint">Delivering to the team\u2026</span>
|
|
2361
|
+
</div>
|
|
2362
|
+
${this.renderSlaLine()}
|
|
2363
|
+
</div>
|
|
2364
|
+
`;
|
|
2365
|
+
}
|
|
2366
|
+
const idShort = `#${this.lastReportId.slice(0, 8)}`;
|
|
2367
|
+
const dashboard = (this.config.dashboardUrl ?? "").replace(/\/$/, "");
|
|
2368
|
+
const trackHref = dashboard ? `${dashboard}/reports/${encodeURIComponent(this.lastReportId)}` : "";
|
|
2369
|
+
return `
|
|
2370
|
+
<div class="mushi-success-receipt" role="status">
|
|
2371
|
+
<div class="mushi-success-receipt-row">
|
|
2372
|
+
<span class="mushi-success-receipt-label">Receipt</span>
|
|
2373
|
+
<button
|
|
2374
|
+
type="button"
|
|
2375
|
+
class="mushi-success-receipt-id"
|
|
2376
|
+
data-action="copy-report-id"
|
|
2377
|
+
data-copy-id="${escapeHtml(this.lastReportId)}"
|
|
2378
|
+
title="Copy report id ${escapeHtml(this.lastReportId)}"
|
|
2379
|
+
aria-label="Copy report id ${escapeHtml(this.lastReportId)}"
|
|
2380
|
+
>${escapeHtml(idShort)}<span class="mushi-success-receipt-copy" aria-hidden="true">\u2398</span></button>
|
|
2381
|
+
</div>
|
|
2382
|
+
${trackHref ? `
|
|
2383
|
+
<a
|
|
2384
|
+
class="mushi-success-receipt-track"
|
|
2385
|
+
href="${escapeHtml(trackHref)}"
|
|
2386
|
+
target="_blank"
|
|
2387
|
+
rel="noopener noreferrer"
|
|
2388
|
+
>Track on Mushi <span aria-hidden="true">\u2197</span></a>
|
|
2389
|
+
` : ""}
|
|
2390
|
+
${this.renderSlaLine()}
|
|
2391
|
+
</div>
|
|
2392
|
+
`;
|
|
2393
|
+
}
|
|
2394
|
+
renderSlaLine() {
|
|
2395
|
+
const sla = (this.config.responseSlaLabel ?? "").trim();
|
|
2396
|
+
if (sla) {
|
|
2397
|
+
return `<div class="mushi-success-sla">${escapeHtml(sla)}</div>`;
|
|
2398
|
+
}
|
|
2399
|
+
return `<div class="mushi-success-sla mushi-success-sla-default">A human will look at this within a working day.</div>`;
|
|
2400
|
+
}
|
|
2104
2401
|
/**
|
|
2105
2402
|
* Reciprocity footer on the success step: closes the feedback loop by
|
|
2106
2403
|
* attributing where the report goes, sets a response expectation, and
|
|
@@ -2192,8 +2489,15 @@ var MushiWidget = class {
|
|
|
2192
2489
|
this.step = "category";
|
|
2193
2490
|
this.selectedCategory = null;
|
|
2194
2491
|
} else if (this.step === "details") {
|
|
2195
|
-
this.
|
|
2196
|
-
|
|
2492
|
+
if (this.viaFeatureRequest) {
|
|
2493
|
+
this.step = "category";
|
|
2494
|
+
this.selectedCategory = null;
|
|
2495
|
+
this.selectedIntent = null;
|
|
2496
|
+
this.viaFeatureRequest = false;
|
|
2497
|
+
} else {
|
|
2498
|
+
this.step = "intent";
|
|
2499
|
+
this.selectedIntent = null;
|
|
2500
|
+
}
|
|
2197
2501
|
} else if (this.step === "reports") {
|
|
2198
2502
|
this.step = "category";
|
|
2199
2503
|
} else if (this.step === "report-detail") {
|
|
@@ -2205,6 +2509,13 @@ var MushiWidget = class {
|
|
|
2205
2509
|
panel.querySelector('[data-action="reports"]')?.addEventListener("click", () => {
|
|
2206
2510
|
void this.loadReporterReports();
|
|
2207
2511
|
});
|
|
2512
|
+
panel.querySelector('[data-action="feature-request"]')?.addEventListener("click", () => {
|
|
2513
|
+
this.selectedCategory = "other";
|
|
2514
|
+
this.selectedIntent = FEATURE_REQUEST_INTENT;
|
|
2515
|
+
this.viaFeatureRequest = true;
|
|
2516
|
+
this.step = "details";
|
|
2517
|
+
this.render();
|
|
2518
|
+
});
|
|
2208
2519
|
panel.querySelectorAll("[data-report-id]").forEach((btn) => {
|
|
2209
2520
|
btn.addEventListener("click", () => {
|
|
2210
2521
|
const reportId = btn.dataset.reportId;
|
|
@@ -2214,6 +2525,27 @@ var MushiWidget = class {
|
|
|
2214
2525
|
panel.querySelector('[data-action="reporter-reply"]')?.addEventListener("click", () => {
|
|
2215
2526
|
void this.submitReporterReply(panel);
|
|
2216
2527
|
});
|
|
2528
|
+
panel.querySelector('[data-action="copy-report-id"]')?.addEventListener("click", (e) => {
|
|
2529
|
+
const btn = e.currentTarget;
|
|
2530
|
+
const id = btn.dataset.copyId;
|
|
2531
|
+
if (!id) return;
|
|
2532
|
+
const restore = btn.innerHTML;
|
|
2533
|
+
const done = () => {
|
|
2534
|
+
btn.innerHTML = "Copied \u2713";
|
|
2535
|
+
window.setTimeout(() => {
|
|
2536
|
+
if (btn.isConnected) btn.innerHTML = restore;
|
|
2537
|
+
}, 1600);
|
|
2538
|
+
};
|
|
2539
|
+
try {
|
|
2540
|
+
if (navigator.clipboard?.writeText) {
|
|
2541
|
+
void navigator.clipboard.writeText(id).then(done).catch(() => done());
|
|
2542
|
+
} else {
|
|
2543
|
+
done();
|
|
2544
|
+
}
|
|
2545
|
+
} catch {
|
|
2546
|
+
done();
|
|
2547
|
+
}
|
|
2548
|
+
});
|
|
2217
2549
|
panel.querySelectorAll("[data-category]").forEach((btn) => {
|
|
2218
2550
|
btn.addEventListener("click", () => {
|
|
2219
2551
|
this.selectedCategory = btn.dataset.category;
|
|
@@ -2277,21 +2609,46 @@ var MushiWidget = class {
|
|
|
2277
2609
|
}
|
|
2278
2610
|
this.submitting = true;
|
|
2279
2611
|
this.submittedAt = /* @__PURE__ */ new Date();
|
|
2612
|
+
this.lastReportId = null;
|
|
2613
|
+
this.lastSubmitQueuedOffline = false;
|
|
2280
2614
|
this.render();
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2615
|
+
const outcomeP = (async () => {
|
|
2616
|
+
try {
|
|
2617
|
+
const ret = this.callbacks.onSubmit({
|
|
2618
|
+
category: this.selectedCategory,
|
|
2619
|
+
description,
|
|
2620
|
+
intent: this.selectedIntent ?? void 0
|
|
2621
|
+
});
|
|
2622
|
+
if (ret && typeof ret.then === "function") {
|
|
2623
|
+
const outcome = await ret;
|
|
2624
|
+
return outcome ?? null;
|
|
2625
|
+
}
|
|
2626
|
+
return null;
|
|
2627
|
+
} catch {
|
|
2628
|
+
return { reportId: null, queuedOffline: true };
|
|
2629
|
+
}
|
|
2630
|
+
})();
|
|
2286
2631
|
this.successTimer = setTimeout(() => {
|
|
2287
2632
|
this.successTimer = null;
|
|
2288
2633
|
this.submitting = false;
|
|
2289
2634
|
this.step = "success";
|
|
2290
2635
|
this.render();
|
|
2291
|
-
|
|
2292
|
-
this.
|
|
2293
|
-
if (
|
|
2294
|
-
|
|
2636
|
+
void outcomeP.then((outcome) => {
|
|
2637
|
+
if (this.step !== "success") return;
|
|
2638
|
+
if (outcome) {
|
|
2639
|
+
this.lastReportId = outcome.reportId ?? null;
|
|
2640
|
+
this.lastSubmitQueuedOffline = Boolean(outcome.queuedOffline);
|
|
2641
|
+
this.render();
|
|
2642
|
+
}
|
|
2643
|
+
if (this.autoCloseTimer !== null) {
|
|
2644
|
+
clearTimeout(this.autoCloseTimer);
|
|
2645
|
+
}
|
|
2646
|
+
const closeDelayMs = this.lastReportId && this.config.dashboardUrl ? 6e3 : 2800;
|
|
2647
|
+
this.autoCloseTimer = setTimeout(() => {
|
|
2648
|
+
this.autoCloseTimer = null;
|
|
2649
|
+
if (this.step === "success") this.close();
|
|
2650
|
+
}, closeDelayMs);
|
|
2651
|
+
});
|
|
2295
2652
|
}, 500);
|
|
2296
2653
|
};
|
|
2297
2654
|
panel.querySelector('[data-action="submit"]')?.addEventListener("click", submitReport);
|
|
@@ -2934,16 +3291,50 @@ function truncateUrl(url) {
|
|
|
2934
3291
|
function createScreenshotCapture(options = {}) {
|
|
2935
3292
|
let activeOptions = options;
|
|
2936
3293
|
async function take() {
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
const
|
|
2944
|
-
|
|
3294
|
+
try {
|
|
3295
|
+
if (typeof document === "undefined") return null;
|
|
3296
|
+
const canvas = document.createElement("canvas");
|
|
3297
|
+
const ctx = canvas.getContext("2d");
|
|
3298
|
+
if (!ctx) return null;
|
|
3299
|
+
const width = window.innerWidth;
|
|
3300
|
+
const height = window.innerHeight;
|
|
3301
|
+
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
3302
|
+
canvas.width = width * dpr;
|
|
3303
|
+
canvas.height = height * dpr;
|
|
3304
|
+
ctx.scale(dpr, dpr);
|
|
3305
|
+
const safeDocument = buildPrivacySafeDocument(activeOptions.privacy);
|
|
3306
|
+
const svgData = `
|
|
3307
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
|
|
3308
|
+
<foreignObject width="100%" height="100%">
|
|
3309
|
+
<div xmlns="http://www.w3.org/1999/xhtml">
|
|
3310
|
+
${new XMLSerializer().serializeToString(safeDocument)}
|
|
3311
|
+
</div>
|
|
3312
|
+
</foreignObject>
|
|
3313
|
+
</svg>
|
|
3314
|
+
`;
|
|
3315
|
+
const img = new Image();
|
|
3316
|
+
const blob = new Blob([svgData], { type: "image/svg+xml;charset=utf-8" });
|
|
3317
|
+
const url = URL.createObjectURL(blob);
|
|
3318
|
+
return new Promise((resolve) => {
|
|
3319
|
+
img.onload = () => {
|
|
3320
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
3321
|
+
URL.revokeObjectURL(url);
|
|
3322
|
+
try {
|
|
3323
|
+
const dataUrl = canvas.toDataURL("image/jpeg", 0.7);
|
|
3324
|
+
resolve(dataUrl);
|
|
3325
|
+
} catch {
|
|
3326
|
+
resolve(null);
|
|
3327
|
+
}
|
|
3328
|
+
};
|
|
3329
|
+
img.onerror = () => {
|
|
3330
|
+
URL.revokeObjectURL(url);
|
|
3331
|
+
resolve(null);
|
|
3332
|
+
};
|
|
3333
|
+
img.src = url;
|
|
3334
|
+
});
|
|
3335
|
+
} catch {
|
|
3336
|
+
return null;
|
|
2945
3337
|
}
|
|
2946
|
-
return svgResult;
|
|
2947
3338
|
}
|
|
2948
3339
|
return {
|
|
2949
3340
|
take,
|
|
@@ -2952,108 +3343,16 @@ function createScreenshotCapture(options = {}) {
|
|
|
2952
3343
|
}
|
|
2953
3344
|
};
|
|
2954
3345
|
}
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
if (!ctx) return { ok: false, reason: "unsupported", message: "Canvas 2d context unavailable" };
|
|
2960
|
-
const width = window.innerWidth;
|
|
2961
|
-
const height = window.innerHeight;
|
|
2962
|
-
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
2963
|
-
canvas.width = width * dpr;
|
|
2964
|
-
canvas.height = height * dpr;
|
|
2965
|
-
ctx.scale(dpr, dpr);
|
|
2966
|
-
const safeDocument = buildPrivacySafeDocument(privacy);
|
|
2967
|
-
const svgData = `
|
|
2968
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
|
|
2969
|
-
<foreignObject width="100%" height="100%">
|
|
2970
|
-
<div xmlns="http://www.w3.org/1999/xhtml">
|
|
2971
|
-
${new XMLSerializer().serializeToString(safeDocument)}
|
|
2972
|
-
</div>
|
|
2973
|
-
</foreignObject>
|
|
2974
|
-
</svg>
|
|
2975
|
-
`;
|
|
2976
|
-
const img = new Image();
|
|
2977
|
-
const blob = new Blob([svgData], { type: "image/svg+xml;charset=utf-8" });
|
|
2978
|
-
const url = URL.createObjectURL(blob);
|
|
2979
|
-
const loadResult = await new Promise((resolve) => {
|
|
2980
|
-
img.onload = () => resolve("loaded");
|
|
2981
|
-
img.onerror = () => resolve("error");
|
|
2982
|
-
const timeout = setTimeout(() => resolve("error"), 5e3);
|
|
2983
|
-
img.onload = () => {
|
|
2984
|
-
clearTimeout(timeout);
|
|
2985
|
-
resolve("loaded");
|
|
2986
|
-
};
|
|
2987
|
-
});
|
|
2988
|
-
URL.revokeObjectURL(url);
|
|
2989
|
-
if (loadResult === "error") {
|
|
2990
|
-
return { ok: false, reason: "load-error", message: "SVG image load failed" };
|
|
2991
|
-
}
|
|
2992
|
-
ctx.drawImage(img, 0, 0, width, height);
|
|
2993
|
-
try {
|
|
2994
|
-
const dataUrl = canvas.toDataURL("image/jpeg", 0.75);
|
|
2995
|
-
return { ok: true, dataUrl };
|
|
2996
|
-
} catch (err) {
|
|
2997
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2998
|
-
return { ok: false, reason: "tainted", message };
|
|
2999
|
-
}
|
|
3000
|
-
} catch (err) {
|
|
3001
|
-
return { ok: false, reason: "error", message: err instanceof Error ? err.message : String(err) };
|
|
3002
|
-
}
|
|
3003
|
-
}
|
|
3004
|
-
async function tryDisplayMediaCapture() {
|
|
3005
|
-
if (typeof navigator === "undefined" || !("mediaDevices" in navigator)) {
|
|
3006
|
-
return { ok: false, reason: "unsupported", message: "mediaDevices not available" };
|
|
3007
|
-
}
|
|
3008
|
-
const mediaDevices = navigator.mediaDevices;
|
|
3009
|
-
if (typeof mediaDevices.getDisplayMedia !== "function") {
|
|
3010
|
-
return { ok: false, reason: "unsupported", message: "getDisplayMedia not available" };
|
|
3011
|
-
}
|
|
3012
|
-
let stream = null;
|
|
3013
|
-
try {
|
|
3014
|
-
stream = await mediaDevices.getDisplayMedia({
|
|
3015
|
-
video: { displaySurface: "browser" },
|
|
3016
|
-
audio: false
|
|
3017
|
-
});
|
|
3018
|
-
const track = stream.getVideoTracks()[0];
|
|
3019
|
-
if (!track) return { ok: false, reason: "error", message: "No video track" };
|
|
3020
|
-
const imageCapture = new window.ImageCapture(track);
|
|
3021
|
-
const bitmap = await imageCapture.grabFrame();
|
|
3022
|
-
const canvas = document.createElement("canvas");
|
|
3023
|
-
canvas.width = bitmap.width;
|
|
3024
|
-
canvas.height = bitmap.height;
|
|
3025
|
-
const ctx = canvas.getContext("2d");
|
|
3026
|
-
ctx.drawImage(bitmap, 0, 0);
|
|
3027
|
-
bitmap.close();
|
|
3028
|
-
const dataUrl = canvas.toDataURL("image/jpeg", 0.85);
|
|
3029
|
-
return { ok: true, dataUrl };
|
|
3030
|
-
} catch (err) {
|
|
3031
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
3032
|
-
if (err instanceof Error && err.name === "NotAllowedError") {
|
|
3033
|
-
return { ok: false, reason: "cancelled", message };
|
|
3034
|
-
}
|
|
3035
|
-
return { ok: false, reason: "error", message };
|
|
3036
|
-
} finally {
|
|
3037
|
-
stream?.getTracks().forEach((t) => t.stop());
|
|
3038
|
-
}
|
|
3039
|
-
}
|
|
3346
|
+
var DEFAULT_REDACT_SELECTORS = [
|
|
3347
|
+
'input[type="password"]',
|
|
3348
|
+
"[data-mushi-redact]"
|
|
3349
|
+
];
|
|
3040
3350
|
function buildPrivacySafeDocument(privacy) {
|
|
3041
3351
|
const clone = document.documentElement.cloneNode(true);
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
if (url.origin !== window.location.origin) {
|
|
3047
|
-
img.removeAttribute("src");
|
|
3048
|
-
img.removeAttribute("srcset");
|
|
3049
|
-
}
|
|
3050
|
-
} catch {
|
|
3051
|
-
}
|
|
3052
|
-
}
|
|
3053
|
-
for (const el of Array.from(clone.querySelectorAll("[style]"))) {
|
|
3054
|
-
const style = el.getAttribute("style") ?? "";
|
|
3055
|
-
if (/url\(["']?https?:\/\/(?!localhost)/.test(style)) {
|
|
3056
|
-
el.setAttribute("style", style.replace(/url\([^)]*\)/g, "none"));
|
|
3352
|
+
const redactSelectors = privacy?.redactSelectors !== void 0 ? privacy.redactSelectors : DEFAULT_REDACT_SELECTORS;
|
|
3353
|
+
for (const selector of redactSelectors) {
|
|
3354
|
+
for (const el of safeQueryAll(clone, selector)) {
|
|
3355
|
+
redactElement(el);
|
|
3057
3356
|
}
|
|
3058
3357
|
}
|
|
3059
3358
|
for (const selector of privacy?.blockSelectors ?? []) {
|
|
@@ -3075,6 +3374,19 @@ function safeQueryAll(root, selector) {
|
|
|
3075
3374
|
return [];
|
|
3076
3375
|
}
|
|
3077
3376
|
}
|
|
3377
|
+
function redactElement(el) {
|
|
3378
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
3379
|
+
el.value = "";
|
|
3380
|
+
el.setAttribute("value", "");
|
|
3381
|
+
}
|
|
3382
|
+
el.textContent = "";
|
|
3383
|
+
el.setAttribute(
|
|
3384
|
+
"style",
|
|
3385
|
+
`${el.getAttribute("style") ?? ""};background:#000!important;color:#000!important;text-shadow:none!important;border-color:#000!important;`
|
|
3386
|
+
);
|
|
3387
|
+
el.setAttribute("data-mushi-redacted", "true");
|
|
3388
|
+
while (el.firstChild) el.removeChild(el.firstChild);
|
|
3389
|
+
}
|
|
3078
3390
|
function maskElement(el) {
|
|
3079
3391
|
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
3080
3392
|
el.value = "";
|
|
@@ -3090,6 +3402,15 @@ function maskElement(el) {
|
|
|
3090
3402
|
}
|
|
3091
3403
|
|
|
3092
3404
|
// src/capture/performance.ts
|
|
3405
|
+
var INP_DURATION_THRESHOLD_MS = 40;
|
|
3406
|
+
function describeElement(target) {
|
|
3407
|
+
if (!target || !target.tagName) return void 0;
|
|
3408
|
+
const el = target;
|
|
3409
|
+
const tag = el.tagName.toLowerCase();
|
|
3410
|
+
const id = el.id ? `#${el.id}` : "";
|
|
3411
|
+
const cls = el.classList && el.classList.length > 0 ? `.${el.classList[0]}` : "";
|
|
3412
|
+
return `${tag}${id}${cls}`;
|
|
3413
|
+
}
|
|
3093
3414
|
function createPerformanceCapture() {
|
|
3094
3415
|
const metrics = {};
|
|
3095
3416
|
const observers = [];
|
|
@@ -3141,6 +3462,53 @@ function createPerformanceCapture() {
|
|
|
3141
3462
|
observers.push(longTaskObserver);
|
|
3142
3463
|
} catch {
|
|
3143
3464
|
}
|
|
3465
|
+
try {
|
|
3466
|
+
const seenInteractions = /* @__PURE__ */ new Map();
|
|
3467
|
+
const inpObserver = new PerformanceObserver((list) => {
|
|
3468
|
+
for (const entry of list.getEntries()) {
|
|
3469
|
+
const interactionId = entry.interactionId;
|
|
3470
|
+
if (!interactionId) continue;
|
|
3471
|
+
const prev = seenInteractions.get(interactionId) ?? 0;
|
|
3472
|
+
if (entry.duration > prev) {
|
|
3473
|
+
seenInteractions.set(interactionId, entry.duration);
|
|
3474
|
+
}
|
|
3475
|
+
if (entry.duration > (metrics.inp ?? 0)) {
|
|
3476
|
+
metrics.inp = entry.duration;
|
|
3477
|
+
const inputDelay = entry.processingStart - entry.startTime;
|
|
3478
|
+
const processingDuration = entry.processingEnd - entry.processingStart;
|
|
3479
|
+
const presentationDelay = entry.startTime + entry.duration - entry.processingEnd;
|
|
3480
|
+
metrics.inpAttribution = {
|
|
3481
|
+
eventType: entry.name,
|
|
3482
|
+
targetSelector: describeElement(entry.target),
|
|
3483
|
+
inputDelay: Math.max(0, inputDelay),
|
|
3484
|
+
processingDuration: Math.max(0, processingDuration),
|
|
3485
|
+
presentationDelay: Math.max(0, presentationDelay)
|
|
3486
|
+
};
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
});
|
|
3490
|
+
inpObserver.observe({
|
|
3491
|
+
type: "event",
|
|
3492
|
+
// `durationThreshold` filters out fast (< 40 ms) interactions
|
|
3493
|
+
// that sit below human perception. Spec-recommended floor.
|
|
3494
|
+
durationThreshold: INP_DURATION_THRESHOLD_MS,
|
|
3495
|
+
buffered: true
|
|
3496
|
+
});
|
|
3497
|
+
observers.push(inpObserver);
|
|
3498
|
+
} catch {
|
|
3499
|
+
}
|
|
3500
|
+
try {
|
|
3501
|
+
const fidObserver = new PerformanceObserver((list) => {
|
|
3502
|
+
for (const entry of list.getEntries()) {
|
|
3503
|
+
if (metrics.fid === void 0) {
|
|
3504
|
+
metrics.fid = entry.processingStart - entry.startTime;
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
});
|
|
3508
|
+
fidObserver.observe({ type: "first-input", buffered: true });
|
|
3509
|
+
observers.push(fidObserver);
|
|
3510
|
+
} catch {
|
|
3511
|
+
}
|
|
3144
3512
|
}
|
|
3145
3513
|
if (typeof performance !== "undefined" && performance.getEntriesByType) {
|
|
3146
3514
|
try {
|
|
@@ -3798,6 +4166,13 @@ function tagSentryScope(reportId, options = {}) {
|
|
|
3798
4166
|
}
|
|
3799
4167
|
|
|
3800
4168
|
// src/proactive-triggers.ts
|
|
4169
|
+
var DEFAULT_EXCLUDE_ROUTES = [
|
|
4170
|
+
"/login",
|
|
4171
|
+
"/logout",
|
|
4172
|
+
"/signup",
|
|
4173
|
+
"/sso/*",
|
|
4174
|
+
"/auth/*"
|
|
4175
|
+
];
|
|
3801
4176
|
function setupProactiveTriggers(callbacks, config = {}) {
|
|
3802
4177
|
const cleanups = [];
|
|
3803
4178
|
if (config.rageClick !== false) {
|
|
@@ -3868,6 +4243,83 @@ function setupProactiveTriggers(callbacks, config = {}) {
|
|
|
3868
4243
|
globalThis.fetch = origFetch;
|
|
3869
4244
|
});
|
|
3870
4245
|
}
|
|
4246
|
+
const pageDwellEnabled = config.pageDwell === true || typeof config.pageDwell === "object" && config.pageDwell !== null;
|
|
4247
|
+
if (pageDwellEnabled && typeof window !== "undefined") {
|
|
4248
|
+
let isExcluded2 = function(path) {
|
|
4249
|
+
return excludeRoutes.some((pattern) => {
|
|
4250
|
+
if (pattern.endsWith("/*")) {
|
|
4251
|
+
return path.startsWith(pattern.slice(0, -2));
|
|
4252
|
+
}
|
|
4253
|
+
return path === pattern || path.startsWith(pattern + "/");
|
|
4254
|
+
});
|
|
4255
|
+
}, fire2 = function() {
|
|
4256
|
+
const path = window.location?.pathname ?? "";
|
|
4257
|
+
if (isExcluded2(path)) return;
|
|
4258
|
+
callbacks.onTrigger("page_dwell", { thresholdMs, path });
|
|
4259
|
+
}, arm2 = function() {
|
|
4260
|
+
if (timer) clearTimeout(timer);
|
|
4261
|
+
const path = window.location?.pathname ?? "";
|
|
4262
|
+
if (!isExcluded2(path)) {
|
|
4263
|
+
timer = setTimeout(fire2, thresholdMs);
|
|
4264
|
+
}
|
|
4265
|
+
}, reset2 = function() {
|
|
4266
|
+
const path = window.location?.pathname ?? "";
|
|
4267
|
+
if (path !== lastPath) {
|
|
4268
|
+
lastPath = path;
|
|
4269
|
+
arm2();
|
|
4270
|
+
}
|
|
4271
|
+
};
|
|
4272
|
+
const dwellCfg = typeof config.pageDwell === "object" ? config.pageDwell ?? {} : {};
|
|
4273
|
+
const thresholdMs = dwellCfg.thresholdMs || 5 * 60 * 1e3;
|
|
4274
|
+
const excludeRoutes = dwellCfg.excludeRoutes !== void 0 ? dwellCfg.excludeRoutes : DEFAULT_EXCLUDE_ROUTES;
|
|
4275
|
+
let timer = null;
|
|
4276
|
+
let lastPath = window.location?.pathname ?? "";
|
|
4277
|
+
arm2();
|
|
4278
|
+
const history2 = window.history;
|
|
4279
|
+
const origPush = history2?.pushState;
|
|
4280
|
+
const origReplace = history2?.replaceState;
|
|
4281
|
+
if (history2 && origPush && origReplace) {
|
|
4282
|
+
history2.pushState = function(...args) {
|
|
4283
|
+
const result = origPush.apply(this, args);
|
|
4284
|
+
reset2();
|
|
4285
|
+
return result;
|
|
4286
|
+
};
|
|
4287
|
+
history2.replaceState = function(...args) {
|
|
4288
|
+
const result = origReplace.apply(this, args);
|
|
4289
|
+
reset2();
|
|
4290
|
+
return result;
|
|
4291
|
+
};
|
|
4292
|
+
}
|
|
4293
|
+
const onPop = () => reset2();
|
|
4294
|
+
window.addEventListener("popstate", onPop);
|
|
4295
|
+
cleanups.push(() => {
|
|
4296
|
+
if (timer) clearTimeout(timer);
|
|
4297
|
+
window.removeEventListener("popstate", onPop);
|
|
4298
|
+
if (history2 && origPush) history2.pushState = origPush;
|
|
4299
|
+
if (history2 && origReplace) history2.replaceState = origReplace;
|
|
4300
|
+
});
|
|
4301
|
+
}
|
|
4302
|
+
const firstSessionEnabled = config.firstSession === true || typeof config.firstSession === "object" && config.firstSession !== null;
|
|
4303
|
+
if (firstSessionEnabled && typeof window !== "undefined") {
|
|
4304
|
+
const opts = typeof config.firstSession === "object" ? config.firstSession ?? {} : {};
|
|
4305
|
+
const delayMs = opts.delayMs ?? 45 * 1e3;
|
|
4306
|
+
const storageKey = opts.storageKey ?? (config.projectId ? `mushi:${config.projectId}:firstSessionShown` : "mushi:firstSessionShown");
|
|
4307
|
+
let alreadyShown = false;
|
|
4308
|
+
try {
|
|
4309
|
+
alreadyShown = window.localStorage?.getItem(storageKey) === "1";
|
|
4310
|
+
} catch {
|
|
4311
|
+
}
|
|
4312
|
+
if (!alreadyShown) {
|
|
4313
|
+
const timer = setTimeout(() => {
|
|
4314
|
+
try {
|
|
4315
|
+
window.localStorage?.setItem(storageKey, "1");
|
|
4316
|
+
} catch {
|
|
4317
|
+
}
|
|
4318
|
+
callbacks.onTrigger("first_session", { delayMs });
|
|
4319
|
+
}, delayMs);
|
|
4320
|
+
cleanups.push(() => clearTimeout(timer));
|
|
4321
|
+
}
|
|
4322
|
+
}
|
|
3871
4323
|
if (config.errorBoundary) {
|
|
3872
4324
|
let handleError2 = function(event) {
|
|
3873
4325
|
callbacks.onTrigger("error_boundary", {
|
|
@@ -3971,7 +4423,7 @@ function createProactiveManager(config = {}) {
|
|
|
3971
4423
|
|
|
3972
4424
|
// src/version.ts
|
|
3973
4425
|
var MUSHI_SDK_PACKAGE = "@mushi-mushi/web";
|
|
3974
|
-
var MUSHI_SDK_VERSION = "1.
|
|
4426
|
+
var MUSHI_SDK_VERSION = "1.6.0" ;
|
|
3975
4427
|
|
|
3976
4428
|
// src/mushi.ts
|
|
3977
4429
|
var instance = null;
|
|
@@ -4156,7 +4608,8 @@ function createInstance(config) {
|
|
|
4156
4608
|
onSubmit: async ({ category, description, intent }) => {
|
|
4157
4609
|
log.info("Report submitted", { category, intent });
|
|
4158
4610
|
proactiveManager?.recordSubmission();
|
|
4159
|
-
await submitReport(category, description, intent);
|
|
4611
|
+
const outcome = await submitReport(category, description, intent);
|
|
4612
|
+
return outcome ?? { reportId: null, queuedOffline: true };
|
|
4160
4613
|
},
|
|
4161
4614
|
onOpen: () => {
|
|
4162
4615
|
log.debug("Widget opened");
|
|
@@ -4176,21 +4629,8 @@ function createInstance(config) {
|
|
|
4176
4629
|
onScreenshotRequest: async () => {
|
|
4177
4630
|
if (!screenshotCap || activeConfig.capture?.screenshot === "off") return;
|
|
4178
4631
|
log.debug("Taking screenshot");
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
if (result.ok) {
|
|
4182
|
-
pendingScreenshot = result.dataUrl;
|
|
4183
|
-
widget.setScreenshotAttached(true);
|
|
4184
|
-
log.debug("Screenshot captured");
|
|
4185
|
-
} else {
|
|
4186
|
-
pendingScreenshot = null;
|
|
4187
|
-
if (result.reason !== "cancelled") {
|
|
4188
|
-
widget.setScreenshotError(true);
|
|
4189
|
-
log.debug("Screenshot failed", { reason: result.reason, message: result.message });
|
|
4190
|
-
} else {
|
|
4191
|
-
widget.setScreenshotCapturing(false);
|
|
4192
|
-
}
|
|
4193
|
-
}
|
|
4632
|
+
pendingScreenshot = await screenshotCap.take();
|
|
4633
|
+
widget.setScreenshotAttached(pendingScreenshot !== null);
|
|
4194
4634
|
},
|
|
4195
4635
|
onScreenshotRemove: () => {
|
|
4196
4636
|
log.debug("Screenshot attachment removed");
|
|
@@ -4241,7 +4681,7 @@ function createInstance(config) {
|
|
|
4241
4681
|
let proactiveTriggers = null;
|
|
4242
4682
|
let proactiveManager = null;
|
|
4243
4683
|
const proactiveCfg = activeConfig.proactive;
|
|
4244
|
-
const hasAnyProactive = proactiveCfg && (proactiveCfg.rageClick !== false || proactiveCfg.longTask !== false || proactiveCfg.apiCascade !== false || proactiveCfg.errorBoundary === true);
|
|
4684
|
+
const hasAnyProactive = proactiveCfg && (proactiveCfg.rageClick !== false || proactiveCfg.longTask !== false || proactiveCfg.apiCascade !== false || proactiveCfg.errorBoundary === true || Boolean(proactiveCfg.pageDwell) || Boolean(proactiveCfg.firstSession));
|
|
4245
4685
|
if (hasAnyProactive && typeof document !== "undefined") {
|
|
4246
4686
|
proactiveManager = createProactiveManager(proactiveCfg?.cooldown);
|
|
4247
4687
|
proactiveTriggers = setupProactiveTriggers(
|
|
@@ -4254,7 +4694,11 @@ function createInstance(config) {
|
|
|
4254
4694
|
log.info("Proactive trigger fired", { type, context });
|
|
4255
4695
|
pendingProactiveTrigger = type;
|
|
4256
4696
|
emit("proactive:triggered", { type, context });
|
|
4257
|
-
|
|
4697
|
+
if (type === "first_session") {
|
|
4698
|
+
widget.pulseTrigger?.();
|
|
4699
|
+
} else {
|
|
4700
|
+
widget.open();
|
|
4701
|
+
}
|
|
4258
4702
|
}
|
|
4259
4703
|
},
|
|
4260
4704
|
{
|
|
@@ -4262,14 +4706,19 @@ function createInstance(config) {
|
|
|
4262
4706
|
longTask: proactiveCfg?.longTask,
|
|
4263
4707
|
apiCascade: proactiveCfg?.apiCascade,
|
|
4264
4708
|
apiEndpoint: resolveApiEndpoint(activeConfig),
|
|
4265
|
-
errorBoundary: proactiveCfg?.errorBoundary
|
|
4709
|
+
errorBoundary: proactiveCfg?.errorBoundary,
|
|
4710
|
+
pageDwell: proactiveCfg?.pageDwell,
|
|
4711
|
+
firstSession: proactiveCfg?.firstSession,
|
|
4712
|
+
projectId: bootstrapConfig.projectId
|
|
4266
4713
|
}
|
|
4267
4714
|
);
|
|
4268
4715
|
log.debug("Proactive triggers enabled", {
|
|
4269
4716
|
rageClick: proactiveCfg?.rageClick !== false,
|
|
4270
4717
|
longTask: proactiveCfg?.longTask !== false,
|
|
4271
4718
|
apiCascade: proactiveCfg?.apiCascade !== false,
|
|
4272
|
-
errorBoundary: proactiveCfg?.errorBoundary === true
|
|
4719
|
+
errorBoundary: proactiveCfg?.errorBoundary === true,
|
|
4720
|
+
pageDwell: Boolean(proactiveCfg?.pageDwell),
|
|
4721
|
+
firstSession: Boolean(proactiveCfg?.firstSession)
|
|
4273
4722
|
});
|
|
4274
4723
|
}
|
|
4275
4724
|
offlineQueue.startAutoSync(apiClient2);
|
|
@@ -4343,7 +4792,7 @@ function createInstance(config) {
|
|
|
4343
4792
|
const filterResult = preFilter.check(description);
|
|
4344
4793
|
if (!filterResult.passed) {
|
|
4345
4794
|
log.info("Report blocked by pre-filter", { reason: filterResult.reason });
|
|
4346
|
-
return;
|
|
4795
|
+
return void 0;
|
|
4347
4796
|
}
|
|
4348
4797
|
const wasm = config.preFilter?.wasmClassifier;
|
|
4349
4798
|
if (wasm) {
|
|
@@ -4364,7 +4813,7 @@ function createInstance(config) {
|
|
|
4364
4813
|
confidence: verdict.confidence,
|
|
4365
4814
|
reason: verdict.reason
|
|
4366
4815
|
});
|
|
4367
|
-
return;
|
|
4816
|
+
return void 0;
|
|
4368
4817
|
}
|
|
4369
4818
|
log.debug("On-device classifier verdict", { ...verdict });
|
|
4370
4819
|
} catch (err) {
|
|
@@ -4375,7 +4824,7 @@ function createInstance(config) {
|
|
|
4375
4824
|
}
|
|
4376
4825
|
if (!rateLimiter.tryConsume()) {
|
|
4377
4826
|
log.warn("Report throttled \u2014 rate limit exceeded");
|
|
4378
|
-
return;
|
|
4827
|
+
return void 0;
|
|
4379
4828
|
}
|
|
4380
4829
|
const scrubbedDescription = piiScrubber.scrub(preFilter.truncate(description));
|
|
4381
4830
|
const sentryCtx = config.sentry ? captureSentryContext(config.sentry) : void 0;
|
|
@@ -4448,17 +4897,43 @@ function createInstance(config) {
|
|
|
4448
4897
|
};
|
|
4449
4898
|
config.integrations.custom(builder);
|
|
4450
4899
|
}
|
|
4451
|
-
|
|
4900
|
+
let finalReport = report;
|
|
4901
|
+
if (config.beforeSendFeedback) {
|
|
4902
|
+
try {
|
|
4903
|
+
const hookResult = await Promise.race([
|
|
4904
|
+
Promise.resolve(config.beforeSendFeedback(report)),
|
|
4905
|
+
// 2s timeout — async hooks must not block the user's "submit"
|
|
4906
|
+
// for longer than the network would. Falls back to original.
|
|
4907
|
+
new Promise(
|
|
4908
|
+
(resolve) => setTimeout(() => resolve(report), 2e3)
|
|
4909
|
+
)
|
|
4910
|
+
]);
|
|
4911
|
+
if (hookResult === null) {
|
|
4912
|
+
log.info("Report dropped by beforeSendFeedback hook", { reportId: report.id });
|
|
4913
|
+
return;
|
|
4914
|
+
}
|
|
4915
|
+
finalReport = hookResult;
|
|
4916
|
+
} catch (err) {
|
|
4917
|
+
log.warn("beforeSendFeedback hook threw \u2014 sending unmodified report", {
|
|
4918
|
+
error: err instanceof Error ? err.message : String(err)
|
|
4919
|
+
});
|
|
4920
|
+
}
|
|
4921
|
+
}
|
|
4922
|
+
emit("report:submitted", { reportId: finalReport.id });
|
|
4452
4923
|
if (typeof navigator !== "undefined" && !navigator.onLine) {
|
|
4453
|
-
await offlineQueue.enqueue(
|
|
4454
|
-
log.info("Offline \u2014 report queued", { reportId:
|
|
4455
|
-
emit("report:queued", { reportId:
|
|
4924
|
+
await offlineQueue.enqueue(finalReport);
|
|
4925
|
+
log.info("Offline \u2014 report queued", { reportId: finalReport.id });
|
|
4926
|
+
emit("report:queued", { reportId: finalReport.id });
|
|
4456
4927
|
return;
|
|
4457
4928
|
}
|
|
4458
|
-
const result = await apiClient2.submitReport(
|
|
4929
|
+
const result = await apiClient2.submitReport(finalReport);
|
|
4459
4930
|
if (result.ok) {
|
|
4460
4931
|
log.info("Report sent", { reportId: result.data?.reportId });
|
|
4461
4932
|
emit("report:sent", { reportId: result.data?.reportId });
|
|
4933
|
+
if (result.data?.cursorAgentId) {
|
|
4934
|
+
const d = result.data;
|
|
4935
|
+
emit("report:dispatched", { reportId: d.reportId, agentId: d.cursorAgentId, fixId: d.fixId });
|
|
4936
|
+
}
|
|
4462
4937
|
breadcrumbs.add({
|
|
4463
4938
|
category: "lifecycle",
|
|
4464
4939
|
level: "info",
|
|
@@ -4475,18 +4950,23 @@ function createInstance(config) {
|
|
|
4475
4950
|
} catch {
|
|
4476
4951
|
}
|
|
4477
4952
|
} else {
|
|
4478
|
-
log.warn("Report failed, queuing for retry", { reportId:
|
|
4479
|
-
await offlineQueue.enqueue(
|
|
4480
|
-
emit("report:failed", { reportId:
|
|
4953
|
+
log.warn("Report failed, queuing for retry", { reportId: finalReport.id, error: result.error });
|
|
4954
|
+
await offlineQueue.enqueue(finalReport);
|
|
4955
|
+
emit("report:failed", { reportId: finalReport.id, error: result.error });
|
|
4481
4956
|
breadcrumbs.add({
|
|
4482
4957
|
category: "lifecycle",
|
|
4483
4958
|
level: "warning",
|
|
4484
|
-
message: `Mushi report queued for retry (${
|
|
4959
|
+
message: `Mushi report queued for retry (${finalReport.id})`
|
|
4485
4960
|
});
|
|
4486
4961
|
}
|
|
4487
4962
|
pendingScreenshot = null;
|
|
4488
4963
|
pendingElement = null;
|
|
4489
4964
|
pendingProactiveTrigger = null;
|
|
4965
|
+
if (result?.ok) {
|
|
4966
|
+
const serverId = result.data?.reportId ?? report.id;
|
|
4967
|
+
return { reportId: serverId, queuedOffline: false };
|
|
4968
|
+
}
|
|
4969
|
+
return { reportId: null, queuedOffline: true };
|
|
4490
4970
|
}
|
|
4491
4971
|
const sdk = {
|
|
4492
4972
|
report(options) {
|
|
@@ -4738,11 +5218,43 @@ function createInstance(config) {
|
|
|
4738
5218
|
recordActivity(action, metadata) {
|
|
4739
5219
|
if (!activeConfig.rewards?.enabled) return;
|
|
4740
5220
|
enqueue({ action, metadata });
|
|
5221
|
+
},
|
|
5222
|
+
pulseTrigger() {
|
|
5223
|
+
widget.pulseTrigger?.();
|
|
4741
5224
|
}
|
|
4742
5225
|
};
|
|
4743
5226
|
if (typeof globalThis !== "undefined" && (bootstrapConfig.debug ?? false)) {
|
|
4744
5227
|
exposeMarketingRecorder(widget);
|
|
4745
5228
|
}
|
|
5229
|
+
if (typeof window !== "undefined" && typeof localStorage !== "undefined") {
|
|
5230
|
+
const SENTINEL_KEY = "mushi:last-run";
|
|
5231
|
+
let crashed = null;
|
|
5232
|
+
try {
|
|
5233
|
+
const previous = localStorage.getItem(SENTINEL_KEY);
|
|
5234
|
+
crashed = previous === null ? null : previous === "unfinished";
|
|
5235
|
+
localStorage.setItem(SENTINEL_KEY, "unfinished");
|
|
5236
|
+
} catch {
|
|
5237
|
+
crashed = null;
|
|
5238
|
+
}
|
|
5239
|
+
try {
|
|
5240
|
+
window.addEventListener("pagehide", () => {
|
|
5241
|
+
try {
|
|
5242
|
+
localStorage.setItem(SENTINEL_KEY, "clean");
|
|
5243
|
+
} catch {
|
|
5244
|
+
}
|
|
5245
|
+
});
|
|
5246
|
+
} catch {
|
|
5247
|
+
}
|
|
5248
|
+
if (typeof bootstrapConfig.onCrashedLastRun === "function") {
|
|
5249
|
+
try {
|
|
5250
|
+
bootstrapConfig.onCrashedLastRun(crashed);
|
|
5251
|
+
} catch (err) {
|
|
5252
|
+
log.warn("onCrashedLastRun hook threw", {
|
|
5253
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5254
|
+
});
|
|
5255
|
+
}
|
|
5256
|
+
}
|
|
5257
|
+
}
|
|
4746
5258
|
return sdk;
|
|
4747
5259
|
}
|
|
4748
5260
|
function mergeRuntimeConfig(config, runtime) {
|
|
@@ -4996,6 +5508,8 @@ function createNoopInstance() {
|
|
|
4996
5508
|
getReputation: async () => null,
|
|
4997
5509
|
getTier: async () => null,
|
|
4998
5510
|
recordActivity: () => {
|
|
5511
|
+
},
|
|
5512
|
+
pulseTrigger: () => {
|
|
4999
5513
|
}
|
|
5000
5514
|
};
|
|
5001
5515
|
}
|