@mushi-mushi/web 0.5.1 → 0.8.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 +4 -0
- package/README.md +169 -5
- package/SECURITY.md +74 -0
- package/dist/index.cjs +1123 -152
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +79 -7
- package/dist/index.d.ts +79 -7
- package/dist/index.js +1124 -154
- package/dist/index.js.map +1 -1
- package/dist/test-utils.cjs +17 -0
- package/dist/test-utils.cjs.map +1 -1
- package/dist/test-utils.d.cts +21 -2
- package/dist/test-utils.d.ts +21 -2
- package/dist/test-utils.js +15 -1
- package/dist/test-utils.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -245,11 +245,6 @@ function getWidgetStyles(theme) {
|
|
|
245
245
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
246
246
|
button { font-family: inherit; }
|
|
247
247
|
|
|
248
|
-
/* \u2500\u2500 Trigger \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
249
|
-
A small "stamp card" \u2014 soft rounded square (4px radius), paper
|
|
250
|
-
background, vermillion bottom edge that reads as the inked face
|
|
251
|
-
of a real \u5370\u9451. A pulsing dot in the top-right hints there's a
|
|
252
|
-
channel here without needing a notification badge. */
|
|
253
248
|
.mushi-trigger {
|
|
254
249
|
position: fixed;
|
|
255
250
|
width: 52px;
|
|
@@ -303,10 +298,53 @@ function getWidgetStyles(theme) {
|
|
|
303
298
|
outline: 2px solid ${vermillion};
|
|
304
299
|
outline-offset: 3px;
|
|
305
300
|
}
|
|
306
|
-
.mushi-trigger.bottom-right {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
301
|
+
.mushi-trigger.bottom-right {
|
|
302
|
+
bottom: var(--mushi-bottom, calc(24px + env(safe-area-inset-bottom, 0px)));
|
|
303
|
+
right: var(--mushi-right, calc(24px + env(safe-area-inset-right, 0px)));
|
|
304
|
+
}
|
|
305
|
+
.mushi-trigger.bottom-left {
|
|
306
|
+
bottom: var(--mushi-bottom, calc(24px + env(safe-area-inset-bottom, 0px)));
|
|
307
|
+
left: var(--mushi-left, calc(24px + env(safe-area-inset-left, 0px)));
|
|
308
|
+
}
|
|
309
|
+
.mushi-trigger.top-right {
|
|
310
|
+
top: var(--mushi-top, calc(24px + env(safe-area-inset-top, 0px)));
|
|
311
|
+
right: var(--mushi-right, calc(24px + env(safe-area-inset-right, 0px)));
|
|
312
|
+
}
|
|
313
|
+
.mushi-trigger.top-left {
|
|
314
|
+
top: var(--mushi-top, calc(24px + env(safe-area-inset-top, 0px)));
|
|
315
|
+
left: var(--mushi-left, calc(24px + env(safe-area-inset-left, 0px)));
|
|
316
|
+
}
|
|
317
|
+
.mushi-trigger.edge-tab {
|
|
318
|
+
width: 32px;
|
|
319
|
+
height: 88px;
|
|
320
|
+
border-radius: 4px 0 0 4px;
|
|
321
|
+
writing-mode: vertical-rl;
|
|
322
|
+
text-orientation: upright;
|
|
323
|
+
font-size: 16px;
|
|
324
|
+
box-shadow:
|
|
325
|
+
0 1px 0 ${rule},
|
|
326
|
+
0 10px 24px -14px rgba(14,13,11,0.45),
|
|
327
|
+
inset -3px 0 0 ${vermillion};
|
|
328
|
+
}
|
|
329
|
+
.mushi-trigger.edge-tab.bottom-right,
|
|
330
|
+
.mushi-trigger.edge-tab.top-right {
|
|
331
|
+
right: var(--mushi-right, 0);
|
|
332
|
+
}
|
|
333
|
+
.mushi-trigger.edge-tab.bottom-left,
|
|
334
|
+
.mushi-trigger.edge-tab.top-left {
|
|
335
|
+
left: var(--mushi-left, 0);
|
|
336
|
+
border-radius: 0 4px 4px 0;
|
|
337
|
+
box-shadow:
|
|
338
|
+
0 1px 0 ${rule},
|
|
339
|
+
0 10px 24px -14px rgba(14,13,11,0.45),
|
|
340
|
+
inset 3px 0 0 ${vermillion};
|
|
341
|
+
}
|
|
342
|
+
.mushi-trigger.shrunk {
|
|
343
|
+
width: 36px;
|
|
344
|
+
height: 36px;
|
|
345
|
+
opacity: 0.82;
|
|
346
|
+
transform: scale(0.92);
|
|
347
|
+
}
|
|
310
348
|
|
|
311
349
|
@keyframes mushi-pulse {
|
|
312
350
|
0% { box-shadow: 0 0 0 0 ${vermillion}; opacity: 1; }
|
|
@@ -314,12 +352,6 @@ function getWidgetStyles(theme) {
|
|
|
314
352
|
100% { box-shadow: 0 0 0 0 rgba(224,60,44,0); opacity: 1; }
|
|
315
353
|
}
|
|
316
354
|
|
|
317
|
-
/* \u2500\u2500 Panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
318
|
-
Paper-card. Sharper corners (6px) than typical SaaS modals
|
|
319
|
-
(which default to 12-16px and read as plastic). Two-layer shadow:
|
|
320
|
-
one hairline that sells the paper edge, one diffuse that lifts
|
|
321
|
-
the panel off the underlying app. No backdrop-filter \u2014 we want
|
|
322
|
-
the widget to feel like it sits ON the page, not blur INTO it. */
|
|
323
355
|
.mushi-panel {
|
|
324
356
|
position: fixed;
|
|
325
357
|
width: 384px;
|
|
@@ -339,10 +371,49 @@ function getWidgetStyles(theme) {
|
|
|
339
371
|
}
|
|
340
372
|
.mushi-panel.open { animation: mushi-stamp-in 320ms ${easeStamp} both; }
|
|
341
373
|
.mushi-panel.closed { display: none; }
|
|
342
|
-
.mushi-panel.bottom-right {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
374
|
+
.mushi-panel.bottom-right {
|
|
375
|
+
bottom: var(--mushi-panel-bottom, calc(var(--mushi-bottom, 24px) + 64px));
|
|
376
|
+
right: var(--mushi-right, calc(24px + env(safe-area-inset-right, 0px)));
|
|
377
|
+
--mushi-origin: bottom right;
|
|
378
|
+
}
|
|
379
|
+
.mushi-panel.bottom-left {
|
|
380
|
+
bottom: var(--mushi-panel-bottom, calc(var(--mushi-bottom, 24px) + 64px));
|
|
381
|
+
left: var(--mushi-left, calc(24px + env(safe-area-inset-left, 0px)));
|
|
382
|
+
--mushi-origin: bottom left;
|
|
383
|
+
}
|
|
384
|
+
.mushi-panel.top-right {
|
|
385
|
+
top: var(--mushi-panel-top, calc(var(--mushi-top, 24px) + 64px));
|
|
386
|
+
right: var(--mushi-right, calc(24px + env(safe-area-inset-right, 0px)));
|
|
387
|
+
--mushi-origin: top right;
|
|
388
|
+
}
|
|
389
|
+
.mushi-panel.top-left {
|
|
390
|
+
top: var(--mushi-panel-top, calc(var(--mushi-top, 24px) + 64px));
|
|
391
|
+
left: var(--mushi-left, calc(24px + env(safe-area-inset-left, 0px)));
|
|
392
|
+
--mushi-origin: top left;
|
|
393
|
+
}
|
|
394
|
+
.mushi-outdated {
|
|
395
|
+
margin: 12px 14px 0;
|
|
396
|
+
padding: 10px 12px;
|
|
397
|
+
border: 1px solid ${vermillionWash};
|
|
398
|
+
background: ${vermillionWash};
|
|
399
|
+
color: ${vermillionInk};
|
|
400
|
+
font-family: ${fontBody};
|
|
401
|
+
font-size: 12px;
|
|
402
|
+
line-height: 1.4;
|
|
403
|
+
}
|
|
404
|
+
.mushi-outdated strong {
|
|
405
|
+
display: block;
|
|
406
|
+
font-family: ${fontMono};
|
|
407
|
+
font-size: 10px;
|
|
408
|
+
letter-spacing: 0.12em;
|
|
409
|
+
text-transform: uppercase;
|
|
410
|
+
margin-bottom: 2px;
|
|
411
|
+
}
|
|
412
|
+
.mushi-outdated span {
|
|
413
|
+
display: block;
|
|
414
|
+
margin-top: 3px;
|
|
415
|
+
color: ${inkMuted};
|
|
416
|
+
}
|
|
346
417
|
|
|
347
418
|
@keyframes mushi-stamp-in {
|
|
348
419
|
0% { opacity: 0; transform: scale(0.94) translateY(6px); }
|
|
@@ -350,10 +421,6 @@ function getWidgetStyles(theme) {
|
|
|
350
421
|
100% { opacity: 1; transform: scale(1) translateY(0); }
|
|
351
422
|
}
|
|
352
423
|
|
|
353
|
-
/* \u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
354
|
-
Editorial masthead: small mono eyebrow ("MUSHI / REPORT") on top,
|
|
355
|
-
serif display headline below, mono step counter on the far right.
|
|
356
|
-
A single hairline separates header from body \u2014 no card stacking. */
|
|
357
424
|
.mushi-header {
|
|
358
425
|
padding: 18px 20px 14px;
|
|
359
426
|
border-bottom: 1px solid ${rule};
|
|
@@ -450,10 +517,6 @@ function getWidgetStyles(theme) {
|
|
|
450
517
|
.mushi-body::-webkit-scrollbar { width: 6px; }
|
|
451
518
|
.mushi-body::-webkit-scrollbar-thumb { background: ${inkFaint}; border-radius: 3px; }
|
|
452
519
|
|
|
453
|
-
/* \u2500\u2500 Step 1: Categories as a contents-page list \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
454
|
-
No boxes. Hairline rules between rows. Hovering a row pulls a
|
|
455
|
-
vermillion arrow in from the right and tints the row label \u2014
|
|
456
|
-
reads like flipping through an index card. */
|
|
457
520
|
.mushi-option-btn {
|
|
458
521
|
display: grid;
|
|
459
522
|
grid-template-columns: auto 1fr auto;
|
|
@@ -505,12 +568,76 @@ function getWidgetStyles(theme) {
|
|
|
505
568
|
transform: translateX(-4px);
|
|
506
569
|
transition: opacity 220ms ${easeStamp}, transform 220ms ${easeStamp}, color 220ms ${easeStamp};
|
|
507
570
|
}
|
|
571
|
+
.mushi-report-row {
|
|
572
|
+
width: 100%;
|
|
573
|
+
display: grid;
|
|
574
|
+
grid-template-columns: auto 1fr auto;
|
|
575
|
+
gap: 8px;
|
|
576
|
+
align-items: center;
|
|
577
|
+
padding: 10px 0;
|
|
578
|
+
border: 0;
|
|
579
|
+
border-bottom: 1px solid ${rule};
|
|
580
|
+
background: transparent;
|
|
581
|
+
color: ${ink};
|
|
582
|
+
cursor: pointer;
|
|
583
|
+
text-align: left;
|
|
584
|
+
}
|
|
585
|
+
.mushi-report-status {
|
|
586
|
+
font-family: ${fontMono};
|
|
587
|
+
font-size: 10px;
|
|
588
|
+
color: ${vermillion};
|
|
589
|
+
text-transform: uppercase;
|
|
590
|
+
}
|
|
591
|
+
.mushi-report-title {
|
|
592
|
+
font-size: 13px;
|
|
593
|
+
overflow: hidden;
|
|
594
|
+
text-overflow: ellipsis;
|
|
595
|
+
white-space: nowrap;
|
|
596
|
+
}
|
|
597
|
+
.mushi-thread-summary {
|
|
598
|
+
border-bottom: 1px solid ${rule};
|
|
599
|
+
padding-bottom: 10px;
|
|
600
|
+
margin-bottom: 10px;
|
|
601
|
+
}
|
|
602
|
+
.mushi-thread-summary span {
|
|
603
|
+
font-family: ${fontMono};
|
|
604
|
+
font-size: 10px;
|
|
605
|
+
color: ${vermillion};
|
|
606
|
+
text-transform: uppercase;
|
|
607
|
+
}
|
|
608
|
+
.mushi-thread {
|
|
609
|
+
display: grid;
|
|
610
|
+
gap: 8px;
|
|
611
|
+
max-height: 180px;
|
|
612
|
+
overflow: auto;
|
|
613
|
+
margin-bottom: 12px;
|
|
614
|
+
}
|
|
615
|
+
.mushi-thread-comment {
|
|
616
|
+
padding: 8px 10px;
|
|
617
|
+
border: 1px solid ${rule};
|
|
618
|
+
background: ${isDark ? "rgba(242,235,221,0.04)" : "rgba(14,13,11,0.03)"};
|
|
619
|
+
}
|
|
620
|
+
.mushi-thread-comment.reporter {
|
|
621
|
+
border-color: ${vermillionWash};
|
|
622
|
+
background: ${vermillionWash};
|
|
623
|
+
}
|
|
624
|
+
.mushi-thread-comment strong {
|
|
625
|
+
display: block;
|
|
626
|
+
font-family: ${fontMono};
|
|
627
|
+
font-size: 10px;
|
|
628
|
+
letter-spacing: 0.08em;
|
|
629
|
+
text-transform: uppercase;
|
|
630
|
+
margin-bottom: 3px;
|
|
631
|
+
}
|
|
632
|
+
.mushi-thread-comment p,
|
|
633
|
+
.mushi-muted,
|
|
634
|
+
.mushi-error-inline {
|
|
635
|
+
font-size: 12px;
|
|
636
|
+
color: ${inkMuted};
|
|
637
|
+
line-height: 1.45;
|
|
638
|
+
}
|
|
639
|
+
.mushi-error-inline { color: ${vermillion}; }
|
|
508
640
|
|
|
509
|
-
/* \u2500\u2500 Step 2: Selected-category breadcrumb + intent text-buttons \u2500
|
|
510
|
-
Breadcrumb is a thin chip with the kanji-stamp aesthetic carried
|
|
511
|
-
over (vermillion left rule). Intents are inline TEXT buttons
|
|
512
|
-
with vermillion underlines on hover \u2014 not pill-shaped chips,
|
|
513
|
-
which is the SaaS default and not what we are. */
|
|
514
641
|
.mushi-selected-category {
|
|
515
642
|
display: inline-flex;
|
|
516
643
|
align-items: center;
|
|
@@ -566,10 +693,6 @@ function getWidgetStyles(theme) {
|
|
|
566
693
|
box-shadow: inset 2px 0 0 ${vermillion};
|
|
567
694
|
}
|
|
568
695
|
|
|
569
|
-
/* \u2500\u2500 Step 3: Borderless textarea + minimal attach pills \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
570
|
-
The textarea has no box around it \u2014 just a hairline underline
|
|
571
|
-
that turns vermillion on focus. Encourages writing rather than
|
|
572
|
-
form-filling. */
|
|
573
696
|
.mushi-textarea {
|
|
574
697
|
width: 100%;
|
|
575
698
|
min-height: 96px;
|
|
@@ -622,15 +745,21 @@ function getWidgetStyles(theme) {
|
|
|
622
745
|
border-color: ${vermillion};
|
|
623
746
|
background: ${vermillionWash};
|
|
624
747
|
}
|
|
748
|
+
.mushi-attach-btn.danger {
|
|
749
|
+
color: ${vermillionInk};
|
|
750
|
+
border-color: ${vermillionWash};
|
|
751
|
+
background: transparent;
|
|
752
|
+
}
|
|
753
|
+
.mushi-attach-btn.danger:hover {
|
|
754
|
+
color: ${vermillion};
|
|
755
|
+
border-color: ${vermillion};
|
|
756
|
+
background: ${vermillionWash};
|
|
757
|
+
}
|
|
625
758
|
.mushi-attach-btn:focus-visible {
|
|
626
759
|
outline: 2px solid ${vermillion};
|
|
627
760
|
outline-offset: 2px;
|
|
628
761
|
}
|
|
629
762
|
|
|
630
|
-
/* \u2500\u2500 Footer + submit (vermillion stamp) \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
|
|
631
|
-
Submit button is the heaviest visual moment in the widget \u2014
|
|
632
|
-
vermillion fill, mono-caps label, send arrow. Holds an ink-
|
|
633
|
-
bloom pseudo-element that animates outward when pressed. */
|
|
634
763
|
.mushi-footer {
|
|
635
764
|
padding: 14px 22px 16px;
|
|
636
765
|
border-top: 1px solid ${rule};
|
|
@@ -695,10 +824,17 @@ function getWidgetStyles(theme) {
|
|
|
695
824
|
}
|
|
696
825
|
.mushi-submit:hover .mushi-submit-arrow { transform: translateX(3px); }
|
|
697
826
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
827
|
+
.mushi-brand-footer {
|
|
828
|
+
padding: 9px 14px 11px;
|
|
829
|
+
border-top: 1px solid ${rule};
|
|
830
|
+
color: ${inkFaint};
|
|
831
|
+
font-family: ${fontMono};
|
|
832
|
+
font-size: 9px;
|
|
833
|
+
letter-spacing: 0.16em;
|
|
834
|
+
text-align: center;
|
|
835
|
+
text-transform: uppercase;
|
|
836
|
+
}
|
|
837
|
+
|
|
702
838
|
.mushi-step-indicator {
|
|
703
839
|
display: flex;
|
|
704
840
|
align-items: center;
|
|
@@ -726,10 +862,6 @@ function getWidgetStyles(theme) {
|
|
|
726
862
|
}
|
|
727
863
|
.mushi-step-sep { width: 14px; height: 1px; background: ${rule}; }
|
|
728
864
|
|
|
729
|
-
/* \u2500\u2500 Success: \u6731\u5370 stamp animation \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
|
|
730
|
-
The success state is the signature moment. A vermillion ring
|
|
731
|
-
scribes itself, then a "RECEIVED" mono-caps label fades in at
|
|
732
|
-
the centre, evoking a hanko being pressed onto the form. */
|
|
733
865
|
.mushi-success {
|
|
734
866
|
text-align: center;
|
|
735
867
|
padding: 28px 16px 20px;
|
|
@@ -793,9 +925,6 @@ function getWidgetStyles(theme) {
|
|
|
793
925
|
100% { opacity: 1; transform: rotate(-6deg) scale(1); }
|
|
794
926
|
}
|
|
795
927
|
|
|
796
|
-
/* \u2500\u2500 Error \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
797
|
-
Inline editorial note rather than a red box. Vermillion left
|
|
798
|
-
rule keeps the same accent language. */
|
|
799
928
|
.mushi-error {
|
|
800
929
|
margin-top: 10px;
|
|
801
930
|
padding: 8px 0 8px 10px;
|
|
@@ -806,9 +935,6 @@ function getWidgetStyles(theme) {
|
|
|
806
935
|
letter-spacing: 0.02em;
|
|
807
936
|
}
|
|
808
937
|
|
|
809
|
-
/* \u2500\u2500 Reduced motion \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
810
|
-
Honour the OS preference: kill every transition + animation
|
|
811
|
-
except the focus underline (which is critical feedback). */
|
|
812
938
|
@media (prefers-reduced-motion: reduce) {
|
|
813
939
|
*,
|
|
814
940
|
*::before,
|
|
@@ -838,12 +964,51 @@ var TOTAL_STEPS = 3;
|
|
|
838
964
|
var STEP_NUMBER = {
|
|
839
965
|
category: 1,
|
|
840
966
|
intent: 2,
|
|
841
|
-
details: 3
|
|
842
|
-
};
|
|
967
|
+
details: 3};
|
|
843
968
|
function isSubmitShortcut(e) {
|
|
844
969
|
return (e.metaKey || e.ctrlKey) && e.key === "Enter";
|
|
845
970
|
}
|
|
971
|
+
function escapeHtml(value) {
|
|
972
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
973
|
+
}
|
|
846
974
|
var MushiWidget = class {
|
|
975
|
+
constructor(config = {}, callbacks, sdkVersion = "0.7.0") {
|
|
976
|
+
this.sdkVersion = sdkVersion;
|
|
977
|
+
this.config = {
|
|
978
|
+
position: config.position ?? "bottom-right",
|
|
979
|
+
anchor: config.anchor ?? {},
|
|
980
|
+
theme: config.theme ?? "auto",
|
|
981
|
+
// Falsy-OR (NOT `??`) on purpose: `triggerText: ''` is semantically
|
|
982
|
+
// nonsense — it would render a labelless, glyphless trigger button
|
|
983
|
+
// that users can't see or aim at. Treat empty string the same as
|
|
984
|
+
// omitted so any caller that wires this to a cleared form input or
|
|
985
|
+
// pastes a legacy snippet that emitted `triggerText: ""` (see
|
|
986
|
+
// apps/admin/src/lib/sdkSnippets.ts widgetLines history) still gets
|
|
987
|
+
// the default 🐛 and a visible button.
|
|
988
|
+
triggerText: config.triggerText || "\u{1F41B}",
|
|
989
|
+
expandedTitle: config.expandedTitle ?? "",
|
|
990
|
+
mode: config.mode ?? "conversational",
|
|
991
|
+
locale: config.locale ?? "auto",
|
|
992
|
+
zIndex: config.zIndex ?? 99999,
|
|
993
|
+
trigger: config.trigger ?? "auto",
|
|
994
|
+
attachToSelector: config.attachToSelector ?? "",
|
|
995
|
+
inset: config.inset ?? {},
|
|
996
|
+
respectSafeArea: config.respectSafeArea ?? true,
|
|
997
|
+
hideOnSelector: config.hideOnSelector ?? "",
|
|
998
|
+
hideOnRoutes: config.hideOnRoutes ?? [],
|
|
999
|
+
environments: config.environments ?? {},
|
|
1000
|
+
smartHide: config.smartHide ?? false,
|
|
1001
|
+
draggable: config.draggable ?? false,
|
|
1002
|
+
brandFooter: config.brandFooter ?? true,
|
|
1003
|
+
outdatedBanner: config.outdatedBanner ?? "auto"
|
|
1004
|
+
};
|
|
1005
|
+
this.callbacks = callbacks;
|
|
1006
|
+
this.locale = getLocale(this.config.locale === "auto" ? void 0 : this.config.locale);
|
|
1007
|
+
this.host = document.createElement("div");
|
|
1008
|
+
this.host.id = "mushi-mushi-widget";
|
|
1009
|
+
this.shadow = this.host.attachShadow({ mode: "closed" });
|
|
1010
|
+
}
|
|
1011
|
+
sdkVersion;
|
|
847
1012
|
host;
|
|
848
1013
|
shadow;
|
|
849
1014
|
config;
|
|
@@ -854,8 +1019,21 @@ var MushiWidget = class {
|
|
|
854
1019
|
selectedCategory = null;
|
|
855
1020
|
selectedIntent = null;
|
|
856
1021
|
screenshotAttached = false;
|
|
1022
|
+
allowScreenshotRemove = true;
|
|
857
1023
|
elementSelected = false;
|
|
858
1024
|
submitting = false;
|
|
1025
|
+
triggerVisible = true;
|
|
1026
|
+
triggerShrunk = false;
|
|
1027
|
+
triggerHiddenByScroll = false;
|
|
1028
|
+
sdkFreshness = null;
|
|
1029
|
+
reporterReports = [];
|
|
1030
|
+
reporterComments = [];
|
|
1031
|
+
selectedReportId = null;
|
|
1032
|
+
reporterLoading = false;
|
|
1033
|
+
reporterError = null;
|
|
1034
|
+
attachedLaunchers = [];
|
|
1035
|
+
smartHideCleanup = null;
|
|
1036
|
+
smartHideTimer = null;
|
|
859
1037
|
/** Captured at the moment of submit so the success ledger metadata
|
|
860
1038
|
* ("REPORT · 14:23:07 JST") doesn't drift while the success step
|
|
861
1039
|
* is on screen. */
|
|
@@ -866,45 +1044,42 @@ var MushiWidget = class {
|
|
|
866
1044
|
* root) for up to ~3.3s after destroy. */
|
|
867
1045
|
successTimer = null;
|
|
868
1046
|
autoCloseTimer = null;
|
|
869
|
-
constructor(config = {}, callbacks) {
|
|
870
|
-
this.config = {
|
|
871
|
-
position: config.position ?? "bottom-right",
|
|
872
|
-
theme: config.theme ?? "auto",
|
|
873
|
-
// Falsy-OR (NOT `??`) on purpose: `triggerText: ''` is semantically
|
|
874
|
-
// nonsense — it would render a labelless, glyphless trigger button
|
|
875
|
-
// that users can't see or aim at. Treat empty string the same as
|
|
876
|
-
// omitted so any caller that wires this to a cleared form input or
|
|
877
|
-
// pastes a legacy snippet that emitted `triggerText: ""` (see
|
|
878
|
-
// apps/admin/src/lib/sdkSnippets.ts widgetLines history) still gets
|
|
879
|
-
// the default 🐛 and a visible button.
|
|
880
|
-
triggerText: config.triggerText || "\u{1F41B}",
|
|
881
|
-
expandedTitle: config.expandedTitle ?? "",
|
|
882
|
-
mode: config.mode ?? "conversational",
|
|
883
|
-
locale: config.locale ?? "auto",
|
|
884
|
-
zIndex: config.zIndex ?? 99999
|
|
885
|
-
};
|
|
886
|
-
this.callbacks = callbacks;
|
|
887
|
-
this.locale = getLocale(this.config.locale === "auto" ? void 0 : this.config.locale);
|
|
888
|
-
this.host = document.createElement("div");
|
|
889
|
-
this.host.id = "mushi-mushi-widget";
|
|
890
|
-
this.shadow = this.host.attachShadow({ mode: "closed" });
|
|
891
|
-
}
|
|
892
1047
|
mount() {
|
|
1048
|
+
if (this.host.isConnected) return;
|
|
893
1049
|
document.body.appendChild(this.host);
|
|
1050
|
+
this.syncAttachedLaunchers();
|
|
1051
|
+
this.syncSmartHide();
|
|
894
1052
|
this.render();
|
|
895
1053
|
}
|
|
1054
|
+
getIsMounted() {
|
|
1055
|
+
return this.host.isConnected;
|
|
1056
|
+
}
|
|
896
1057
|
updateConfig(config = {}) {
|
|
897
1058
|
this.config = {
|
|
898
1059
|
...this.config,
|
|
899
1060
|
...config.position ? { position: config.position } : {},
|
|
1061
|
+
...config.anchor !== void 0 ? { anchor: config.anchor } : {},
|
|
900
1062
|
...config.theme ? { theme: config.theme } : {},
|
|
901
1063
|
...config.triggerText !== void 0 ? { triggerText: config.triggerText || "\u{1F41B}" } : {},
|
|
902
1064
|
...config.expandedTitle !== void 0 ? { expandedTitle: config.expandedTitle } : {},
|
|
903
1065
|
...config.mode ? { mode: config.mode } : {},
|
|
904
1066
|
...config.locale ? { locale: config.locale } : {},
|
|
905
|
-
...config.zIndex !== void 0 ? { zIndex: config.zIndex } : {}
|
|
1067
|
+
...config.zIndex !== void 0 ? { zIndex: config.zIndex } : {},
|
|
1068
|
+
...config.trigger ? { trigger: config.trigger } : {},
|
|
1069
|
+
...config.attachToSelector !== void 0 ? { attachToSelector: config.attachToSelector } : {},
|
|
1070
|
+
...config.inset !== void 0 ? { inset: config.inset } : {},
|
|
1071
|
+
...config.respectSafeArea !== void 0 ? { respectSafeArea: config.respectSafeArea } : {},
|
|
1072
|
+
...config.hideOnSelector !== void 0 ? { hideOnSelector: config.hideOnSelector } : {},
|
|
1073
|
+
...config.hideOnRoutes !== void 0 ? { hideOnRoutes: config.hideOnRoutes } : {},
|
|
1074
|
+
...config.environments !== void 0 ? { environments: config.environments } : {},
|
|
1075
|
+
...config.smartHide !== void 0 ? { smartHide: config.smartHide } : {},
|
|
1076
|
+
...config.draggable !== void 0 ? { draggable: config.draggable } : {},
|
|
1077
|
+
...config.brandFooter !== void 0 ? { brandFooter: config.brandFooter } : {},
|
|
1078
|
+
...config.outdatedBanner !== void 0 ? { outdatedBanner: config.outdatedBanner } : {}
|
|
906
1079
|
};
|
|
907
1080
|
this.locale = getLocale(this.config.locale === "auto" ? void 0 : this.config.locale);
|
|
1081
|
+
this.syncAttachedLaunchers();
|
|
1082
|
+
this.syncSmartHide();
|
|
908
1083
|
this.render();
|
|
909
1084
|
}
|
|
910
1085
|
open(options) {
|
|
@@ -935,14 +1110,46 @@ var MushiWidget = class {
|
|
|
935
1110
|
getIsOpen() {
|
|
936
1111
|
return this.isOpen;
|
|
937
1112
|
}
|
|
1113
|
+
showTrigger() {
|
|
1114
|
+
this.triggerVisible = true;
|
|
1115
|
+
this.render();
|
|
1116
|
+
}
|
|
1117
|
+
hideTrigger() {
|
|
1118
|
+
this.triggerVisible = false;
|
|
1119
|
+
this.render();
|
|
1120
|
+
}
|
|
1121
|
+
setTrigger(trigger) {
|
|
1122
|
+
this.updateConfig({ trigger });
|
|
1123
|
+
}
|
|
1124
|
+
attachTo(selectorOrElement, options = {}) {
|
|
1125
|
+
const elements = typeof selectorOrElement === "string" ? Array.from(document.querySelectorAll(selectorOrElement)) : [selectorOrElement];
|
|
1126
|
+
const cleanups = elements.map((el) => {
|
|
1127
|
+
const onClick = (event) => {
|
|
1128
|
+
event.preventDefault();
|
|
1129
|
+
this.updateConfig(options);
|
|
1130
|
+
this.open();
|
|
1131
|
+
};
|
|
1132
|
+
el.addEventListener("click", onClick);
|
|
1133
|
+
return () => el.removeEventListener("click", onClick);
|
|
1134
|
+
});
|
|
1135
|
+
return () => cleanups.forEach((cleanup) => cleanup());
|
|
1136
|
+
}
|
|
938
1137
|
setScreenshotAttached(attached) {
|
|
939
1138
|
this.screenshotAttached = attached;
|
|
940
1139
|
if (this.isOpen) this.render();
|
|
941
1140
|
}
|
|
1141
|
+
setAllowScreenshotRemove(allow) {
|
|
1142
|
+
this.allowScreenshotRemove = allow;
|
|
1143
|
+
if (this.isOpen) this.render();
|
|
1144
|
+
}
|
|
942
1145
|
setElementSelected(selected) {
|
|
943
1146
|
this.elementSelected = selected;
|
|
944
1147
|
if (this.isOpen) this.render();
|
|
945
1148
|
}
|
|
1149
|
+
setSdkFreshness(info) {
|
|
1150
|
+
this.sdkFreshness = info;
|
|
1151
|
+
if (this.isOpen) this.render();
|
|
1152
|
+
}
|
|
946
1153
|
destroy() {
|
|
947
1154
|
if (this.successTimer !== null) {
|
|
948
1155
|
clearTimeout(this.successTimer);
|
|
@@ -952,8 +1159,83 @@ var MushiWidget = class {
|
|
|
952
1159
|
clearTimeout(this.autoCloseTimer);
|
|
953
1160
|
this.autoCloseTimer = null;
|
|
954
1161
|
}
|
|
1162
|
+
if (this.smartHideTimer !== null) {
|
|
1163
|
+
clearTimeout(this.smartHideTimer);
|
|
1164
|
+
this.smartHideTimer = null;
|
|
1165
|
+
}
|
|
1166
|
+
this.smartHideCleanup?.();
|
|
1167
|
+
this.smartHideCleanup = null;
|
|
1168
|
+
this.attachedLaunchers.forEach((cleanup) => cleanup());
|
|
1169
|
+
this.attachedLaunchers = [];
|
|
955
1170
|
this.host.remove();
|
|
956
1171
|
}
|
|
1172
|
+
syncAttachedLaunchers() {
|
|
1173
|
+
this.attachedLaunchers.forEach((cleanup) => cleanup());
|
|
1174
|
+
this.attachedLaunchers = [];
|
|
1175
|
+
if (this.config.trigger !== "attach" || !this.config.attachToSelector) return;
|
|
1176
|
+
if (typeof document === "undefined") return;
|
|
1177
|
+
this.attachedLaunchers.push(this.attachTo(this.config.attachToSelector));
|
|
1178
|
+
}
|
|
1179
|
+
syncSmartHide() {
|
|
1180
|
+
this.smartHideCleanup?.();
|
|
1181
|
+
this.smartHideCleanup = null;
|
|
1182
|
+
this.triggerShrunk = false;
|
|
1183
|
+
this.triggerHiddenByScroll = false;
|
|
1184
|
+
if (!this.config.smartHide || typeof window === "undefined") return;
|
|
1185
|
+
const smart = this.config.smartHide === true ? { onScroll: "shrink", onIdleMs: 900 } : this.config.smartHide;
|
|
1186
|
+
if (!smart.onScroll) return;
|
|
1187
|
+
const onScroll = () => {
|
|
1188
|
+
if (smart.onScroll === "hide") {
|
|
1189
|
+
this.triggerHiddenByScroll = true;
|
|
1190
|
+
} else {
|
|
1191
|
+
this.triggerShrunk = true;
|
|
1192
|
+
}
|
|
1193
|
+
this.render();
|
|
1194
|
+
if (this.smartHideTimer !== null) clearTimeout(this.smartHideTimer);
|
|
1195
|
+
this.smartHideTimer = setTimeout(() => {
|
|
1196
|
+
this.triggerHiddenByScroll = false;
|
|
1197
|
+
this.triggerShrunk = false;
|
|
1198
|
+
this.render();
|
|
1199
|
+
}, smart.onIdleMs ?? 900);
|
|
1200
|
+
};
|
|
1201
|
+
window.addEventListener("scroll", onScroll, { passive: true });
|
|
1202
|
+
this.smartHideCleanup = () => window.removeEventListener("scroll", onScroll);
|
|
1203
|
+
}
|
|
1204
|
+
shouldRenderTrigger() {
|
|
1205
|
+
if (!this.triggerVisible) return false;
|
|
1206
|
+
if (this.triggerHiddenByScroll) return false;
|
|
1207
|
+
if (this.config.trigger === "manual" || this.config.trigger === "hidden" || this.config.trigger === "attach") {
|
|
1208
|
+
return false;
|
|
1209
|
+
}
|
|
1210
|
+
if (this.isMobileSmartHidden()) return false;
|
|
1211
|
+
if (this.isRouteHidden()) return false;
|
|
1212
|
+
if (this.config.hideOnSelector && document.querySelector(this.config.hideOnSelector)) return false;
|
|
1213
|
+
const action = this.config.environments[this.detectEnvironment()];
|
|
1214
|
+
return action !== "never" && action !== "manual";
|
|
1215
|
+
}
|
|
1216
|
+
effectiveTrigger() {
|
|
1217
|
+
if (!this.config.smartHide || typeof window === "undefined") return this.config.trigger;
|
|
1218
|
+
const smart = this.config.smartHide === true ? { onMobile: "edge-tab" } : this.config.smartHide;
|
|
1219
|
+
if (window.matchMedia("(max-width: 768px)").matches && smart.onMobile === "edge-tab") {
|
|
1220
|
+
return "edge-tab";
|
|
1221
|
+
}
|
|
1222
|
+
return this.config.trigger;
|
|
1223
|
+
}
|
|
1224
|
+
isMobileSmartHidden() {
|
|
1225
|
+
if (!this.config.smartHide || typeof window === "undefined") return false;
|
|
1226
|
+
const smart = this.config.smartHide === true ? { onMobile: "edge-tab" } : this.config.smartHide;
|
|
1227
|
+
return window.matchMedia("(max-width: 768px)").matches && smart.onMobile === "hide";
|
|
1228
|
+
}
|
|
1229
|
+
detectEnvironment() {
|
|
1230
|
+
const host = typeof location !== "undefined" ? location.hostname : "";
|
|
1231
|
+
if (host === "localhost" || host === "127.0.0.1" || host.endsWith(".local")) return "development";
|
|
1232
|
+
if (/\b(staging|stage|preview|dev)\b/i.test(host)) return "staging";
|
|
1233
|
+
return "production";
|
|
1234
|
+
}
|
|
1235
|
+
isRouteHidden() {
|
|
1236
|
+
if (!this.config.hideOnRoutes.length || typeof location === "undefined") return false;
|
|
1237
|
+
return this.config.hideOnRoutes.some((route) => location.pathname.includes(route));
|
|
1238
|
+
}
|
|
957
1239
|
getTheme() {
|
|
958
1240
|
if (this.config.theme !== "auto") return this.config.theme;
|
|
959
1241
|
if (typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
|
@@ -969,31 +1251,59 @@ var MushiWidget = class {
|
|
|
969
1251
|
const style = document.createElement("style");
|
|
970
1252
|
style.textContent = getWidgetStyles(theme);
|
|
971
1253
|
this.shadow.appendChild(style);
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1254
|
+
if (this.shouldRenderTrigger()) {
|
|
1255
|
+
const effectiveTrigger = this.effectiveTrigger();
|
|
1256
|
+
const trigger = document.createElement("button");
|
|
1257
|
+
trigger.className = `mushi-trigger ${pos}${effectiveTrigger === "edge-tab" ? " edge-tab" : ""}${this.triggerShrunk ? " shrunk" : ""}`;
|
|
1258
|
+
trigger.textContent = this.config.triggerText;
|
|
1259
|
+
trigger.setAttribute("aria-label", t.widget.trigger);
|
|
1260
|
+
trigger.setAttribute("aria-haspopup", "dialog");
|
|
1261
|
+
trigger.setAttribute("aria-expanded", String(this.isOpen));
|
|
1262
|
+
trigger.style.zIndex = String(this.config.zIndex);
|
|
1263
|
+
this.applyInsetVars(trigger);
|
|
1264
|
+
trigger.addEventListener("click", () => {
|
|
1265
|
+
if (this.isOpen) this.close();
|
|
1266
|
+
else this.open();
|
|
1267
|
+
});
|
|
1268
|
+
this.shadow.appendChild(trigger);
|
|
1269
|
+
}
|
|
984
1270
|
const panel = document.createElement("div");
|
|
985
1271
|
panel.className = `mushi-panel ${pos}${this.isOpen ? " open" : " closed"}`;
|
|
986
1272
|
panel.setAttribute("role", "dialog");
|
|
987
1273
|
panel.setAttribute("aria-modal", "true");
|
|
988
1274
|
panel.setAttribute("aria-label", t.widget.title);
|
|
989
1275
|
panel.style.zIndex = String(this.config.zIndex + 1);
|
|
1276
|
+
this.applyInsetVars(panel);
|
|
990
1277
|
if (this.isOpen) {
|
|
991
|
-
panel.innerHTML = this.renderStep()
|
|
1278
|
+
panel.innerHTML = `${this.renderOutdatedBanner()}${this.renderStep()}${this.renderBrandFooter()}`;
|
|
992
1279
|
this.shadow.appendChild(panel);
|
|
993
1280
|
this.attachHandlers(panel);
|
|
994
1281
|
this.trapFocus(panel);
|
|
995
1282
|
}
|
|
996
1283
|
}
|
|
1284
|
+
applyInsetVars(el) {
|
|
1285
|
+
const { anchor } = this.config;
|
|
1286
|
+
if (anchor && Object.keys(anchor).length > 0) {
|
|
1287
|
+
["top", "right", "bottom", "left"].forEach((edge) => {
|
|
1288
|
+
const value = anchor[edge];
|
|
1289
|
+
if (value !== void 0) el.style.setProperty(`--mushi-${edge}`, value);
|
|
1290
|
+
});
|
|
1291
|
+
el.style.setProperty("--mushi-safe-area", this.config.respectSafeArea ? "1" : "0");
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
const { inset } = this.config;
|
|
1295
|
+
if (!this.config.respectSafeArea) {
|
|
1296
|
+
["top", "right", "bottom", "left"].forEach((edge) => {
|
|
1297
|
+
if (inset[edge] === void 0) el.style.setProperty(`--mushi-${edge}`, "24px");
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
["top", "right", "bottom", "left"].forEach((edge) => {
|
|
1301
|
+
const value = inset[edge];
|
|
1302
|
+
if (value === void 0) return;
|
|
1303
|
+
el.style.setProperty(`--mushi-${edge}`, value === "auto" ? "auto" : `${value}px`);
|
|
1304
|
+
});
|
|
1305
|
+
el.style.setProperty("--mushi-safe-area", this.config.respectSafeArea ? "1" : "0");
|
|
1306
|
+
}
|
|
997
1307
|
renderStep() {
|
|
998
1308
|
switch (this.step) {
|
|
999
1309
|
case "category":
|
|
@@ -1004,8 +1314,29 @@ var MushiWidget = class {
|
|
|
1004
1314
|
return this.renderDetailsStep();
|
|
1005
1315
|
case "success":
|
|
1006
1316
|
return this.renderSuccessStep();
|
|
1317
|
+
case "reports":
|
|
1318
|
+
return this.renderReportsStep();
|
|
1319
|
+
case "report-detail":
|
|
1320
|
+
return this.renderReportDetailStep();
|
|
1007
1321
|
}
|
|
1008
1322
|
}
|
|
1323
|
+
renderOutdatedBanner() {
|
|
1324
|
+
if (!this.sdkFreshness) return "";
|
|
1325
|
+
if (this.config.outdatedBanner === "off" || this.config.outdatedBanner === "console-only") return "";
|
|
1326
|
+
const { latest, current, deprecated, message } = this.sdkFreshness;
|
|
1327
|
+
if (!latest && !deprecated) return "";
|
|
1328
|
+
return `
|
|
1329
|
+
<div class="mushi-outdated" role="status">
|
|
1330
|
+
<strong>Mushi SDK ${escapeHtml(current)}</strong>
|
|
1331
|
+
${latest ? `latest is ${escapeHtml(latest)}.` : "needs attention."}
|
|
1332
|
+
${message ? `<span>${escapeHtml(message)}</span>` : ""}
|
|
1333
|
+
</div>
|
|
1334
|
+
`;
|
|
1335
|
+
}
|
|
1336
|
+
renderBrandFooter() {
|
|
1337
|
+
if (this.config.brandFooter === false) return "";
|
|
1338
|
+
return `<div class="mushi-brand-footer">Powered by Mushi v${escapeHtml(this.sdkVersion)}</div>`;
|
|
1339
|
+
}
|
|
1009
1340
|
/**
|
|
1010
1341
|
* Editorial masthead. Always carries:
|
|
1011
1342
|
* • the brand mark (虫 kanji on vermillion, "MUSHI" in mono above)
|
|
@@ -1064,11 +1395,61 @@ var MushiWidget = class {
|
|
|
1064
1395
|
return `
|
|
1065
1396
|
${this.renderHeader({ title: t.step1.heading, step: STEP_NUMBER.category })}
|
|
1066
1397
|
<div class="mushi-body" role="radiogroup" aria-label="${t.step1.heading}">
|
|
1398
|
+
<button type="button" class="mushi-option-btn mushi-reports-entry" data-action="reports">
|
|
1399
|
+
<span class="mushi-option-icon" aria-hidden="true">\u{1F4EC}</span>
|
|
1400
|
+
<div class="mushi-option-text">
|
|
1401
|
+
<span class="mushi-option-label">Your reports${this.unreadCount() ? ` (${this.unreadCount()} new)` : ""}</span>
|
|
1402
|
+
<span class="mushi-option-desc">See status, developer replies, and respond</span>
|
|
1403
|
+
</div>
|
|
1404
|
+
<span class="mushi-option-arrow" aria-hidden="true">\u2192</span>
|
|
1405
|
+
</button>
|
|
1067
1406
|
${categories}
|
|
1068
1407
|
</div>
|
|
1069
1408
|
${this.renderStepIndicator(STEP_NUMBER.category)}
|
|
1070
1409
|
`;
|
|
1071
1410
|
}
|
|
1411
|
+
renderReportsStep() {
|
|
1412
|
+
const reports = this.reporterReports.map((report) => `
|
|
1413
|
+
<button type="button" class="mushi-report-row" data-report-id="${escapeHtml(report.id)}">
|
|
1414
|
+
<span class="mushi-report-status">${escapeHtml(report.status)}</span>
|
|
1415
|
+
<span class="mushi-report-title">${escapeHtml(report.summary ?? report.description ?? `Report ${report.id.slice(0, 8)}`)}</span>
|
|
1416
|
+
${report.unread_count ? `<b>${report.unread_count}</b>` : ""}
|
|
1417
|
+
</button>
|
|
1418
|
+
`).join("");
|
|
1419
|
+
return `
|
|
1420
|
+
${this.renderHeader({ title: "Your reports", showBack: true, eyebrow: "Mushi \xB7 Inbox" })}
|
|
1421
|
+
<div class="mushi-body">
|
|
1422
|
+
${this.reporterLoading ? '<p class="mushi-muted">Loading reports\u2026</p>' : ""}
|
|
1423
|
+
${this.reporterError ? `<p class="mushi-error-inline">${escapeHtml(this.reporterError)}</p>` : ""}
|
|
1424
|
+
${reports || (!this.reporterLoading ? '<p class="mushi-muted">No reports from this browser yet.</p>' : "")}
|
|
1425
|
+
</div>
|
|
1426
|
+
`;
|
|
1427
|
+
}
|
|
1428
|
+
renderReportDetailStep() {
|
|
1429
|
+
const report = this.reporterReports.find((r) => r.id === this.selectedReportId);
|
|
1430
|
+
const comments = this.reporterComments.map((comment) => `
|
|
1431
|
+
<div class="mushi-thread-comment ${comment.author_kind}">
|
|
1432
|
+
<strong>${escapeHtml(comment.author_kind === "reporter" ? "You" : comment.author_name ?? "Developer")}</strong>
|
|
1433
|
+
<p>${escapeHtml(comment.body)}</p>
|
|
1434
|
+
</div>
|
|
1435
|
+
`).join("");
|
|
1436
|
+
return `
|
|
1437
|
+
${this.renderHeader({ title: "Report thread", showBack: true, eyebrow: "Mushi \xB7 Inbox" })}
|
|
1438
|
+
<div class="mushi-body">
|
|
1439
|
+
<div class="mushi-thread-summary">
|
|
1440
|
+
<span>${escapeHtml(report?.status ?? "unknown")}</span>
|
|
1441
|
+
<p>${escapeHtml(report?.summary ?? report?.description ?? "Report details")}</p>
|
|
1442
|
+
</div>
|
|
1443
|
+
<div class="mushi-thread">
|
|
1444
|
+
${this.reporterLoading ? '<p class="mushi-muted">Loading thread\u2026</p>' : comments || '<p class="mushi-muted">No developer replies yet.</p>'}
|
|
1445
|
+
</div>
|
|
1446
|
+
<textarea class="mushi-textarea" data-role="reporter-reply" rows="3" placeholder="Reply to the developer\u2026"></textarea>
|
|
1447
|
+
<button type="button" class="mushi-submit" data-action="reporter-reply">
|
|
1448
|
+
<span>Reply</span><span class="mushi-submit-arrow" aria-hidden="true">\u2192</span>
|
|
1449
|
+
</button>
|
|
1450
|
+
</div>
|
|
1451
|
+
`;
|
|
1452
|
+
}
|
|
1072
1453
|
renderIntentStep() {
|
|
1073
1454
|
const t = this.locale;
|
|
1074
1455
|
const cat = this.selectedCategory;
|
|
@@ -1108,6 +1489,7 @@ var MushiWidget = class {
|
|
|
1108
1489
|
<button type="button" class="mushi-attach-btn${this.screenshotAttached ? " active" : ""}" data-action="screenshot">
|
|
1109
1490
|
\u{1F4F8} ${this.screenshotAttached ? t.step3.screenshotAttached : t.step3.screenshotButton}
|
|
1110
1491
|
</button>
|
|
1492
|
+
${this.screenshotAttached && this.allowScreenshotRemove ? '<button type="button" class="mushi-attach-btn danger" data-action="remove-screenshot">\u2715 Remove screenshot</button>' : ""}
|
|
1111
1493
|
<button type="button" class="mushi-attach-btn${this.elementSelected ? " active" : ""}" data-action="element">
|
|
1112
1494
|
\u{1F3AF} ${this.elementSelected ? t.step3.elementSelected : t.step3.elementButton}
|
|
1113
1495
|
</button>
|
|
@@ -1160,9 +1542,26 @@ var MushiWidget = class {
|
|
|
1160
1542
|
} else if (this.step === "details") {
|
|
1161
1543
|
this.step = "intent";
|
|
1162
1544
|
this.selectedIntent = null;
|
|
1545
|
+
} else if (this.step === "reports") {
|
|
1546
|
+
this.step = "category";
|
|
1547
|
+
} else if (this.step === "report-detail") {
|
|
1548
|
+
this.step = "reports";
|
|
1549
|
+
this.selectedReportId = null;
|
|
1163
1550
|
}
|
|
1164
1551
|
this.render();
|
|
1165
1552
|
});
|
|
1553
|
+
panel.querySelector('[data-action="reports"]')?.addEventListener("click", () => {
|
|
1554
|
+
void this.loadReporterReports();
|
|
1555
|
+
});
|
|
1556
|
+
panel.querySelectorAll("[data-report-id]").forEach((btn) => {
|
|
1557
|
+
btn.addEventListener("click", () => {
|
|
1558
|
+
const reportId = btn.dataset.reportId;
|
|
1559
|
+
if (reportId) void this.loadReporterComments(reportId);
|
|
1560
|
+
});
|
|
1561
|
+
});
|
|
1562
|
+
panel.querySelector('[data-action="reporter-reply"]')?.addEventListener("click", () => {
|
|
1563
|
+
void this.submitReporterReply(panel);
|
|
1564
|
+
});
|
|
1166
1565
|
panel.querySelectorAll("[data-category]").forEach((btn) => {
|
|
1167
1566
|
btn.addEventListener("click", () => {
|
|
1168
1567
|
this.selectedCategory = btn.dataset.category;
|
|
@@ -1180,6 +1579,9 @@ var MushiWidget = class {
|
|
|
1180
1579
|
panel.querySelector('[data-action="screenshot"]')?.addEventListener("click", () => {
|
|
1181
1580
|
this.callbacks.onScreenshotRequest();
|
|
1182
1581
|
});
|
|
1582
|
+
panel.querySelector('[data-action="remove-screenshot"]')?.addEventListener("click", () => {
|
|
1583
|
+
this.callbacks.onScreenshotRemove?.();
|
|
1584
|
+
});
|
|
1183
1585
|
panel.querySelector('[data-action="element"]')?.addEventListener("click", () => {
|
|
1184
1586
|
this.callbacks.onElementSelectorRequest?.();
|
|
1185
1587
|
});
|
|
@@ -1237,6 +1639,57 @@ var MushiWidget = class {
|
|
|
1237
1639
|
if (focusable.length > 0) focusable[0].focus();
|
|
1238
1640
|
});
|
|
1239
1641
|
}
|
|
1642
|
+
unreadCount() {
|
|
1643
|
+
return this.reporterReports.reduce((sum, report) => sum + (report.unread_count ?? 0), 0);
|
|
1644
|
+
}
|
|
1645
|
+
async loadReporterReports() {
|
|
1646
|
+
this.step = "reports";
|
|
1647
|
+
this.reporterLoading = true;
|
|
1648
|
+
this.reporterError = null;
|
|
1649
|
+
this.render();
|
|
1650
|
+
try {
|
|
1651
|
+
this.reporterReports = await this.callbacks.onReporterReportsRequest?.() ?? [];
|
|
1652
|
+
} catch (err) {
|
|
1653
|
+
this.reporterError = err instanceof Error ? err.message : "Could not load reports.";
|
|
1654
|
+
} finally {
|
|
1655
|
+
this.reporterLoading = false;
|
|
1656
|
+
this.render();
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
async loadReporterComments(reportId) {
|
|
1660
|
+
this.selectedReportId = reportId;
|
|
1661
|
+
this.step = "report-detail";
|
|
1662
|
+
this.reporterLoading = true;
|
|
1663
|
+
this.reporterError = null;
|
|
1664
|
+
this.render();
|
|
1665
|
+
try {
|
|
1666
|
+
this.reporterComments = await this.callbacks.onReporterCommentsRequest?.(reportId) ?? [];
|
|
1667
|
+
} catch (err) {
|
|
1668
|
+
this.reporterError = err instanceof Error ? err.message : "Could not load thread.";
|
|
1669
|
+
} finally {
|
|
1670
|
+
this.reporterLoading = false;
|
|
1671
|
+
this.render();
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
async submitReporterReply(panel) {
|
|
1675
|
+
const reportId = this.selectedReportId;
|
|
1676
|
+
const textarea = panel.querySelector('[data-role="reporter-reply"]');
|
|
1677
|
+
const replyButton = panel.querySelector('[data-action="reporter-reply"]');
|
|
1678
|
+
const body = textarea?.value.trim() ?? "";
|
|
1679
|
+
if (!reportId || !body || this.reporterLoading) return;
|
|
1680
|
+
this.reporterLoading = true;
|
|
1681
|
+
if (replyButton) replyButton.disabled = true;
|
|
1682
|
+
this.render();
|
|
1683
|
+
try {
|
|
1684
|
+
await this.callbacks.onReporterReply?.(reportId, body);
|
|
1685
|
+
if (textarea) textarea.value = "";
|
|
1686
|
+
await this.loadReporterComments(reportId);
|
|
1687
|
+
} catch (err) {
|
|
1688
|
+
this.reporterError = err instanceof Error ? err.message : "Could not send reply.";
|
|
1689
|
+
this.reporterLoading = false;
|
|
1690
|
+
this.render();
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1240
1693
|
};
|
|
1241
1694
|
|
|
1242
1695
|
// src/capture/console.ts
|
|
@@ -1288,35 +1741,109 @@ function createConsoleCapture() {
|
|
|
1288
1741
|
}
|
|
1289
1742
|
};
|
|
1290
1743
|
}
|
|
1744
|
+
var DEFAULT_INTERNAL_URL_MATCHERS = [
|
|
1745
|
+
/\/v1\/sdk(?:\/|$)/,
|
|
1746
|
+
/\/v1\/reports(?:\/|$)/,
|
|
1747
|
+
/\/v1\/notifications(?:\/|$)/,
|
|
1748
|
+
/\/v1\/reputation(?:\/|$)/
|
|
1749
|
+
];
|
|
1750
|
+
function getRequestUrl(input) {
|
|
1751
|
+
if (typeof input === "string") return input;
|
|
1752
|
+
if (input instanceof URL) return input.href;
|
|
1753
|
+
return input.url;
|
|
1754
|
+
}
|
|
1755
|
+
function getInternalRequestKind(input, init) {
|
|
1756
|
+
const marker = init?.[core.MUSHI_INTERNAL_INIT_MARKER];
|
|
1757
|
+
if (marker) return marker;
|
|
1758
|
+
const initHeader = readHeader(init?.headers, core.MUSHI_INTERNAL_HEADER);
|
|
1759
|
+
if (initHeader) return initHeader;
|
|
1760
|
+
if (typeof Request !== "undefined" && input instanceof Request) {
|
|
1761
|
+
const requestHeader = input.headers.get(core.MUSHI_INTERNAL_HEADER);
|
|
1762
|
+
if (requestHeader) return requestHeader;
|
|
1763
|
+
}
|
|
1764
|
+
return null;
|
|
1765
|
+
}
|
|
1766
|
+
function shouldIgnoreMushiUrl(url, options = {}) {
|
|
1767
|
+
const matchers = [...DEFAULT_INTERNAL_URL_MATCHERS, ...options.ignoreUrls ?? []];
|
|
1768
|
+
if (matchers.some((matcher) => matchesUrl(url, matcher))) return true;
|
|
1769
|
+
const endpoint = normalizeUrlPrefix(options.apiEndpoint);
|
|
1770
|
+
return endpoint ? normalizeComparableUrl(url).startsWith(endpoint) : false;
|
|
1771
|
+
}
|
|
1772
|
+
function matchesUrl(url, matcher) {
|
|
1773
|
+
if (typeof matcher === "string") {
|
|
1774
|
+
return normalizeComparableUrl(url).includes(matcher);
|
|
1775
|
+
}
|
|
1776
|
+
matcher.lastIndex = 0;
|
|
1777
|
+
return matcher.test(url);
|
|
1778
|
+
}
|
|
1779
|
+
function normalizeUrlPrefix(url) {
|
|
1780
|
+
if (!url) return null;
|
|
1781
|
+
return normalizeComparableUrl(url).replace(/\/+$/, "");
|
|
1782
|
+
}
|
|
1783
|
+
function isLocalhostEndpoint(url) {
|
|
1784
|
+
if (!url) return false;
|
|
1785
|
+
try {
|
|
1786
|
+
const parsed = new URL(url);
|
|
1787
|
+
return parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "::1" || parsed.hostname.endsWith(".localhost");
|
|
1788
|
+
} catch {
|
|
1789
|
+
return /\blocalhost\b|127\.0\.0\.1/.test(url);
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
function normalizeComparableUrl(url) {
|
|
1793
|
+
try {
|
|
1794
|
+
return new URL(url, typeof location !== "undefined" ? location.href : "http://localhost").href;
|
|
1795
|
+
} catch {
|
|
1796
|
+
return url;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
function readHeader(headers, name) {
|
|
1800
|
+
if (!headers) return null;
|
|
1801
|
+
if (typeof Headers !== "undefined" && headers instanceof Headers) {
|
|
1802
|
+
return headers.get(name);
|
|
1803
|
+
}
|
|
1804
|
+
if (Array.isArray(headers)) {
|
|
1805
|
+
const found = headers.find(([key]) => key.toLowerCase() === name.toLowerCase());
|
|
1806
|
+
return found?.[1] ?? null;
|
|
1807
|
+
}
|
|
1808
|
+
const record = headers;
|
|
1809
|
+
return record[name] ?? record[name.toLowerCase()] ?? null;
|
|
1810
|
+
}
|
|
1291
1811
|
|
|
1292
1812
|
// src/capture/network.ts
|
|
1293
1813
|
var MAX_ENTRIES2 = 30;
|
|
1294
|
-
function createNetworkCapture() {
|
|
1814
|
+
function createNetworkCapture(options = {}) {
|
|
1295
1815
|
const entries = [];
|
|
1296
1816
|
const originalFetch = globalThis.fetch;
|
|
1817
|
+
let activeOptions = options;
|
|
1297
1818
|
globalThis.fetch = async function mushiFetchInterceptor(input, init) {
|
|
1298
1819
|
const startTime = Date.now();
|
|
1299
1820
|
const method = init?.method?.toUpperCase() ?? "GET";
|
|
1300
|
-
const url =
|
|
1821
|
+
const url = getRequestUrl(input);
|
|
1822
|
+
const internalKind = getInternalRequestKind(input, init);
|
|
1823
|
+
const shouldRecord = !internalKind && !shouldIgnoreMushiUrl(url, activeOptions);
|
|
1301
1824
|
try {
|
|
1302
1825
|
const response = await originalFetch.call(globalThis, input, init);
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1826
|
+
if (shouldRecord) {
|
|
1827
|
+
addEntry({
|
|
1828
|
+
method,
|
|
1829
|
+
url: truncateUrl(url),
|
|
1830
|
+
status: response.status,
|
|
1831
|
+
duration: Date.now() - startTime,
|
|
1832
|
+
timestamp: startTime
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1310
1835
|
return response;
|
|
1311
1836
|
} catch (error) {
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1837
|
+
if (shouldRecord) {
|
|
1838
|
+
addEntry({
|
|
1839
|
+
method,
|
|
1840
|
+
url: truncateUrl(url),
|
|
1841
|
+
status: 0,
|
|
1842
|
+
duration: Date.now() - startTime,
|
|
1843
|
+
timestamp: startTime,
|
|
1844
|
+
error: error instanceof Error ? error.message : "Network error"
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1320
1847
|
throw error;
|
|
1321
1848
|
}
|
|
1322
1849
|
};
|
|
@@ -1333,6 +1860,9 @@ function createNetworkCapture() {
|
|
|
1333
1860
|
clear() {
|
|
1334
1861
|
entries.length = 0;
|
|
1335
1862
|
},
|
|
1863
|
+
updateOptions(nextOptions) {
|
|
1864
|
+
activeOptions = nextOptions;
|
|
1865
|
+
},
|
|
1336
1866
|
destroy() {
|
|
1337
1867
|
globalThis.fetch = originalFetch;
|
|
1338
1868
|
}
|
|
@@ -1352,7 +1882,8 @@ function truncateUrl(url) {
|
|
|
1352
1882
|
}
|
|
1353
1883
|
|
|
1354
1884
|
// src/capture/screenshot.ts
|
|
1355
|
-
function createScreenshotCapture() {
|
|
1885
|
+
function createScreenshotCapture(options = {}) {
|
|
1886
|
+
let activeOptions = options;
|
|
1356
1887
|
async function take() {
|
|
1357
1888
|
try {
|
|
1358
1889
|
if (typeof document === "undefined") return null;
|
|
@@ -1365,11 +1896,12 @@ function createScreenshotCapture() {
|
|
|
1365
1896
|
canvas.width = width * dpr;
|
|
1366
1897
|
canvas.height = height * dpr;
|
|
1367
1898
|
ctx.scale(dpr, dpr);
|
|
1899
|
+
const safeDocument = buildPrivacySafeDocument(activeOptions.privacy);
|
|
1368
1900
|
const svgData = `
|
|
1369
1901
|
<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
|
|
1370
1902
|
<foreignObject width="100%" height="100%">
|
|
1371
1903
|
<div xmlns="http://www.w3.org/1999/xhtml">
|
|
1372
|
-
${new XMLSerializer().serializeToString(
|
|
1904
|
+
${new XMLSerializer().serializeToString(safeDocument)}
|
|
1373
1905
|
</div>
|
|
1374
1906
|
</foreignObject>
|
|
1375
1907
|
</svg>
|
|
@@ -1398,7 +1930,46 @@ function createScreenshotCapture() {
|
|
|
1398
1930
|
return null;
|
|
1399
1931
|
}
|
|
1400
1932
|
}
|
|
1401
|
-
return {
|
|
1933
|
+
return {
|
|
1934
|
+
take,
|
|
1935
|
+
updateOptions(nextOptions) {
|
|
1936
|
+
activeOptions = nextOptions;
|
|
1937
|
+
}
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
function buildPrivacySafeDocument(privacy) {
|
|
1941
|
+
const clone = document.documentElement.cloneNode(true);
|
|
1942
|
+
for (const selector of privacy?.blockSelectors ?? []) {
|
|
1943
|
+
for (const el of safeQueryAll(clone, selector)) {
|
|
1944
|
+
el.remove();
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
for (const selector of privacy?.maskSelectors ?? []) {
|
|
1948
|
+
for (const el of safeQueryAll(clone, selector)) {
|
|
1949
|
+
maskElement(el);
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
return clone;
|
|
1953
|
+
}
|
|
1954
|
+
function safeQueryAll(root, selector) {
|
|
1955
|
+
try {
|
|
1956
|
+
return Array.from(root.querySelectorAll(selector));
|
|
1957
|
+
} catch {
|
|
1958
|
+
return [];
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
function maskElement(el) {
|
|
1962
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
1963
|
+
el.value = "";
|
|
1964
|
+
el.setAttribute("value", "");
|
|
1965
|
+
el.setAttribute("placeholder", "\u2022\u2022\u2022\u2022");
|
|
1966
|
+
}
|
|
1967
|
+
el.textContent = el.children.length === 0 ? "\u2022\u2022\u2022\u2022" : el.textContent;
|
|
1968
|
+
el.setAttribute(
|
|
1969
|
+
"style",
|
|
1970
|
+
`${el.getAttribute("style") ?? ""};background:#8f8f8f!important;color:transparent!important;text-shadow:none!important;`
|
|
1971
|
+
);
|
|
1972
|
+
el.setAttribute("data-mushi-masked", "true");
|
|
1402
1973
|
}
|
|
1403
1974
|
|
|
1404
1975
|
// src/capture/performance.ts
|
|
@@ -1587,6 +2158,108 @@ function createElementSelector() {
|
|
|
1587
2158
|
return { activate, deactivate, isActive: () => active };
|
|
1588
2159
|
}
|
|
1589
2160
|
|
|
2161
|
+
// src/capture/timeline.ts
|
|
2162
|
+
var MAX_TIMELINE_ENTRIES = 120;
|
|
2163
|
+
function createTimelineCapture() {
|
|
2164
|
+
const entries = [];
|
|
2165
|
+
const originalPushState = history.pushState;
|
|
2166
|
+
const originalReplaceState = history.replaceState;
|
|
2167
|
+
const handlePopState = () => recordRoute("popstate");
|
|
2168
|
+
const handleHashChange = () => recordRoute("hashchange");
|
|
2169
|
+
recordRoute("initial");
|
|
2170
|
+
function record(entry) {
|
|
2171
|
+
entries.push(entry);
|
|
2172
|
+
if (entries.length > MAX_TIMELINE_ENTRIES) entries.shift();
|
|
2173
|
+
}
|
|
2174
|
+
function recordRoute(source) {
|
|
2175
|
+
if (typeof location === "undefined") return;
|
|
2176
|
+
record({
|
|
2177
|
+
ts: Date.now(),
|
|
2178
|
+
kind: "route",
|
|
2179
|
+
payload: {
|
|
2180
|
+
source,
|
|
2181
|
+
route: `${location.pathname}${location.search}${location.hash}`,
|
|
2182
|
+
href: location.href
|
|
2183
|
+
}
|
|
2184
|
+
});
|
|
2185
|
+
}
|
|
2186
|
+
function handleClick(event) {
|
|
2187
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
2188
|
+
if (!target) return;
|
|
2189
|
+
const el = target.closest('button,a,[role="button"],input,textarea,select,[data-mushi-track]') ?? target;
|
|
2190
|
+
record({
|
|
2191
|
+
ts: Date.now(),
|
|
2192
|
+
kind: "click",
|
|
2193
|
+
payload: {
|
|
2194
|
+
tag: el.tagName.toLowerCase(),
|
|
2195
|
+
id: el.id || void 0,
|
|
2196
|
+
text: textSnippet(el)
|
|
2197
|
+
}
|
|
2198
|
+
});
|
|
2199
|
+
}
|
|
2200
|
+
history.pushState = function mushiPushState(...args) {
|
|
2201
|
+
const result = originalPushState.apply(this, args);
|
|
2202
|
+
recordRoute("pushState");
|
|
2203
|
+
return result;
|
|
2204
|
+
};
|
|
2205
|
+
history.replaceState = function mushiReplaceState(...args) {
|
|
2206
|
+
const result = originalReplaceState.apply(this, args);
|
|
2207
|
+
recordRoute("replaceState");
|
|
2208
|
+
return result;
|
|
2209
|
+
};
|
|
2210
|
+
window.addEventListener("popstate", handlePopState);
|
|
2211
|
+
window.addEventListener("hashchange", handleHashChange);
|
|
2212
|
+
document.addEventListener("click", handleClick, true);
|
|
2213
|
+
return {
|
|
2214
|
+
setScreen(screen) {
|
|
2215
|
+
record({
|
|
2216
|
+
ts: Date.now(),
|
|
2217
|
+
kind: "screen",
|
|
2218
|
+
payload: screen
|
|
2219
|
+
});
|
|
2220
|
+
},
|
|
2221
|
+
getEntries(input = {}) {
|
|
2222
|
+
const merged = [
|
|
2223
|
+
...entries,
|
|
2224
|
+
...(input.consoleLogs ?? []).map((log) => ({
|
|
2225
|
+
ts: log.timestamp,
|
|
2226
|
+
kind: "log",
|
|
2227
|
+
payload: {
|
|
2228
|
+
level: log.level,
|
|
2229
|
+
message: log.message
|
|
2230
|
+
}
|
|
2231
|
+
})),
|
|
2232
|
+
...(input.networkLogs ?? []).map((network) => ({
|
|
2233
|
+
ts: network.timestamp,
|
|
2234
|
+
kind: "request",
|
|
2235
|
+
payload: {
|
|
2236
|
+
method: network.method,
|
|
2237
|
+
url: network.url,
|
|
2238
|
+
status: network.status,
|
|
2239
|
+
duration: network.duration,
|
|
2240
|
+
error: network.error
|
|
2241
|
+
}
|
|
2242
|
+
}))
|
|
2243
|
+
].sort((a, b) => a.ts - b.ts);
|
|
2244
|
+
return merged.slice(-MAX_TIMELINE_ENTRIES);
|
|
2245
|
+
},
|
|
2246
|
+
clear() {
|
|
2247
|
+
entries.length = 0;
|
|
2248
|
+
},
|
|
2249
|
+
destroy() {
|
|
2250
|
+
history.pushState = originalPushState;
|
|
2251
|
+
history.replaceState = originalReplaceState;
|
|
2252
|
+
window.removeEventListener("popstate", handlePopState);
|
|
2253
|
+
window.removeEventListener("hashchange", handleHashChange);
|
|
2254
|
+
document.removeEventListener("click", handleClick, true);
|
|
2255
|
+
}
|
|
2256
|
+
};
|
|
2257
|
+
}
|
|
2258
|
+
function textSnippet(el) {
|
|
2259
|
+
const text = (el.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
2260
|
+
return text ? text.slice(0, 80) : void 0;
|
|
2261
|
+
}
|
|
2262
|
+
|
|
1590
2263
|
// src/sentry.ts
|
|
1591
2264
|
function getSentryGlobal() {
|
|
1592
2265
|
try {
|
|
@@ -1673,36 +2346,25 @@ function setupProactiveTriggers(callbacks, config = {}) {
|
|
|
1673
2346
|
} catch {
|
|
1674
2347
|
}
|
|
1675
2348
|
}
|
|
1676
|
-
|
|
2349
|
+
const apiCascade = normalizeApiCascadeConfig(config.apiCascade);
|
|
2350
|
+
if (apiCascade.enabled) {
|
|
1677
2351
|
const failedRequests = [];
|
|
1678
2352
|
const origFetch = globalThis.fetch;
|
|
1679
2353
|
globalThis.fetch = async function(...args) {
|
|
2354
|
+
const [input, init] = args;
|
|
2355
|
+
const url = getRequestUrl(input);
|
|
2356
|
+
const ignoreFailure = Boolean(getInternalRequestKind(input, init)) || shouldIgnoreMushiUrl(url, {
|
|
2357
|
+
apiEndpoint: config.apiEndpoint,
|
|
2358
|
+
ignoreUrls: apiCascade.ignoreUrls
|
|
2359
|
+
});
|
|
1680
2360
|
try {
|
|
1681
2361
|
const res = await origFetch.apply(this, args);
|
|
1682
|
-
if (!res.ok && res.status >= 400) {
|
|
1683
|
-
|
|
1684
|
-
failedRequests.push(now);
|
|
1685
|
-
const recentFailures = failedRequests.filter((t) => now - t < 1e4);
|
|
1686
|
-
if (recentFailures.length >= 3) {
|
|
1687
|
-
callbacks.onTrigger("api_cascade", {
|
|
1688
|
-
failureCount: recentFailures.length,
|
|
1689
|
-
windowMs: 1e4
|
|
1690
|
-
});
|
|
1691
|
-
failedRequests.length = 0;
|
|
1692
|
-
}
|
|
2362
|
+
if (!ignoreFailure && !res.ok && res.status >= 400) {
|
|
2363
|
+
recordApiFailure(failedRequests, callbacks);
|
|
1693
2364
|
}
|
|
1694
2365
|
return res;
|
|
1695
2366
|
} catch (err) {
|
|
1696
|
-
|
|
1697
|
-
failedRequests.push(now);
|
|
1698
|
-
const recentFailures = failedRequests.filter((t) => now - t < 1e4);
|
|
1699
|
-
if (recentFailures.length >= 3) {
|
|
1700
|
-
callbacks.onTrigger("api_cascade", {
|
|
1701
|
-
failureCount: recentFailures.length,
|
|
1702
|
-
windowMs: 1e4
|
|
1703
|
-
});
|
|
1704
|
-
failedRequests.length = 0;
|
|
1705
|
-
}
|
|
2367
|
+
if (!ignoreFailure) recordApiFailure(failedRequests, callbacks);
|
|
1706
2368
|
throw err;
|
|
1707
2369
|
}
|
|
1708
2370
|
};
|
|
@@ -1737,6 +2399,28 @@ function setupProactiveTriggers(callbacks, config = {}) {
|
|
|
1737
2399
|
}
|
|
1738
2400
|
};
|
|
1739
2401
|
}
|
|
2402
|
+
function normalizeApiCascadeConfig(config) {
|
|
2403
|
+
if (config === false) return { enabled: false, ignoreUrls: [] };
|
|
2404
|
+
if (config && typeof config === "object") {
|
|
2405
|
+
return {
|
|
2406
|
+
enabled: config.enabled !== false,
|
|
2407
|
+
ignoreUrls: config.ignoreUrls ?? []
|
|
2408
|
+
};
|
|
2409
|
+
}
|
|
2410
|
+
return { enabled: true, ignoreUrls: [] };
|
|
2411
|
+
}
|
|
2412
|
+
function recordApiFailure(failedRequests, callbacks) {
|
|
2413
|
+
const now = Date.now();
|
|
2414
|
+
failedRequests.push(now);
|
|
2415
|
+
const recentFailures = failedRequests.filter((t) => now - t < 1e4);
|
|
2416
|
+
if (recentFailures.length >= 3) {
|
|
2417
|
+
callbacks.onTrigger("api_cascade", {
|
|
2418
|
+
failureCount: recentFailures.length,
|
|
2419
|
+
windowMs: 1e4
|
|
2420
|
+
});
|
|
2421
|
+
failedRequests.length = 0;
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
1740
2424
|
|
|
1741
2425
|
// src/proactive-manager.ts
|
|
1742
2426
|
var STORAGE_KEY_LAST_DISMISS = "mushi:lastDismiss";
|
|
@@ -1789,6 +2473,10 @@ function createProactiveManager(config = {}) {
|
|
|
1789
2473
|
return { shouldShow, recordDismissal, recordSubmission, reset };
|
|
1790
2474
|
}
|
|
1791
2475
|
|
|
2476
|
+
// src/version.ts
|
|
2477
|
+
var MUSHI_SDK_PACKAGE = "@mushi-mushi/web";
|
|
2478
|
+
var MUSHI_SDK_VERSION = "0.8.0" ;
|
|
2479
|
+
|
|
1792
2480
|
// src/mushi.ts
|
|
1793
2481
|
var instance = null;
|
|
1794
2482
|
var Mushi = class {
|
|
@@ -1818,18 +2506,21 @@ var Mushi = class {
|
|
|
1818
2506
|
instance?.destroy();
|
|
1819
2507
|
instance = null;
|
|
1820
2508
|
}
|
|
2509
|
+
static diagnose() {
|
|
2510
|
+
return instance?.diagnose() ?? diagnoseWithoutInstance();
|
|
2511
|
+
}
|
|
1821
2512
|
};
|
|
1822
2513
|
function createInstance(config) {
|
|
1823
|
-
const bootstrapConfig = config;
|
|
1824
|
-
let activeConfig =
|
|
2514
|
+
const bootstrapConfig = applyPresetConfig(config);
|
|
2515
|
+
let activeConfig = bootstrapConfig;
|
|
1825
2516
|
const log = config.debug ?? false ? core.createLogger({ scope: "mushi", level: "debug", format: "pretty" }) : core.noopLogger;
|
|
1826
2517
|
const apiClient = core.createApiClient({
|
|
1827
|
-
projectId:
|
|
1828
|
-
apiKey:
|
|
1829
|
-
...
|
|
2518
|
+
projectId: bootstrapConfig.projectId,
|
|
2519
|
+
apiKey: bootstrapConfig.apiKey,
|
|
2520
|
+
...bootstrapConfig.apiEndpoint ? { apiEndpoint: bootstrapConfig.apiEndpoint } : {}
|
|
1830
2521
|
});
|
|
1831
|
-
const preFilter = core.createPreFilter(
|
|
1832
|
-
const offlineQueue = core.createOfflineQueue(
|
|
2522
|
+
const preFilter = core.createPreFilter(bootstrapConfig.preFilter);
|
|
2523
|
+
const offlineQueue = core.createOfflineQueue(bootstrapConfig.offline);
|
|
1833
2524
|
const rateLimiter = core.createRateLimiter({ maxBurst: 10, refillRate: 1, refillIntervalMs: 5e3 });
|
|
1834
2525
|
const piiScrubber = core.createPiiScrubber();
|
|
1835
2526
|
let consoleCap = null;
|
|
@@ -1837,6 +2528,8 @@ function createInstance(config) {
|
|
|
1837
2528
|
let perfCap = null;
|
|
1838
2529
|
let screenshotCap = null;
|
|
1839
2530
|
let elementSelector = null;
|
|
2531
|
+
const timelineCap = createTimelineCapture();
|
|
2532
|
+
let widget;
|
|
1840
2533
|
function syncCaptureModules() {
|
|
1841
2534
|
if (activeConfig.capture?.console !== false) {
|
|
1842
2535
|
consoleCap ??= createConsoleCapture();
|
|
@@ -1845,7 +2538,15 @@ function createInstance(config) {
|
|
|
1845
2538
|
consoleCap = null;
|
|
1846
2539
|
}
|
|
1847
2540
|
if (activeConfig.capture?.network !== false) {
|
|
1848
|
-
|
|
2541
|
+
const networkOptions = {
|
|
2542
|
+
apiEndpoint: resolveApiEndpoint(activeConfig),
|
|
2543
|
+
ignoreUrls: activeConfig.capture?.ignoreUrls
|
|
2544
|
+
};
|
|
2545
|
+
if (networkCap) {
|
|
2546
|
+
networkCap.updateOptions(networkOptions);
|
|
2547
|
+
} else {
|
|
2548
|
+
networkCap = createNetworkCapture(networkOptions);
|
|
2549
|
+
}
|
|
1849
2550
|
} else {
|
|
1850
2551
|
networkCap?.destroy();
|
|
1851
2552
|
networkCap = null;
|
|
@@ -1856,8 +2557,18 @@ function createInstance(config) {
|
|
|
1856
2557
|
perfCap?.destroy();
|
|
1857
2558
|
perfCap = null;
|
|
1858
2559
|
}
|
|
1859
|
-
|
|
2560
|
+
if (activeConfig.capture?.screenshot !== "off") {
|
|
2561
|
+
const screenshotOptions = { privacy: activeConfig.privacy };
|
|
2562
|
+
if (screenshotCap) {
|
|
2563
|
+
screenshotCap.updateOptions(screenshotOptions);
|
|
2564
|
+
} else {
|
|
2565
|
+
screenshotCap = createScreenshotCapture(screenshotOptions);
|
|
2566
|
+
}
|
|
2567
|
+
} else {
|
|
2568
|
+
screenshotCap = null;
|
|
2569
|
+
}
|
|
1860
2570
|
if (!screenshotCap) pendingScreenshot = null;
|
|
2571
|
+
widget.setAllowScreenshotRemove(activeConfig.privacy?.allowUserRemoveScreenshot !== false);
|
|
1861
2572
|
if (activeConfig.capture?.elementSelector !== false) {
|
|
1862
2573
|
elementSelector ??= createElementSelector();
|
|
1863
2574
|
} else {
|
|
@@ -1873,10 +2584,10 @@ function createInstance(config) {
|
|
|
1873
2584
|
let pendingScreenshot = null;
|
|
1874
2585
|
let pendingElement = null;
|
|
1875
2586
|
let pendingProactiveTrigger = null;
|
|
2587
|
+
let runtimeConfigLoaded = false;
|
|
1876
2588
|
let userInfo = null;
|
|
1877
2589
|
const customMetadata = {};
|
|
1878
|
-
|
|
1879
|
-
const widget = new MushiWidget(config.widget, {
|
|
2590
|
+
widget = new MushiWidget(bootstrapConfig.widget, {
|
|
1880
2591
|
onSubmit: async ({ category, description, intent }) => {
|
|
1881
2592
|
log.info("Report submitted", { category, intent });
|
|
1882
2593
|
proactiveManager?.recordSubmission();
|
|
@@ -1903,6 +2614,11 @@ function createInstance(config) {
|
|
|
1903
2614
|
pendingScreenshot = await screenshotCap.take();
|
|
1904
2615
|
widget.setScreenshotAttached(pendingScreenshot !== null);
|
|
1905
2616
|
},
|
|
2617
|
+
onScreenshotRemove: () => {
|
|
2618
|
+
log.debug("Screenshot attachment removed");
|
|
2619
|
+
pendingScreenshot = null;
|
|
2620
|
+
widget.setScreenshotAttached(false);
|
|
2621
|
+
},
|
|
1906
2622
|
onElementSelectorRequest: async () => {
|
|
1907
2623
|
if (!elementSelector || activeConfig.capture?.elementSelector === false) return;
|
|
1908
2624
|
log.debug("Element selector activated");
|
|
@@ -1912,8 +2628,23 @@ function createInstance(config) {
|
|
|
1912
2628
|
widget.setElementSelected(true);
|
|
1913
2629
|
log.debug("Element selected", { tagName: el.tagName, xpath: el.xpath });
|
|
1914
2630
|
}
|
|
2631
|
+
},
|
|
2632
|
+
async onReporterReportsRequest() {
|
|
2633
|
+
const result = await apiClient.listReporterReports(core.getReporterToken());
|
|
2634
|
+
if (!result.ok) throw new Error(result.error?.message ?? "Could not load reports");
|
|
2635
|
+
return result.data?.reports ?? [];
|
|
2636
|
+
},
|
|
2637
|
+
async onReporterCommentsRequest(reportId) {
|
|
2638
|
+
const result = await apiClient.listReporterComments(reportId, core.getReporterToken());
|
|
2639
|
+
if (!result.ok) throw new Error(result.error?.message ?? "Could not load thread");
|
|
2640
|
+
return result.data?.comments ?? [];
|
|
2641
|
+
},
|
|
2642
|
+
async onReporterReply(reportId, body) {
|
|
2643
|
+
const result = await apiClient.replyToReporterReport(reportId, core.getReporterToken(), body);
|
|
2644
|
+
if (!result.ok) throw new Error(result.error?.message ?? "Could not send reply");
|
|
1915
2645
|
}
|
|
1916
|
-
});
|
|
2646
|
+
}, MUSHI_SDK_VERSION);
|
|
2647
|
+
syncCaptureModules();
|
|
1917
2648
|
if (typeof document !== "undefined") {
|
|
1918
2649
|
if (document.readyState === "loading") {
|
|
1919
2650
|
document.addEventListener("DOMContentLoaded", () => widget.mount());
|
|
@@ -1923,7 +2654,7 @@ function createInstance(config) {
|
|
|
1923
2654
|
}
|
|
1924
2655
|
let proactiveTriggers = null;
|
|
1925
2656
|
let proactiveManager = null;
|
|
1926
|
-
const proactiveCfg =
|
|
2657
|
+
const proactiveCfg = activeConfig.proactive;
|
|
1927
2658
|
const hasAnyProactive = proactiveCfg && (proactiveCfg.rageClick !== false || proactiveCfg.longTask !== false || proactiveCfg.apiCascade !== false || proactiveCfg.errorBoundary === true);
|
|
1928
2659
|
if (hasAnyProactive && typeof document !== "undefined") {
|
|
1929
2660
|
proactiveManager = createProactiveManager(proactiveCfg?.cooldown);
|
|
@@ -1944,6 +2675,7 @@ function createInstance(config) {
|
|
|
1944
2675
|
rageClick: proactiveCfg?.rageClick,
|
|
1945
2676
|
longTask: proactiveCfg?.longTask,
|
|
1946
2677
|
apiCascade: proactiveCfg?.apiCascade,
|
|
2678
|
+
apiEndpoint: resolveApiEndpoint(activeConfig),
|
|
1947
2679
|
errorBoundary: proactiveCfg?.errorBoundary
|
|
1948
2680
|
}
|
|
1949
2681
|
);
|
|
@@ -1959,6 +2691,7 @@ function createInstance(config) {
|
|
|
1959
2691
|
if (result.sent > 0) log.info("Synced offline reports", { sent: result.sent });
|
|
1960
2692
|
});
|
|
1961
2693
|
function applyRuntimeConfig(runtime) {
|
|
2694
|
+
runtimeConfigLoaded = true;
|
|
1962
2695
|
if (runtime.enabled === false) {
|
|
1963
2696
|
activeConfig = bootstrapConfig;
|
|
1964
2697
|
clearCachedRuntimeConfig(config.projectId);
|
|
@@ -1972,7 +2705,7 @@ function createInstance(config) {
|
|
|
1972
2705
|
if (runtime.widget) widget.updateConfig(activeConfig.widget);
|
|
1973
2706
|
log.debug("Applied runtime SDK config", { version: runtime.version });
|
|
1974
2707
|
}
|
|
1975
|
-
if (config
|
|
2708
|
+
if (shouldUseRuntimeConfig(config)) {
|
|
1976
2709
|
const cached = readCachedRuntimeConfig(config.projectId);
|
|
1977
2710
|
if (cached) applyRuntimeConfig(cached);
|
|
1978
2711
|
apiClient.getSdkConfig().then((result) => {
|
|
@@ -1985,8 +2718,41 @@ function createInstance(config) {
|
|
|
1985
2718
|
}).catch((err) => {
|
|
1986
2719
|
log.debug("Runtime SDK config fetch failed", { error: err instanceof Error ? err.message : String(err) });
|
|
1987
2720
|
});
|
|
2721
|
+
} else if (config.runtimeConfig !== false && isLocalhostEndpoint(resolveApiEndpoint(config))) {
|
|
2722
|
+
log.debug("Runtime SDK config skipped for localhost apiEndpoint; set runtimeConfig: true to force it");
|
|
1988
2723
|
}
|
|
2724
|
+
void checkSdkFreshness();
|
|
1989
2725
|
log.info("Initialized", { projectId: config.projectId });
|
|
2726
|
+
async function checkSdkFreshness() {
|
|
2727
|
+
if (activeConfig.widget?.outdatedBanner === "off") return;
|
|
2728
|
+
const cached = readCachedSdkVersion(MUSHI_SDK_PACKAGE);
|
|
2729
|
+
if (cached) applySdkFreshness(cached);
|
|
2730
|
+
const result = await apiClient.getLatestSdkVersion(MUSHI_SDK_PACKAGE);
|
|
2731
|
+
if (!result.ok || !result.data) return;
|
|
2732
|
+
cacheSdkVersion(MUSHI_SDK_PACKAGE, result.data);
|
|
2733
|
+
applySdkFreshness(result.data);
|
|
2734
|
+
}
|
|
2735
|
+
function applySdkFreshness(info) {
|
|
2736
|
+
const latest = info.latest;
|
|
2737
|
+
const outdated = Boolean(latest && isVersionOlder(MUSHI_SDK_VERSION, latest));
|
|
2738
|
+
if (!outdated && !info.deprecated) return;
|
|
2739
|
+
const message = info.deprecationMessage ?? (outdated ? `Update ${MUSHI_SDK_PACKAGE} to ${latest}.` : null);
|
|
2740
|
+
log.warn("Mushi SDK is outdated", {
|
|
2741
|
+
package: MUSHI_SDK_PACKAGE,
|
|
2742
|
+
current: MUSHI_SDK_VERSION,
|
|
2743
|
+
latest,
|
|
2744
|
+
deprecated: info.deprecated,
|
|
2745
|
+
message
|
|
2746
|
+
});
|
|
2747
|
+
if (activeConfig.widget?.outdatedBanner !== "console-only") {
|
|
2748
|
+
widget.setSdkFreshness({
|
|
2749
|
+
latest,
|
|
2750
|
+
current: MUSHI_SDK_VERSION,
|
|
2751
|
+
deprecated: info.deprecated,
|
|
2752
|
+
message
|
|
2753
|
+
});
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
1990
2756
|
async function submitReport(category, description, intent) {
|
|
1991
2757
|
const filterResult = preFilter.check(description);
|
|
1992
2758
|
if (!filterResult.passed) {
|
|
@@ -2028,6 +2794,8 @@ function createInstance(config) {
|
|
|
2028
2794
|
const scrubbedDescription = piiScrubber.scrub(preFilter.truncate(description));
|
|
2029
2795
|
const sentryCtx = config.sentry ? captureSentryContext(config.sentry) : void 0;
|
|
2030
2796
|
const fingerprintHash = await core.getDeviceFingerprintHash().catch(() => null);
|
|
2797
|
+
const consoleLogs = activeConfig.capture?.console === false ? void 0 : consoleCap?.getEntries();
|
|
2798
|
+
const networkLogs = activeConfig.capture?.network === false ? void 0 : networkCap?.getEntries();
|
|
2031
2799
|
const report = {
|
|
2032
2800
|
id: crypto.randomUUID?.() ?? `mushi_${Date.now()}_${Math.random().toString(36).slice(2)}`,
|
|
2033
2801
|
projectId: config.projectId,
|
|
@@ -2035,9 +2803,10 @@ function createInstance(config) {
|
|
|
2035
2803
|
description: scrubbedDescription,
|
|
2036
2804
|
userIntent: intent,
|
|
2037
2805
|
environment: core.captureEnvironment(),
|
|
2038
|
-
consoleLogs
|
|
2039
|
-
networkLogs
|
|
2806
|
+
consoleLogs,
|
|
2807
|
+
networkLogs,
|
|
2040
2808
|
performanceMetrics: activeConfig.capture?.performance === false ? void 0 : perfCap?.getMetrics(),
|
|
2809
|
+
timeline: timelineCap.getEntries({ consoleLogs, networkLogs }),
|
|
2041
2810
|
screenshotDataUrl: pendingScreenshot ?? void 0,
|
|
2042
2811
|
selectedElement: pendingElement ?? void 0,
|
|
2043
2812
|
metadata: {
|
|
@@ -2049,6 +2818,8 @@ function createInstance(config) {
|
|
|
2049
2818
|
reporterToken: core.getReporterToken(),
|
|
2050
2819
|
...fingerprintHash ? { fingerprintHash } : {},
|
|
2051
2820
|
appVersion: config.integrations?.vercel?.analyticsId,
|
|
2821
|
+
sdkPackage: MUSHI_SDK_PACKAGE,
|
|
2822
|
+
sdkVersion: MUSHI_SDK_VERSION,
|
|
2052
2823
|
proactiveTrigger: pendingProactiveTrigger ?? void 0,
|
|
2053
2824
|
sentryEventId: sentryCtx?.eventId,
|
|
2054
2825
|
sentryReplayId: sentryCtx?.replayId,
|
|
@@ -2103,18 +2874,45 @@ function createInstance(config) {
|
|
|
2103
2874
|
setMetadata(key, value) {
|
|
2104
2875
|
customMetadata[key] = value;
|
|
2105
2876
|
},
|
|
2877
|
+
setScreen(screen) {
|
|
2878
|
+
timelineCap.setScreen(screen);
|
|
2879
|
+
},
|
|
2106
2880
|
isOpen() {
|
|
2107
2881
|
return widget.getIsOpen();
|
|
2108
2882
|
},
|
|
2109
2883
|
open() {
|
|
2110
2884
|
widget.open();
|
|
2111
2885
|
},
|
|
2886
|
+
openWith(category) {
|
|
2887
|
+
widget.open({ category });
|
|
2888
|
+
},
|
|
2889
|
+
show() {
|
|
2890
|
+
widget.showTrigger();
|
|
2891
|
+
},
|
|
2892
|
+
hide() {
|
|
2893
|
+
widget.hideTrigger();
|
|
2894
|
+
},
|
|
2895
|
+
attachTo(selectorOrElement, options) {
|
|
2896
|
+
return widget.attachTo(selectorOrElement, options);
|
|
2897
|
+
},
|
|
2898
|
+
setTrigger(trigger) {
|
|
2899
|
+
widget.setTrigger(trigger);
|
|
2900
|
+
},
|
|
2112
2901
|
close() {
|
|
2113
2902
|
widget.close();
|
|
2114
2903
|
},
|
|
2115
2904
|
updateConfig(runtimeConfig) {
|
|
2116
2905
|
applyRuntimeConfig(runtimeConfig);
|
|
2117
2906
|
},
|
|
2907
|
+
diagnose() {
|
|
2908
|
+
return runDiagnostics({
|
|
2909
|
+
apiEndpoint: resolveApiEndpoint(activeConfig),
|
|
2910
|
+
widgetMounted: widget.getIsMounted(),
|
|
2911
|
+
runtimeConfigLoaded,
|
|
2912
|
+
captureScreenshotAvailable: screenshotCap !== null,
|
|
2913
|
+
captureNetworkIntercepting: networkCap !== null
|
|
2914
|
+
});
|
|
2915
|
+
},
|
|
2118
2916
|
destroy() {
|
|
2119
2917
|
proactiveTriggers?.destroy();
|
|
2120
2918
|
proactiveManager?.reset();
|
|
@@ -2123,6 +2921,7 @@ function createInstance(config) {
|
|
|
2123
2921
|
networkCap?.destroy();
|
|
2124
2922
|
perfCap?.destroy();
|
|
2125
2923
|
elementSelector?.deactivate();
|
|
2924
|
+
timelineCap.destroy();
|
|
2126
2925
|
offlineQueue.stopAutoSync();
|
|
2127
2926
|
listeners.clear();
|
|
2128
2927
|
instance = null;
|
|
@@ -2144,6 +2943,7 @@ function createInstance(config) {
|
|
|
2144
2943
|
category,
|
|
2145
2944
|
description,
|
|
2146
2945
|
environment: core.captureEnvironment(),
|
|
2946
|
+
timeline: timelineCap.getEntries(),
|
|
2147
2947
|
metadata: {
|
|
2148
2948
|
...input.metadata ?? {},
|
|
2149
2949
|
...userInfo ? { user: userInfo } : {},
|
|
@@ -2155,6 +2955,8 @@ function createInstance(config) {
|
|
|
2155
2955
|
},
|
|
2156
2956
|
sessionId: core.getSessionId(),
|
|
2157
2957
|
reporterToken: core.getReporterToken(),
|
|
2958
|
+
sdkPackage: MUSHI_SDK_PACKAGE,
|
|
2959
|
+
sdkVersion: MUSHI_SDK_VERSION,
|
|
2158
2960
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2159
2961
|
};
|
|
2160
2962
|
emit("report:submitted", { reportId: report.id });
|
|
@@ -2184,21 +2986,140 @@ function createInstance(config) {
|
|
|
2184
2986
|
return sdk;
|
|
2185
2987
|
}
|
|
2186
2988
|
function mergeRuntimeConfig(config, runtime) {
|
|
2989
|
+
const nativeTrigger = runtime.native?.triggerMode;
|
|
2990
|
+
const widgetTrigger = runtime.widget?.trigger ?? (nativeTrigger === "none" || nativeTrigger === "shake" ? "manual" : void 0);
|
|
2187
2991
|
return {
|
|
2188
2992
|
...config,
|
|
2189
2993
|
widget: {
|
|
2190
2994
|
...config.widget,
|
|
2191
|
-
...runtime.widget
|
|
2995
|
+
...runtime.widget,
|
|
2996
|
+
...widgetTrigger ? { trigger: widgetTrigger } : {}
|
|
2192
2997
|
},
|
|
2193
2998
|
capture: {
|
|
2194
2999
|
...config.capture,
|
|
2195
3000
|
...runtime.capture
|
|
3001
|
+
},
|
|
3002
|
+
privacy: {
|
|
3003
|
+
...config.privacy
|
|
3004
|
+
}
|
|
3005
|
+
};
|
|
3006
|
+
}
|
|
3007
|
+
function applyPresetConfig(config) {
|
|
3008
|
+
if (!config.preset) return config;
|
|
3009
|
+
const preset = presetDefaults(config.preset);
|
|
3010
|
+
return {
|
|
3011
|
+
...config,
|
|
3012
|
+
widget: {
|
|
3013
|
+
...preset.widget,
|
|
3014
|
+
...config.widget
|
|
3015
|
+
},
|
|
3016
|
+
capture: {
|
|
3017
|
+
...preset.capture,
|
|
3018
|
+
...config.capture
|
|
3019
|
+
},
|
|
3020
|
+
proactive: {
|
|
3021
|
+
...preset.proactive,
|
|
3022
|
+
...config.proactive,
|
|
3023
|
+
cooldown: {
|
|
3024
|
+
...preset.proactive?.cooldown,
|
|
3025
|
+
...config.proactive?.cooldown
|
|
3026
|
+
}
|
|
2196
3027
|
}
|
|
2197
3028
|
};
|
|
2198
3029
|
}
|
|
3030
|
+
function presetDefaults(preset) {
|
|
3031
|
+
switch (preset) {
|
|
3032
|
+
case "manual-only":
|
|
3033
|
+
return {
|
|
3034
|
+
widget: { trigger: "manual", outdatedBanner: "console-only" },
|
|
3035
|
+
capture: { console: true, network: true, performance: false, screenshot: "on-report", elementSelector: false },
|
|
3036
|
+
proactive: { rageClick: false, longTask: false, apiCascade: false, errorBoundary: false }
|
|
3037
|
+
};
|
|
3038
|
+
case "beta-loud":
|
|
3039
|
+
return {
|
|
3040
|
+
widget: { trigger: "auto", outdatedBanner: "banner" },
|
|
3041
|
+
capture: { console: true, network: true, performance: true, screenshot: "auto", elementSelector: true },
|
|
3042
|
+
proactive: { rageClick: true, longTask: true, apiCascade: true, errorBoundary: true }
|
|
3043
|
+
};
|
|
3044
|
+
case "internal-debug":
|
|
3045
|
+
return {
|
|
3046
|
+
widget: { trigger: "auto", outdatedBanner: "banner", brandFooter: true },
|
|
3047
|
+
capture: { console: true, network: true, performance: true, screenshot: "auto", elementSelector: true },
|
|
3048
|
+
proactive: {
|
|
3049
|
+
rageClick: true,
|
|
3050
|
+
longTask: true,
|
|
3051
|
+
apiCascade: true,
|
|
3052
|
+
errorBoundary: true,
|
|
3053
|
+
cooldown: { maxProactivePerSession: 10, dismissCooldownHours: 0, suppressAfterDismissals: 99 }
|
|
3054
|
+
}
|
|
3055
|
+
};
|
|
3056
|
+
case "production-calm":
|
|
3057
|
+
return {
|
|
3058
|
+
widget: { trigger: "auto", outdatedBanner: "console-only" },
|
|
3059
|
+
capture: { console: true, network: true, performance: false, screenshot: "on-report", elementSelector: false },
|
|
3060
|
+
proactive: { rageClick: false, longTask: false, apiCascade: false, errorBoundary: false }
|
|
3061
|
+
};
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
function resolveApiEndpoint(config) {
|
|
3065
|
+
return config.apiEndpoint ?? core.DEFAULT_API_ENDPOINT;
|
|
3066
|
+
}
|
|
3067
|
+
function shouldUseRuntimeConfig(config) {
|
|
3068
|
+
if (config.runtimeConfig === false) return false;
|
|
3069
|
+
if (config.runtimeConfig === true) return true;
|
|
3070
|
+
return !isLocalhostEndpoint(resolveApiEndpoint(config));
|
|
3071
|
+
}
|
|
3072
|
+
async function runDiagnostics(options) {
|
|
3073
|
+
const endpoint = await probeApiEndpoint(options.apiEndpoint);
|
|
3074
|
+
return {
|
|
3075
|
+
apiEndpointReachable: endpoint.reachable,
|
|
3076
|
+
cspAllowsEndpoint: endpoint.cspAllowed,
|
|
3077
|
+
widgetMounted: options.widgetMounted,
|
|
3078
|
+
shadowDomAvailable: typeof HTMLElement !== "undefined" && typeof HTMLElement.prototype.attachShadow === "function",
|
|
3079
|
+
dialogSupported: typeof HTMLDialogElement !== "undefined",
|
|
3080
|
+
runtimeConfigLoaded: options.runtimeConfigLoaded,
|
|
3081
|
+
captureScreenshotAvailable: options.captureScreenshotAvailable,
|
|
3082
|
+
captureNetworkIntercepting: options.captureNetworkIntercepting,
|
|
3083
|
+
sdkVersion: MUSHI_SDK_VERSION
|
|
3084
|
+
};
|
|
3085
|
+
}
|
|
3086
|
+
async function diagnoseWithoutInstance() {
|
|
3087
|
+
return {
|
|
3088
|
+
apiEndpointReachable: false,
|
|
3089
|
+
cspAllowsEndpoint: false,
|
|
3090
|
+
widgetMounted: false,
|
|
3091
|
+
shadowDomAvailable: typeof HTMLElement !== "undefined" && typeof HTMLElement.prototype.attachShadow === "function",
|
|
3092
|
+
dialogSupported: typeof HTMLDialogElement !== "undefined",
|
|
3093
|
+
runtimeConfigLoaded: false,
|
|
3094
|
+
captureScreenshotAvailable: false,
|
|
3095
|
+
captureNetworkIntercepting: false,
|
|
3096
|
+
sdkVersion: MUSHI_SDK_VERSION
|
|
3097
|
+
};
|
|
3098
|
+
}
|
|
3099
|
+
async function probeApiEndpoint(apiEndpoint) {
|
|
3100
|
+
if (typeof fetch === "undefined") return { reachable: false, cspAllowed: false };
|
|
3101
|
+
const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
3102
|
+
const timer = controller ? setTimeout(() => controller.abort(), 3e3) : null;
|
|
3103
|
+
try {
|
|
3104
|
+
const response = await fetch(`${apiEndpoint.replace(/\/$/, "")}/health`, {
|
|
3105
|
+
method: "GET",
|
|
3106
|
+
cache: "no-store",
|
|
3107
|
+
...controller ? { signal: controller.signal } : {},
|
|
3108
|
+
[core.MUSHI_INTERNAL_INIT_MARKER]: "diagnose"
|
|
3109
|
+
});
|
|
3110
|
+
return { reachable: response.ok, cspAllowed: true };
|
|
3111
|
+
} catch {
|
|
3112
|
+
return { reachable: false, cspAllowed: false };
|
|
3113
|
+
} finally {
|
|
3114
|
+
if (timer) clearTimeout(timer);
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
2199
3117
|
function runtimeConfigCacheKey(projectId) {
|
|
2200
3118
|
return `mushi:sdk-config:${projectId}`;
|
|
2201
3119
|
}
|
|
3120
|
+
function sdkVersionCacheKey(packageName) {
|
|
3121
|
+
return `mushi:sdk-version:${packageName}`;
|
|
3122
|
+
}
|
|
2202
3123
|
function readCachedRuntimeConfig(projectId) {
|
|
2203
3124
|
if (typeof localStorage === "undefined") return null;
|
|
2204
3125
|
try {
|
|
@@ -2227,6 +3148,42 @@ function clearCachedRuntimeConfig(projectId) {
|
|
|
2227
3148
|
} catch {
|
|
2228
3149
|
}
|
|
2229
3150
|
}
|
|
3151
|
+
function readCachedSdkVersion(packageName) {
|
|
3152
|
+
if (typeof localStorage === "undefined") return null;
|
|
3153
|
+
try {
|
|
3154
|
+
const raw = localStorage.getItem(sdkVersionCacheKey(packageName));
|
|
3155
|
+
if (!raw) return null;
|
|
3156
|
+
const parsed = JSON.parse(raw);
|
|
3157
|
+
if (!parsed.data || !parsed.cachedAt || Date.now() - parsed.cachedAt > 864e5) return null;
|
|
3158
|
+
return parsed.data;
|
|
3159
|
+
} catch {
|
|
3160
|
+
return null;
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
function cacheSdkVersion(packageName, data) {
|
|
3164
|
+
if (typeof localStorage === "undefined") return;
|
|
3165
|
+
try {
|
|
3166
|
+
localStorage.setItem(sdkVersionCacheKey(packageName), JSON.stringify({
|
|
3167
|
+
cachedAt: Date.now(),
|
|
3168
|
+
data
|
|
3169
|
+
}));
|
|
3170
|
+
} catch {
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
function isVersionOlder(current, latest) {
|
|
3174
|
+
const currentParts = parseVersion(current);
|
|
3175
|
+
const latestParts = parseVersion(latest);
|
|
3176
|
+
for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
|
|
3177
|
+
const cur = currentParts[i] ?? 0;
|
|
3178
|
+
const next = latestParts[i] ?? 0;
|
|
3179
|
+
if (cur < next) return true;
|
|
3180
|
+
if (cur > next) return false;
|
|
3181
|
+
}
|
|
3182
|
+
return false;
|
|
3183
|
+
}
|
|
3184
|
+
function parseVersion(version) {
|
|
3185
|
+
return version.split(/[.-]/).map((part) => Number.parseInt(part, 10)).filter((part) => Number.isFinite(part));
|
|
3186
|
+
}
|
|
2230
3187
|
function createNoopInstance() {
|
|
2231
3188
|
return {
|
|
2232
3189
|
report: () => {
|
|
@@ -2237,6 +3194,8 @@ function createNoopInstance() {
|
|
|
2237
3194
|
},
|
|
2238
3195
|
setMetadata: () => {
|
|
2239
3196
|
},
|
|
3197
|
+
setScreen: () => {
|
|
3198
|
+
},
|
|
2240
3199
|
isOpen: () => false,
|
|
2241
3200
|
open: () => {
|
|
2242
3201
|
},
|
|
@@ -2244,6 +3203,17 @@ function createNoopInstance() {
|
|
|
2244
3203
|
},
|
|
2245
3204
|
updateConfig: () => {
|
|
2246
3205
|
},
|
|
3206
|
+
diagnose: diagnoseWithoutInstance,
|
|
3207
|
+
openWith: () => {
|
|
3208
|
+
},
|
|
3209
|
+
show: () => {
|
|
3210
|
+
},
|
|
3211
|
+
hide: () => {
|
|
3212
|
+
},
|
|
3213
|
+
attachTo: () => () => {
|
|
3214
|
+
},
|
|
3215
|
+
setTrigger: () => {
|
|
3216
|
+
},
|
|
2247
3217
|
destroy: () => {
|
|
2248
3218
|
instance = null;
|
|
2249
3219
|
},
|
|
@@ -2261,6 +3231,7 @@ exports.createNetworkCapture = createNetworkCapture;
|
|
|
2261
3231
|
exports.createPerformanceCapture = createPerformanceCapture;
|
|
2262
3232
|
exports.createProactiveManager = createProactiveManager;
|
|
2263
3233
|
exports.createScreenshotCapture = createScreenshotCapture;
|
|
3234
|
+
exports.createTimelineCapture = createTimelineCapture;
|
|
2264
3235
|
exports.getAvailableLocales = getAvailableLocales;
|
|
2265
3236
|
exports.getLocale = getLocale;
|
|
2266
3237
|
exports.setupProactiveTriggers = setupProactiveTriggers;
|