@h-ai/feedback 0.1.0-alpha.74 → 0.1.0-alpha.76

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.
@@ -1,4 +1,4 @@
1
- import { feedbackM, HaiFeedbackError, redactFeedbackText } from '../chunk-UIVT267C.js';
1
+ import { feedbackM, HaiFeedbackError, redactFeedbackText } from '../chunk-7W47WZXY.js';
2
2
  import { ok, core, err } from '@h-ai/core';
3
3
  import { resolveFeedbackBudget, FeedbackSubmitInputSchema, FeedbackBudgetSchema, FeedbackEvidenceInputSchema, FeedbackReportSchema } from '@h-ai/api-contract/feedback';
4
4
 
@@ -45,7 +45,14 @@ async function saveFeedbackFile(bytes, filename, contentType = "application/zip"
45
45
  // src/client/feedback-client-image.ts
46
46
  var PRIVATE_SELECTOR = 'input[type="password"],[data-feedback-private],.rr-block,.rr-mask';
47
47
  var CONTROL_SELECTOR = 'input,textarea,select,[contenteditable]:not([contenteditable="false"])';
48
- async function captureFeedbackScreenshot() {
48
+ async function captureFeedbackScreenshot(signal) {
49
+ signal?.throwIfAborted();
50
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT);
51
+ let nodes = 0;
52
+ while (walker.nextNode()) {
53
+ if (++nodes > 1e4)
54
+ throw new RangeError("Screenshot document budget exceeded");
55
+ }
49
56
  if (document.fonts) {
50
57
  let timeout;
51
58
  try {
@@ -55,6 +62,7 @@ async function captureFeedbackScreenshot() {
55
62
  }
56
63
  }
57
64
  const { default: html2canvas } = await import('html2canvas');
65
+ signal?.throwIfAborted();
58
66
  const canvas = await html2canvas(document.body, {
59
67
  width: window.innerWidth,
60
68
  height: window.innerHeight,
@@ -71,6 +79,7 @@ async function captureFeedbackScreenshot() {
71
79
  imageTimeout: 1e3,
72
80
  ignoreElements: (element) => element.matches("[data-feedback-ignore],iframe,video"),
73
81
  onclone: (clone) => {
82
+ signal?.throwIfAborted();
74
83
  const colorCanvas = clone.createElement("canvas");
75
84
  colorCanvas.width = 1;
76
85
  colorCanvas.height = 1;
@@ -124,7 +133,10 @@ async function captureFeedbackScreenshot() {
124
133
  }
125
134
  });
126
135
  try {
127
- return await canvasBytes(canvas);
136
+ signal?.throwIfAborted();
137
+ const bytes = await canvasBytes(canvas);
138
+ signal?.throwIfAborted();
139
+ return bytes;
128
140
  } finally {
129
141
  canvas.width = 0;
130
142
  canvas.height = 0;
@@ -394,6 +406,8 @@ function createFeedbackClient() {
394
406
  let flushTimer;
395
407
  let stopReplay;
396
408
  let screenshotCaptures = 0;
409
+ let captureTask;
410
+ let replayRequested = false;
397
411
  let unsubscribeLogger;
398
412
  let source = "";
399
413
  let keyScreenshots = [];
@@ -412,7 +426,8 @@ function createFeedbackClient() {
412
426
  const key = code === "NOT_INITIALIZED" ? "feedback_notInitialized" : code === "LIMIT_EXCEEDED" ? "feedback_limitExceeded" : code === "INVALID_INPUT" ? "feedback_invalidInput" : "feedback_operationFailed";
413
427
  const traceId = core.context.get().traceId ?? core.context.get().requestId ?? core.id.uuid();
414
428
  const error = { ...HaiFeedbackError[code], traceId, message: message ?? feedbackM(key), cause };
415
- core.logger[cause === void 0 ? "warn" : "error"]("Feedback client operation failed", { module: "feedback", traceId, error });
429
+ if (!(closing && code === "NOT_INITIALIZED"))
430
+ core.logger[cause === void 0 ? "warn" : "error"]("Feedback client operation failed", { module: "feedback", traceId, error });
416
431
  return err({ ...error, message: core.error.formatMessage(error) });
417
432
  }
418
433
  function limits() {
@@ -484,9 +499,9 @@ function createFeedbackClient() {
484
499
  schedulePersist();
485
500
  appendToDraft(item, size);
486
501
  }
487
- function appendToDraft(item, size) {
502
+ function appendToDraft(item, size, snapshot = false) {
488
503
  const bounds = limits();
489
- if (draft && !collectionFinished && !collectionSealed && (Date.now() <= deadline && afterCount < bounds.after || item.kind === "environment") && draft.evidence.length < bounds.events && draftBytes + size <= bounds.bytes) {
504
+ if (draft && !collectionFinished && !collectionSealed && (snapshot || Date.now() <= deadline && afterCount < bounds.after || item.kind === "environment") && draft.evidence.length < bounds.events && draftBytes + size <= bounds.bytes) {
490
505
  draft.evidence.push(item);
491
506
  draftBytes += size;
492
507
  if (item.kind === "log" || item.kind === "error")
@@ -510,6 +525,8 @@ function createFeedbackClient() {
510
525
  const numeric = [input.maxEvents, input.maxBytes, input.maxAgeMs, input.beforeCount, input.afterCount, input.afterTimeoutMs, input.screenshotTimeoutMs, input.maxKeyScreenshots, input.keyScreenshotThrottleMs];
511
526
  if (numeric.some((value) => value !== void 0 && (!Number.isSafeInteger(value) || value < 0)) || input.persistDraft && !input.persistenceKey)
512
527
  return failure("INVALID_INPUT");
528
+ if (input.replayStart !== void 0 && !["init", "prepare"].includes(input.replayStart))
529
+ return failure("INVALID_INPUT");
513
530
  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)
514
531
  return failure("INVALID_INPUT");
515
532
  const initVersion = sessionVersion;
@@ -597,11 +614,17 @@ function createFeedbackClient() {
597
614
  void saveFeedbackCheckpoint(storageKey, buffer).catch(() => missing.add("browser-persistence-unavailable"));
598
615
  void flush();
599
616
  });
600
- if (input.replay)
617
+ replayRequested = input.replayStart === "init";
618
+ if (input.replay && replayRequested)
601
619
  await startReplay(input);
602
620
  if (closing || config !== input)
603
621
  return failure("NOT_INITIALIZED");
604
622
  flushTimer = setInterval(() => {
623
+ if (draft && Date.now() > deadline && input.replayStart !== "init") {
624
+ replayRequested = false;
625
+ stopReplay?.();
626
+ stopReplay = void 0;
627
+ }
605
628
  if (draft?.report)
606
629
  void flush();
607
630
  }, 1e3);
@@ -616,8 +639,12 @@ function createFeedbackClient() {
616
639
  async function startReplay(activeConfig) {
617
640
  try {
618
641
  const { record: start } = await import('@rrweb/record');
619
- if (closing || config !== activeConfig || screenshotCaptures || stopReplay)
642
+ if (closing || config !== activeConfig || stopReplay)
643
+ return;
644
+ if (screenshotCaptures) {
645
+ missing.add("replay-deferred-by-capture");
620
646
  return;
647
+ }
621
648
  let firstEvent = true;
622
649
  stopReplay = start({
623
650
  maskAllInputs: true,
@@ -643,7 +670,7 @@ function createFeedbackClient() {
643
670
  if (!safe.success)
644
671
  return;
645
672
  replayChunks.at(-1)?.push(safe.data);
646
- appendToDraft(safe.data, new TextEncoder().encode(JSON.stringify(safe.data)).byteLength);
673
+ appendToDraft(safe.data, new TextEncoder().encode(JSON.stringify(safe.data)).byteLength, event.type === 2 || event.type === 4);
647
674
  while (replayChunks.length > 2)
648
675
  replayChunks.shift();
649
676
  const count = replayChunks.reduce((total, chunk) => total + chunk.length, 0);
@@ -660,7 +687,7 @@ function createFeedbackClient() {
660
687
  }
661
688
  }
662
689
  function captureKeyScreenshot() {
663
- if (!config || config.screenshot === false || config.keyScreenshots === false || config.maxKeyScreenshots === 0 || keyScreenshotPromise || Date.now() - lastKeyScreenshot < (config.keyScreenshotThrottleMs ?? 1e4))
690
+ if (!config || config.screenshot === false || config.keyScreenshots !== true || config.maxKeyScreenshots === 0 || keyScreenshotPromise || captureTask || Date.now() - lastKeyScreenshot < (config.keyScreenshotThrottleMs ?? 1e4))
664
691
  return keyScreenshotPromise ?? Promise.resolve();
665
692
  const activeConfig = config;
666
693
  lastKeyScreenshot = Date.now();
@@ -688,19 +715,30 @@ function createFeedbackClient() {
688
715
  return keyScreenshotPromise;
689
716
  }
690
717
  function takeScreenshot(activeConfig) {
718
+ if (captureTask) {
719
+ if (captureTask.config !== activeConfig || captureTask.controller.signal.aborted)
720
+ return Promise.reject(new Error("Screenshot task is still stopping"));
721
+ return bounded(captureTask.promise, activeConfig.screenshotTimeoutMs ?? 3e3, captureTask.controller);
722
+ }
723
+ const controller = new AbortController();
691
724
  screenshotCaptures++;
692
725
  stopReplay?.();
693
726
  stopReplay = void 0;
694
727
  const captured = (async () => {
695
728
  try {
696
- return await (activeConfig.captureScreenshot?.() ?? captureFeedbackScreenshot());
729
+ const bytes = await (activeConfig.captureScreenshot?.({ signal: controller.signal }) ?? captureFeedbackScreenshot(controller.signal));
730
+ controller.signal.throwIfAborted();
731
+ return bytes;
697
732
  } finally {
733
+ await Promise.resolve();
698
734
  screenshotCaptures--;
699
- if (!screenshotCaptures && !closing && config === activeConfig && activeConfig.replay)
700
- await startReplay(activeConfig);
735
+ captureTask = void 0;
736
+ if (!screenshotCaptures && !closing && config?.replay && replayRequested)
737
+ await startReplay(config);
701
738
  }
702
739
  })();
703
- return bounded(captured, activeConfig.screenshotTimeoutMs ?? 3e3);
740
+ captureTask = { promise: captured, controller, config: activeConfig };
741
+ return bounded(captured, activeConfig.screenshotTimeoutMs ?? 3e3, controller);
704
742
  }
705
743
  async function doPrepare() {
706
744
  if (!config || closing)
@@ -722,6 +760,9 @@ function createFeedbackClient() {
722
760
  if (!captured.success)
723
761
  return captured;
724
762
  }
763
+ replayRequested = true;
764
+ if (config.replay && !stopReplay)
765
+ await startReplay(config);
725
766
  return ok(draft);
726
767
  }
727
768
  collectionFinished = false;
@@ -776,6 +817,9 @@ function createFeedbackClient() {
776
817
  draft.missing.push("key-screenshot-budget-exceeded");
777
818
  else draft.attachments.push(image);
778
819
  }
820
+ replayRequested = true;
821
+ if (config.replay && !stopReplay)
822
+ await startReplay(config);
779
823
  schedulePersist();
780
824
  preparedPage = pageIdentity();
781
825
  return ok(draft);
@@ -926,6 +970,8 @@ function createFeedbackClient() {
926
970
  if (!result.success)
927
971
  return result;
928
972
  collectionFinished = true;
973
+ if (config.replayStart !== "init")
974
+ pause();
929
975
  const updatedReport = FeedbackReportSchema.safeParse(result.data);
930
976
  if (updatedReport.success)
931
977
  draft.report = updatedReport.data;
@@ -1055,8 +1101,15 @@ function createFeedbackClient() {
1055
1101
  });
1056
1102
  return closePromise;
1057
1103
  }
1104
+ function pause() {
1105
+ replayRequested = false;
1106
+ stopReplay?.();
1107
+ stopReplay = void 0;
1108
+ captureTask?.controller.abort();
1109
+ }
1058
1110
  async function doClose() {
1059
1111
  closing = true;
1112
+ pause();
1060
1113
  saveController.abort();
1061
1114
  unsubscribeLogger?.();
1062
1115
  unsubscribeLogger = void 0;
@@ -1088,6 +1141,7 @@ function createFeedbackClient() {
1088
1141
  lastError = void 0;
1089
1142
  errorWindows = [];
1090
1143
  replayChunks = [];
1144
+ replayRequested = false;
1091
1145
  keyScreenshots = [];
1092
1146
  lastKeyScreenshot = 0;
1093
1147
  initialIds.clear();
@@ -1097,7 +1151,7 @@ function createFeedbackClient() {
1097
1151
  collectionFinished = false;
1098
1152
  collectionSealed = false;
1099
1153
  }
1100
- return { init, prepare, captureScreenshot, submit, flush, record, getDraft: () => draft, getView, releasePreviews, annotate, addFiles, exportDraft, async saveDraft(input) {
1154
+ return { init, prepare, pause, captureScreenshot, submit, flush, record, getDraft: () => draft, getView, releasePreviews, annotate, addFiles, exportDraft, async saveDraft(input) {
1101
1155
  const session = config;
1102
1156
  const exported = await exportDraft(input);
1103
1157
  if (!exported.success)
@@ -1110,14 +1164,24 @@ function createFeedbackClient() {
1110
1164
  } };
1111
1165
  }
1112
1166
  var feedbackClient = createFeedbackClient();
1113
- async function bounded(promise, ms) {
1167
+ async function bounded(promise, ms, controller) {
1114
1168
  let timer;
1169
+ let aborted;
1115
1170
  try {
1116
1171
  return await Promise.race([promise, new Promise((_resolve, reject) => {
1117
- timer = setTimeout(() => reject(new Error("Capture timed out")), ms);
1172
+ aborted = () => reject(new Error("Capture cancelled"));
1173
+ if (controller.signal.aborted)
1174
+ aborted();
1175
+ else controller.signal.addEventListener("abort", aborted, { once: true });
1176
+ timer = setTimeout(() => {
1177
+ reject(new Error("Capture timed out"));
1178
+ controller.abort();
1179
+ }, ms);
1118
1180
  })]);
1119
1181
  } finally {
1120
1182
  clearTimeout(timer);
1183
+ if (aborted)
1184
+ controller.signal.removeEventListener("abort", aborted);
1121
1185
  }
1122
1186
  }
1123
1187
  async function attachment(name, contentType, bytes) {
@@ -1142,6 +1206,35 @@ function createFeedbackReview(actions) {
1142
1206
  controller.abort();
1143
1207
  controller = new AbortController();
1144
1208
  },
1209
+ async retryReport(id) {
1210
+ const signal = controller.signal;
1211
+ try {
1212
+ const loaded = await actions.get({ id });
1213
+ if (signal.aborted)
1214
+ return failed();
1215
+ if (!loaded.success)
1216
+ return loaded;
1217
+ const targets = /* @__PURE__ */ new Set();
1218
+ if (["partial", "failed"].includes(loaded.data.report.collectionStatus))
1219
+ targets.add("collect");
1220
+ for (const job of loaded.data.jobs) {
1221
+ if (job.status === "failed" && (job.type === "collect" || job.type === "notify" || job.type === "webhook"))
1222
+ targets.add(job.type);
1223
+ }
1224
+ for (const target of targets) {
1225
+ if (signal.aborted)
1226
+ return failed();
1227
+ const retried = await actions.retry({ id, target });
1228
+ if (signal.aborted)
1229
+ return failed();
1230
+ if (!retried.success)
1231
+ return retried;
1232
+ }
1233
+ return ok(void 0);
1234
+ } catch {
1235
+ return failed();
1236
+ }
1237
+ },
1145
1238
  async loadReplay(id) {
1146
1239
  const signal = controller.signal;
1147
1240
  const events = [];