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