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