@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/dist/index.cjs CHANGED
@@ -2,7 +2,41 @@
2
2
 
3
3
  var core = require('@mushi-mushi/core');
4
4
 
5
- // src/mushi.ts
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.submitReporterFeedback("not_fixed");
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.11.0" ;
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: pendingScreenshot ?? void 0,
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
- const result = await apiClient2.submitReport(finalReport);
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;