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