@usero/sdk 1.1.0 → 1.1.2
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/plugins/session-replay.cjs +2 -1
- package/dist/plugins/session-replay.cjs.map +1 -1
- package/dist/plugins/session-replay.d.cts +5 -0
- package/dist/plugins/session-replay.d.ts +5 -0
- package/dist/plugins/session-replay.js +2 -1
- package/dist/plugins/session-replay.js.map +1 -1
- package/dist/plugins/user-test.cjs +106 -17
- package/dist/plugins/user-test.cjs.map +1 -1
- package/dist/plugins/user-test.d.cts +5 -0
- package/dist/plugins/user-test.d.ts +5 -0
- package/dist/plugins/user-test.js +106 -17
- package/dist/plugins/user-test.js.map +1 -1
- package/dist/react.cjs +51 -1
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +5 -0
- package/dist/react.d.ts +5 -0
- package/dist/react.js +51 -1
- package/dist/react.js.map +1 -1
- package/dist/usero.iife.js +22 -22
- package/dist/usero.iife.js.map +1 -1
- package/dist/vanilla.cjs +51 -1
- package/dist/vanilla.cjs.map +1 -1
- package/dist/vanilla.d.cts +5 -0
- package/dist/vanilla.d.ts +5 -0
- package/dist/vanilla.js +51 -1
- package/dist/vanilla.js.map +1 -1
- package/package.json +1 -1
|
@@ -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,16 @@ 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
|
+
}
|
|
857
|
+
if (extras.sdkSessionId) body.sdkSessionId = extras.sdkSessionId;
|
|
858
|
+
if (typeof extras.replayOffsetMs === "number") {
|
|
859
|
+
body.replayOffsetMs = Math.max(0, Math.round(extras.replayOffsetMs));
|
|
860
|
+
}
|
|
826
861
|
const res = await fetch(`${apiUrl.replace(/\/$/, "")}/api/user-test-sessions/${encodeURIComponent(sessionId)}/finalise`, {
|
|
827
862
|
method: "POST",
|
|
828
863
|
headers: { "Content-Type": "application/json" },
|
|
@@ -834,7 +869,7 @@ async function finaliseSession(apiUrl, sessionId, durationSeconds, extras = {})
|
|
|
834
869
|
return false;
|
|
835
870
|
}
|
|
836
871
|
}
|
|
837
|
-
async function
|
|
872
|
+
async function postNoteOnce(apiUrl, sessionId, atMs, text, logger) {
|
|
838
873
|
try {
|
|
839
874
|
const res = await fetch(`${apiUrl.replace(/\/$/, "")}/api/user-test-sessions/${encodeURIComponent(sessionId)}/notes`, {
|
|
840
875
|
method: "POST",
|
|
@@ -844,14 +879,26 @@ async function postNote(apiUrl, sessionId, atMs, text, logger) {
|
|
|
844
879
|
});
|
|
845
880
|
if (!res.ok) {
|
|
846
881
|
logger.warn(`note POST rejected with ${res.status}`);
|
|
847
|
-
return false;
|
|
882
|
+
return { ok: false, transient: res.status >= 500 || res.status === 408 || res.status === 429 };
|
|
848
883
|
}
|
|
849
|
-
|
|
884
|
+
let id;
|
|
885
|
+
try {
|
|
886
|
+
const json = await res.json();
|
|
887
|
+
if (typeof json.id === "string") id = json.id;
|
|
888
|
+
} catch {
|
|
889
|
+
}
|
|
890
|
+
return { ok: true, id, transient: false };
|
|
850
891
|
} catch (err) {
|
|
851
892
|
logger.warn("note POST failed", err);
|
|
852
|
-
return false;
|
|
893
|
+
return { ok: false, transient: true };
|
|
853
894
|
}
|
|
854
895
|
}
|
|
896
|
+
async function postNoteWithRetry(apiUrl, sessionId, atMs, text, logger) {
|
|
897
|
+
const first = await postNoteOnce(apiUrl, sessionId, atMs, text, logger);
|
|
898
|
+
if (first.ok || !first.transient) return first;
|
|
899
|
+
await new Promise((resolve) => setTimeout(resolve, 400 + Math.floor(Math.random() * 200)));
|
|
900
|
+
return postNoteOnce(apiUrl, sessionId, atMs, text, logger);
|
|
901
|
+
}
|
|
855
902
|
async function flushPendingFromIdb(store, ctx) {
|
|
856
903
|
if (!store.sessionId) return;
|
|
857
904
|
const pending = await idbListChunks(store.sessionId);
|
|
@@ -972,7 +1019,9 @@ function stopRecording(store) {
|
|
|
972
1019
|
}
|
|
973
1020
|
async function finishFlow(store, ctx, opts) {
|
|
974
1021
|
if (store.cancelled) return;
|
|
1022
|
+
if (store.finishFlowRan) return;
|
|
975
1023
|
if (store.indicatorState === "finishing" || store.indicatorState === "done") return;
|
|
1024
|
+
store.finishFlowRan = true;
|
|
976
1025
|
store.indicatorState = "finishing";
|
|
977
1026
|
flushMuteIfActive(store);
|
|
978
1027
|
renderIndicatorState(store);
|
|
@@ -980,10 +1029,24 @@ async function finishFlow(store, ctx, opts) {
|
|
|
980
1029
|
await store.uploadQueue;
|
|
981
1030
|
await flushPendingFromIdb(store, ctx);
|
|
982
1031
|
const durationSeconds = (Date.now() - store.startedAt) / 1e3;
|
|
1032
|
+
const replayLinkage = {};
|
|
1033
|
+
const linkageSdkSessionId = ctx.getSdkSessionId ? ctx.getSdkSessionId() : void 0;
|
|
1034
|
+
if (linkageSdkSessionId) replayLinkage.sdkSessionId = linkageSdkSessionId;
|
|
1035
|
+
if (store.replayOffsetAtStartMs !== null) {
|
|
1036
|
+
replayLinkage.replayOffsetMs = store.replayOffsetAtStartMs;
|
|
1037
|
+
}
|
|
983
1038
|
if (store.sessionId) {
|
|
1039
|
+
const unackedNotes = store.notes.filter((n) => !n.acked).map((n) => ({ atMs: n.atMs, text: n.text }));
|
|
984
1040
|
const ok = await finaliseSession(store.options.apiUrl, store.sessionId, durationSeconds, {
|
|
985
|
-
mutedSegments: store.mutedSegments
|
|
1041
|
+
mutedSegments: store.mutedSegments,
|
|
1042
|
+
notes: unackedNotes,
|
|
1043
|
+
...replayLinkage
|
|
986
1044
|
});
|
|
1045
|
+
if (ok) {
|
|
1046
|
+
for (const n of store.notes) {
|
|
1047
|
+
if (!n.acked) n.acked = true;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
987
1050
|
store.indicatorState = ok ? "done" : "error";
|
|
988
1051
|
} else {
|
|
989
1052
|
store.indicatorState = "error";
|
|
@@ -994,10 +1057,16 @@ async function finishFlow(store, ctx, opts) {
|
|
|
994
1057
|
onSubmitNote: async (text) => {
|
|
995
1058
|
if (!store.sessionId) return;
|
|
996
1059
|
store.endNote = text;
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
endNote: text
|
|
1060
|
+
const stillUnacked = store.notes.filter((n) => !n.acked).map((n) => ({ atMs: n.atMs, text: n.text }));
|
|
1061
|
+
const ok = await finaliseSession(store.options.apiUrl, store.sessionId, durationSeconds, {
|
|
1062
|
+
endNote: text,
|
|
1063
|
+
notes: stillUnacked,
|
|
1064
|
+
...replayLinkage
|
|
1000
1065
|
});
|
|
1066
|
+
if (!ok) throw new Error("finalise failed");
|
|
1067
|
+
for (const n of store.notes) {
|
|
1068
|
+
if (!n.acked) n.acked = true;
|
|
1069
|
+
}
|
|
1001
1070
|
},
|
|
1002
1071
|
onSkip: () => {
|
|
1003
1072
|
}
|
|
@@ -1044,10 +1113,13 @@ function userTest(options = {}) {
|
|
|
1044
1113
|
mutedSinceMs: null,
|
|
1045
1114
|
mutedSegments: [],
|
|
1046
1115
|
muteToastShown: false,
|
|
1116
|
+
muteToastTimers: [],
|
|
1047
1117
|
notes: [],
|
|
1048
1118
|
notesPopoverOpen: false,
|
|
1049
1119
|
notePopoverAtMs: null,
|
|
1050
|
-
endNote: ""
|
|
1120
|
+
endNote: "",
|
|
1121
|
+
finishFlowRan: false,
|
|
1122
|
+
replayOffsetAtStartMs: null
|
|
1051
1123
|
};
|
|
1052
1124
|
ctx.setStore(store);
|
|
1053
1125
|
const onFinish = () => {
|
|
@@ -1077,11 +1149,19 @@ function userTest(options = {}) {
|
|
|
1077
1149
|
store,
|
|
1078
1150
|
(text) => {
|
|
1079
1151
|
const atMs = store.notePopoverAtMs ?? Math.max(0, Date.now() - store.startedAt);
|
|
1080
|
-
|
|
1152
|
+
const note = { atMs, text, acked: false };
|
|
1153
|
+
store.notes.push(note);
|
|
1081
1154
|
closeNote();
|
|
1082
1155
|
renderNotesCount(store);
|
|
1083
1156
|
if (store.sessionId) {
|
|
1084
|
-
|
|
1157
|
+
const sessionId = store.sessionId;
|
|
1158
|
+
void (async () => {
|
|
1159
|
+
const result = await postNoteWithRetry(store.options.apiUrl, sessionId, atMs, text, ctx.logger);
|
|
1160
|
+
if (result.ok) {
|
|
1161
|
+
note.acked = true;
|
|
1162
|
+
if (result.id) note.serverId = result.id;
|
|
1163
|
+
}
|
|
1164
|
+
})();
|
|
1085
1165
|
}
|
|
1086
1166
|
},
|
|
1087
1167
|
() => closeNote()
|
|
@@ -1134,6 +1214,8 @@ function userTest(options = {}) {
|
|
|
1134
1214
|
}
|
|
1135
1215
|
store.sessionId = created.sessionId;
|
|
1136
1216
|
store.clientId = created.clientId;
|
|
1217
|
+
const replayStartMs = ctx.getReplayStartMs ? ctx.getReplayStartMs() : null;
|
|
1218
|
+
store.replayOffsetAtStartMs = replayStartMs === null ? null : Math.max(0, store.startedAt - replayStartMs);
|
|
1137
1219
|
store.tasks = created.tasks;
|
|
1138
1220
|
if (store.tasks.length > 0 && store.indicatorRoot && !merged.hideIndicator) {
|
|
1139
1221
|
const bar = store.indicatorRoot.querySelector(".bar");
|
|
@@ -1164,6 +1246,13 @@ function userTest(options = {}) {
|
|
|
1164
1246
|
document.removeEventListener("keydown", store.keydownHandler);
|
|
1165
1247
|
store.keydownHandler = null;
|
|
1166
1248
|
}
|
|
1249
|
+
for (const id of store.muteToastTimers) {
|
|
1250
|
+
try {
|
|
1251
|
+
window.clearTimeout(id);
|
|
1252
|
+
} catch {
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
store.muteToastTimers = [];
|
|
1167
1256
|
if (store.indicator && store.indicator.parentNode) {
|
|
1168
1257
|
store.indicator.parentNode.removeChild(store.indicator);
|
|
1169
1258
|
}
|