@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.
- package/README.en.md +14 -4
- package/README.ja.md +14 -4
- package/README.md +14 -4
- package/dist/bundle/index.d.ts +2 -0
- package/dist/bundle/index.js +1 -1
- package/dist/{chunk-UIVT267C.js → chunk-7W47WZXY.js} +10 -6
- package/dist/chunk-7W47WZXY.js.map +1 -0
- package/dist/{chunk-5QWKURWU.js → chunk-KGMLTOMM.js} +5 -3
- package/dist/chunk-KGMLTOMM.js.map +1 -0
- package/dist/client/index.d.ts +12 -2
- package/dist/client/index.js +110 -17
- package/dist/client/index.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +276 -57
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/dist/chunk-5QWKURWU.js.map +0 -1
- package/dist/chunk-UIVT267C.js.map +0 -1
package/dist/client/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { feedbackM, HaiFeedbackError, redactFeedbackText } from '../chunk-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ||
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
700
|
-
|
|
735
|
+
captureTask = void 0;
|
|
736
|
+
if (!screenshotCaptures && !closing && config?.replay && replayRequested)
|
|
737
|
+
await startReplay(config);
|
|
701
738
|
}
|
|
702
739
|
})();
|
|
703
|
-
|
|
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
|
-
|
|
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 = [];
|