@mushi-mushi/web 1.5.0 → 1.7.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 +785 -151
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +108 -10
- package/dist/index.d.ts +108 -10
- package/dist/index.js +785 -151
- 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
|
}
|
|
@@ -1276,6 +1417,145 @@ function getWidgetStyles(theme) {
|
|
|
1276
1417
|
font-size: 10.5px;
|
|
1277
1418
|
}
|
|
1278
1419
|
|
|
1420
|
+
/* \u2500\u2500\u2500 Banner launcher (trigger: 'banner') \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
1421
|
+
|
|
1422
|
+
.mushi-banner {
|
|
1423
|
+
position: fixed;
|
|
1424
|
+
left: 0;
|
|
1425
|
+
right: 0;
|
|
1426
|
+
height: 36px;
|
|
1427
|
+
display: flex;
|
|
1428
|
+
align-items: center;
|
|
1429
|
+
justify-content: center;
|
|
1430
|
+
gap: 10px;
|
|
1431
|
+
padding: 0 16px;
|
|
1432
|
+
font-family: ${fontMono};
|
|
1433
|
+
font-size: 11.5px;
|
|
1434
|
+
letter-spacing: 0.04em;
|
|
1435
|
+
white-space: nowrap;
|
|
1436
|
+
overflow: hidden;
|
|
1437
|
+
z-index: var(--mushi-banner-z, 99998);
|
|
1438
|
+
animation: mushi-banner-slide-in 0.3s ${easeStamp} both;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
.mushi-banner.top { top: 0; }
|
|
1442
|
+
.mushi-banner.bottom { bottom: 0; }
|
|
1443
|
+
|
|
1444
|
+
/* --- neon variant (electric lime \u2014 dev / beta tool aesthetic) --- */
|
|
1445
|
+
.mushi-banner.neon {
|
|
1446
|
+
background: #0FFF50;
|
|
1447
|
+
color: #0a1a0a;
|
|
1448
|
+
border-bottom: 1.5px solid #00C43A;
|
|
1449
|
+
}
|
|
1450
|
+
.mushi-banner.neon.bottom {
|
|
1451
|
+
border-top: 1.5px solid #00C43A;
|
|
1452
|
+
border-bottom: none;
|
|
1453
|
+
}
|
|
1454
|
+
.mushi-banner.neon .mushi-banner-btn {
|
|
1455
|
+
background: rgba(0,0,0,0.14);
|
|
1456
|
+
color: #0a1a0a;
|
|
1457
|
+
border: 1px solid rgba(0,0,0,0.22);
|
|
1458
|
+
}
|
|
1459
|
+
.mushi-banner.neon .mushi-banner-btn:hover {
|
|
1460
|
+
background: rgba(0,0,0,0.22);
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
/* --- brand variant (vermillion \u2014 editorial, app-quality) --- */
|
|
1464
|
+
.mushi-banner.brand {
|
|
1465
|
+
background: ${vermillion};
|
|
1466
|
+
color: #fff;
|
|
1467
|
+
border-bottom: 1.5px solid ${isDark ? "#C4321E" : "#B52F1F"};
|
|
1468
|
+
}
|
|
1469
|
+
.mushi-banner.brand.bottom {
|
|
1470
|
+
border-top: 1.5px solid ${isDark ? "#C4321E" : "#B52F1F"};
|
|
1471
|
+
border-bottom: none;
|
|
1472
|
+
}
|
|
1473
|
+
.mushi-banner.brand .mushi-banner-btn {
|
|
1474
|
+
background: rgba(255,255,255,0.18);
|
|
1475
|
+
color: #fff;
|
|
1476
|
+
border: 1px solid rgba(255,255,255,0.32);
|
|
1477
|
+
}
|
|
1478
|
+
.mushi-banner.brand .mushi-banner-btn:hover {
|
|
1479
|
+
background: rgba(255,255,255,0.28);
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
/* --- subtle variant (hairline, muted \u2014 least disruptive) --- */
|
|
1483
|
+
.mushi-banner.subtle {
|
|
1484
|
+
background: ${isDark ? "rgba(242,235,221,0.06)" : "rgba(14,13,11,0.04)"};
|
|
1485
|
+
color: ${inkMuted};
|
|
1486
|
+
border-bottom: 1px solid ${rule};
|
|
1487
|
+
}
|
|
1488
|
+
.mushi-banner.subtle.bottom {
|
|
1489
|
+
border-top: 1px solid ${rule};
|
|
1490
|
+
border-bottom: none;
|
|
1491
|
+
}
|
|
1492
|
+
.mushi-banner.subtle .mushi-banner-btn {
|
|
1493
|
+
background: ${isDark ? "rgba(242,235,221,0.10)" : "rgba(14,13,11,0.07)"};
|
|
1494
|
+
color: ${ink};
|
|
1495
|
+
border: 1px solid ${rule};
|
|
1496
|
+
}
|
|
1497
|
+
.mushi-banner.subtle .mushi-banner-btn:hover {
|
|
1498
|
+
background: ${isDark ? "rgba(242,235,221,0.16)" : "rgba(14,13,11,0.12)"};
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
.mushi-banner-label {
|
|
1502
|
+
flex: 1;
|
|
1503
|
+
text-align: center;
|
|
1504
|
+
overflow: hidden;
|
|
1505
|
+
text-overflow: ellipsis;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
.mushi-banner-btn {
|
|
1509
|
+
display: inline-flex;
|
|
1510
|
+
align-items: center;
|
|
1511
|
+
gap: 4px;
|
|
1512
|
+
padding: 3px 10px;
|
|
1513
|
+
border-radius: 3px;
|
|
1514
|
+
cursor: pointer;
|
|
1515
|
+
font: inherit;
|
|
1516
|
+
letter-spacing: inherit;
|
|
1517
|
+
transition: background 0.15s ease, opacity 0.15s ease;
|
|
1518
|
+
flex-shrink: 0;
|
|
1519
|
+
height: 24px;
|
|
1520
|
+
line-height: 1;
|
|
1521
|
+
}
|
|
1522
|
+
.mushi-banner-btn:focus-visible {
|
|
1523
|
+
outline: 2px solid ${vermillion};
|
|
1524
|
+
outline-offset: 2px;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
.mushi-banner-dismiss {
|
|
1528
|
+
background: transparent !important;
|
|
1529
|
+
border: none !important;
|
|
1530
|
+
opacity: 0.65;
|
|
1531
|
+
cursor: pointer;
|
|
1532
|
+
font-size: 14px;
|
|
1533
|
+
line-height: 1;
|
|
1534
|
+
padding: 4px 8px;
|
|
1535
|
+
margin-left: auto;
|
|
1536
|
+
flex-shrink: 0;
|
|
1537
|
+
color: inherit;
|
|
1538
|
+
border-radius: 3px;
|
|
1539
|
+
transition: opacity 0.15s, background 0.15s;
|
|
1540
|
+
}
|
|
1541
|
+
.mushi-banner-dismiss:hover {
|
|
1542
|
+
opacity: 1;
|
|
1543
|
+
background: rgba(0,0,0,0.12) !important;
|
|
1544
|
+
}
|
|
1545
|
+
.mushi-banner.neon .mushi-banner-dismiss:hover { background: rgba(0,0,0,0.18) !important; }
|
|
1546
|
+
|
|
1547
|
+
@keyframes mushi-banner-slide-in {
|
|
1548
|
+
from { transform: translateY(calc(-1 * 100%)); opacity: 0.5; }
|
|
1549
|
+
to { transform: translateY(0); opacity: 1; }
|
|
1550
|
+
}
|
|
1551
|
+
.mushi-banner.bottom {
|
|
1552
|
+
animation-name: mushi-banner-slide-in-bottom;
|
|
1553
|
+
}
|
|
1554
|
+
@keyframes mushi-banner-slide-in-bottom {
|
|
1555
|
+
from { transform: translateY(100%); opacity: 0.5; }
|
|
1556
|
+
to { transform: translateY(0); opacity: 1; }
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1279
1559
|
@media (prefers-reduced-motion: reduce) {
|
|
1280
1560
|
*,
|
|
1281
1561
|
*::before,
|
|
@@ -1299,6 +1579,7 @@ var CATEGORY_ICONS = {
|
|
|
1299
1579
|
confusing: "\u{1F615}",
|
|
1300
1580
|
other: "\u{1F4DD}"
|
|
1301
1581
|
};
|
|
1582
|
+
var FEATURE_REQUEST_INTENT = "Feature request";
|
|
1302
1583
|
function pad2(n) {
|
|
1303
1584
|
return n < 10 ? `0${n}` : String(n);
|
|
1304
1585
|
}
|
|
@@ -1313,7 +1594,7 @@ function isSubmitShortcut(e) {
|
|
|
1313
1594
|
function escapeHtml(value) {
|
|
1314
1595
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1315
1596
|
}
|
|
1316
|
-
var MushiWidget = class {
|
|
1597
|
+
var MushiWidget = class _MushiWidget {
|
|
1317
1598
|
constructor(config = {}, callbacks, sdkVersion = "0.7.0") {
|
|
1318
1599
|
this.sdkVersion = sdkVersion;
|
|
1319
1600
|
this.config = {
|
|
@@ -1333,6 +1614,7 @@ var MushiWidget = class {
|
|
|
1333
1614
|
locale: config.locale ?? "auto",
|
|
1334
1615
|
zIndex: config.zIndex ?? 99999,
|
|
1335
1616
|
trigger: config.trigger ?? "auto",
|
|
1617
|
+
bannerConfig: config.bannerConfig ?? {},
|
|
1336
1618
|
attachToSelector: config.attachToSelector ?? "",
|
|
1337
1619
|
inset: config.inset ?? {},
|
|
1338
1620
|
respectSafeArea: config.respectSafeArea ?? true,
|
|
@@ -1344,7 +1626,12 @@ var MushiWidget = class {
|
|
|
1344
1626
|
brandFooter: config.brandFooter ?? true,
|
|
1345
1627
|
outdatedBanner: config.outdatedBanner ?? "auto",
|
|
1346
1628
|
betaMode: config.betaMode ?? {},
|
|
1347
|
-
minDescriptionLength: config.minDescriptionLength ?? 20
|
|
1629
|
+
minDescriptionLength: config.minDescriptionLength ?? 20,
|
|
1630
|
+
dashboardUrl: config.dashboardUrl ?? "",
|
|
1631
|
+
responseSlaLabel: config.responseSlaLabel ?? "",
|
|
1632
|
+
featureRequestCard: config.featureRequestCard ?? true,
|
|
1633
|
+
featureRequestLabel: config.featureRequestLabel ?? "",
|
|
1634
|
+
featureRequestDescription: config.featureRequestDescription ?? ""
|
|
1348
1635
|
};
|
|
1349
1636
|
this.callbacks = callbacks;
|
|
1350
1637
|
this.locale = getLocale(this.config.locale === "auto" ? void 0 : this.config.locale);
|
|
@@ -1362,6 +1649,13 @@ var MushiWidget = class {
|
|
|
1362
1649
|
step = "category";
|
|
1363
1650
|
selectedCategory = null;
|
|
1364
1651
|
selectedIntent = null;
|
|
1652
|
+
/**
|
|
1653
|
+
* True when the user took the "Feature request" shortcut. We track this
|
|
1654
|
+
* separately from `selectedCategory='other'` so the Back button on the
|
|
1655
|
+
* details step jumps straight back to the category picker instead of
|
|
1656
|
+
* landing on the intent picker the user explicitly skipped.
|
|
1657
|
+
*/
|
|
1658
|
+
viaFeatureRequest = false;
|
|
1365
1659
|
screenshotAttached = false;
|
|
1366
1660
|
screenshotCapturing = false;
|
|
1367
1661
|
screenshotError = false;
|
|
@@ -1399,6 +1693,19 @@ var MushiWidget = class {
|
|
|
1399
1693
|
successTimer = null;
|
|
1400
1694
|
autoCloseTimer = null;
|
|
1401
1695
|
rewardsState = null;
|
|
1696
|
+
/** Server-confirmed id for the just-submitted report. Surfaces in
|
|
1697
|
+
* the success step as a copyable receipt + optional deep link to
|
|
1698
|
+
* the Mushi console (when `dashboardUrl` is configured). Cleared
|
|
1699
|
+
* on every new `open()` so a re-opened widget never reuses a
|
|
1700
|
+
* stale id from the previous session. */
|
|
1701
|
+
lastReportId = null;
|
|
1702
|
+
/** True when the just-submitted report was queued offline (no
|
|
1703
|
+
* network, or the API errored and went into the retry queue).
|
|
1704
|
+
* Drives a different success copy so the user knows the report
|
|
1705
|
+
* hasn't actually reached the console yet. */
|
|
1706
|
+
lastSubmitQueuedOffline = false;
|
|
1707
|
+
/** Whether the user has clicked ✕ on the header banner this session. */
|
|
1708
|
+
bannerDismissed = false;
|
|
1402
1709
|
mount() {
|
|
1403
1710
|
if (this.host.isConnected) return;
|
|
1404
1711
|
document.body.appendChild(this.host);
|
|
@@ -1433,7 +1740,12 @@ var MushiWidget = class {
|
|
|
1433
1740
|
...config.brandFooter !== void 0 ? { brandFooter: config.brandFooter } : {},
|
|
1434
1741
|
...config.outdatedBanner !== void 0 ? { outdatedBanner: config.outdatedBanner } : {},
|
|
1435
1742
|
...config.betaMode !== void 0 ? { betaMode: config.betaMode } : {},
|
|
1436
|
-
...config.minDescriptionLength !== void 0 ? { minDescriptionLength: config.minDescriptionLength } : {}
|
|
1743
|
+
...config.minDescriptionLength !== void 0 ? { minDescriptionLength: config.minDescriptionLength } : {},
|
|
1744
|
+
...config.dashboardUrl !== void 0 ? { dashboardUrl: config.dashboardUrl } : {},
|
|
1745
|
+
...config.responseSlaLabel !== void 0 ? { responseSlaLabel: config.responseSlaLabel } : {},
|
|
1746
|
+
...config.featureRequestCard !== void 0 ? { featureRequestCard: config.featureRequestCard } : {},
|
|
1747
|
+
...config.featureRequestLabel !== void 0 ? { featureRequestLabel: config.featureRequestLabel } : {},
|
|
1748
|
+
...config.featureRequestDescription !== void 0 ? { featureRequestDescription: config.featureRequestDescription } : {}
|
|
1437
1749
|
};
|
|
1438
1750
|
this.locale = getLocale(this.config.locale === "auto" ? void 0 : this.config.locale);
|
|
1439
1751
|
this.syncAttachedLaunchers();
|
|
@@ -1451,7 +1763,15 @@ var MushiWidget = class {
|
|
|
1451
1763
|
this.submitting = false;
|
|
1452
1764
|
this.submittedAt = null;
|
|
1453
1765
|
this.removeSelectorHint();
|
|
1454
|
-
|
|
1766
|
+
this.lastReportId = null;
|
|
1767
|
+
this.lastSubmitQueuedOffline = false;
|
|
1768
|
+
this.viaFeatureRequest = false;
|
|
1769
|
+
if (options?.featureRequest) {
|
|
1770
|
+
this.selectedCategory = "other";
|
|
1771
|
+
this.selectedIntent = FEATURE_REQUEST_INTENT;
|
|
1772
|
+
this.viaFeatureRequest = true;
|
|
1773
|
+
this.step = "details";
|
|
1774
|
+
} else if (options?.category) {
|
|
1455
1775
|
this.selectedCategory = options.category;
|
|
1456
1776
|
this.selectedIntent = null;
|
|
1457
1777
|
this.step = "intent";
|
|
@@ -1469,6 +1789,22 @@ var MushiWidget = class {
|
|
|
1469
1789
|
this.render();
|
|
1470
1790
|
this.callbacks.onClose();
|
|
1471
1791
|
}
|
|
1792
|
+
/**
|
|
1793
|
+
* Briefly highlight the trigger button (a soft pulse + tooltip) without
|
|
1794
|
+
* opening the full reporter panel. Use for first-session welcome nudges
|
|
1795
|
+
* and other "by the way, this exists" prompts where forcing the panel
|
|
1796
|
+
* open would feel aggressive. Honours `position: 'none'` (no-op when
|
|
1797
|
+
* the trigger button is hidden).
|
|
1798
|
+
*/
|
|
1799
|
+
pulseTrigger() {
|
|
1800
|
+
if (this.isOpen) return;
|
|
1801
|
+
const trigger = this.shadow.querySelector(".mushi-trigger");
|
|
1802
|
+
if (!trigger) return;
|
|
1803
|
+
trigger.classList.add("mushi-trigger-pulse");
|
|
1804
|
+
window.setTimeout(() => {
|
|
1805
|
+
trigger.classList.remove("mushi-trigger-pulse");
|
|
1806
|
+
}, 2400);
|
|
1807
|
+
}
|
|
1472
1808
|
getIsOpen() {
|
|
1473
1809
|
return this.isOpen;
|
|
1474
1810
|
}
|
|
@@ -1678,7 +2014,7 @@ var MushiWidget = class {
|
|
|
1678
2014
|
shouldRenderTrigger() {
|
|
1679
2015
|
if (!this.triggerVisible) return false;
|
|
1680
2016
|
if (this.triggerHiddenByScroll) return false;
|
|
1681
|
-
if (this.config.trigger === "manual" || this.config.trigger === "hidden" || this.config.trigger === "attach") {
|
|
2017
|
+
if (this.config.trigger === "manual" || this.config.trigger === "hidden" || this.config.trigger === "attach" || this.config.trigger === "banner") {
|
|
1682
2018
|
return false;
|
|
1683
2019
|
}
|
|
1684
2020
|
if (this.isMobileSmartHidden()) return false;
|
|
@@ -1687,6 +2023,81 @@ var MushiWidget = class {
|
|
|
1687
2023
|
const action = this.config.environments[this.detectEnvironment()];
|
|
1688
2024
|
return action !== "never" && action !== "manual";
|
|
1689
2025
|
}
|
|
2026
|
+
/** Height of the banner in px — kept in sync with the CSS `.mushi-banner` height (36px). */
|
|
2027
|
+
static BANNER_HEIGHT = 36;
|
|
2028
|
+
/** CSS property applied to document.body so host-app content doesn't slide under the banner. */
|
|
2029
|
+
static BODY_NUDGE_PROP = "--mushi-banner-offset";
|
|
2030
|
+
applyBodyNudge(position) {
|
|
2031
|
+
const h = `${_MushiWidget.BANNER_HEIGHT}px`;
|
|
2032
|
+
if (position === "top") {
|
|
2033
|
+
document.documentElement.style.setProperty(_MushiWidget.BODY_NUDGE_PROP, h);
|
|
2034
|
+
if (!document.body.style.paddingTop) {
|
|
2035
|
+
document.body.style.paddingTop = h;
|
|
2036
|
+
document.body.dataset.mushiBannerNudged = "top";
|
|
2037
|
+
}
|
|
2038
|
+
} else {
|
|
2039
|
+
document.documentElement.style.setProperty(_MushiWidget.BODY_NUDGE_PROP, h);
|
|
2040
|
+
if (!document.body.style.paddingBottom) {
|
|
2041
|
+
document.body.style.paddingBottom = h;
|
|
2042
|
+
document.body.dataset.mushiBannerNudged = "bottom";
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
removeBodyNudge() {
|
|
2047
|
+
document.documentElement.style.removeProperty(_MushiWidget.BODY_NUDGE_PROP);
|
|
2048
|
+
const nudged = document.body.dataset.mushiBannerNudged;
|
|
2049
|
+
if (nudged === "top") {
|
|
2050
|
+
document.body.style.paddingTop = "";
|
|
2051
|
+
delete document.body.dataset.mushiBannerNudged;
|
|
2052
|
+
} else if (nudged === "bottom") {
|
|
2053
|
+
document.body.style.paddingBottom = "";
|
|
2054
|
+
delete document.body.dataset.mushiBannerNudged;
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
renderBanner() {
|
|
2058
|
+
if (this.config.trigger !== "banner") return;
|
|
2059
|
+
if (this.bannerDismissed) {
|
|
2060
|
+
this.removeBodyNudge();
|
|
2061
|
+
return;
|
|
2062
|
+
}
|
|
2063
|
+
if (!this.triggerVisible) return;
|
|
2064
|
+
if (this.isRouteHidden()) return;
|
|
2065
|
+
const bc = this.config.bannerConfig ?? {};
|
|
2066
|
+
const variant = bc.variant ?? "brand";
|
|
2067
|
+
const position = bc.position ?? "top";
|
|
2068
|
+
const bugLabel = bc.bugCta ?? "\u{1F41B} Report a bug";
|
|
2069
|
+
const showFeat = bc.featureCta !== false;
|
|
2070
|
+
const featLabel = bc.featureCtaLabel ?? "\u2728 Request feature";
|
|
2071
|
+
const zIdx = bc.zIndex ?? (this.config.zIndex ?? 99999) - 1;
|
|
2072
|
+
const banner = document.createElement("div");
|
|
2073
|
+
banner.className = `mushi-banner ${variant} ${position}`;
|
|
2074
|
+
banner.style.setProperty("--mushi-banner-z", String(zIdx));
|
|
2075
|
+
banner.setAttribute("role", "banner");
|
|
2076
|
+
const bugBtn = document.createElement("button");
|
|
2077
|
+
bugBtn.className = "mushi-banner-btn";
|
|
2078
|
+
bugBtn.textContent = bugLabel;
|
|
2079
|
+
bugBtn.addEventListener("click", () => this.open());
|
|
2080
|
+
const dismissBtn = document.createElement("button");
|
|
2081
|
+
dismissBtn.className = "mushi-banner-dismiss";
|
|
2082
|
+
dismissBtn.textContent = "\u2715";
|
|
2083
|
+
dismissBtn.setAttribute("aria-label", "Dismiss feedback banner");
|
|
2084
|
+
dismissBtn.addEventListener("click", () => {
|
|
2085
|
+
this.bannerDismissed = true;
|
|
2086
|
+
this.removeBodyNudge();
|
|
2087
|
+
this.render();
|
|
2088
|
+
});
|
|
2089
|
+
banner.appendChild(bugBtn);
|
|
2090
|
+
if (showFeat) {
|
|
2091
|
+
const featBtn = document.createElement("button");
|
|
2092
|
+
featBtn.className = "mushi-banner-btn";
|
|
2093
|
+
featBtn.textContent = featLabel;
|
|
2094
|
+
featBtn.addEventListener("click", () => this.open({ featureRequest: true }));
|
|
2095
|
+
banner.appendChild(featBtn);
|
|
2096
|
+
}
|
|
2097
|
+
banner.appendChild(dismissBtn);
|
|
2098
|
+
this.shadow.appendChild(banner);
|
|
2099
|
+
this.applyBodyNudge(position);
|
|
2100
|
+
}
|
|
1690
2101
|
effectiveTrigger() {
|
|
1691
2102
|
if (!this.config.smartHide || typeof window === "undefined") return this.config.trigger;
|
|
1692
2103
|
const smart = this.config.smartHide === true ? { onMobile: "edge-tab" } : this.config.smartHide;
|
|
@@ -1725,6 +2136,7 @@ var MushiWidget = class {
|
|
|
1725
2136
|
const style = document.createElement("style");
|
|
1726
2137
|
style.textContent = getWidgetStyles(theme);
|
|
1727
2138
|
this.shadow.appendChild(style);
|
|
2139
|
+
this.renderBanner();
|
|
1728
2140
|
if (this.shouldRenderTrigger()) {
|
|
1729
2141
|
const effectiveTrigger = this.effectiveTrigger();
|
|
1730
2142
|
const trigger = document.createElement("button");
|
|
@@ -1891,12 +2303,46 @@ var MushiWidget = class {
|
|
|
1891
2303
|
</div>
|
|
1892
2304
|
<span class="mushi-option-arrow" aria-hidden="true">\u2192</span>
|
|
1893
2305
|
</button>
|
|
2306
|
+
${this.renderFeatureRequestEntry()}
|
|
1894
2307
|
${categories}
|
|
1895
2308
|
${this.rewardsState ? this.renderRewardsNudge() : ""}
|
|
1896
2309
|
</div>
|
|
1897
2310
|
${this.renderStepIndicator(STEP_NUMBER.category)}
|
|
1898
2311
|
`;
|
|
1899
2312
|
}
|
|
2313
|
+
/**
|
|
2314
|
+
* First-class "Feature request" entry rendered at the top of the
|
|
2315
|
+
* category step. Beta apps consistently get more useful signal when
|
|
2316
|
+
* the user has a no-friction path to say "I wish this did X" — burying
|
|
2317
|
+
* it as an intent under the "Other" category drops feature submissions
|
|
2318
|
+
* by ~40% in industry studies (Userpilot, Usersnap 2025).
|
|
2319
|
+
*
|
|
2320
|
+
* Wire format: still routes through the standard `other` category with
|
|
2321
|
+
* a `user_category = 'Feature request'` stamp, so we don't need a DB
|
|
2322
|
+
* migration. The admin console filters on that string to surface the
|
|
2323
|
+
* Feature-request swimlane.
|
|
2324
|
+
*/
|
|
2325
|
+
renderFeatureRequestEntry() {
|
|
2326
|
+
const enabled = this.config.featureRequestCard !== false;
|
|
2327
|
+
if (!enabled) return "";
|
|
2328
|
+
const label = this.config.featureRequestLabel ?? "Feature request";
|
|
2329
|
+
const desc = this.config.featureRequestDescription ?? "Suggest something new \u2014 even rough ideas help us prioritise";
|
|
2330
|
+
return `
|
|
2331
|
+
<button
|
|
2332
|
+
type="button"
|
|
2333
|
+
class="mushi-option-btn mushi-feature-entry"
|
|
2334
|
+
data-action="feature-request"
|
|
2335
|
+
aria-label="${escapeHtml(label)}"
|
|
2336
|
+
>
|
|
2337
|
+
<span class="mushi-option-icon" aria-hidden="true">\u2728</span>
|
|
2338
|
+
<div class="mushi-option-text">
|
|
2339
|
+
<span class="mushi-option-label">${escapeHtml(label)}</span>
|
|
2340
|
+
<span class="mushi-option-desc">${escapeHtml(desc)}</span>
|
|
2341
|
+
</div>
|
|
2342
|
+
<span class="mushi-option-arrow" aria-hidden="true">\u2192</span>
|
|
2343
|
+
</button>
|
|
2344
|
+
`;
|
|
2345
|
+
}
|
|
1900
2346
|
/** Collapsible "What's new" changelog row. Closes the reporter feedback loop. */
|
|
1901
2347
|
renderBetaChangelog() {
|
|
1902
2348
|
const entries = this.config.betaMode?.changelogItems;
|
|
@@ -2095,12 +2541,81 @@ var MushiWidget = class {
|
|
|
2095
2541
|
</div>
|
|
2096
2542
|
<div class="mushi-success-headline">${t.widget.submitted}</div>
|
|
2097
2543
|
<div class="mushi-success-meta">REPORT \xB7 ${time}</div>
|
|
2544
|
+
${this.renderSuccessReceipt()}
|
|
2098
2545
|
${this.rewardsState ? this.renderSuccessRewards() : ""}
|
|
2099
2546
|
${this.config.betaMode?.enabled ? this.renderBetaSuccessFooter() : ""}
|
|
2100
2547
|
</div>
|
|
2101
2548
|
</div>
|
|
2102
2549
|
`;
|
|
2103
2550
|
}
|
|
2551
|
+
/**
|
|
2552
|
+
* Two-way receipt block. Until the host's `onSubmit` resolves with a
|
|
2553
|
+
* server-confirmed report id, we show a discreet "delivering..." pill so
|
|
2554
|
+
* the user knows their submission is still in flight. Once we have the
|
|
2555
|
+
* id, we surface a short monospaced id + a copy button + an optional
|
|
2556
|
+
* "Track on Mushi" deep link to `dashboardUrl/reports/<id>` so the user
|
|
2557
|
+
* can watch the status walk through queued -> classified -> fixed in
|
|
2558
|
+
* real time (Peak-End rule: the last impression sticks). If we never
|
|
2559
|
+
* get an id (offline retry queue), we say so explicitly rather than
|
|
2560
|
+
* pretending everything is fine.
|
|
2561
|
+
*/
|
|
2562
|
+
renderSuccessReceipt() {
|
|
2563
|
+
if (this.lastSubmitQueuedOffline) {
|
|
2564
|
+
return `
|
|
2565
|
+
<div class="mushi-success-receipt" role="status">
|
|
2566
|
+
<div class="mushi-success-receipt-row mushi-success-receipt-warn">
|
|
2567
|
+
<span class="mushi-success-receipt-label">Queued offline</span>
|
|
2568
|
+
<span class="mushi-success-receipt-hint">We’ll send it the moment you’re back online.</span>
|
|
2569
|
+
</div>
|
|
2570
|
+
</div>
|
|
2571
|
+
`;
|
|
2572
|
+
}
|
|
2573
|
+
if (!this.lastReportId) {
|
|
2574
|
+
return `
|
|
2575
|
+
<div class="mushi-success-receipt" role="status">
|
|
2576
|
+
<div class="mushi-success-receipt-row">
|
|
2577
|
+
<span class="mushi-success-receipt-spinner" aria-hidden="true"></span>
|
|
2578
|
+
<span class="mushi-success-receipt-hint">Delivering to the team\u2026</span>
|
|
2579
|
+
</div>
|
|
2580
|
+
${this.renderSlaLine()}
|
|
2581
|
+
</div>
|
|
2582
|
+
`;
|
|
2583
|
+
}
|
|
2584
|
+
const idShort = `#${this.lastReportId.slice(0, 8)}`;
|
|
2585
|
+
const dashboard = (this.config.dashboardUrl ?? "").replace(/\/$/, "");
|
|
2586
|
+
const trackHref = dashboard ? `${dashboard}/reports/${encodeURIComponent(this.lastReportId)}` : "";
|
|
2587
|
+
return `
|
|
2588
|
+
<div class="mushi-success-receipt" role="status">
|
|
2589
|
+
<div class="mushi-success-receipt-row">
|
|
2590
|
+
<span class="mushi-success-receipt-label">Receipt</span>
|
|
2591
|
+
<button
|
|
2592
|
+
type="button"
|
|
2593
|
+
class="mushi-success-receipt-id"
|
|
2594
|
+
data-action="copy-report-id"
|
|
2595
|
+
data-copy-id="${escapeHtml(this.lastReportId)}"
|
|
2596
|
+
title="Copy report id ${escapeHtml(this.lastReportId)}"
|
|
2597
|
+
aria-label="Copy report id ${escapeHtml(this.lastReportId)}"
|
|
2598
|
+
>${escapeHtml(idShort)}<span class="mushi-success-receipt-copy" aria-hidden="true">\u2398</span></button>
|
|
2599
|
+
</div>
|
|
2600
|
+
${trackHref ? `
|
|
2601
|
+
<a
|
|
2602
|
+
class="mushi-success-receipt-track"
|
|
2603
|
+
href="${escapeHtml(trackHref)}"
|
|
2604
|
+
target="_blank"
|
|
2605
|
+
rel="noopener noreferrer"
|
|
2606
|
+
>Track on Mushi <span aria-hidden="true">\u2197</span></a>
|
|
2607
|
+
` : ""}
|
|
2608
|
+
${this.renderSlaLine()}
|
|
2609
|
+
</div>
|
|
2610
|
+
`;
|
|
2611
|
+
}
|
|
2612
|
+
renderSlaLine() {
|
|
2613
|
+
const sla = (this.config.responseSlaLabel ?? "").trim();
|
|
2614
|
+
if (sla) {
|
|
2615
|
+
return `<div class="mushi-success-sla">${escapeHtml(sla)}</div>`;
|
|
2616
|
+
}
|
|
2617
|
+
return `<div class="mushi-success-sla mushi-success-sla-default">A human will look at this within a working day.</div>`;
|
|
2618
|
+
}
|
|
2104
2619
|
/**
|
|
2105
2620
|
* Reciprocity footer on the success step: closes the feedback loop by
|
|
2106
2621
|
* attributing where the report goes, sets a response expectation, and
|
|
@@ -2192,8 +2707,15 @@ var MushiWidget = class {
|
|
|
2192
2707
|
this.step = "category";
|
|
2193
2708
|
this.selectedCategory = null;
|
|
2194
2709
|
} else if (this.step === "details") {
|
|
2195
|
-
this.
|
|
2196
|
-
|
|
2710
|
+
if (this.viaFeatureRequest) {
|
|
2711
|
+
this.step = "category";
|
|
2712
|
+
this.selectedCategory = null;
|
|
2713
|
+
this.selectedIntent = null;
|
|
2714
|
+
this.viaFeatureRequest = false;
|
|
2715
|
+
} else {
|
|
2716
|
+
this.step = "intent";
|
|
2717
|
+
this.selectedIntent = null;
|
|
2718
|
+
}
|
|
2197
2719
|
} else if (this.step === "reports") {
|
|
2198
2720
|
this.step = "category";
|
|
2199
2721
|
} else if (this.step === "report-detail") {
|
|
@@ -2205,6 +2727,13 @@ var MushiWidget = class {
|
|
|
2205
2727
|
panel.querySelector('[data-action="reports"]')?.addEventListener("click", () => {
|
|
2206
2728
|
void this.loadReporterReports();
|
|
2207
2729
|
});
|
|
2730
|
+
panel.querySelector('[data-action="feature-request"]')?.addEventListener("click", () => {
|
|
2731
|
+
this.selectedCategory = "other";
|
|
2732
|
+
this.selectedIntent = FEATURE_REQUEST_INTENT;
|
|
2733
|
+
this.viaFeatureRequest = true;
|
|
2734
|
+
this.step = "details";
|
|
2735
|
+
this.render();
|
|
2736
|
+
});
|
|
2208
2737
|
panel.querySelectorAll("[data-report-id]").forEach((btn) => {
|
|
2209
2738
|
btn.addEventListener("click", () => {
|
|
2210
2739
|
const reportId = btn.dataset.reportId;
|
|
@@ -2214,6 +2743,27 @@ var MushiWidget = class {
|
|
|
2214
2743
|
panel.querySelector('[data-action="reporter-reply"]')?.addEventListener("click", () => {
|
|
2215
2744
|
void this.submitReporterReply(panel);
|
|
2216
2745
|
});
|
|
2746
|
+
panel.querySelector('[data-action="copy-report-id"]')?.addEventListener("click", (e) => {
|
|
2747
|
+
const btn = e.currentTarget;
|
|
2748
|
+
const id = btn.dataset.copyId;
|
|
2749
|
+
if (!id) return;
|
|
2750
|
+
const restore = btn.innerHTML;
|
|
2751
|
+
const done = () => {
|
|
2752
|
+
btn.innerHTML = "Copied \u2713";
|
|
2753
|
+
window.setTimeout(() => {
|
|
2754
|
+
if (btn.isConnected) btn.innerHTML = restore;
|
|
2755
|
+
}, 1600);
|
|
2756
|
+
};
|
|
2757
|
+
try {
|
|
2758
|
+
if (navigator.clipboard?.writeText) {
|
|
2759
|
+
void navigator.clipboard.writeText(id).then(done).catch(() => done());
|
|
2760
|
+
} else {
|
|
2761
|
+
done();
|
|
2762
|
+
}
|
|
2763
|
+
} catch {
|
|
2764
|
+
done();
|
|
2765
|
+
}
|
|
2766
|
+
});
|
|
2217
2767
|
panel.querySelectorAll("[data-category]").forEach((btn) => {
|
|
2218
2768
|
btn.addEventListener("click", () => {
|
|
2219
2769
|
this.selectedCategory = btn.dataset.category;
|
|
@@ -2277,21 +2827,46 @@ var MushiWidget = class {
|
|
|
2277
2827
|
}
|
|
2278
2828
|
this.submitting = true;
|
|
2279
2829
|
this.submittedAt = /* @__PURE__ */ new Date();
|
|
2830
|
+
this.lastReportId = null;
|
|
2831
|
+
this.lastSubmitQueuedOffline = false;
|
|
2280
2832
|
this.render();
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2833
|
+
const outcomeP = (async () => {
|
|
2834
|
+
try {
|
|
2835
|
+
const ret = this.callbacks.onSubmit({
|
|
2836
|
+
category: this.selectedCategory,
|
|
2837
|
+
description,
|
|
2838
|
+
intent: this.selectedIntent ?? void 0
|
|
2839
|
+
});
|
|
2840
|
+
if (ret && typeof ret.then === "function") {
|
|
2841
|
+
const outcome = await ret;
|
|
2842
|
+
return outcome ?? null;
|
|
2843
|
+
}
|
|
2844
|
+
return null;
|
|
2845
|
+
} catch {
|
|
2846
|
+
return { reportId: null, queuedOffline: true };
|
|
2847
|
+
}
|
|
2848
|
+
})();
|
|
2286
2849
|
this.successTimer = setTimeout(() => {
|
|
2287
2850
|
this.successTimer = null;
|
|
2288
2851
|
this.submitting = false;
|
|
2289
2852
|
this.step = "success";
|
|
2290
2853
|
this.render();
|
|
2291
|
-
|
|
2292
|
-
this.
|
|
2293
|
-
if (
|
|
2294
|
-
|
|
2854
|
+
void outcomeP.then((outcome) => {
|
|
2855
|
+
if (this.step !== "success") return;
|
|
2856
|
+
if (outcome) {
|
|
2857
|
+
this.lastReportId = outcome.reportId ?? null;
|
|
2858
|
+
this.lastSubmitQueuedOffline = Boolean(outcome.queuedOffline);
|
|
2859
|
+
this.render();
|
|
2860
|
+
}
|
|
2861
|
+
if (this.autoCloseTimer !== null) {
|
|
2862
|
+
clearTimeout(this.autoCloseTimer);
|
|
2863
|
+
}
|
|
2864
|
+
const closeDelayMs = this.lastReportId && this.config.dashboardUrl ? 6e3 : 2800;
|
|
2865
|
+
this.autoCloseTimer = setTimeout(() => {
|
|
2866
|
+
this.autoCloseTimer = null;
|
|
2867
|
+
if (this.step === "success") this.close();
|
|
2868
|
+
}, closeDelayMs);
|
|
2869
|
+
});
|
|
2295
2870
|
}, 500);
|
|
2296
2871
|
};
|
|
2297
2872
|
panel.querySelector('[data-action="submit"]')?.addEventListener("click", submitReport);
|
|
@@ -2934,16 +3509,50 @@ function truncateUrl(url) {
|
|
|
2934
3509
|
function createScreenshotCapture(options = {}) {
|
|
2935
3510
|
let activeOptions = options;
|
|
2936
3511
|
async function take() {
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
const
|
|
2944
|
-
|
|
3512
|
+
try {
|
|
3513
|
+
if (typeof document === "undefined") return null;
|
|
3514
|
+
const canvas = document.createElement("canvas");
|
|
3515
|
+
const ctx = canvas.getContext("2d");
|
|
3516
|
+
if (!ctx) return null;
|
|
3517
|
+
const width = window.innerWidth;
|
|
3518
|
+
const height = window.innerHeight;
|
|
3519
|
+
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
3520
|
+
canvas.width = width * dpr;
|
|
3521
|
+
canvas.height = height * dpr;
|
|
3522
|
+
ctx.scale(dpr, dpr);
|
|
3523
|
+
const safeDocument = buildPrivacySafeDocument(activeOptions.privacy);
|
|
3524
|
+
const svgData = `
|
|
3525
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
|
|
3526
|
+
<foreignObject width="100%" height="100%">
|
|
3527
|
+
<div xmlns="http://www.w3.org/1999/xhtml">
|
|
3528
|
+
${new XMLSerializer().serializeToString(safeDocument)}
|
|
3529
|
+
</div>
|
|
3530
|
+
</foreignObject>
|
|
3531
|
+
</svg>
|
|
3532
|
+
`;
|
|
3533
|
+
const img = new Image();
|
|
3534
|
+
const blob = new Blob([svgData], { type: "image/svg+xml;charset=utf-8" });
|
|
3535
|
+
const url = URL.createObjectURL(blob);
|
|
3536
|
+
return new Promise((resolve) => {
|
|
3537
|
+
img.onload = () => {
|
|
3538
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
3539
|
+
URL.revokeObjectURL(url);
|
|
3540
|
+
try {
|
|
3541
|
+
const dataUrl = canvas.toDataURL("image/jpeg", 0.7);
|
|
3542
|
+
resolve(dataUrl);
|
|
3543
|
+
} catch {
|
|
3544
|
+
resolve(null);
|
|
3545
|
+
}
|
|
3546
|
+
};
|
|
3547
|
+
img.onerror = () => {
|
|
3548
|
+
URL.revokeObjectURL(url);
|
|
3549
|
+
resolve(null);
|
|
3550
|
+
};
|
|
3551
|
+
img.src = url;
|
|
3552
|
+
});
|
|
3553
|
+
} catch {
|
|
3554
|
+
return null;
|
|
2945
3555
|
}
|
|
2946
|
-
return svgResult;
|
|
2947
3556
|
}
|
|
2948
3557
|
return {
|
|
2949
3558
|
take,
|
|
@@ -2952,108 +3561,16 @@ function createScreenshotCapture(options = {}) {
|
|
|
2952
3561
|
}
|
|
2953
3562
|
};
|
|
2954
3563
|
}
|
|
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
|
-
}
|
|
3564
|
+
var DEFAULT_REDACT_SELECTORS = [
|
|
3565
|
+
'input[type="password"]',
|
|
3566
|
+
"[data-mushi-redact]"
|
|
3567
|
+
];
|
|
3040
3568
|
function buildPrivacySafeDocument(privacy) {
|
|
3041
3569
|
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"));
|
|
3570
|
+
const redactSelectors = privacy?.redactSelectors !== void 0 ? privacy.redactSelectors : DEFAULT_REDACT_SELECTORS;
|
|
3571
|
+
for (const selector of redactSelectors) {
|
|
3572
|
+
for (const el of safeQueryAll(clone, selector)) {
|
|
3573
|
+
redactElement(el);
|
|
3057
3574
|
}
|
|
3058
3575
|
}
|
|
3059
3576
|
for (const selector of privacy?.blockSelectors ?? []) {
|
|
@@ -3075,6 +3592,19 @@ function safeQueryAll(root, selector) {
|
|
|
3075
3592
|
return [];
|
|
3076
3593
|
}
|
|
3077
3594
|
}
|
|
3595
|
+
function redactElement(el) {
|
|
3596
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
3597
|
+
el.value = "";
|
|
3598
|
+
el.setAttribute("value", "");
|
|
3599
|
+
}
|
|
3600
|
+
el.textContent = "";
|
|
3601
|
+
el.setAttribute(
|
|
3602
|
+
"style",
|
|
3603
|
+
`${el.getAttribute("style") ?? ""};background:#000!important;color:#000!important;text-shadow:none!important;border-color:#000!important;`
|
|
3604
|
+
);
|
|
3605
|
+
el.setAttribute("data-mushi-redacted", "true");
|
|
3606
|
+
while (el.firstChild) el.removeChild(el.firstChild);
|
|
3607
|
+
}
|
|
3078
3608
|
function maskElement(el) {
|
|
3079
3609
|
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
3080
3610
|
el.value = "";
|
|
@@ -3854,6 +4384,13 @@ function tagSentryScope(reportId, options = {}) {
|
|
|
3854
4384
|
}
|
|
3855
4385
|
|
|
3856
4386
|
// src/proactive-triggers.ts
|
|
4387
|
+
var DEFAULT_EXCLUDE_ROUTES = [
|
|
4388
|
+
"/login",
|
|
4389
|
+
"/logout",
|
|
4390
|
+
"/signup",
|
|
4391
|
+
"/sso/*",
|
|
4392
|
+
"/auth/*"
|
|
4393
|
+
];
|
|
3857
4394
|
function setupProactiveTriggers(callbacks, config = {}) {
|
|
3858
4395
|
const cleanups = [];
|
|
3859
4396
|
if (config.rageClick !== false) {
|
|
@@ -3924,6 +4461,83 @@ function setupProactiveTriggers(callbacks, config = {}) {
|
|
|
3924
4461
|
globalThis.fetch = origFetch;
|
|
3925
4462
|
});
|
|
3926
4463
|
}
|
|
4464
|
+
const pageDwellEnabled = config.pageDwell === true || typeof config.pageDwell === "object" && config.pageDwell !== null;
|
|
4465
|
+
if (pageDwellEnabled && typeof window !== "undefined") {
|
|
4466
|
+
let isExcluded2 = function(path) {
|
|
4467
|
+
return excludeRoutes.some((pattern) => {
|
|
4468
|
+
if (pattern.endsWith("/*")) {
|
|
4469
|
+
return path.startsWith(pattern.slice(0, -2));
|
|
4470
|
+
}
|
|
4471
|
+
return path === pattern || path.startsWith(pattern + "/");
|
|
4472
|
+
});
|
|
4473
|
+
}, fire2 = function() {
|
|
4474
|
+
const path = window.location?.pathname ?? "";
|
|
4475
|
+
if (isExcluded2(path)) return;
|
|
4476
|
+
callbacks.onTrigger("page_dwell", { thresholdMs, path });
|
|
4477
|
+
}, arm2 = function() {
|
|
4478
|
+
if (timer) clearTimeout(timer);
|
|
4479
|
+
const path = window.location?.pathname ?? "";
|
|
4480
|
+
if (!isExcluded2(path)) {
|
|
4481
|
+
timer = setTimeout(fire2, thresholdMs);
|
|
4482
|
+
}
|
|
4483
|
+
}, reset2 = function() {
|
|
4484
|
+
const path = window.location?.pathname ?? "";
|
|
4485
|
+
if (path !== lastPath) {
|
|
4486
|
+
lastPath = path;
|
|
4487
|
+
arm2();
|
|
4488
|
+
}
|
|
4489
|
+
};
|
|
4490
|
+
const dwellCfg = typeof config.pageDwell === "object" ? config.pageDwell ?? {} : {};
|
|
4491
|
+
const thresholdMs = dwellCfg.thresholdMs || 5 * 60 * 1e3;
|
|
4492
|
+
const excludeRoutes = dwellCfg.excludeRoutes !== void 0 ? dwellCfg.excludeRoutes : DEFAULT_EXCLUDE_ROUTES;
|
|
4493
|
+
let timer = null;
|
|
4494
|
+
let lastPath = window.location?.pathname ?? "";
|
|
4495
|
+
arm2();
|
|
4496
|
+
const history2 = window.history;
|
|
4497
|
+
const origPush = history2?.pushState;
|
|
4498
|
+
const origReplace = history2?.replaceState;
|
|
4499
|
+
if (history2 && origPush && origReplace) {
|
|
4500
|
+
history2.pushState = function(...args) {
|
|
4501
|
+
const result = origPush.apply(this, args);
|
|
4502
|
+
reset2();
|
|
4503
|
+
return result;
|
|
4504
|
+
};
|
|
4505
|
+
history2.replaceState = function(...args) {
|
|
4506
|
+
const result = origReplace.apply(this, args);
|
|
4507
|
+
reset2();
|
|
4508
|
+
return result;
|
|
4509
|
+
};
|
|
4510
|
+
}
|
|
4511
|
+
const onPop = () => reset2();
|
|
4512
|
+
window.addEventListener("popstate", onPop);
|
|
4513
|
+
cleanups.push(() => {
|
|
4514
|
+
if (timer) clearTimeout(timer);
|
|
4515
|
+
window.removeEventListener("popstate", onPop);
|
|
4516
|
+
if (history2 && origPush) history2.pushState = origPush;
|
|
4517
|
+
if (history2 && origReplace) history2.replaceState = origReplace;
|
|
4518
|
+
});
|
|
4519
|
+
}
|
|
4520
|
+
const firstSessionEnabled = config.firstSession === true || typeof config.firstSession === "object" && config.firstSession !== null;
|
|
4521
|
+
if (firstSessionEnabled && typeof window !== "undefined") {
|
|
4522
|
+
const opts = typeof config.firstSession === "object" ? config.firstSession ?? {} : {};
|
|
4523
|
+
const delayMs = opts.delayMs ?? 45 * 1e3;
|
|
4524
|
+
const storageKey = opts.storageKey ?? (config.projectId ? `mushi:${config.projectId}:firstSessionShown` : "mushi:firstSessionShown");
|
|
4525
|
+
let alreadyShown = false;
|
|
4526
|
+
try {
|
|
4527
|
+
alreadyShown = window.localStorage?.getItem(storageKey) === "1";
|
|
4528
|
+
} catch {
|
|
4529
|
+
}
|
|
4530
|
+
if (!alreadyShown) {
|
|
4531
|
+
const timer = setTimeout(() => {
|
|
4532
|
+
try {
|
|
4533
|
+
window.localStorage?.setItem(storageKey, "1");
|
|
4534
|
+
} catch {
|
|
4535
|
+
}
|
|
4536
|
+
callbacks.onTrigger("first_session", { delayMs });
|
|
4537
|
+
}, delayMs);
|
|
4538
|
+
cleanups.push(() => clearTimeout(timer));
|
|
4539
|
+
}
|
|
4540
|
+
}
|
|
3927
4541
|
if (config.errorBoundary) {
|
|
3928
4542
|
let handleError2 = function(event) {
|
|
3929
4543
|
callbacks.onTrigger("error_boundary", {
|
|
@@ -4027,7 +4641,7 @@ function createProactiveManager(config = {}) {
|
|
|
4027
4641
|
|
|
4028
4642
|
// src/version.ts
|
|
4029
4643
|
var MUSHI_SDK_PACKAGE = "@mushi-mushi/web";
|
|
4030
|
-
var MUSHI_SDK_VERSION = "1.
|
|
4644
|
+
var MUSHI_SDK_VERSION = "1.7.0" ;
|
|
4031
4645
|
|
|
4032
4646
|
// src/mushi.ts
|
|
4033
4647
|
var instance = null;
|
|
@@ -4212,7 +4826,8 @@ function createInstance(config) {
|
|
|
4212
4826
|
onSubmit: async ({ category, description, intent }) => {
|
|
4213
4827
|
log.info("Report submitted", { category, intent });
|
|
4214
4828
|
proactiveManager?.recordSubmission();
|
|
4215
|
-
await submitReport(category, description, intent);
|
|
4829
|
+
const outcome = await submitReport(category, description, intent);
|
|
4830
|
+
return outcome ?? { reportId: null, queuedOffline: true };
|
|
4216
4831
|
},
|
|
4217
4832
|
onOpen: () => {
|
|
4218
4833
|
log.debug("Widget opened");
|
|
@@ -4232,21 +4847,8 @@ function createInstance(config) {
|
|
|
4232
4847
|
onScreenshotRequest: async () => {
|
|
4233
4848
|
if (!screenshotCap || activeConfig.capture?.screenshot === "off") return;
|
|
4234
4849
|
log.debug("Taking screenshot");
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
if (result.ok) {
|
|
4238
|
-
pendingScreenshot = result.dataUrl;
|
|
4239
|
-
widget.setScreenshotAttached(true);
|
|
4240
|
-
log.debug("Screenshot captured");
|
|
4241
|
-
} else {
|
|
4242
|
-
pendingScreenshot = null;
|
|
4243
|
-
if (result.reason !== "cancelled") {
|
|
4244
|
-
widget.setScreenshotError(true);
|
|
4245
|
-
log.debug("Screenshot failed", { reason: result.reason, message: result.message });
|
|
4246
|
-
} else {
|
|
4247
|
-
widget.setScreenshotCapturing(false);
|
|
4248
|
-
}
|
|
4249
|
-
}
|
|
4850
|
+
pendingScreenshot = await screenshotCap.take();
|
|
4851
|
+
widget.setScreenshotAttached(pendingScreenshot !== null);
|
|
4250
4852
|
},
|
|
4251
4853
|
onScreenshotRemove: () => {
|
|
4252
4854
|
log.debug("Screenshot attachment removed");
|
|
@@ -4297,7 +4899,7 @@ function createInstance(config) {
|
|
|
4297
4899
|
let proactiveTriggers = null;
|
|
4298
4900
|
let proactiveManager = null;
|
|
4299
4901
|
const proactiveCfg = activeConfig.proactive;
|
|
4300
|
-
const hasAnyProactive = proactiveCfg && (proactiveCfg.rageClick !== false || proactiveCfg.longTask !== false || proactiveCfg.apiCascade !== false || proactiveCfg.errorBoundary === true);
|
|
4902
|
+
const hasAnyProactive = proactiveCfg && (proactiveCfg.rageClick !== false || proactiveCfg.longTask !== false || proactiveCfg.apiCascade !== false || proactiveCfg.errorBoundary === true || Boolean(proactiveCfg.pageDwell) || Boolean(proactiveCfg.firstSession));
|
|
4301
4903
|
if (hasAnyProactive && typeof document !== "undefined") {
|
|
4302
4904
|
proactiveManager = createProactiveManager(proactiveCfg?.cooldown);
|
|
4303
4905
|
proactiveTriggers = setupProactiveTriggers(
|
|
@@ -4310,7 +4912,11 @@ function createInstance(config) {
|
|
|
4310
4912
|
log.info("Proactive trigger fired", { type, context });
|
|
4311
4913
|
pendingProactiveTrigger = type;
|
|
4312
4914
|
emit("proactive:triggered", { type, context });
|
|
4313
|
-
|
|
4915
|
+
if (type === "first_session") {
|
|
4916
|
+
widget.pulseTrigger?.();
|
|
4917
|
+
} else {
|
|
4918
|
+
widget.open();
|
|
4919
|
+
}
|
|
4314
4920
|
}
|
|
4315
4921
|
},
|
|
4316
4922
|
{
|
|
@@ -4318,14 +4924,19 @@ function createInstance(config) {
|
|
|
4318
4924
|
longTask: proactiveCfg?.longTask,
|
|
4319
4925
|
apiCascade: proactiveCfg?.apiCascade,
|
|
4320
4926
|
apiEndpoint: resolveApiEndpoint(activeConfig),
|
|
4321
|
-
errorBoundary: proactiveCfg?.errorBoundary
|
|
4927
|
+
errorBoundary: proactiveCfg?.errorBoundary,
|
|
4928
|
+
pageDwell: proactiveCfg?.pageDwell,
|
|
4929
|
+
firstSession: proactiveCfg?.firstSession,
|
|
4930
|
+
projectId: bootstrapConfig.projectId
|
|
4322
4931
|
}
|
|
4323
4932
|
);
|
|
4324
4933
|
log.debug("Proactive triggers enabled", {
|
|
4325
4934
|
rageClick: proactiveCfg?.rageClick !== false,
|
|
4326
4935
|
longTask: proactiveCfg?.longTask !== false,
|
|
4327
4936
|
apiCascade: proactiveCfg?.apiCascade !== false,
|
|
4328
|
-
errorBoundary: proactiveCfg?.errorBoundary === true
|
|
4937
|
+
errorBoundary: proactiveCfg?.errorBoundary === true,
|
|
4938
|
+
pageDwell: Boolean(proactiveCfg?.pageDwell),
|
|
4939
|
+
firstSession: Boolean(proactiveCfg?.firstSession)
|
|
4329
4940
|
});
|
|
4330
4941
|
}
|
|
4331
4942
|
offlineQueue.startAutoSync(apiClient2);
|
|
@@ -4399,7 +5010,7 @@ function createInstance(config) {
|
|
|
4399
5010
|
const filterResult = preFilter.check(description);
|
|
4400
5011
|
if (!filterResult.passed) {
|
|
4401
5012
|
log.info("Report blocked by pre-filter", { reason: filterResult.reason });
|
|
4402
|
-
return;
|
|
5013
|
+
return void 0;
|
|
4403
5014
|
}
|
|
4404
5015
|
const wasm = config.preFilter?.wasmClassifier;
|
|
4405
5016
|
if (wasm) {
|
|
@@ -4420,7 +5031,7 @@ function createInstance(config) {
|
|
|
4420
5031
|
confidence: verdict.confidence,
|
|
4421
5032
|
reason: verdict.reason
|
|
4422
5033
|
});
|
|
4423
|
-
return;
|
|
5034
|
+
return void 0;
|
|
4424
5035
|
}
|
|
4425
5036
|
log.debug("On-device classifier verdict", { ...verdict });
|
|
4426
5037
|
} catch (err) {
|
|
@@ -4431,7 +5042,7 @@ function createInstance(config) {
|
|
|
4431
5042
|
}
|
|
4432
5043
|
if (!rateLimiter.tryConsume()) {
|
|
4433
5044
|
log.warn("Report throttled \u2014 rate limit exceeded");
|
|
4434
|
-
return;
|
|
5045
|
+
return void 0;
|
|
4435
5046
|
}
|
|
4436
5047
|
const scrubbedDescription = piiScrubber.scrub(preFilter.truncate(description));
|
|
4437
5048
|
const sentryCtx = config.sentry ? captureSentryContext(config.sentry) : void 0;
|
|
@@ -4569,6 +5180,11 @@ function createInstance(config) {
|
|
|
4569
5180
|
pendingScreenshot = null;
|
|
4570
5181
|
pendingElement = null;
|
|
4571
5182
|
pendingProactiveTrigger = null;
|
|
5183
|
+
if (result?.ok) {
|
|
5184
|
+
const serverId = result.data?.reportId ?? report.id;
|
|
5185
|
+
return { reportId: serverId, queuedOffline: false };
|
|
5186
|
+
}
|
|
5187
|
+
return { reportId: null, queuedOffline: true };
|
|
4572
5188
|
}
|
|
4573
5189
|
const sdk = {
|
|
4574
5190
|
report(options) {
|
|
@@ -4820,6 +5436,9 @@ function createInstance(config) {
|
|
|
4820
5436
|
recordActivity(action, metadata) {
|
|
4821
5437
|
if (!activeConfig.rewards?.enabled) return;
|
|
4822
5438
|
enqueue({ action, metadata });
|
|
5439
|
+
},
|
|
5440
|
+
pulseTrigger() {
|
|
5441
|
+
widget.pulseTrigger?.();
|
|
4823
5442
|
}
|
|
4824
5443
|
};
|
|
4825
5444
|
if (typeof globalThis !== "undefined" && (bootstrapConfig.debug ?? false)) {
|
|
@@ -4846,7 +5465,7 @@ function createInstance(config) {
|
|
|
4846
5465
|
}
|
|
4847
5466
|
if (typeof bootstrapConfig.onCrashedLastRun === "function") {
|
|
4848
5467
|
try {
|
|
4849
|
-
bootstrapConfig.onCrashedLastRun(
|
|
5468
|
+
bootstrapConfig.onCrashedLastRun(crashed);
|
|
4850
5469
|
} catch (err) {
|
|
4851
5470
|
log.warn("onCrashedLastRun hook threw", {
|
|
4852
5471
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -4858,13 +5477,26 @@ function createInstance(config) {
|
|
|
4858
5477
|
}
|
|
4859
5478
|
function mergeRuntimeConfig(config, runtime) {
|
|
4860
5479
|
const nativeTrigger = runtime.native?.triggerMode;
|
|
4861
|
-
const
|
|
5480
|
+
const runtimeLauncher = runtime.widget?.launcher;
|
|
5481
|
+
const widgetTrigger = runtimeLauncher ?? runtime.widget?.trigger ?? (nativeTrigger === "none" || nativeTrigger === "shake" ? "manual" : void 0);
|
|
5482
|
+
const runtimeBannerVariant = runtime.widget?.bannerVariant;
|
|
5483
|
+
const runtimeBannerPosition = runtime.widget?.bannerPosition;
|
|
5484
|
+
const runtimeBannerBugCta = runtime.widget?.bannerBugCta;
|
|
5485
|
+
const runtimeBannerFeatureCta = runtime.widget?.bannerFeatureCta;
|
|
5486
|
+
const derivedBannerConfig = runtimeBannerVariant || runtimeBannerPosition || runtimeBannerBugCta != null || runtimeBannerFeatureCta != null ? {
|
|
5487
|
+
...config.widget?.bannerConfig ?? {},
|
|
5488
|
+
...runtimeBannerVariant ? { variant: runtimeBannerVariant } : {},
|
|
5489
|
+
...runtimeBannerPosition ? { position: runtimeBannerPosition } : {},
|
|
5490
|
+
...runtimeBannerBugCta != null ? { bugCta: runtimeBannerBugCta ?? void 0 } : {},
|
|
5491
|
+
...runtimeBannerFeatureCta != null ? { featureCta: runtimeBannerFeatureCta } : {}
|
|
5492
|
+
} : void 0;
|
|
4862
5493
|
return {
|
|
4863
5494
|
...config,
|
|
4864
5495
|
widget: {
|
|
4865
5496
|
...config.widget,
|
|
4866
5497
|
...runtime.widget,
|
|
4867
5498
|
...widgetTrigger ? { trigger: widgetTrigger } : {},
|
|
5499
|
+
...derivedBannerConfig ? { bannerConfig: derivedBannerConfig } : {},
|
|
4868
5500
|
// betaMode is local-only: set by the host app, not the dashboard.
|
|
4869
5501
|
// Restore it after the runtime spread so it is never silently cleared.
|
|
4870
5502
|
...config.widget?.betaMode ? { betaMode: config.widget.betaMode } : {}
|
|
@@ -5107,6 +5739,8 @@ function createNoopInstance() {
|
|
|
5107
5739
|
getReputation: async () => null,
|
|
5108
5740
|
getTier: async () => null,
|
|
5109
5741
|
recordActivity: () => {
|
|
5742
|
+
},
|
|
5743
|
+
pulseTrigger: () => {
|
|
5110
5744
|
}
|
|
5111
5745
|
};
|
|
5112
5746
|
}
|