@h-ai/feedback 1.0.0-beta.2 → 1.0.0-beta.20

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.
@@ -36,7 +36,10 @@ async function saveFeedbackFile(bytes, filename, contentType = "application/zip"
36
36
  }
37
37
  return ok({ status: "saved" });
38
38
  } catch (cause) {
39
- const error = core.error.toPublic({ ...HaiFeedbackError.OPERATION_FAILED, message: feedbackM("feedback_operationFailed") });
39
+ const error = core.error.toPublic({
40
+ ...HaiFeedbackError.OPERATION_FAILED,
41
+ message: feedbackM("feedback_operationFailed")
42
+ });
40
43
  core.logger.error("Feedback file save failed", { traceId: error.traceId, error: cause });
41
44
  return err(error);
42
45
  }
@@ -56,7 +59,10 @@ async function captureFeedbackScreenshot(signal) {
56
59
  if (document.fonts) {
57
60
  let timeout;
58
61
  try {
59
- await Promise.race([document.fonts.ready, new Promise((resolve) => timeout = setTimeout(resolve, 500))]);
62
+ await Promise.race([
63
+ document.fonts.ready,
64
+ new Promise((resolve) => timeout = setTimeout(resolve, 500))
65
+ ]);
60
66
  } finally {
61
67
  clearTimeout(timeout);
62
68
  }
@@ -88,7 +94,17 @@ async function captureFeedbackScreenshot(signal) {
88
94
  if (paint && clone.defaultView) {
89
95
  for (const element of clone.querySelectorAll("*")) {
90
96
  const computed = clone.defaultView.getComputedStyle(element);
91
- for (const property of ["color", "background-color", "border-top-color", "border-bottom-color", "border-left-color", "border-right-color", "outline-color", "text-decoration-color", "-webkit-text-stroke-color"]) {
97
+ for (const property of [
98
+ "color",
99
+ "background-color",
100
+ "border-top-color",
101
+ "border-bottom-color",
102
+ "border-left-color",
103
+ "border-right-color",
104
+ "outline-color",
105
+ "text-decoration-color",
106
+ "-webkit-text-stroke-color"
107
+ ]) {
92
108
  const raw = computed.getPropertyValue(property);
93
109
  if (!/oklch|oklab|color\(|lab\(|lch\(/i.test(raw))
94
110
  continue;
@@ -143,18 +159,38 @@ async function captureFeedbackScreenshot(signal) {
143
159
  }
144
160
  }
145
161
  async function annotateFeedbackImage(bytes, annotation) {
146
- if (![annotation.x, annotation.y, annotation.width, annotation.height].every(Number.isFinite) || annotation.width <= 0 || annotation.height <= 0)
162
+ if (![
163
+ annotation.x,
164
+ annotation.y,
165
+ annotation.width,
166
+ annotation.height
167
+ ].every(Number.isFinite) || annotation.width <= 0 || annotation.height <= 0) {
147
168
  throw new Error("Invalid annotation rectangle");
169
+ }
148
170
  const image = new Image();
149
- const imageUrl = URL.createObjectURL(new Blob([new Uint8Array(bytes)], { type: "image/png" }));
171
+ const imageUrl = URL.createObjectURL(new Blob([
172
+ new Uint8Array(bytes)
173
+ ], { type: "image/png" }));
150
174
  const canvas = document.createElement("canvas");
151
175
  try {
152
176
  image.src = imageUrl;
153
177
  await image.decode();
154
178
  if (annotation.coordinateSpace === "ratio") {
155
- if ([annotation.x, annotation.y, annotation.width, annotation.height].some((value) => value < 0 || value > 1))
179
+ if ([
180
+ annotation.x,
181
+ annotation.y,
182
+ annotation.width,
183
+ annotation.height
184
+ ].some((value) => value < 0 || value > 1)) {
156
185
  throw new Error("Invalid annotation ratio");
157
- annotation = { ...annotation, x: annotation.x * image.naturalWidth, y: annotation.y * image.naturalHeight, width: annotation.width * image.naturalWidth, height: annotation.height * image.naturalHeight };
186
+ }
187
+ annotation = {
188
+ ...annotation,
189
+ x: annotation.x * image.naturalWidth,
190
+ y: annotation.y * image.naturalHeight,
191
+ width: annotation.width * image.naturalWidth,
192
+ height: annotation.height * image.naturalHeight
193
+ };
158
194
  }
159
195
  canvas.width = image.naturalWidth;
160
196
  canvas.height = image.naturalHeight;
@@ -196,8 +232,9 @@ function sanitizeFeedbackData(value) {
196
232
  return sanitizeFeedbackText(input).slice(0, 8192);
197
233
  if (input instanceof Error)
198
234
  return visit({ name: input.name, message: input.message, stack: input.stack }, depth + 1);
199
- if (!input || typeof input !== "object")
235
+ if (!input || typeof input !== "object") {
200
236
  return input === void 0 || typeof input === "function" || typeof input === "symbol" ? null : typeof input === "bigint" ? String(input) : input;
237
+ }
201
238
  if (seen.has(input))
202
239
  return "[CIRCULAR]";
203
240
  seen.add(input);
@@ -356,7 +393,9 @@ function feedbackFileContentType(file) {
356
393
  ["application/x-gzip", "application/gzip"]
357
394
  ]);
358
395
  const normalized = aliases.get(type) ?? type;
359
- return [...extensions.values()].includes(normalized) ? normalized : void 0;
396
+ return [
397
+ ...extensions.values()
398
+ ].includes(normalized) ? normalized : void 0;
360
399
  }
361
400
  function createFeedbackClient() {
362
401
  let config;
@@ -380,14 +419,28 @@ function createFeedbackClient() {
380
419
  previews.delete(id);
381
420
  }
382
421
  }
383
- return { attachments: files.map((file) => {
384
- let preview = previews.get(file.id);
385
- if (!preview) {
386
- preview = { checksum: file.checksum, url: URL.createObjectURL(new Blob([new Uint8Array(file.bytes).buffer], { type: file.contentType })) };
387
- previews.set(file.id, preview);
388
- }
389
- return { id: file.id, name: file.name, size: file.size, mimeType: file.contentType, url: preview.url };
390
- }), evidence: draft?.evidence ?? [], missing: draft?.missing ?? [], budget: { ...budget } };
422
+ return {
423
+ attachments: files.map((file) => {
424
+ let preview = previews.get(file.id);
425
+ if (!preview) {
426
+ preview = {
427
+ checksum: file.checksum,
428
+ url: URL.createObjectURL(new Blob([new Uint8Array(file.bytes).buffer], { type: file.contentType }))
429
+ };
430
+ previews.set(file.id, preview);
431
+ }
432
+ return {
433
+ id: file.id,
434
+ name: file.name,
435
+ size: file.size,
436
+ mimeType: file.contentType,
437
+ url: preview.url
438
+ };
439
+ }),
440
+ evidence: draft?.evidence ?? [],
441
+ missing: draft?.missing ?? [],
442
+ budget: { ...budget }
443
+ };
391
444
  }
392
445
  let initializing = false;
393
446
  let sequence = 0;
@@ -427,13 +480,28 @@ function createFeedbackClient() {
427
480
  function failure(code = "OPERATION_FAILED", cause, message) {
428
481
  const key = code === "NOT_INITIALIZED" ? "feedback_notInitialized" : code === "LIMIT_EXCEEDED" ? "feedback_limitExceeded" : code === "INVALID_INPUT" ? "feedback_invalidInput" : "feedback_operationFailed";
429
482
  const traceId = core.context.get().traceId ?? core.context.get().requestId ?? core.id.uuid();
430
- const error = { ...HaiFeedbackError[code], traceId, message: message ?? feedbackM(key), cause };
431
- if (!(closing && code === "NOT_INITIALIZED"))
483
+ const error = {
484
+ ...HaiFeedbackError[code],
485
+ traceId,
486
+ message: message ?? feedbackM(key),
487
+ cause
488
+ };
489
+ if (!(closing && code === "NOT_INITIALIZED")) {
432
490
  core.logger[cause === void 0 ? "warn" : "error"]("Feedback client operation failed", { module: "feedback", traceId, error });
433
- return err({ ...error, message: core.error.formatMessage(error) });
491
+ }
492
+ return err({
493
+ ...error,
494
+ message: core.error.formatMessage(error)
495
+ });
434
496
  }
435
497
  function limits() {
436
- return { events: Math.min(config?.maxEvents ?? 1e3, budget.maxEvidenceCount), bytes: Math.min(config?.maxBytes ?? 2 * 1024 * 1024, budget.maxEvidenceBytes), age: config?.maxAgeMs ?? 3e5, before: config?.beforeCount ?? 100, after: config?.afterCount ?? 100 };
498
+ return {
499
+ events: Math.min(config?.maxEvents ?? 1e3, budget.maxEvidenceCount),
500
+ bytes: Math.min(config?.maxBytes ?? 2 * 1024 * 1024, budget.maxEvidenceBytes),
501
+ age: config?.maxAgeMs ?? 3e5,
502
+ before: config?.beforeCount ?? 100,
503
+ after: config?.afterCount ?? 100
504
+ };
437
505
  }
438
506
  function schedulePersist() {
439
507
  if (!config?.persistDraft || persistTimer)
@@ -489,8 +557,17 @@ function createFeedbackClient() {
489
557
  if (item.kind === "error" || item.kind === "request" && item.data.success === false) {
490
558
  lastError = item;
491
559
  const before = buffer.filter((entry2) => (entry2.kind === "log" || entry2.kind === "error") && entry2.id !== item.id);
492
- const evidence = [...before.slice(Math.max(0, before.length - bounds.before)), item];
493
- errorWindows.push({ anchor: item, evidence, after: 0, deadline: item.timestamp + (config.afterTimeoutMs ?? 3e4), bytes: new TextEncoder().encode(JSON.stringify(evidence)).byteLength });
560
+ const evidence = [
561
+ ...before.slice(Math.max(0, before.length - bounds.before)),
562
+ item
563
+ ];
564
+ errorWindows.push({
565
+ anchor: item,
566
+ evidence,
567
+ after: 0,
568
+ deadline: item.timestamp + (config.afterTimeoutMs ?? 3e4),
569
+ bytes: new TextEncoder().encode(JSON.stringify(evidence)).byteLength
570
+ });
494
571
  void captureKeyScreenshot();
495
572
  }
496
573
  errorWindows = errorWindows.filter((window2) => window2.anchor.timestamp >= Date.now() - bounds.age);
@@ -522,15 +599,42 @@ function createFeedbackClient() {
522
599
  try {
523
600
  if (config || closePromise)
524
601
  await close();
525
- if (typeof window === "undefined" || !input.appId || !input.actions || ["submit", "appendEvidence", "completeCollection", "upload"].some((key) => typeof input.actions[key] !== "function"))
602
+ if (typeof window === "undefined" || !input.appId || !input.actions || [
603
+ "submit",
604
+ "appendEvidence",
605
+ "completeCollection",
606
+ "upload"
607
+ ].some((key) => typeof input.actions[key] !== "function")) {
526
608
  return failure("INVALID_INPUT");
527
- const numeric = [input.maxEvents, input.maxBytes, input.maxAgeMs, input.beforeCount, input.afterCount, input.afterTimeoutMs, input.screenshotTimeoutMs, input.maxKeyScreenshots, input.keyScreenshotThrottleMs];
609
+ }
610
+ const numeric = [
611
+ input.maxEvents,
612
+ input.maxBytes,
613
+ input.maxAgeMs,
614
+ input.beforeCount,
615
+ input.afterCount,
616
+ input.afterTimeoutMs,
617
+ input.screenshotTimeoutMs,
618
+ input.maxKeyScreenshots,
619
+ input.keyScreenshotThrottleMs
620
+ ];
528
621
  if (numeric.some((value) => value !== void 0 && (!Number.isSafeInteger(value) || value < 0)) || input.persistDraft && !input.persistenceKey)
529
622
  return failure("INVALID_INPUT");
530
623
  if (input.replayStart !== void 0 && !["init", "prepare"].includes(input.replayStart))
531
624
  return failure("INVALID_INPUT");
532
- if (!FeedbackSubmitInputSchema.pick({ appId: true, appVersion: true, buildId: true, commit: true }).safeParse({ appId: input.appId, appVersion: input.appVersion, buildId: input.buildId, commit: input.commit }).success)
625
+ if (!FeedbackSubmitInputSchema.pick({
626
+ appId: true,
627
+ appVersion: true,
628
+ buildId: true,
629
+ commit: true
630
+ }).safeParse({
631
+ appId: input.appId,
632
+ appVersion: input.appVersion,
633
+ buildId: input.buildId,
634
+ commit: input.commit
635
+ }).success) {
533
636
  return failure("INVALID_INPUT");
637
+ }
534
638
  const initVersion = sessionVersion;
535
639
  const policy = input.actions.policy ? await input.actions.policy().catch(() => failure("OPERATION_FAILED")) : ok(input.budget ?? resolveFeedbackBudget());
536
640
  if (initVersion !== sessionVersion)
@@ -578,16 +682,35 @@ function createFeedbackClient() {
578
682
  unsubscribeLogger = core.logger.observe((entry) => {
579
683
  if (entry.context.module === "feedback" || entry.context.feedback === true)
580
684
  return;
581
- record({ kind: ["error", "fatal"].includes(entry.level) ? "error" : "log", requestId: typeof entry.context.requestId === "string" ? entry.context.requestId : typeof entry.context.traceId === "string" ? entry.context.traceId : void 0, data: { level: entry.level, message: entry.message, context: entry.context } });
685
+ record({
686
+ kind: ["error", "fatal"].includes(entry.level) ? "error" : "log",
687
+ requestId: typeof entry.context.requestId === "string" ? entry.context.requestId : typeof entry.context.traceId === "string" ? entry.context.traceId : void 0,
688
+ data: { level: entry.level, message: entry.message, context: entry.context }
689
+ });
582
690
  });
583
- listen("error", (event) => record({ kind: "error", data: { message: event.message, stack: event.error instanceof Error ? event.error.stack : void 0, filename: feedbackPageUrl(event.filename || location.href), line: event.lineno, column: event.colno } }));
584
- listen("unhandledrejection", (event) => record({ kind: "error", data: { reason: event.reason } }));
691
+ listen("error", (event) => record({
692
+ kind: "error",
693
+ data: {
694
+ message: event.message,
695
+ stack: event.error instanceof Error ? event.error.stack : void 0,
696
+ filename: feedbackPageUrl(event.filename || location.href),
697
+ line: event.lineno,
698
+ column: event.colno
699
+ }
700
+ }));
701
+ listen("unhandledrejection", (event) => record({
702
+ kind: "error",
703
+ data: { reason: event.reason }
704
+ }));
585
705
  const click = (event) => {
586
706
  if (!(event.target instanceof Element) || event.target.closest("[data-feedback-ignore]"))
587
707
  return;
588
708
  const target = event.target.closest("[data-feedback-action],button,a");
589
709
  if (target && !target.closest("[data-feedback-private]")) {
590
- record({ kind: "interaction", data: { action: target.getAttribute("data-feedback-action") ?? target.tagName.toLowerCase(), route: location.pathname } });
710
+ record({
711
+ kind: "interaction",
712
+ data: { action: target.getAttribute("data-feedback-action") ?? target.tagName.toLowerCase(), route: location.pathname }
713
+ });
591
714
  if (target.hasAttribute("data-feedback-action"))
592
715
  void captureKeyScreenshot();
593
716
  }
@@ -598,7 +721,10 @@ function createFeedbackClient() {
598
721
  for (const level of ["debug", "info", "warn", "error"]) {
599
722
  const original = globalThis.console[level];
600
723
  const wrapper = (...args) => {
601
- record({ kind: level === "error" ? "error" : "log", data: { channel: "console", level, arguments: args } });
724
+ record({
725
+ kind: level === "error" ? "error" : "log",
726
+ data: { channel: "console", level, arguments: args }
727
+ });
602
728
  original.apply(globalThis.console, args);
603
729
  };
604
730
  globalThis.console[level] = wrapper;
@@ -668,7 +794,14 @@ function createFeedbackClient() {
668
794
  missing.add("replay-snapshot-truncated");
669
795
  return;
670
796
  }
671
- const safe = FeedbackEvidenceInputSchema.safeParse({ id: core.id.uuid(), source, sequence: ++sequence, kind: "replay", timestamp: Date.now(), data: { event: payload } });
797
+ const safe = FeedbackEvidenceInputSchema.safeParse({
798
+ id: core.id.uuid(),
799
+ source,
800
+ sequence: ++sequence,
801
+ kind: "replay",
802
+ timestamp: Date.now(),
803
+ data: { event: payload }
804
+ });
672
805
  if (!safe.success)
673
806
  return;
674
807
  replayChunks.at(-1)?.push(safe.data);
@@ -800,7 +933,20 @@ function createFeedbackClient() {
800
933
  if (closing || config !== activeConfig)
801
934
  return failure("NOT_INITIALIZED");
802
935
  keyScreenshots = keyScreenshots.filter((item) => item.timestamp >= now - limits().age);
803
- draft = { submissionId: core.id.uuid(), anchorTime: anchor?.timestamp ?? now, anchorKind: anchor ? "error" : "feedback", errorId: anchor?.errorId, evidence: [...selected, ...other, ...replayChunks.flat()].sort((a, b) => a.sequence - b.sequence), attachments: [], missing: [...missing], expiresAt: now + 864e5 };
936
+ draft = {
937
+ submissionId: core.id.uuid(),
938
+ anchorTime: anchor?.timestamp ?? now,
939
+ anchorKind: anchor ? "error" : "feedback",
940
+ errorId: anchor?.errorId,
941
+ evidence: [
942
+ ...selected,
943
+ ...other,
944
+ ...replayChunks.flat()
945
+ ].sort((a, b) => a.sequence - b.sequence),
946
+ attachments: [],
947
+ missing: [...missing],
948
+ expiresAt: now + 864e5
949
+ };
804
950
  while (draft.evidence.length > limits().events || new TextEncoder().encode(JSON.stringify(draft.evidence)).byteLength > limits().bytes) {
805
951
  draft.evidence.shift();
806
952
  missing.add("browser-draft-truncated");
@@ -842,7 +988,22 @@ function createFeedbackClient() {
842
988
  function recordEnvironment() {
843
989
  if (!config)
844
990
  return;
845
- record({ kind: "environment", data: { appId: config.appId, appVersion: config.appVersion, buildId: config.buildId, commit: config.commit, url: feedbackPageUrl(location.href), browser: navigator.userAgent, language: navigator.language, timezone: new Intl.DateTimeFormat().resolvedOptions().timeZone, viewport: { width: innerWidth, height: innerHeight, pixelRatio: devicePixelRatio }, online: navigator.onLine, cookies: collectFeedbackCookies(document.cookie, config.cookieAllowlist ?? []) } });
991
+ record({
992
+ kind: "environment",
993
+ data: {
994
+ appId: config.appId,
995
+ appVersion: config.appVersion,
996
+ buildId: config.buildId,
997
+ commit: config.commit,
998
+ url: feedbackPageUrl(location.href),
999
+ browser: navigator.userAgent,
1000
+ language: navigator.language,
1001
+ timezone: new Intl.DateTimeFormat().resolvedOptions().timeZone,
1002
+ viewport: { width: innerWidth, height: innerHeight, pixelRatio: devicePixelRatio },
1003
+ online: navigator.onLine,
1004
+ cookies: collectFeedbackCookies(document.cookie, config.cookieAllowlist ?? [])
1005
+ }
1006
+ });
846
1007
  }
847
1008
  function pageIdentity() {
848
1009
  return `${location.origin}${location.pathname}${location.hash.startsWith("#/") ? location.hash.split("?")[0] : ""}`;
@@ -868,14 +1029,16 @@ function createFeedbackClient() {
868
1029
  const image = await attachment(name ?? previousName ?? `screenshot-${core.id.uuid()}.png`, "image/png", bytes);
869
1030
  if (closing || config !== activeConfig || draft !== current || current.submission || previous && (!current.attachments.includes(previous) || previous.checksum !== previousChecksum))
870
1031
  return err(HaiFeedbackError.CONFLICT, feedbackM("feedback_conflict"));
871
- if (current.attachments.reduce((total, item) => total + (item === previous ? 0 : item.size), image.size) > budget.maxAttachmentsBytes)
1032
+ if (current.attachments.reduce((total, item) => total + (item === previous ? 0 : item.size), image.size) > budget.maxAttachmentsBytes) {
872
1033
  return failure("LIMIT_EXCEEDED");
873
- if (previous)
1034
+ }
1035
+ if (previous) {
874
1036
  current.attachments = current.attachments.map((item) => item === previous ? { ...image, id: previous.id } : item);
875
- else if (current.attachments.length < budget.maxAttachmentCount)
1037
+ } else if (current.attachments.length < budget.maxAttachmentCount) {
876
1038
  current.attachments.unshift(image);
877
- else
1039
+ } else {
878
1040
  return failure("LIMIT_EXCEEDED");
1041
+ }
879
1042
  if (!name)
880
1043
  recordEnvironment();
881
1044
  preparedPage = pageIdentity();
@@ -911,7 +1074,15 @@ function createFeedbackClient() {
911
1074
  }
912
1075
  draftBytes = new TextEncoder().encode(JSON.stringify(current.evidence)).byteLength;
913
1076
  }
914
- const fingerprint = await digestHex(new TextEncoder().encode(JSON.stringify({ description: sanitizeFeedbackData({ text: input.description ?? "" }).text, category: input.category ?? "", contact: sanitizeFeedbackData({ text: input.contact ?? "" }).text, files: (input.files ?? []).map((item) => ({ name: item.name, size: item.size, lastModified: item.lastModified })), removed: [...input.removedAttachmentIds ?? []].sort() })));
1077
+ const fingerprint = await digestHex(new TextEncoder().encode(JSON.stringify({
1078
+ description: sanitizeFeedbackData({ text: input.description ?? "" }).text,
1079
+ category: input.category ?? "",
1080
+ contact: sanitizeFeedbackData({ text: input.contact ?? "" }).text,
1081
+ files: (input.files ?? []).map((item) => ({ name: item.name, size: item.size, lastModified: item.lastModified })),
1082
+ removed: [
1083
+ ...input.removedAttachmentIds ?? []
1084
+ ].sort()
1085
+ })));
915
1086
  if (closing || config !== activeConfig || draft !== current)
916
1087
  return failure("NOT_INITIALIZED");
917
1088
  if (current.submission && current.userInputFingerprint && fingerprint !== current.userInputFingerprint)
@@ -925,7 +1096,20 @@ function createFeedbackClient() {
925
1096
  }
926
1097
  if (closing || config !== activeConfig || draft !== current)
927
1098
  return failure("NOT_INITIALIZED");
928
- current.submission ??= FeedbackSubmitInputSchema.parse({ submissionId: current.submissionId, appId: config.appId, appVersion: config.appVersion, buildId: config.buildId, commit: config.commit, description: typeof input.description === "string" ? String(sanitizeFeedbackData({ text: input.description }).text) : void 0, category: input.category, contact: input.contact, url: feedbackPageUrl(location.href), anchor: { kind: current.anchorKind, timestamp: current.anchorTime, errorId: current.errorId }, evidence: current.evidence, attachments: current.attachments.map(({ bytes: _bytes, ...metadata }) => metadata) });
1099
+ current.submission ??= FeedbackSubmitInputSchema.parse({
1100
+ submissionId: current.submissionId,
1101
+ appId: config.appId,
1102
+ appVersion: config.appVersion,
1103
+ buildId: config.buildId,
1104
+ commit: config.commit,
1105
+ description: typeof input.description === "string" ? String(sanitizeFeedbackData({ text: input.description }).text) : void 0,
1106
+ category: input.category,
1107
+ contact: input.contact,
1108
+ url: feedbackPageUrl(location.href),
1109
+ anchor: { kind: current.anchorKind, timestamp: current.anchorTime, errorId: current.errorId },
1110
+ evidence: current.evidence,
1111
+ attachments: current.attachments.map(({ bytes: _bytes, ...metadata }) => metadata)
1112
+ });
929
1113
  current.userInputFingerprint = fingerprint;
930
1114
  schedulePersist();
931
1115
  const result = await activeConfig.actions.submit(current.submission);
@@ -953,7 +1137,11 @@ function createFeedbackClient() {
953
1137
  return ok(void 0);
954
1138
  if (uploadedIds.has(item.id))
955
1139
  continue;
956
- const result = await config.actions.upload({ id: draft.report.id, attachmentId: item.id, dataBase64: base64(item.bytes) });
1140
+ const result = await config.actions.upload({
1141
+ id: draft.report.id,
1142
+ attachmentId: item.id,
1143
+ dataBase64: base64(item.bytes)
1144
+ });
957
1145
  if (!result.success)
958
1146
  return result;
959
1147
  uploadedIds.add(item.id);
@@ -979,7 +1167,15 @@ function createFeedbackClient() {
979
1167
  return ok(void 0);
980
1168
  }
981
1169
  if (collectionSealed && !discardOnClose) {
982
- const result = await config.actions.completeCollection({ id: draft.report.id, missing: [.../* @__PURE__ */ new Set([...draft.missing, ...missing])] });
1170
+ const result = await config.actions.completeCollection({
1171
+ id: draft.report.id,
1172
+ missing: [
1173
+ .../* @__PURE__ */ new Set([
1174
+ ...draft.missing,
1175
+ ...missing
1176
+ ])
1177
+ ]
1178
+ });
983
1179
  if (!result.success)
984
1180
  return result;
985
1181
  collectionFinished = true;
@@ -1031,8 +1227,9 @@ function createFeedbackClient() {
1031
1227
  const bytes = await annotateFeedbackImage(originalBytes, mark);
1032
1228
  const name = /\.[^.]+$/.test(item.name) ? item.name.replace(/\.[^.]+$/, ".png") : `${item.name}.png`;
1033
1229
  const updated = await attachment(name, "image/png", bytes);
1034
- if (updated.size > budget.maxAttachmentBytes || current.attachments.reduce((sum, file) => sum + (file === item ? 0 : file.size), updated.size) > budget.maxAttachmentsBytes)
1230
+ if (updated.size > budget.maxAttachmentBytes || current.attachments.reduce((sum, file) => sum + (file === item ? 0 : file.size), updated.size) > budget.maxAttachmentsBytes) {
1035
1231
  return failure("LIMIT_EXCEEDED");
1232
+ }
1036
1233
  if (closing || draft !== current || current.submission || !current.attachments.includes(item) || item.bytes !== originalBytes)
1037
1234
  return err(HaiFeedbackError.CONFLICT, feedbackM("feedback_conflict"));
1038
1235
  Object.assign(item, updated, { id });
@@ -1117,7 +1314,31 @@ function createFeedbackClient() {
1117
1314
  attachments.push(item);
1118
1315
  }
1119
1316
  }
1120
- return createFeedbackBundle({ maxBytes: budget.maxTotalBytes, report: { ...draft.report ?? { id: draft.submissionId, appId: config?.appId, appVersion: config?.appVersion, buildId: config?.buildId, commit: config?.commit, url: draft.evidence.filter((item) => item.kind === "environment").at(-1)?.data.url, anchor: { kind: draft.anchorKind, timestamp: draft.anchorTime }, evidenceRevision: 1 }, ...sanitizeFeedbackData({ description: input.description ?? draft.report?.description ?? "", category: input.category, contact: input.contact }) }, evidence: draft.evidence, attachments: attachments.filter((item) => !input.removedAttachmentIds?.includes(item.id)), missing: [.../* @__PURE__ */ new Set([...draft.missing, ...missing, ...!draft.report ? ["server-evidence-not-obtained", "feedback-not-yet-accepted"] : []])] });
1317
+ return createFeedbackBundle({
1318
+ maxBytes: budget.maxTotalBytes,
1319
+ report: {
1320
+ ...draft.report ?? {
1321
+ id: draft.submissionId,
1322
+ appId: config?.appId,
1323
+ appVersion: config?.appVersion,
1324
+ buildId: config?.buildId,
1325
+ commit: config?.commit,
1326
+ url: draft.evidence.filter((item) => item.kind === "environment").at(-1)?.data.url,
1327
+ anchor: { kind: draft.anchorKind, timestamp: draft.anchorTime },
1328
+ evidenceRevision: 1
1329
+ },
1330
+ ...sanitizeFeedbackData({ description: input.description ?? draft.report?.description ?? "", category: input.category, contact: input.contact })
1331
+ },
1332
+ evidence: draft.evidence,
1333
+ attachments: attachments.filter((item) => !input.removedAttachmentIds?.includes(item.id)),
1334
+ missing: [
1335
+ .../* @__PURE__ */ new Set([
1336
+ ...draft.missing,
1337
+ ...missing,
1338
+ ...!draft.report ? ["server-evidence-not-obtained", "feedback-not-yet-accepted"] : []
1339
+ ])
1340
+ ]
1341
+ });
1121
1342
  } catch (cause) {
1122
1343
  return failure("OPERATION_FAILED", cause);
1123
1344
  }
@@ -1184,33 +1405,53 @@ function createFeedbackClient() {
1184
1405
  flushFailures = 0;
1185
1406
  nextBackgroundFlush = 0;
1186
1407
  }
1187
- return { init, prepare, pause, captureScreenshot, submit, flush, record, getDraft: () => draft, getView, releasePreviews, annotate, addFiles, exportDraft, async saveDraft(input) {
1188
- const session = config;
1189
- const exported = await exportDraft(input);
1190
- if (!exported.success)
1191
- return exported;
1192
- if (closing || !session || session !== config)
1193
- return failure("NOT_INITIALIZED");
1194
- return saveFeedbackFile(exported.data, `feedback-${draft?.submissionId ?? "local"}.zip`, "application/zip", saveController.signal);
1195
- }, close, get budget() {
1196
- return { ...budget };
1197
- } };
1408
+ return {
1409
+ init,
1410
+ prepare,
1411
+ pause,
1412
+ captureScreenshot,
1413
+ submit,
1414
+ flush,
1415
+ record,
1416
+ getDraft: () => draft,
1417
+ getView,
1418
+ releasePreviews,
1419
+ annotate,
1420
+ addFiles,
1421
+ exportDraft,
1422
+ async saveDraft(input) {
1423
+ const session = config;
1424
+ const exported = await exportDraft(input);
1425
+ if (!exported.success)
1426
+ return exported;
1427
+ if (closing || !session || session !== config)
1428
+ return failure("NOT_INITIALIZED");
1429
+ return saveFeedbackFile(exported.data, `feedback-${draft?.submissionId ?? "local"}.zip`, "application/zip", saveController.signal);
1430
+ },
1431
+ close,
1432
+ get budget() {
1433
+ return { ...budget };
1434
+ }
1435
+ };
1198
1436
  }
1199
1437
  var feedbackClient = createFeedbackClient();
1200
1438
  async function bounded(promise, ms, controller) {
1201
1439
  let timer;
1202
1440
  let aborted;
1203
1441
  try {
1204
- return await Promise.race([promise, new Promise((_resolve, reject) => {
1205
- aborted = () => reject(new Error("Capture cancelled"));
1206
- if (controller.signal.aborted)
1207
- aborted();
1208
- else controller.signal.addEventListener("abort", aborted, { once: true });
1209
- timer = setTimeout(() => {
1210
- reject(new Error("Capture timed out"));
1211
- controller.abort();
1212
- }, ms);
1213
- })]);
1442
+ return await Promise.race([
1443
+ promise,
1444
+ new Promise((_resolve, reject) => {
1445
+ aborted = () => reject(new Error("Capture cancelled"));
1446
+ if (controller.signal.aborted)
1447
+ aborted();
1448
+ else controller.signal.addEventListener("abort", aborted, { once: true });
1449
+ timer = setTimeout(() => {
1450
+ reject(new Error("Capture timed out"));
1451
+ controller.abort();
1452
+ }, ms);
1453
+ })
1454
+ ]);
1214
1455
  } finally {
1215
1456
  clearTimeout(timer);
1216
1457
  if (aborted)
@@ -1219,11 +1460,20 @@ async function bounded(promise, ms, controller) {
1219
1460
  }
1220
1461
  async function attachment(name, contentType, bytes) {
1221
1462
  const checksum = await digestHex(bytes);
1222
- return { id: core.id.uuid(), name: String(sanitizeFeedbackData({ text: name }).text).slice(0, 255), contentType, size: bytes.byteLength, checksum, bytes };
1463
+ return {
1464
+ id: core.id.uuid(),
1465
+ name: String(sanitizeFeedbackData({ text: name }).text).slice(0, 255),
1466
+ contentType,
1467
+ size: bytes.byteLength,
1468
+ checksum,
1469
+ bytes
1470
+ };
1223
1471
  }
1224
1472
  async function digestHex(bytes) {
1225
1473
  const digest = await globalThis.crypto.subtle.digest("SHA-256", new Uint8Array(bytes));
1226
- return [...new Uint8Array(digest)].map((value) => value.toString(16).padStart(2, "0")).join("");
1474
+ return [
1475
+ ...new Uint8Array(digest)
1476
+ ].map((value) => value.toString(16).padStart(2, "0")).join("");
1227
1477
  }
1228
1478
  function base64(bytes) {
1229
1479
  let value = "";
@@ -1273,7 +1523,12 @@ function createFeedbackReview(actions) {
1273
1523
  const events = [];
1274
1524
  try {
1275
1525
  for (let page = 1; page <= 100; page++) {
1276
- const result = await actions.getEvidence({ id, kind: "replay", page, pageSize: 100 });
1526
+ const result = await actions.getEvidence({
1527
+ id,
1528
+ kind: "replay",
1529
+ page,
1530
+ pageSize: 100
1531
+ });
1277
1532
  if (signal.aborted)
1278
1533
  return failed();
1279
1534
  if (!result.success)