@usero/sdk 1.1.0 → 1.1.1
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.
|
@@ -623,12 +623,15 @@ function showMuteToast(store) {
|
|
|
623
623
|
toast.setAttribute("role", "status");
|
|
624
624
|
toast.innerHTML = `<strong>Mic off.</strong> Screen is still recording. Tap to unmute.`;
|
|
625
625
|
slot.appendChild(toast);
|
|
626
|
-
window.setTimeout(() => {
|
|
626
|
+
const outer = window.setTimeout(() => {
|
|
627
|
+
if (!toast.isConnected) return;
|
|
627
628
|
toast.setAttribute("data-leaving", "true");
|
|
628
|
-
window.setTimeout(() => {
|
|
629
|
-
toast.remove();
|
|
629
|
+
const inner = window.setTimeout(() => {
|
|
630
|
+
if (toast.isConnected) toast.remove();
|
|
630
631
|
}, 260);
|
|
632
|
+
store.muteToastTimers.push(inner);
|
|
631
633
|
}, 3e3);
|
|
634
|
+
store.muteToastTimers.push(outer);
|
|
632
635
|
}
|
|
633
636
|
function openNotePopover(store, onSave, onCancel) {
|
|
634
637
|
const root = store.indicatorRoot;
|
|
@@ -756,6 +759,17 @@ function showThanksScreen(root, opts) {
|
|
|
756
759
|
sent.textContent = message;
|
|
757
760
|
card.appendChild(sent);
|
|
758
761
|
};
|
|
762
|
+
const ERROR_CLASS = "end-error";
|
|
763
|
+
const showError = (message) => {
|
|
764
|
+
const prior = form.querySelector(`.${ERROR_CLASS}`);
|
|
765
|
+
if (prior) prior.remove();
|
|
766
|
+
const err = document.createElement("p");
|
|
767
|
+
err.className = ERROR_CLASS;
|
|
768
|
+
err.textContent = message;
|
|
769
|
+
err.setAttribute("role", "alert");
|
|
770
|
+
err.style.cssText = "margin:10px 0 0;font-size:12.5px;color:#b91c1c;text-align:center;";
|
|
771
|
+
form.appendChild(err);
|
|
772
|
+
};
|
|
759
773
|
const submit = async () => {
|
|
760
774
|
const text = ta.value.trim();
|
|
761
775
|
ta.disabled = true;
|
|
@@ -763,10 +777,21 @@ function showThanksScreen(root, opts) {
|
|
|
763
777
|
const submitBtn = form.querySelector("button.primary");
|
|
764
778
|
if (submitBtn) submitBtn.disabled = true;
|
|
765
779
|
if (text) {
|
|
766
|
-
|
|
767
|
-
|
|
780
|
+
try {
|
|
781
|
+
await Promise.race([
|
|
782
|
+
Promise.resolve(opts.onSubmitNote(text)),
|
|
783
|
+
new Promise((_, reject) => {
|
|
784
|
+
window.setTimeout(() => reject(new Error("timeout")), 3e4);
|
|
785
|
+
})
|
|
786
|
+
]);
|
|
787
|
+
swapToSent("Thanks. You can close this tab.");
|
|
788
|
+
} catch {
|
|
789
|
+
ta.disabled = false;
|
|
790
|
+
skipBtn.disabled = false;
|
|
791
|
+
if (submitBtn) submitBtn.disabled = false;
|
|
792
|
+
showError("Couldn't save your note. Try again?");
|
|
793
|
+
}
|
|
768
794
|
} else {
|
|
769
|
-
opts.onSkip();
|
|
770
795
|
swapToSent("All good. You can close this tab.");
|
|
771
796
|
}
|
|
772
797
|
};
|
|
@@ -823,6 +848,12 @@ async function finaliseSession(apiUrl, sessionId, durationSeconds, extras = {})
|
|
|
823
848
|
}
|
|
824
849
|
const trimmedEndNote = extras.endNote?.trim();
|
|
825
850
|
if (trimmedEndNote) body.endNote = trimmedEndNote;
|
|
851
|
+
if (extras.notes && extras.notes.length > 0) {
|
|
852
|
+
body.notes = extras.notes.slice(0, 200).map((n) => ({
|
|
853
|
+
atMs: Math.max(0, Math.round(n.atMs)),
|
|
854
|
+
text: n.text
|
|
855
|
+
}));
|
|
856
|
+
}
|
|
826
857
|
const res = await fetch(`${apiUrl.replace(/\/$/, "")}/api/user-test-sessions/${encodeURIComponent(sessionId)}/finalise`, {
|
|
827
858
|
method: "POST",
|
|
828
859
|
headers: { "Content-Type": "application/json" },
|
|
@@ -834,7 +865,7 @@ async function finaliseSession(apiUrl, sessionId, durationSeconds, extras = {})
|
|
|
834
865
|
return false;
|
|
835
866
|
}
|
|
836
867
|
}
|
|
837
|
-
async function
|
|
868
|
+
async function postNoteOnce(apiUrl, sessionId, atMs, text, logger) {
|
|
838
869
|
try {
|
|
839
870
|
const res = await fetch(`${apiUrl.replace(/\/$/, "")}/api/user-test-sessions/${encodeURIComponent(sessionId)}/notes`, {
|
|
840
871
|
method: "POST",
|
|
@@ -844,14 +875,26 @@ async function postNote(apiUrl, sessionId, atMs, text, logger) {
|
|
|
844
875
|
});
|
|
845
876
|
if (!res.ok) {
|
|
846
877
|
logger.warn(`note POST rejected with ${res.status}`);
|
|
847
|
-
return false;
|
|
878
|
+
return { ok: false, transient: res.status >= 500 || res.status === 408 || res.status === 429 };
|
|
848
879
|
}
|
|
849
|
-
|
|
880
|
+
let id;
|
|
881
|
+
try {
|
|
882
|
+
const json = await res.json();
|
|
883
|
+
if (typeof json.id === "string") id = json.id;
|
|
884
|
+
} catch {
|
|
885
|
+
}
|
|
886
|
+
return { ok: true, id, transient: false };
|
|
850
887
|
} catch (err) {
|
|
851
888
|
logger.warn("note POST failed", err);
|
|
852
|
-
return false;
|
|
889
|
+
return { ok: false, transient: true };
|
|
853
890
|
}
|
|
854
891
|
}
|
|
892
|
+
async function postNoteWithRetry(apiUrl, sessionId, atMs, text, logger) {
|
|
893
|
+
const first = await postNoteOnce(apiUrl, sessionId, atMs, text, logger);
|
|
894
|
+
if (first.ok || !first.transient) return first;
|
|
895
|
+
await new Promise((resolve) => setTimeout(resolve, 400 + Math.floor(Math.random() * 200)));
|
|
896
|
+
return postNoteOnce(apiUrl, sessionId, atMs, text, logger);
|
|
897
|
+
}
|
|
855
898
|
async function flushPendingFromIdb(store, ctx) {
|
|
856
899
|
if (!store.sessionId) return;
|
|
857
900
|
const pending = await idbListChunks(store.sessionId);
|
|
@@ -972,7 +1015,9 @@ function stopRecording(store) {
|
|
|
972
1015
|
}
|
|
973
1016
|
async function finishFlow(store, ctx, opts) {
|
|
974
1017
|
if (store.cancelled) return;
|
|
1018
|
+
if (store.finishFlowRan) return;
|
|
975
1019
|
if (store.indicatorState === "finishing" || store.indicatorState === "done") return;
|
|
1020
|
+
store.finishFlowRan = true;
|
|
976
1021
|
store.indicatorState = "finishing";
|
|
977
1022
|
flushMuteIfActive(store);
|
|
978
1023
|
renderIndicatorState(store);
|
|
@@ -981,9 +1026,16 @@ async function finishFlow(store, ctx, opts) {
|
|
|
981
1026
|
await flushPendingFromIdb(store, ctx);
|
|
982
1027
|
const durationSeconds = (Date.now() - store.startedAt) / 1e3;
|
|
983
1028
|
if (store.sessionId) {
|
|
1029
|
+
const unackedNotes = store.notes.filter((n) => !n.acked).map((n) => ({ atMs: n.atMs, text: n.text }));
|
|
984
1030
|
const ok = await finaliseSession(store.options.apiUrl, store.sessionId, durationSeconds, {
|
|
985
|
-
mutedSegments: store.mutedSegments
|
|
1031
|
+
mutedSegments: store.mutedSegments,
|
|
1032
|
+
notes: unackedNotes
|
|
986
1033
|
});
|
|
1034
|
+
if (ok) {
|
|
1035
|
+
for (const n of store.notes) {
|
|
1036
|
+
if (!n.acked) n.acked = true;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
987
1039
|
store.indicatorState = ok ? "done" : "error";
|
|
988
1040
|
} else {
|
|
989
1041
|
store.indicatorState = "error";
|
|
@@ -994,10 +1046,15 @@ async function finishFlow(store, ctx, opts) {
|
|
|
994
1046
|
onSubmitNote: async (text) => {
|
|
995
1047
|
if (!store.sessionId) return;
|
|
996
1048
|
store.endNote = text;
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
endNote: text
|
|
1049
|
+
const stillUnacked = store.notes.filter((n) => !n.acked).map((n) => ({ atMs: n.atMs, text: n.text }));
|
|
1050
|
+
const ok = await finaliseSession(store.options.apiUrl, store.sessionId, durationSeconds, {
|
|
1051
|
+
endNote: text,
|
|
1052
|
+
notes: stillUnacked
|
|
1000
1053
|
});
|
|
1054
|
+
if (!ok) throw new Error("finalise failed");
|
|
1055
|
+
for (const n of store.notes) {
|
|
1056
|
+
if (!n.acked) n.acked = true;
|
|
1057
|
+
}
|
|
1001
1058
|
},
|
|
1002
1059
|
onSkip: () => {
|
|
1003
1060
|
}
|
|
@@ -1044,10 +1101,12 @@ function userTest(options = {}) {
|
|
|
1044
1101
|
mutedSinceMs: null,
|
|
1045
1102
|
mutedSegments: [],
|
|
1046
1103
|
muteToastShown: false,
|
|
1104
|
+
muteToastTimers: [],
|
|
1047
1105
|
notes: [],
|
|
1048
1106
|
notesPopoverOpen: false,
|
|
1049
1107
|
notePopoverAtMs: null,
|
|
1050
|
-
endNote: ""
|
|
1108
|
+
endNote: "",
|
|
1109
|
+
finishFlowRan: false
|
|
1051
1110
|
};
|
|
1052
1111
|
ctx.setStore(store);
|
|
1053
1112
|
const onFinish = () => {
|
|
@@ -1077,11 +1136,19 @@ function userTest(options = {}) {
|
|
|
1077
1136
|
store,
|
|
1078
1137
|
(text) => {
|
|
1079
1138
|
const atMs = store.notePopoverAtMs ?? Math.max(0, Date.now() - store.startedAt);
|
|
1080
|
-
|
|
1139
|
+
const note = { atMs, text, acked: false };
|
|
1140
|
+
store.notes.push(note);
|
|
1081
1141
|
closeNote();
|
|
1082
1142
|
renderNotesCount(store);
|
|
1083
1143
|
if (store.sessionId) {
|
|
1084
|
-
|
|
1144
|
+
const sessionId = store.sessionId;
|
|
1145
|
+
void (async () => {
|
|
1146
|
+
const result = await postNoteWithRetry(store.options.apiUrl, sessionId, atMs, text, ctx.logger);
|
|
1147
|
+
if (result.ok) {
|
|
1148
|
+
note.acked = true;
|
|
1149
|
+
if (result.id) note.serverId = result.id;
|
|
1150
|
+
}
|
|
1151
|
+
})();
|
|
1085
1152
|
}
|
|
1086
1153
|
},
|
|
1087
1154
|
() => closeNote()
|
|
@@ -1164,6 +1231,13 @@ function userTest(options = {}) {
|
|
|
1164
1231
|
document.removeEventListener("keydown", store.keydownHandler);
|
|
1165
1232
|
store.keydownHandler = null;
|
|
1166
1233
|
}
|
|
1234
|
+
for (const id of store.muteToastTimers) {
|
|
1235
|
+
try {
|
|
1236
|
+
window.clearTimeout(id);
|
|
1237
|
+
} catch {
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
store.muteToastTimers = [];
|
|
1167
1241
|
if (store.indicator && store.indicator.parentNode) {
|
|
1168
1242
|
store.indicator.parentNode.removeChild(store.indicator);
|
|
1169
1243
|
}
|