@usero/sdk 1.1.6 → 1.1.8
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 +15 -3
- package/dist/plugins/session-replay.cjs.map +1 -1
- package/dist/plugins/session-replay.d.cts +1 -0
- package/dist/plugins/session-replay.d.ts +1 -0
- package/dist/plugins/session-replay.js +15 -3
- package/dist/plugins/session-replay.js.map +1 -1
- package/dist/plugins/user-test.cjs +92 -12
- package/dist/plugins/user-test.cjs.map +1 -1
- package/dist/plugins/user-test.js +92 -12
- package/dist/plugins/user-test.js.map +1 -1
- package/package.json +1 -1
- package/dist/plugins/user-test.d.cts +0 -69
- package/dist/plugins/user-test.d.ts +0 -69
|
@@ -4,7 +4,12 @@ var DEFAULT_API_URL = "https://usero.io";
|
|
|
4
4
|
// src/plugins/user-test.ts
|
|
5
5
|
var DEFAULT_OPTIONS = {
|
|
6
6
|
queryParam: "usero_test",
|
|
7
|
-
|
|
7
|
+
// 10s (not 30) so at most ~10s of audio is at risk if the tab is torn
|
|
8
|
+
// down before a flush, and so a session shorter than the old 30s window
|
|
9
|
+
// still emits at least one chunk (previously its single buffered chunk was
|
|
10
|
+
// never flushed and its audio was lost). Tradeoff: ~3x the R2 writes /
|
|
11
|
+
// upload requests per session; 10s is an acceptable balance, don't go lower.
|
|
12
|
+
chunkSeconds: 10,
|
|
8
13
|
apiUrl: DEFAULT_API_URL,
|
|
9
14
|
testerName: "",
|
|
10
15
|
hideIndicator: false
|
|
@@ -217,12 +222,37 @@ function buildIndicator(host, store, callbacks) {
|
|
|
217
222
|
color: #fcd34d;
|
|
218
223
|
}
|
|
219
224
|
.mic[data-mic-state="muted"]:hover { background: rgba(251, 191, 36, 0.26); }
|
|
220
|
-
.
|
|
221
|
-
|
|
222
|
-
|
|
225
|
+
/* Connecting: getUserMedia pending. Steady amber tint reads as "working",
|
|
226
|
+
distinct from the live red pulse and the failed state below. The icon
|
|
227
|
+
gets a gentle non-pulsing breathe so it feels alive without alarming. */
|
|
228
|
+
.mic[data-mic-state="connecting"] {
|
|
229
|
+
background: rgba(251, 191, 36, 0.14);
|
|
230
|
+
border-color: rgba(251, 191, 36, 0.32);
|
|
231
|
+
color: #fcd34d;
|
|
223
232
|
cursor: default;
|
|
224
233
|
}
|
|
225
|
-
.mic[data-mic-state="
|
|
234
|
+
.mic[data-mic-state="connecting"]:hover { background: rgba(251, 191, 36, 0.14); }
|
|
235
|
+
.mic[data-mic-state="connecting"] .mic-icon {
|
|
236
|
+
color: #fbbf24;
|
|
237
|
+
animation: micBreathe 1.4s ease-in-out infinite;
|
|
238
|
+
}
|
|
239
|
+
/* Failed terminal state, actionable. Tappable affordance: clearer border,
|
|
240
|
+
pointer cursor, brightens on hover/focus to invite the retry tap. */
|
|
241
|
+
.mic[data-mic-state="none"] {
|
|
242
|
+
background: rgba(255,255,255,0.05);
|
|
243
|
+
border-color: rgba(255,255,255,0.14);
|
|
244
|
+
color: rgba(255,255,255,0.72);
|
|
245
|
+
cursor: pointer;
|
|
246
|
+
}
|
|
247
|
+
.mic[data-mic-state="none"]:hover {
|
|
248
|
+
background: rgba(255,255,255,0.12);
|
|
249
|
+
border-color: rgba(255,255,255,0.24);
|
|
250
|
+
color: #fff;
|
|
251
|
+
}
|
|
252
|
+
@keyframes micBreathe {
|
|
253
|
+
0%, 100% { opacity: 0.55; }
|
|
254
|
+
50% { opacity: 1; }
|
|
255
|
+
}
|
|
226
256
|
.mic-icon { width: 13px; height: 13px; display: inline-block; flex-shrink: 0; }
|
|
227
257
|
.mic-label { font-weight: 500; letter-spacing: 0.01em; white-space: nowrap; }
|
|
228
258
|
|
|
@@ -691,7 +721,10 @@ function micChipState(store) {
|
|
|
691
721
|
if (store.indicatorState === "finishing" || store.indicatorState === "done" || store.indicatorState === "error") {
|
|
692
722
|
return "inactive";
|
|
693
723
|
}
|
|
694
|
-
if (!store.hasMicPermission)
|
|
724
|
+
if (!store.hasMicPermission) {
|
|
725
|
+
if (store.micAcquiring) return "connecting";
|
|
726
|
+
return "none";
|
|
727
|
+
}
|
|
695
728
|
return store.muted ? "muted" : "recording";
|
|
696
729
|
}
|
|
697
730
|
function renderIndicatorState(store) {
|
|
@@ -706,6 +739,8 @@ function renderIndicatorState(store) {
|
|
|
706
739
|
dot.setAttribute("data-state", store.indicatorState);
|
|
707
740
|
const chipState = micChipState(store);
|
|
708
741
|
mic.setAttribute("data-mic-state", chipState === "inactive" ? "none" : chipState);
|
|
742
|
+
mic.removeAttribute("data-mic-fail");
|
|
743
|
+
if (chipState === "none") mic.setAttribute("data-mic-fail", store.micFailReason ?? "blocked");
|
|
709
744
|
switch (store.indicatorState) {
|
|
710
745
|
case "recording":
|
|
711
746
|
case "no-audio":
|
|
@@ -740,13 +775,23 @@ function renderIndicatorState(store) {
|
|
|
740
775
|
mic.setAttribute("aria-pressed", "true");
|
|
741
776
|
mic.removeAttribute("tabindex");
|
|
742
777
|
break;
|
|
743
|
-
case "
|
|
744
|
-
micIcon.innerHTML =
|
|
745
|
-
micLabel.textContent = "
|
|
746
|
-
mic.setAttribute("aria-label", "
|
|
778
|
+
case "connecting":
|
|
779
|
+
micIcon.innerHTML = MIC_ICON_SVG;
|
|
780
|
+
micLabel.textContent = "Connecting mic";
|
|
781
|
+
mic.setAttribute("aria-label", "Connecting microphone");
|
|
747
782
|
mic.setAttribute("aria-pressed", "false");
|
|
748
783
|
mic.setAttribute("tabindex", "-1");
|
|
749
784
|
break;
|
|
785
|
+
case "none": {
|
|
786
|
+
micIcon.innerHTML = MIC_MUTED_ICON_SVG;
|
|
787
|
+
const failLabel = store.micFailReason === "not-found" ? "No mic found, tap to retry" : "Mic blocked, tap to retry";
|
|
788
|
+
const failAria = store.micFailReason === "not-found" ? "No microphone found, tap to retry. Replay continues." : "Microphone blocked, tap to retry. Replay continues.";
|
|
789
|
+
micLabel.textContent = failLabel;
|
|
790
|
+
mic.setAttribute("aria-label", failAria);
|
|
791
|
+
mic.setAttribute("aria-pressed", "false");
|
|
792
|
+
mic.removeAttribute("tabindex");
|
|
793
|
+
break;
|
|
794
|
+
}
|
|
750
795
|
case "inactive":
|
|
751
796
|
micIcon.innerHTML = MIC_ICON_SVG;
|
|
752
797
|
micLabel.textContent = store.indicatorState === "finishing" ? "Saving" : store.indicatorState === "done" ? "Saved" : "Save failed";
|
|
@@ -1325,8 +1370,13 @@ function enqueueChunk(store, ctx, blob) {
|
|
|
1325
1370
|
});
|
|
1326
1371
|
}
|
|
1327
1372
|
async function startRecording(store, ctx) {
|
|
1373
|
+
store.micAcquiring = true;
|
|
1374
|
+
store.micFailReason = null;
|
|
1375
|
+
renderIndicatorState(store);
|
|
1328
1376
|
if (!isMediaRecorderSupported()) {
|
|
1329
1377
|
ctx.logger.warn("MediaRecorder not supported, continuing without audio");
|
|
1378
|
+
store.micAcquiring = false;
|
|
1379
|
+
store.micFailReason = "unsupported";
|
|
1330
1380
|
store.indicatorState = "no-audio";
|
|
1331
1381
|
renderIndicatorState(store);
|
|
1332
1382
|
return;
|
|
@@ -1336,12 +1386,17 @@ async function startRecording(store, ctx) {
|
|
|
1336
1386
|
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
1337
1387
|
} catch (err) {
|
|
1338
1388
|
ctx.logger.warn("mic permission denied or unavailable", err);
|
|
1389
|
+
store.micAcquiring = false;
|
|
1390
|
+
const name = err instanceof Error ? err.name : "";
|
|
1391
|
+
store.micFailReason = name === "NotFoundError" || name === "DevicesNotFoundError" ? "not-found" : "blocked";
|
|
1339
1392
|
store.indicatorState = "no-audio";
|
|
1340
1393
|
renderIndicatorState(store);
|
|
1341
1394
|
return;
|
|
1342
1395
|
}
|
|
1343
1396
|
store.stream = stream;
|
|
1344
1397
|
store.hasMicPermission = true;
|
|
1398
|
+
store.micAcquiring = false;
|
|
1399
|
+
store.micFailReason = null;
|
|
1345
1400
|
const mimeType = pickMimeType();
|
|
1346
1401
|
let recorder;
|
|
1347
1402
|
try {
|
|
@@ -1350,6 +1405,9 @@ async function startRecording(store, ctx) {
|
|
|
1350
1405
|
ctx.logger.error("MediaRecorder construction failed", err);
|
|
1351
1406
|
stream.getTracks().forEach((t) => t.stop());
|
|
1352
1407
|
store.stream = null;
|
|
1408
|
+
store.hasMicPermission = false;
|
|
1409
|
+
store.micAcquiring = false;
|
|
1410
|
+
store.micFailReason = "unsupported";
|
|
1353
1411
|
store.indicatorState = "no-audio";
|
|
1354
1412
|
renderIndicatorState(store);
|
|
1355
1413
|
return;
|
|
@@ -1364,6 +1422,10 @@ async function startRecording(store, ctx) {
|
|
|
1364
1422
|
ctx.logger.error("MediaRecorder error", event);
|
|
1365
1423
|
});
|
|
1366
1424
|
recorder.start(store.options.chunkSeconds * 1e3);
|
|
1425
|
+
if (store.indicatorState === "no-audio") {
|
|
1426
|
+
store.indicatorState = "recording";
|
|
1427
|
+
}
|
|
1428
|
+
renderIndicatorState(store);
|
|
1367
1429
|
}
|
|
1368
1430
|
function toggleMute(store) {
|
|
1369
1431
|
if (!store.stream || !store.hasMicPermission) return false;
|
|
@@ -1513,12 +1575,15 @@ function userTest(options = {}) {
|
|
|
1513
1575
|
indicatorRoot: null,
|
|
1514
1576
|
indicatorState: "recording",
|
|
1515
1577
|
pageHideHandler: null,
|
|
1578
|
+
visibilityHandler: null,
|
|
1516
1579
|
options: { ...merged, apiUrl },
|
|
1517
1580
|
tasks: [],
|
|
1518
1581
|
tasksPanelOpen: readTasksPanelOpen(),
|
|
1519
1582
|
outsidePointerHandler: null,
|
|
1520
1583
|
keydownHandler: null,
|
|
1521
1584
|
hasMicPermission: false,
|
|
1585
|
+
micAcquiring: true,
|
|
1586
|
+
micFailReason: null,
|
|
1522
1587
|
muted: false,
|
|
1523
1588
|
mutedSinceMs: null,
|
|
1524
1589
|
mutedSegments: [],
|
|
@@ -1543,7 +1608,12 @@ function userTest(options = {}) {
|
|
|
1543
1608
|
};
|
|
1544
1609
|
const onToggleTasks = () => setPanelOpen(!store.tasksPanelOpen);
|
|
1545
1610
|
const onToggleMute = () => {
|
|
1546
|
-
if (!store.hasMicPermission)
|
|
1611
|
+
if (!store.hasMicPermission) {
|
|
1612
|
+
if (!store.micAcquiring && store.indicatorState !== "finishing" && store.indicatorState !== "done" && store.indicatorState !== "error") {
|
|
1613
|
+
void startRecording(store, ctx);
|
|
1614
|
+
}
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1547
1617
|
const ok = toggleMute(store);
|
|
1548
1618
|
if (!ok) return;
|
|
1549
1619
|
if (store.muted) showMuteToast(store);
|
|
@@ -1613,6 +1683,12 @@ function userTest(options = {}) {
|
|
|
1613
1683
|
};
|
|
1614
1684
|
store.pageHideHandler = pageHide;
|
|
1615
1685
|
window.addEventListener("pagehide", pageHide);
|
|
1686
|
+
const onVisibilityChange = () => {
|
|
1687
|
+
if (document.visibilityState !== "hidden") return;
|
|
1688
|
+
void finishFlow(store, ctx, { showThanks: false });
|
|
1689
|
+
};
|
|
1690
|
+
store.visibilityHandler = onVisibilityChange;
|
|
1691
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
1616
1692
|
void (async () => {
|
|
1617
1693
|
const adoptId = getAdoptSessionId();
|
|
1618
1694
|
const created = adoptId ? await adoptSession(apiUrl, adoptId) : await createSession(apiUrl, slug, readTesterName(merged.testerName));
|
|
@@ -1648,6 +1724,10 @@ function userTest(options = {}) {
|
|
|
1648
1724
|
window.removeEventListener("pagehide", store.pageHideHandler);
|
|
1649
1725
|
store.pageHideHandler = null;
|
|
1650
1726
|
}
|
|
1727
|
+
if (store.visibilityHandler) {
|
|
1728
|
+
document.removeEventListener("visibilitychange", store.visibilityHandler);
|
|
1729
|
+
store.visibilityHandler = null;
|
|
1730
|
+
}
|
|
1651
1731
|
stopRecording(store);
|
|
1652
1732
|
if (store.outsidePointerHandler) {
|
|
1653
1733
|
document.removeEventListener("pointerdown", store.outsidePointerHandler, true);
|
|
@@ -1672,7 +1752,7 @@ function userTest(options = {}) {
|
|
|
1672
1752
|
}
|
|
1673
1753
|
};
|
|
1674
1754
|
}
|
|
1675
|
-
var __test__ = { getTestSlug, pickMimeType, isMediaRecorderSupported };
|
|
1755
|
+
var __test__ = { getTestSlug, pickMimeType, isMediaRecorderSupported, micChipState };
|
|
1676
1756
|
|
|
1677
1757
|
export { __test__, userTest };
|
|
1678
1758
|
//# sourceMappingURL=user-test.js.map
|