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