@mushi-mushi/web 1.11.0 → 1.12.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/README.md +28 -0
- package/dist/index.cjs +526 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +527 -6
- package/dist/index.js.map +1 -1
- package/dist/test-utils.cjs +1 -1
- package/dist/test-utils.js +1 -1
- package/package.json +6 -3
package/dist/index.cjs
CHANGED
|
@@ -2,7 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
var core = require('@mushi-mushi/core');
|
|
4
4
|
|
|
5
|
-
// src/
|
|
5
|
+
// src/capture/compress-screenshot.ts
|
|
6
|
+
async function compressScreenshotDataUrl(dataUrl, maxBytes = core.MAX_SCREENSHOT_DATA_URL_BYTES) {
|
|
7
|
+
if (!dataUrl.startsWith("data:image/")) return dataUrl;
|
|
8
|
+
if (estimateDataUrlBytes(dataUrl) <= maxBytes) return dataUrl;
|
|
9
|
+
if (typeof document === "undefined") return null;
|
|
10
|
+
const img = new Image();
|
|
11
|
+
await new Promise((resolve, reject) => {
|
|
12
|
+
img.onload = () => resolve();
|
|
13
|
+
img.onerror = () => reject(new Error("screenshot_decode_failed"));
|
|
14
|
+
img.src = dataUrl;
|
|
15
|
+
});
|
|
16
|
+
const qualities = [0.65, 0.5, 0.35, 0.25];
|
|
17
|
+
const scales = [1, 0.85, 0.7, 0.55, 0.4];
|
|
18
|
+
for (const scale of scales) {
|
|
19
|
+
const canvas = document.createElement("canvas");
|
|
20
|
+
const w = Math.max(1, Math.round(img.naturalWidth * scale));
|
|
21
|
+
const h = Math.max(1, Math.round(img.naturalHeight * scale));
|
|
22
|
+
canvas.width = w;
|
|
23
|
+
canvas.height = h;
|
|
24
|
+
const ctx = canvas.getContext("2d");
|
|
25
|
+
if (!ctx) continue;
|
|
26
|
+
ctx.drawImage(img, 0, 0, w, h);
|
|
27
|
+
for (const q of qualities) {
|
|
28
|
+
const candidate = canvas.toDataURL("image/jpeg", q);
|
|
29
|
+
if (estimateDataUrlBytes(candidate) <= maxBytes) return candidate;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
function estimateDataUrlBytes(dataUrl) {
|
|
35
|
+
const comma = dataUrl.indexOf(",");
|
|
36
|
+
if (comma < 0) return dataUrl.length;
|
|
37
|
+
const b64 = dataUrl.slice(comma + 1);
|
|
38
|
+
return Math.ceil(b64.length * 3 / 4);
|
|
39
|
+
}
|
|
6
40
|
|
|
7
41
|
// src/i18n/en.ts
|
|
8
42
|
var en = {
|
|
@@ -1005,6 +1039,19 @@ function getWidgetStyles(theme) {
|
|
|
1005
1039
|
outline: 2px solid ${widgetAccent};
|
|
1006
1040
|
outline-offset: 2px;
|
|
1007
1041
|
}
|
|
1042
|
+
.mushi-annotate-host {
|
|
1043
|
+
flex-basis: 100%;
|
|
1044
|
+
margin-top: 8px;
|
|
1045
|
+
}
|
|
1046
|
+
.mushi-annotate-host:empty {
|
|
1047
|
+
margin-top: 0;
|
|
1048
|
+
}
|
|
1049
|
+
.mushi-annotate-toolbar {
|
|
1050
|
+
display: flex;
|
|
1051
|
+
flex-wrap: wrap;
|
|
1052
|
+
gap: 6px;
|
|
1053
|
+
margin-bottom: 8px;
|
|
1054
|
+
}
|
|
1008
1055
|
@keyframes mushi-spin {
|
|
1009
1056
|
to { transform: rotate(360deg); }
|
|
1010
1057
|
}
|
|
@@ -1986,6 +2033,7 @@ var MushiWidget = class _MushiWidget {
|
|
|
1986
2033
|
nudgeTimer = null;
|
|
1987
2034
|
sdkFreshness = null;
|
|
1988
2035
|
reporterReports = [];
|
|
2036
|
+
featureBoard = [];
|
|
1989
2037
|
reporterComments = [];
|
|
1990
2038
|
selectedReportId = null;
|
|
1991
2039
|
reporterLoading = false;
|
|
@@ -2705,6 +2753,8 @@ var MushiWidget = class _MushiWidget {
|
|
|
2705
2753
|
return this.renderReportDetailStep();
|
|
2706
2754
|
case "leaderboard":
|
|
2707
2755
|
return this.renderLeaderboardStep();
|
|
2756
|
+
case "roadmap":
|
|
2757
|
+
return this.renderRoadmapStep();
|
|
2708
2758
|
}
|
|
2709
2759
|
}
|
|
2710
2760
|
renderOutdatedBanner() {
|
|
@@ -2792,6 +2842,15 @@ var MushiWidget = class _MushiWidget {
|
|
|
2792
2842
|
<span class="mushi-option-arrow" aria-hidden="true">\u2192</span>
|
|
2793
2843
|
</button>
|
|
2794
2844
|
${this.renderFeatureRequestEntry()}
|
|
2845
|
+
${this.callbacks.onFeatureBoardRequest ? `
|
|
2846
|
+
<button type="button" class="mushi-option-btn" data-action="roadmap">
|
|
2847
|
+
<span class="mushi-option-icon" aria-hidden="true">\u{1F5F3}\uFE0F</span>
|
|
2848
|
+
<div class="mushi-option-text">
|
|
2849
|
+
<span class="mushi-option-label">Community ideas</span>
|
|
2850
|
+
<span class="mushi-option-desc">Vote on features and see what shipped</span>
|
|
2851
|
+
</div>
|
|
2852
|
+
<span class="mushi-option-arrow" aria-hidden="true">\u2192</span>
|
|
2853
|
+
</button>` : ""}
|
|
2795
2854
|
${categories}
|
|
2796
2855
|
${this.rewardsState ? this.renderRewardsNudge() : ""}
|
|
2797
2856
|
</div>
|
|
@@ -2902,6 +2961,38 @@ var MushiWidget = class _MushiWidget {
|
|
|
2902
2961
|
</div>
|
|
2903
2962
|
`;
|
|
2904
2963
|
}
|
|
2964
|
+
renderRoadmapStep() {
|
|
2965
|
+
const rows = this.featureBoard.map((ticket) => {
|
|
2966
|
+
const id = String(ticket.id ?? "");
|
|
2967
|
+
const subject = escapeHtml(String(ticket.subject ?? "Untitled idea"));
|
|
2968
|
+
const votes = Number(ticket.vote_count ?? 0);
|
|
2969
|
+
const shipped = Boolean(ticket.shipped_at);
|
|
2970
|
+
const voted = Boolean(ticket.my_vote);
|
|
2971
|
+
const status = shipped ? "Shipped" : String(ticket.status_label ?? ticket.status ?? "open");
|
|
2972
|
+
return `
|
|
2973
|
+
<div class="mushi-report-row mushi-roadmap-row">
|
|
2974
|
+
<div class="mushi-report-main">
|
|
2975
|
+
<span class="mushi-report-title">${subject}</span>
|
|
2976
|
+
<span class="mushi-report-meta">
|
|
2977
|
+
<span class="mushi-report-status">${escapeHtml(status)}</span>
|
|
2978
|
+
<span class="mushi-report-when">${votes} vote${votes === 1 ? "" : "s"}</span>
|
|
2979
|
+
</span>
|
|
2980
|
+
</div>
|
|
2981
|
+
${this.callbacks.onFeatureBoardVote ? `
|
|
2982
|
+
<button type="button" class="mushi-vote-btn" data-vote-id="${escapeHtml(id)}" aria-pressed="${voted}">
|
|
2983
|
+
${voted ? "Voted" : "Vote"}
|
|
2984
|
+
</button>` : ""}
|
|
2985
|
+
</div>`;
|
|
2986
|
+
}).join("");
|
|
2987
|
+
return `
|
|
2988
|
+
${this.renderHeader({ title: "Community ideas", showBack: true, eyebrow: "Mushi \xB7 Roadmap" })}
|
|
2989
|
+
<div class="mushi-body">
|
|
2990
|
+
${this.reporterLoading ? '<p class="mushi-muted">Loading ideas\u2026</p>' : ""}
|
|
2991
|
+
${this.reporterError ? `<p class="mushi-error-inline">${escapeHtml(this.reporterError)}</p>` : ""}
|
|
2992
|
+
${rows || (!this.reporterLoading ? '<p class="mushi-muted">No community ideas yet. Be the first to suggest one.</p>' : "")}
|
|
2993
|
+
</div>
|
|
2994
|
+
`;
|
|
2995
|
+
}
|
|
2905
2996
|
renderLeaderboardStep() {
|
|
2906
2997
|
const myRank = this.rewardsState && this.leaderboardEntries ? this.leaderboardEntries.findIndex((e) => e.display_name === "You") + 1 : 0;
|
|
2907
2998
|
const rows = (this.leaderboardEntries ?? []).map((e, i) => `
|
|
@@ -3032,6 +3123,7 @@ var MushiWidget = class _MushiWidget {
|
|
|
3032
3123
|
${escapeHtml(screenshotLabel)}
|
|
3033
3124
|
</button>
|
|
3034
3125
|
${this.screenshotAttached && this.allowScreenshotRemove ? '<button type="button" class="mushi-attach-btn danger" data-action="remove-screenshot" aria-label="Remove screenshot">\u2715 Remove</button>' : ""}
|
|
3126
|
+
${this.screenshotAttached ? '<button type="button" class="mushi-attach-btn" data-action="annotate-screenshot" aria-label="Mark up screenshot">\u270F Mark up</button><div class="mushi-annotate-host" data-role="annotate-host"></div>' : ""}
|
|
3035
3127
|
<button type="button" class="${elementClass}"
|
|
3036
3128
|
data-action="element"
|
|
3037
3129
|
${this.elementCapturing ? "disabled" : ""}
|
|
@@ -3257,12 +3349,23 @@ var MushiWidget = class _MushiWidget {
|
|
|
3257
3349
|
this.selectedReportId = null;
|
|
3258
3350
|
} else if (this.step === "leaderboard") {
|
|
3259
3351
|
this.step = "reports";
|
|
3352
|
+
} else if (this.step === "roadmap") {
|
|
3353
|
+
this.step = "category";
|
|
3260
3354
|
}
|
|
3261
3355
|
this.render();
|
|
3262
3356
|
});
|
|
3263
3357
|
panel.querySelector('[data-action="reports"]')?.addEventListener("click", () => {
|
|
3264
3358
|
void this.loadReporterReports();
|
|
3265
3359
|
});
|
|
3360
|
+
panel.querySelector('[data-action="roadmap"]')?.addEventListener("click", () => {
|
|
3361
|
+
void this.loadFeatureBoard();
|
|
3362
|
+
});
|
|
3363
|
+
panel.querySelectorAll("[data-vote-id]").forEach((btn) => {
|
|
3364
|
+
btn.addEventListener("click", () => {
|
|
3365
|
+
const requestId = btn.dataset.voteId;
|
|
3366
|
+
if (requestId) void this.voteFeatureBoard(requestId);
|
|
3367
|
+
});
|
|
3368
|
+
});
|
|
3266
3369
|
panel.querySelector('[data-action="feature-request"]')?.addEventListener("click", () => {
|
|
3267
3370
|
this.selectedCategory = "other";
|
|
3268
3371
|
this.selectedIntent = FEATURE_REQUEST_INTENT;
|
|
@@ -3288,7 +3391,7 @@ var MushiWidget = class _MushiWidget {
|
|
|
3288
3391
|
void this.submitReporterFeedback("confirms");
|
|
3289
3392
|
});
|
|
3290
3393
|
panel.querySelector('[data-action="reporter-not-fixed"]')?.addEventListener("click", () => {
|
|
3291
|
-
void this.
|
|
3394
|
+
void this.submitReporterReopen();
|
|
3292
3395
|
});
|
|
3293
3396
|
panel.querySelector('[data-action="copy-report-id"]')?.addEventListener("click", (e) => {
|
|
3294
3397
|
const btn = e.currentTarget;
|
|
@@ -3355,6 +3458,12 @@ var MushiWidget = class _MushiWidget {
|
|
|
3355
3458
|
panel.querySelector('[data-action="remove-screenshot"]')?.addEventListener("click", () => {
|
|
3356
3459
|
this.callbacks.onScreenshotRemove?.();
|
|
3357
3460
|
});
|
|
3461
|
+
panel.querySelector('[data-action="annotate-screenshot"]')?.addEventListener("click", () => {
|
|
3462
|
+
const host = panel.querySelector('[data-role="annotate-host"]');
|
|
3463
|
+
if (host && this.callbacks.onScreenshotAnnotateRequest) {
|
|
3464
|
+
void this.callbacks.onScreenshotAnnotateRequest(host);
|
|
3465
|
+
}
|
|
3466
|
+
});
|
|
3358
3467
|
panel.querySelector('[data-action="element"]')?.addEventListener("click", () => {
|
|
3359
3468
|
this.callbacks.onElementSelectorRequest?.();
|
|
3360
3469
|
});
|
|
@@ -3442,6 +3551,53 @@ var MushiWidget = class _MushiWidget {
|
|
|
3442
3551
|
unreadCount() {
|
|
3443
3552
|
return this.reporterReports.reduce((sum, report) => sum + (report.unread_count ?? 0), 0);
|
|
3444
3553
|
}
|
|
3554
|
+
async loadFeatureBoard() {
|
|
3555
|
+
this.step = "roadmap";
|
|
3556
|
+
this.reporterLoading = true;
|
|
3557
|
+
this.reporterError = null;
|
|
3558
|
+
this.render();
|
|
3559
|
+
try {
|
|
3560
|
+
this.featureBoard = await this.callbacks.onFeatureBoardRequest?.() ?? [];
|
|
3561
|
+
} catch (err) {
|
|
3562
|
+
this.reporterError = err instanceof Error ? err.message : "Could not load community ideas.";
|
|
3563
|
+
} finally {
|
|
3564
|
+
this.reporterLoading = false;
|
|
3565
|
+
this.render();
|
|
3566
|
+
}
|
|
3567
|
+
}
|
|
3568
|
+
async voteFeatureBoard(requestId) {
|
|
3569
|
+
if (!this.callbacks.onFeatureBoardVote || this.reporterLoading) return;
|
|
3570
|
+
this.reporterLoading = true;
|
|
3571
|
+
this.render();
|
|
3572
|
+
try {
|
|
3573
|
+
await this.callbacks.onFeatureBoardVote(requestId);
|
|
3574
|
+
this.featureBoard = await this.callbacks.onFeatureBoardRequest?.() ?? this.featureBoard;
|
|
3575
|
+
} catch (err) {
|
|
3576
|
+
this.reporterError = err instanceof Error ? err.message : "Could not update vote.";
|
|
3577
|
+
} finally {
|
|
3578
|
+
this.reporterLoading = false;
|
|
3579
|
+
this.render();
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
async submitReporterReopen() {
|
|
3583
|
+
const reportId = this.selectedReportId;
|
|
3584
|
+
if (!reportId || this.reporterLoading) return;
|
|
3585
|
+
this.reporterLoading = true;
|
|
3586
|
+
this.render();
|
|
3587
|
+
try {
|
|
3588
|
+
if (this.callbacks.onReporterReopen) {
|
|
3589
|
+
await this.callbacks.onReporterReopen(reportId, "Not fixed for me");
|
|
3590
|
+
} else {
|
|
3591
|
+
await this.callbacks.onReporterFeedback?.(reportId, "not_fixed", "Not fixed for me");
|
|
3592
|
+
}
|
|
3593
|
+
await this.loadReporterReports();
|
|
3594
|
+
} catch (err) {
|
|
3595
|
+
this.reporterError = err instanceof Error ? err.message : "Could not reopen report.";
|
|
3596
|
+
} finally {
|
|
3597
|
+
this.reporterLoading = false;
|
|
3598
|
+
this.render();
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3445
3601
|
async loadReporterReports() {
|
|
3446
3602
|
this.step = "reports";
|
|
3447
3603
|
this.reporterLoading = true;
|
|
@@ -4856,6 +5012,256 @@ function createDiscoveryCapture(opts) {
|
|
|
4856
5012
|
};
|
|
4857
5013
|
}
|
|
4858
5014
|
|
|
5015
|
+
// src/capture/replay.ts
|
|
5016
|
+
var DEFAULT_MAX_MS = 3e4;
|
|
5017
|
+
var MAX_EVENTS = 400;
|
|
5018
|
+
var RRWEB_META = 4;
|
|
5019
|
+
var RRWEB_FULL_SNAPSHOT = 2;
|
|
5020
|
+
function trimReplayBuffer(events, maxMs, maxEvents) {
|
|
5021
|
+
const ts = (e) => e.timestamp;
|
|
5022
|
+
const isFullSnapshot = (e) => e.type === RRWEB_FULL_SNAPSHOT;
|
|
5023
|
+
const isMeta = (e) => e.type === RRWEB_META;
|
|
5024
|
+
const cutoff = Date.now() - maxMs;
|
|
5025
|
+
let baseIndex = 0;
|
|
5026
|
+
for (let i = 0; i < events.length; i++) {
|
|
5027
|
+
const t = ts(events[i]);
|
|
5028
|
+
if (typeof t === "number" && t >= cutoff) break;
|
|
5029
|
+
if (isFullSnapshot(events[i])) baseIndex = isMeta(events[i - 1]) ? i - 1 : i;
|
|
5030
|
+
}
|
|
5031
|
+
if (events.length - baseIndex > maxEvents) {
|
|
5032
|
+
for (let i = baseIndex + 1; i < events.length; i++) {
|
|
5033
|
+
if (isFullSnapshot(events[i]) && events.length - i <= maxEvents) {
|
|
5034
|
+
baseIndex = isMeta(events[i - 1]) ? i - 1 : i;
|
|
5035
|
+
break;
|
|
5036
|
+
}
|
|
5037
|
+
}
|
|
5038
|
+
}
|
|
5039
|
+
if (baseIndex > 0) events.splice(0, baseIndex);
|
|
5040
|
+
}
|
|
5041
|
+
function createLiteReplay(maxMs) {
|
|
5042
|
+
const events = [];
|
|
5043
|
+
let active = false;
|
|
5044
|
+
const onClick = (ev) => {
|
|
5045
|
+
if (!active) return;
|
|
5046
|
+
const target = ev.target instanceof Element ? ev.target : null;
|
|
5047
|
+
const tag = target?.tagName?.toLowerCase() ?? "unknown";
|
|
5048
|
+
const testId = target?.closest("[data-testid]")?.getAttribute("data-testid") ?? void 0;
|
|
5049
|
+
events.push({ type: "lite_click", timestamp: Date.now(), data: { tag, testId } });
|
|
5050
|
+
if (events.length > MAX_EVENTS) events.shift();
|
|
5051
|
+
};
|
|
5052
|
+
const stop = () => {
|
|
5053
|
+
active = false;
|
|
5054
|
+
document.removeEventListener("click", onClick, true);
|
|
5055
|
+
};
|
|
5056
|
+
return {
|
|
5057
|
+
start() {
|
|
5058
|
+
if (active) return;
|
|
5059
|
+
active = true;
|
|
5060
|
+
document.addEventListener("click", onClick, true);
|
|
5061
|
+
},
|
|
5062
|
+
stop,
|
|
5063
|
+
flush() {
|
|
5064
|
+
const cutoff = Date.now() - maxMs;
|
|
5065
|
+
return events.filter((e) => e.timestamp >= cutoff);
|
|
5066
|
+
},
|
|
5067
|
+
destroy() {
|
|
5068
|
+
stop();
|
|
5069
|
+
events.length = 0;
|
|
5070
|
+
}
|
|
5071
|
+
};
|
|
5072
|
+
}
|
|
5073
|
+
var rrwebModule = null;
|
|
5074
|
+
async function loadRrweb() {
|
|
5075
|
+
if (rrwebModule) return rrwebModule;
|
|
5076
|
+
try {
|
|
5077
|
+
const specifier = "rrweb";
|
|
5078
|
+
rrwebModule = await import(
|
|
5079
|
+
/* @vite-ignore */
|
|
5080
|
+
specifier
|
|
5081
|
+
);
|
|
5082
|
+
return rrwebModule;
|
|
5083
|
+
} catch {
|
|
5084
|
+
return null;
|
|
5085
|
+
}
|
|
5086
|
+
}
|
|
5087
|
+
async function createReplayCapture(opts) {
|
|
5088
|
+
const maxMs = opts.maxDurationMs ?? DEFAULT_MAX_MS;
|
|
5089
|
+
const rrweb = await loadRrweb();
|
|
5090
|
+
if (!rrweb?.record) {
|
|
5091
|
+
return createLiteReplay(maxMs);
|
|
5092
|
+
}
|
|
5093
|
+
const record = rrweb.record;
|
|
5094
|
+
const events = [];
|
|
5095
|
+
let stopFn = null;
|
|
5096
|
+
let recording = false;
|
|
5097
|
+
const maskSelectors = ['input[type="password"]', ...opts.redactSelectors ?? []];
|
|
5098
|
+
const stop = () => {
|
|
5099
|
+
stopFn?.();
|
|
5100
|
+
stopFn = null;
|
|
5101
|
+
recording = false;
|
|
5102
|
+
};
|
|
5103
|
+
return {
|
|
5104
|
+
start() {
|
|
5105
|
+
if (recording) return;
|
|
5106
|
+
recording = true;
|
|
5107
|
+
stopFn = record({
|
|
5108
|
+
emit(event) {
|
|
5109
|
+
events.push(event);
|
|
5110
|
+
trimReplayBuffer(events, maxMs, MAX_EVENTS);
|
|
5111
|
+
},
|
|
5112
|
+
maskAllInputs: true,
|
|
5113
|
+
// Mask rendered DOM text too — without this, rrweb records every
|
|
5114
|
+
// visible label (emails, names) as plaintext. Hosts that need richer
|
|
5115
|
+
// capture can opt out via their own rrweb integration.
|
|
5116
|
+
maskAllText: true,
|
|
5117
|
+
maskTextSelector: maskSelectors.join(","),
|
|
5118
|
+
// Re-emit a full snapshot roughly once per retained window so trimming
|
|
5119
|
+
// never leaves incrementals without a base snapshot.
|
|
5120
|
+
checkoutEveryNms: maxMs,
|
|
5121
|
+
sampling: { mousemove: false, mouseInteraction: true, scroll: 150, media: 800 }
|
|
5122
|
+
}) ?? null;
|
|
5123
|
+
},
|
|
5124
|
+
stop,
|
|
5125
|
+
flush() {
|
|
5126
|
+
trimReplayBuffer(events, maxMs, MAX_EVENTS);
|
|
5127
|
+
return [...events];
|
|
5128
|
+
},
|
|
5129
|
+
destroy() {
|
|
5130
|
+
stop();
|
|
5131
|
+
events.length = 0;
|
|
5132
|
+
}
|
|
5133
|
+
};
|
|
5134
|
+
}
|
|
5135
|
+
|
|
5136
|
+
// src/capture/screenshot-annotation.ts
|
|
5137
|
+
function createScreenshotAnnotation(imageDataUrl, container) {
|
|
5138
|
+
return new Promise((resolve, reject) => {
|
|
5139
|
+
const img = new Image();
|
|
5140
|
+
img.onload = () => {
|
|
5141
|
+
const wrap = document.createElement("div");
|
|
5142
|
+
wrap.style.position = "relative";
|
|
5143
|
+
wrap.style.display = "inline-block";
|
|
5144
|
+
wrap.style.maxWidth = "100%";
|
|
5145
|
+
const base = document.createElement("canvas");
|
|
5146
|
+
const overlay = document.createElement("canvas");
|
|
5147
|
+
const scale = Math.min(1, 720 / img.width);
|
|
5148
|
+
const w = Math.round(img.width * scale);
|
|
5149
|
+
const h = Math.round(img.height * scale);
|
|
5150
|
+
for (const c of [base, overlay]) {
|
|
5151
|
+
c.width = w;
|
|
5152
|
+
c.height = h;
|
|
5153
|
+
c.style.width = "100%";
|
|
5154
|
+
c.style.height = "auto";
|
|
5155
|
+
c.style.display = "block";
|
|
5156
|
+
}
|
|
5157
|
+
overlay.style.position = "absolute";
|
|
5158
|
+
overlay.style.left = "0";
|
|
5159
|
+
overlay.style.top = "0";
|
|
5160
|
+
overlay.style.cursor = "crosshair";
|
|
5161
|
+
const bctx = base.getContext("2d");
|
|
5162
|
+
const octx = overlay.getContext("2d");
|
|
5163
|
+
if (!bctx || !octx) {
|
|
5164
|
+
reject(new Error("Canvas not supported"));
|
|
5165
|
+
return;
|
|
5166
|
+
}
|
|
5167
|
+
bctx.drawImage(img, 0, 0, w, h);
|
|
5168
|
+
wrap.appendChild(base);
|
|
5169
|
+
wrap.appendChild(overlay);
|
|
5170
|
+
container.appendChild(wrap);
|
|
5171
|
+
let tool = "highlight";
|
|
5172
|
+
let drawing = false;
|
|
5173
|
+
let start = null;
|
|
5174
|
+
const strokes = [];
|
|
5175
|
+
const toLocal = (ev) => {
|
|
5176
|
+
const rect = overlay.getBoundingClientRect();
|
|
5177
|
+
const touch = "touches" in ev ? ev.touches[0] ?? ev.changedTouches[0] : null;
|
|
5178
|
+
const clientX = touch ? touch.clientX : ev.clientX;
|
|
5179
|
+
const clientY = touch ? touch.clientY : ev.clientY;
|
|
5180
|
+
return {
|
|
5181
|
+
x: (clientX - rect.left) / rect.width * w,
|
|
5182
|
+
y: (clientY - rect.top) / rect.height * h
|
|
5183
|
+
};
|
|
5184
|
+
};
|
|
5185
|
+
const redraw = () => {
|
|
5186
|
+
octx.clearRect(0, 0, w, h);
|
|
5187
|
+
for (const s of strokes) {
|
|
5188
|
+
if (s.tool === "highlight" && s.points.length >= 2) {
|
|
5189
|
+
const [a, b] = s.points;
|
|
5190
|
+
octx.fillStyle = "rgba(255, 230, 0, 0.35)";
|
|
5191
|
+
octx.fillRect(
|
|
5192
|
+
Math.min(a.x, b.x),
|
|
5193
|
+
Math.min(a.y, b.y),
|
|
5194
|
+
Math.abs(b.x - a.x),
|
|
5195
|
+
Math.abs(b.y - a.y)
|
|
5196
|
+
);
|
|
5197
|
+
} else if (s.tool === "arrow" && s.points.length >= 2) {
|
|
5198
|
+
const [a, b] = s.points;
|
|
5199
|
+
octx.strokeStyle = "#ef4444";
|
|
5200
|
+
octx.lineWidth = 3;
|
|
5201
|
+
octx.beginPath();
|
|
5202
|
+
octx.moveTo(a.x, a.y);
|
|
5203
|
+
octx.lineTo(b.x, b.y);
|
|
5204
|
+
octx.stroke();
|
|
5205
|
+
}
|
|
5206
|
+
}
|
|
5207
|
+
};
|
|
5208
|
+
const onDown = (ev) => {
|
|
5209
|
+
drawing = true;
|
|
5210
|
+
start = toLocal(ev);
|
|
5211
|
+
};
|
|
5212
|
+
const onUp = (ev) => {
|
|
5213
|
+
if (!drawing || !start) return;
|
|
5214
|
+
drawing = false;
|
|
5215
|
+
const end = toLocal(ev);
|
|
5216
|
+
if (tool === "blur") {
|
|
5217
|
+
const x = Math.min(start.x, end.x);
|
|
5218
|
+
const y = Math.min(start.y, end.y);
|
|
5219
|
+
const bw = Math.abs(end.x - start.x);
|
|
5220
|
+
const bh = Math.abs(end.y - start.y);
|
|
5221
|
+
if (bw >= 1 && bh >= 1) {
|
|
5222
|
+
bctx.filter = "blur(8px)";
|
|
5223
|
+
bctx.drawImage(base, x, y, bw, bh, x, y, bw, bh);
|
|
5224
|
+
bctx.filter = "none";
|
|
5225
|
+
}
|
|
5226
|
+
} else {
|
|
5227
|
+
strokes.push({ tool, points: [start, end] });
|
|
5228
|
+
}
|
|
5229
|
+
start = null;
|
|
5230
|
+
redraw();
|
|
5231
|
+
};
|
|
5232
|
+
overlay.addEventListener("mousedown", onDown);
|
|
5233
|
+
overlay.addEventListener("mouseup", onUp);
|
|
5234
|
+
overlay.addEventListener("touchstart", onDown, { passive: true });
|
|
5235
|
+
overlay.addEventListener("touchend", onUp);
|
|
5236
|
+
resolve({
|
|
5237
|
+
canvas: base,
|
|
5238
|
+
setTool(t) {
|
|
5239
|
+
tool = t;
|
|
5240
|
+
},
|
|
5241
|
+
getDataUrl() {
|
|
5242
|
+
const out = document.createElement("canvas");
|
|
5243
|
+
out.width = w;
|
|
5244
|
+
out.height = h;
|
|
5245
|
+
const ctx = out.getContext("2d");
|
|
5246
|
+
if (!ctx) return imageDataUrl;
|
|
5247
|
+
ctx.drawImage(base, 0, 0);
|
|
5248
|
+
ctx.drawImage(overlay, 0, 0);
|
|
5249
|
+
return out.toDataURL("image/jpeg", 0.85);
|
|
5250
|
+
},
|
|
5251
|
+
destroy() {
|
|
5252
|
+
overlay.removeEventListener("mousedown", onDown);
|
|
5253
|
+
overlay.removeEventListener("mouseup", onUp);
|
|
5254
|
+
overlay.removeEventListener("touchstart", onDown);
|
|
5255
|
+
overlay.removeEventListener("touchend", onUp);
|
|
5256
|
+
wrap.remove();
|
|
5257
|
+
}
|
|
5258
|
+
});
|
|
5259
|
+
};
|
|
5260
|
+
img.onerror = () => reject(new Error("Failed to load screenshot"));
|
|
5261
|
+
img.src = imageDataUrl;
|
|
5262
|
+
});
|
|
5263
|
+
}
|
|
5264
|
+
|
|
4859
5265
|
// src/sentry.ts
|
|
4860
5266
|
function getSentryGlobal() {
|
|
4861
5267
|
try {
|
|
@@ -5339,7 +5745,7 @@ function createProactiveManager(config = {}) {
|
|
|
5339
5745
|
|
|
5340
5746
|
// src/version.ts
|
|
5341
5747
|
var MUSHI_SDK_PACKAGE = "@mushi-mushi/web";
|
|
5342
|
-
var MUSHI_SDK_VERSION = "1.
|
|
5748
|
+
var MUSHI_SDK_VERSION = "1.12.0" ;
|
|
5343
5749
|
|
|
5344
5750
|
// src/mushi.ts
|
|
5345
5751
|
var instance = null;
|
|
@@ -5418,6 +5824,8 @@ function createInstance(config) {
|
|
|
5418
5824
|
let elementSelector = null;
|
|
5419
5825
|
let discoveryCap = null;
|
|
5420
5826
|
const timelineCap = createTimelineCapture();
|
|
5827
|
+
let replayCap = null;
|
|
5828
|
+
let replayGeneration = 0;
|
|
5421
5829
|
let widget;
|
|
5422
5830
|
function syncCaptureModules() {
|
|
5423
5831
|
if (activeConfig.capture?.console !== false) {
|
|
@@ -5501,6 +5909,25 @@ function createInstance(config) {
|
|
|
5501
5909
|
discoveryCap?.destroy();
|
|
5502
5910
|
discoveryCap = null;
|
|
5503
5911
|
}
|
|
5912
|
+
const replayMode = activeConfig.capture?.replay ?? "off";
|
|
5913
|
+
if (replayMode === "rrweb" || replayMode === "lite") {
|
|
5914
|
+
const generation = ++replayGeneration;
|
|
5915
|
+
void createReplayCapture({
|
|
5916
|
+
redactSelectors: activeConfig.privacy?.redactSelectors
|
|
5917
|
+
}).then((cap) => {
|
|
5918
|
+
if (generation !== replayGeneration) {
|
|
5919
|
+
cap.destroy();
|
|
5920
|
+
return;
|
|
5921
|
+
}
|
|
5922
|
+
replayCap?.destroy();
|
|
5923
|
+
replayCap = cap;
|
|
5924
|
+
replayCap.start();
|
|
5925
|
+
});
|
|
5926
|
+
} else {
|
|
5927
|
+
replayGeneration++;
|
|
5928
|
+
replayCap?.destroy();
|
|
5929
|
+
replayCap = null;
|
|
5930
|
+
}
|
|
5504
5931
|
}
|
|
5505
5932
|
const listeners = /* @__PURE__ */ new Map();
|
|
5506
5933
|
function emit(type, data) {
|
|
@@ -5562,6 +5989,7 @@ function createInstance(config) {
|
|
|
5562
5989
|
onOpen: () => {
|
|
5563
5990
|
log.debug("Widget opened");
|
|
5564
5991
|
void autoCaptureScreenshot("open");
|
|
5992
|
+
replayCap?.start();
|
|
5565
5993
|
emit("widget:opened");
|
|
5566
5994
|
},
|
|
5567
5995
|
onClose: () => {
|
|
@@ -5586,6 +6014,55 @@ function createInstance(config) {
|
|
|
5586
6014
|
pendingScreenshot = null;
|
|
5587
6015
|
widget.setScreenshotAttached(false);
|
|
5588
6016
|
},
|
|
6017
|
+
onScreenshotAnnotateRequest: async (container) => {
|
|
6018
|
+
if (!pendingScreenshot) return;
|
|
6019
|
+
if (container.childElementCount > 0) return;
|
|
6020
|
+
let session;
|
|
6021
|
+
try {
|
|
6022
|
+
session = await createScreenshotAnnotation(pendingScreenshot, container);
|
|
6023
|
+
} catch (err) {
|
|
6024
|
+
log.warn("Screenshot annotation failed", {
|
|
6025
|
+
error: err instanceof Error ? err.message : String(err)
|
|
6026
|
+
});
|
|
6027
|
+
return;
|
|
6028
|
+
}
|
|
6029
|
+
const toolbar = document.createElement("div");
|
|
6030
|
+
toolbar.className = "mushi-annotate-toolbar";
|
|
6031
|
+
const finish = (commit) => {
|
|
6032
|
+
{
|
|
6033
|
+
pendingScreenshot = session.getDataUrl();
|
|
6034
|
+
widget.setScreenshotAttached(true);
|
|
6035
|
+
}
|
|
6036
|
+
session.destroy();
|
|
6037
|
+
toolbar.remove();
|
|
6038
|
+
};
|
|
6039
|
+
const tools = [
|
|
6040
|
+
{ id: "highlight", label: "\u270F\uFE0F Highlight" },
|
|
6041
|
+
{ id: "blur", label: "\u{1F512} Blur" },
|
|
6042
|
+
{ id: "arrow", label: "\u2197\uFE0F Arrow" }
|
|
6043
|
+
];
|
|
6044
|
+
for (const toolDef of tools) {
|
|
6045
|
+
const btn = document.createElement("button");
|
|
6046
|
+
btn.type = "button";
|
|
6047
|
+
btn.className = "mushi-attach-btn";
|
|
6048
|
+
btn.dataset.tool = toolDef.id;
|
|
6049
|
+
btn.textContent = toolDef.label;
|
|
6050
|
+
btn.addEventListener("click", () => {
|
|
6051
|
+
session.setTool(toolDef.id);
|
|
6052
|
+
toolbar.querySelectorAll("button[data-tool]").forEach((b) => b.classList.remove("active"));
|
|
6053
|
+
btn.classList.add("active");
|
|
6054
|
+
});
|
|
6055
|
+
toolbar.appendChild(btn);
|
|
6056
|
+
}
|
|
6057
|
+
toolbar.querySelector('button[data-tool="highlight"]')?.classList.add("active");
|
|
6058
|
+
const doneBtn = document.createElement("button");
|
|
6059
|
+
doneBtn.type = "button";
|
|
6060
|
+
doneBtn.className = "mushi-attach-btn";
|
|
6061
|
+
doneBtn.textContent = "\u2713 Done";
|
|
6062
|
+
doneBtn.addEventListener("click", () => finish());
|
|
6063
|
+
toolbar.appendChild(doneBtn);
|
|
6064
|
+
container.insertBefore(toolbar, container.firstChild);
|
|
6065
|
+
},
|
|
5589
6066
|
onElementSelectorRequest: async () => {
|
|
5590
6067
|
if (!elementSelector || activeConfig.capture?.elementSelector === false) return;
|
|
5591
6068
|
log.debug("Element selector activated");
|
|
@@ -5628,6 +6105,16 @@ function createInstance(config) {
|
|
|
5628
6105
|
if (!result.ok) throw new Error(result.error?.message ?? "Could not reopen report");
|
|
5629
6106
|
return result.data?.outcome ?? null;
|
|
5630
6107
|
},
|
|
6108
|
+
async onFeatureBoardRequest() {
|
|
6109
|
+
const result = await apiClient2.listReporterFeatureBoard(core.getReporterToken());
|
|
6110
|
+
if (!result.ok) throw new Error(result.error?.message ?? "Could not load community ideas");
|
|
6111
|
+
return result.data?.tickets ?? [];
|
|
6112
|
+
},
|
|
6113
|
+
async onFeatureBoardVote(requestId) {
|
|
6114
|
+
const result = await apiClient2.voteReporterFeatureBoard(requestId, core.getReporterToken());
|
|
6115
|
+
if (!result.ok) throw new Error(result.error?.message ?? "Could not vote");
|
|
6116
|
+
return result.data ?? { voted: true, action: "added" };
|
|
6117
|
+
},
|
|
5631
6118
|
onLeaderboardOpen() {
|
|
5632
6119
|
widget.setLeaderboard(null, true);
|
|
5633
6120
|
void fetchLeaderboard(10).then((entries) => {
|
|
@@ -5805,6 +6292,10 @@ function createInstance(config) {
|
|
|
5805
6292
|
...sentryCtx.breadcrumbs ? { breadcrumbs: scrubBreadcrumbsForWire(sentryCtx.breadcrumbs) } : {},
|
|
5806
6293
|
...sentryCtx.tags ? { tags: scrubTagsForWire(sentryCtx.tags) } : {}
|
|
5807
6294
|
} : void 0;
|
|
6295
|
+
const screenshotForWire = pendingScreenshot ? await compressScreenshotDataUrl(pendingScreenshot).catch(() => null) : null;
|
|
6296
|
+
if (pendingScreenshot && !screenshotForWire) {
|
|
6297
|
+
log.warn("Screenshot dropped \u2014 could not compress under wire budget");
|
|
6298
|
+
}
|
|
5808
6299
|
const report = {
|
|
5809
6300
|
id: core.newUuid(),
|
|
5810
6301
|
projectId: config.projectId,
|
|
@@ -5816,8 +6307,9 @@ function createInstance(config) {
|
|
|
5816
6307
|
networkLogs,
|
|
5817
6308
|
performanceMetrics: activeConfig.capture?.performance === false ? void 0 : perfCap?.getMetrics(),
|
|
5818
6309
|
timeline: timelineCap.getEntries({ consoleLogs, networkLogs }),
|
|
5819
|
-
screenshotDataUrl:
|
|
6310
|
+
screenshotDataUrl: screenshotForWire ?? void 0,
|
|
5820
6311
|
selectedElement: pendingElement ?? void 0,
|
|
6312
|
+
...replayCap ? { replayEvents: replayCap.flush() } : {},
|
|
5821
6313
|
metadata: {
|
|
5822
6314
|
...customMetadata,
|
|
5823
6315
|
...userInfo ? { user: userInfo } : {},
|
|
@@ -5891,7 +6383,23 @@ function createInstance(config) {
|
|
|
5891
6383
|
emit("report:queued", { reportId: finalReport.id });
|
|
5892
6384
|
return { reportId: null, queuedOffline: true };
|
|
5893
6385
|
}
|
|
5894
|
-
|
|
6386
|
+
let result = await apiClient2.submitReport(finalReport);
|
|
6387
|
+
if (!result.ok && result.error?.code === "PAYLOAD_TOO_LARGE") {
|
|
6388
|
+
if (Array.isArray(finalReport.replayEvents) && finalReport.replayEvents.length > 0) {
|
|
6389
|
+
log.warn("Report too large \u2014 dropping replay buffer and retrying", {
|
|
6390
|
+
reportId: finalReport.id
|
|
6391
|
+
});
|
|
6392
|
+
finalReport = { ...finalReport, replayEvents: void 0 };
|
|
6393
|
+
result = await apiClient2.submitReport(finalReport);
|
|
6394
|
+
}
|
|
6395
|
+
if (!result.ok && result.error?.code === "PAYLOAD_TOO_LARGE" && finalReport.screenshotDataUrl) {
|
|
6396
|
+
log.warn("Report still too large \u2014 dropping screenshot and retrying", {
|
|
6397
|
+
reportId: finalReport.id
|
|
6398
|
+
});
|
|
6399
|
+
finalReport = { ...finalReport, screenshotDataUrl: void 0 };
|
|
6400
|
+
result = await apiClient2.submitReport(finalReport);
|
|
6401
|
+
}
|
|
6402
|
+
}
|
|
5895
6403
|
if (result.ok) {
|
|
5896
6404
|
log.info("Report sent", { reportId: result.data?.reportId });
|
|
5897
6405
|
emit("report:sent", { reportId: result.data?.reportId });
|
|
@@ -5914,6 +6422,17 @@ function createInstance(config) {
|
|
|
5914
6422
|
}
|
|
5915
6423
|
} catch {
|
|
5916
6424
|
}
|
|
6425
|
+
} else if (result.error?.code === "PAYLOAD_TOO_LARGE" || result.error?.code === "SERIALIZE_FAILED") {
|
|
6426
|
+
log.warn("Report exceeds size limit after degradation \u2014 dropping", {
|
|
6427
|
+
reportId: finalReport.id,
|
|
6428
|
+
error: result.error
|
|
6429
|
+
});
|
|
6430
|
+
emit("report:failed", { reportId: finalReport.id, error: result.error });
|
|
6431
|
+
breadcrumbs.add({
|
|
6432
|
+
category: "lifecycle",
|
|
6433
|
+
level: "error",
|
|
6434
|
+
message: `Mushi report dropped \u2014 payload too large (${finalReport.id})`
|
|
6435
|
+
});
|
|
5917
6436
|
} else {
|
|
5918
6437
|
log.warn("Report failed, queuing for retry", { reportId: finalReport.id, error: result.error });
|
|
5919
6438
|
await offlineQueue.enqueue(finalReport);
|
|
@@ -5999,6 +6518,8 @@ function createInstance(config) {
|
|
|
5999
6518
|
timelineCap.destroy();
|
|
6000
6519
|
discoveryCap?.destroy();
|
|
6001
6520
|
discoveryCap = null;
|
|
6521
|
+
replayCap?.destroy();
|
|
6522
|
+
replayCap = null;
|
|
6002
6523
|
offlineQueue.stopAutoSync();
|
|
6003
6524
|
detachAutoBreadcrumbs?.();
|
|
6004
6525
|
detachAutoBreadcrumbs = null;
|