@usero/sdk 0.4.2 → 0.5.0
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.
|
@@ -12,6 +12,7 @@ var DEFAULT_OPTIONS = {
|
|
|
12
12
|
hideIndicator: false
|
|
13
13
|
};
|
|
14
14
|
var TESTER_NAME_STORAGE_KEY = "usero:user-test:tester-name";
|
|
15
|
+
var TASKS_PANEL_OPEN_STORAGE_KEY = "usero:user-test:tasks-panel-open";
|
|
15
16
|
var IDB_NAME = "usero-user-test";
|
|
16
17
|
var IDB_STORE = "pending-chunks";
|
|
17
18
|
function readTesterName(override) {
|
|
@@ -146,32 +147,42 @@ async function uploadChunkWithRetry(apiUrl, sessionId, index, blob, logger, maxA
|
|
|
146
147
|
}
|
|
147
148
|
return false;
|
|
148
149
|
}
|
|
149
|
-
function buildIndicator(host, store, onFinish) {
|
|
150
|
+
function buildIndicator(host, store, onFinish, onToggleTasks) {
|
|
150
151
|
const root = host.attachShadow({ mode: "closed" });
|
|
151
152
|
const style = document.createElement("style");
|
|
152
153
|
style.textContent = `
|
|
153
154
|
:host { all: initial; }
|
|
154
|
-
.
|
|
155
|
+
.anchor {
|
|
155
156
|
position: fixed;
|
|
156
157
|
bottom: calc(env(safe-area-inset-bottom, 0px) + 16px);
|
|
157
|
-
left: 50%;
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
gap: 10px;
|
|
162
|
-
padding: 8px 14px 8px 12px;
|
|
163
|
-
background: rgba(17, 17, 17, 0.78);
|
|
158
|
+
left: 50%; transform: translateX(-50%);
|
|
159
|
+
display: flex; flex-direction: column; align-items: center; gap: 8px;
|
|
160
|
+
z-index: 2147483646; max-width: calc(100vw - 32px);
|
|
161
|
+
font: 13px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
|
164
162
|
color: #fff;
|
|
163
|
+
}
|
|
164
|
+
.bar {
|
|
165
|
+
display: inline-flex; align-items: center; gap: 10px;
|
|
166
|
+
padding: 8px 14px 8px 12px;
|
|
167
|
+
background: rgba(17,17,17,0.78);
|
|
165
168
|
border-radius: 999px;
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
169
|
+
box-shadow: 0 8px 24px rgba(0,0,0,0.18);
|
|
170
|
+
backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
|
|
171
|
+
max-width: 100%;
|
|
172
|
+
}
|
|
173
|
+
.panel {
|
|
174
|
+
background: rgba(17,17,17,0.88);
|
|
175
|
+
border-radius: 14px; padding: 12px 14px 12px 8px;
|
|
176
|
+
line-height: 1.45;
|
|
177
|
+
box-shadow: 0 12px 32px rgba(0,0,0,0.28);
|
|
178
|
+
max-height: min(60vh, 480px);
|
|
179
|
+
max-width: min(420px, calc(100vw - 32px));
|
|
180
|
+
width: max-content; overflow-y: auto;
|
|
174
181
|
}
|
|
182
|
+
.panel[hidden] { display: none; }
|
|
183
|
+
.panel ol { margin: 0; padding-left: 26px; }
|
|
184
|
+
.panel li { margin: 0 0 8px; }
|
|
185
|
+
.panel li:last-child { margin: 0; }
|
|
175
186
|
.dot {
|
|
176
187
|
width: 8px; height: 8px; border-radius: 50%;
|
|
177
188
|
background: #ef4444;
|
|
@@ -193,6 +204,8 @@ function buildIndicator(host, store, onFinish) {
|
|
|
193
204
|
.btn:hover { background: rgba(255,255,255,0.22); }
|
|
194
205
|
.btn:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
|
|
195
206
|
.btn[disabled] { opacity: 0.5; cursor: progress; }
|
|
207
|
+
.tasks-btn[aria-expanded="true"] { background: rgba(255,255,255,0.24); }
|
|
208
|
+
@media (max-width: 420px) { .btn { padding: 9px 14px; min-height: 40px; } }
|
|
196
209
|
.thanks {
|
|
197
210
|
position: fixed; inset: 0;
|
|
198
211
|
display: grid; place-items: center;
|
|
@@ -229,6 +242,11 @@ function buildIndicator(host, store, onFinish) {
|
|
|
229
242
|
.dot { animation: none; }
|
|
230
243
|
}
|
|
231
244
|
`;
|
|
245
|
+
const anchor = document.createElement("div");
|
|
246
|
+
anchor.className = "anchor";
|
|
247
|
+
const panel = document.createElement("div");
|
|
248
|
+
panel.className = "panel";
|
|
249
|
+
panel.hidden = true;
|
|
232
250
|
const bar = document.createElement("div");
|
|
233
251
|
bar.className = "bar";
|
|
234
252
|
bar.setAttribute("role", "status");
|
|
@@ -241,25 +259,70 @@ function buildIndicator(host, store, onFinish) {
|
|
|
241
259
|
label.textContent = "Recording";
|
|
242
260
|
const spacer = document.createElement("span");
|
|
243
261
|
spacer.className = "spacer";
|
|
262
|
+
bar.appendChild(dot);
|
|
263
|
+
bar.appendChild(label);
|
|
264
|
+
bar.appendChild(spacer);
|
|
244
265
|
const btn = document.createElement("button");
|
|
245
266
|
btn.type = "button";
|
|
246
|
-
btn.className = "btn";
|
|
267
|
+
btn.className = "btn finish-btn";
|
|
247
268
|
btn.textContent = "Finish";
|
|
248
269
|
btn.addEventListener("click", onFinish);
|
|
249
|
-
bar.appendChild(dot);
|
|
250
|
-
bar.appendChild(label);
|
|
251
|
-
bar.appendChild(spacer);
|
|
252
270
|
bar.appendChild(btn);
|
|
271
|
+
if (store.tasks.length > 0) installTasksToggle(bar, btn, store, onToggleTasks);
|
|
272
|
+
anchor.appendChild(panel);
|
|
273
|
+
anchor.appendChild(bar);
|
|
253
274
|
root.appendChild(style);
|
|
254
|
-
root.appendChild(
|
|
275
|
+
root.appendChild(anchor);
|
|
255
276
|
return root;
|
|
256
277
|
}
|
|
278
|
+
function installTasksToggle(bar, finishBtn, store, onToggleTasks) {
|
|
279
|
+
const tasksBtn = document.createElement("button");
|
|
280
|
+
tasksBtn.type = "button";
|
|
281
|
+
tasksBtn.className = "btn tasks-btn";
|
|
282
|
+
tasksBtn.textContent = `Tasks (${store.tasks.length})`;
|
|
283
|
+
tasksBtn.setAttribute("aria-expanded", store.tasksPanelOpen ? "true" : "false");
|
|
284
|
+
tasksBtn.addEventListener("click", onToggleTasks);
|
|
285
|
+
bar.insertBefore(tasksBtn, finishBtn);
|
|
286
|
+
}
|
|
287
|
+
function renderTasksPanel(store) {
|
|
288
|
+
const root = store.indicatorRoot;
|
|
289
|
+
if (!root) return;
|
|
290
|
+
const panel = root.querySelector(".panel");
|
|
291
|
+
if (!(panel instanceof HTMLElement)) return;
|
|
292
|
+
if (!panel.firstChild && store.tasks.length > 0) {
|
|
293
|
+
const ol = document.createElement("ol");
|
|
294
|
+
for (const task of store.tasks) {
|
|
295
|
+
const li = document.createElement("li");
|
|
296
|
+
li.textContent = task.prompt;
|
|
297
|
+
ol.appendChild(li);
|
|
298
|
+
}
|
|
299
|
+
panel.appendChild(ol);
|
|
300
|
+
}
|
|
301
|
+
panel.hidden = !store.tasksPanelOpen;
|
|
302
|
+
const tasksBtn = root.querySelector(".tasks-btn");
|
|
303
|
+
if (tasksBtn instanceof HTMLElement) {
|
|
304
|
+
tasksBtn.setAttribute("aria-expanded", store.tasksPanelOpen ? "true" : "false");
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function readTasksPanelOpen() {
|
|
308
|
+
try {
|
|
309
|
+
return window.sessionStorage?.getItem(TASKS_PANEL_OPEN_STORAGE_KEY) === "1";
|
|
310
|
+
} catch {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function writeTasksPanelOpen(open) {
|
|
315
|
+
try {
|
|
316
|
+
window.sessionStorage?.setItem(TASKS_PANEL_OPEN_STORAGE_KEY, open ? "1" : "0");
|
|
317
|
+
} catch {
|
|
318
|
+
}
|
|
319
|
+
}
|
|
257
320
|
function renderIndicatorState(store) {
|
|
258
321
|
const root = store.indicatorRoot;
|
|
259
322
|
if (!root) return;
|
|
260
323
|
const dot = root.querySelector(".dot");
|
|
261
324
|
const label = root.querySelector(".label");
|
|
262
|
-
const btn = root.querySelector(".btn");
|
|
325
|
+
const btn = root.querySelector(".finish-btn");
|
|
263
326
|
if (!(dot instanceof HTMLElement) || !(label instanceof HTMLElement) || !btn) return;
|
|
264
327
|
dot.setAttribute("data-state", store.indicatorState);
|
|
265
328
|
switch (store.indicatorState) {
|
|
@@ -302,6 +365,16 @@ function showThanksScreen(root) {
|
|
|
302
365
|
`;
|
|
303
366
|
root.appendChild(overlay);
|
|
304
367
|
}
|
|
368
|
+
function parseTasks(raw) {
|
|
369
|
+
if (!Array.isArray(raw)) return [];
|
|
370
|
+
const out = raw.flatMap((item) => {
|
|
371
|
+
const t = item;
|
|
372
|
+
if (!t || typeof t.id !== "string" || typeof t.prompt !== "string" || typeof t.sortOrder !== "number") return [];
|
|
373
|
+
return [{ id: t.id, prompt: t.prompt, sortOrder: t.sortOrder }];
|
|
374
|
+
});
|
|
375
|
+
out.sort((a, b) => a.sortOrder - b.sortOrder);
|
|
376
|
+
return out;
|
|
377
|
+
}
|
|
305
378
|
async function createSession(apiUrl, slug, testerName) {
|
|
306
379
|
try {
|
|
307
380
|
const res = await fetch(`${apiUrl.replace(/\/$/, "")}/api/user-test-sessions`, {
|
|
@@ -312,7 +385,7 @@ async function createSession(apiUrl, slug, testerName) {
|
|
|
312
385
|
if (!res.ok) return null;
|
|
313
386
|
const json = await res.json();
|
|
314
387
|
if (typeof json.sessionId !== "string" || typeof json.clientId !== "string") return null;
|
|
315
|
-
return { sessionId: json.sessionId, clientId: json.clientId };
|
|
388
|
+
return { sessionId: json.sessionId, clientId: json.clientId, tasks: parseTasks(json.tasks) };
|
|
316
389
|
} catch {
|
|
317
390
|
return null;
|
|
318
391
|
}
|
|
@@ -469,20 +542,45 @@ function userTest(options = {}) {
|
|
|
469
542
|
indicatorRoot: null,
|
|
470
543
|
indicatorState: "recording",
|
|
471
544
|
pageHideHandler: null,
|
|
472
|
-
options: { ...merged, apiUrl }
|
|
545
|
+
options: { ...merged, apiUrl },
|
|
546
|
+
tasks: [],
|
|
547
|
+
tasksPanelOpen: readTasksPanelOpen(),
|
|
548
|
+
outsidePointerHandler: null,
|
|
549
|
+
keydownHandler: null
|
|
473
550
|
};
|
|
474
551
|
ctx.setStore(store);
|
|
475
552
|
const onFinish = () => {
|
|
476
553
|
void finishFlow(store, ctx, { showThanks: true });
|
|
477
554
|
};
|
|
555
|
+
const setPanelOpen = (open) => {
|
|
556
|
+
if (store.tasksPanelOpen === open) return;
|
|
557
|
+
store.tasksPanelOpen = open;
|
|
558
|
+
writeTasksPanelOpen(open);
|
|
559
|
+
renderTasksPanel(store);
|
|
560
|
+
};
|
|
561
|
+
const onToggleTasks = () => setPanelOpen(!store.tasksPanelOpen);
|
|
478
562
|
if (!merged.hideIndicator) {
|
|
479
563
|
const host = document.createElement("div");
|
|
480
564
|
host.setAttribute("data-usero-user-test", "true");
|
|
481
565
|
document.body.appendChild(host);
|
|
482
566
|
store.indicator = host;
|
|
483
|
-
store.indicatorRoot = buildIndicator(host, store, onFinish);
|
|
567
|
+
store.indicatorRoot = buildIndicator(host, store, onFinish, onToggleTasks);
|
|
484
568
|
renderIndicatorState(store);
|
|
485
569
|
}
|
|
570
|
+
const outsidePointer = (event) => {
|
|
571
|
+
if (!store.tasksPanelOpen) return;
|
|
572
|
+
const host = store.indicator;
|
|
573
|
+
if (!host) return;
|
|
574
|
+
const path = event.composedPath();
|
|
575
|
+
if (!path.includes(host)) setPanelOpen(false);
|
|
576
|
+
};
|
|
577
|
+
const onKeydown = (event) => {
|
|
578
|
+
if (event.key === "Escape" && store.tasksPanelOpen) setPanelOpen(false);
|
|
579
|
+
};
|
|
580
|
+
store.outsidePointerHandler = outsidePointer;
|
|
581
|
+
store.keydownHandler = onKeydown;
|
|
582
|
+
document.addEventListener("pointerdown", outsidePointer, true);
|
|
583
|
+
document.addEventListener("keydown", onKeydown);
|
|
486
584
|
const pageHide = () => {
|
|
487
585
|
void finishFlow(store, ctx, { showThanks: false });
|
|
488
586
|
};
|
|
@@ -499,6 +597,15 @@ function userTest(options = {}) {
|
|
|
499
597
|
}
|
|
500
598
|
store.sessionId = created.sessionId;
|
|
501
599
|
store.clientId = created.clientId;
|
|
600
|
+
store.tasks = created.tasks;
|
|
601
|
+
if (store.tasks.length > 0 && store.indicatorRoot && !merged.hideIndicator) {
|
|
602
|
+
const bar = store.indicatorRoot.querySelector(".bar");
|
|
603
|
+
const finishBtn = bar?.querySelector(".finish-btn");
|
|
604
|
+
if (bar instanceof HTMLElement && finishBtn instanceof HTMLElement && !bar.querySelector(".tasks-btn")) {
|
|
605
|
+
installTasksToggle(bar, finishBtn, store, onToggleTasks);
|
|
606
|
+
}
|
|
607
|
+
renderTasksPanel(store);
|
|
608
|
+
}
|
|
502
609
|
await startRecording(store, ctx);
|
|
503
610
|
renderIndicatorState(store);
|
|
504
611
|
})();
|
|
@@ -512,6 +619,14 @@ function userTest(options = {}) {
|
|
|
512
619
|
store.pageHideHandler = null;
|
|
513
620
|
}
|
|
514
621
|
stopRecording(store);
|
|
622
|
+
if (store.outsidePointerHandler) {
|
|
623
|
+
document.removeEventListener("pointerdown", store.outsidePointerHandler, true);
|
|
624
|
+
store.outsidePointerHandler = null;
|
|
625
|
+
}
|
|
626
|
+
if (store.keydownHandler) {
|
|
627
|
+
document.removeEventListener("keydown", store.keydownHandler);
|
|
628
|
+
store.keydownHandler = null;
|
|
629
|
+
}
|
|
515
630
|
if (store.indicator && store.indicator.parentNode) {
|
|
516
631
|
store.indicator.parentNode.removeChild(store.indicator);
|
|
517
632
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/types.ts","../../src/plugins/user-test.ts"],"names":[],"mappings":"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","file":"user-test.cjs","sourcesContent":["// Shared types used by both the vanilla and React entry points.\n// Keep this file framework-free so the vanilla bundle never pulls react.\n\nexport type FeedbackRating = 1 | 2 | 3 | 4\n\nexport interface FeedbackMetadata {\n\tpageUrl: string\n\tpageTitle: string\n\treferrer?: string\n\ttimestamp: number\n}\n\nexport interface ScreenshotData {\n\tfileName: string\n\turl: string\n\tfileSize: number\n\twidth?: number\n\theight?: number\n\tmimeType: string\n}\n\nexport interface FeedbackSubmission {\n\tclientId: string\n\trating?: FeedbackRating\n\tcomment?: string\n\tuserEmail?: string\n\tpageUrl: string\n\tpageTitle: string\n\treferrer?: string\n\tenvironment?: string\n\tscreenshots?: ScreenshotData[]\n\tmetadata?: Record<string, unknown>\n\t// Legacy gzipped + base64-encoded rrweb event stream. The pre-chunked\n\t// session-replay plugin attached this on submit. The chunked-upload\n\t// plugin (>= 0.4.0) does NOT set this — it ships events out-of-band\n\t// via the chunk endpoints and points at the resulting session replay\n\t// via `sessionReplayId` + `replayOffsetMs`. Kept on the wire one\n\t// release for backward compat with old SaaS deployments that only\n\t// know how to ingest the legacy field.\n\treplayEvents?: string\n\t// Pointer to a SessionReplay row created by the session-replay\n\t// plugin's chunked-upload pipeline, plus the offset within that\n\t// recording at submit time so the dashboard can deep-link.\n\tsessionReplayId?: string\n\treplayOffsetMs?: number\n}\n\nexport interface FeedbackData {\n\trating?: FeedbackRating\n\tcomment?: string\n\tuserEmail?: string\n\tscreenshots?: ScreenshotData[]\n\tmetadata: FeedbackMetadata\n}\n\n// Customer-supplied identity for the session-replay / identify pipeline.\n// Use the declarative form: pass `user` on the React widget (the SDK\n// diffs and auto-fires identify), or supply `getUser` on the vanilla\n// init (the SDK polls it at session start). An imperative `identify()`\n// call exists as an escape hatch only and is intentionally not\n// documented as the headline API.\nexport type UseroUserTraitValue = string | number | boolean | null\nexport type UseroUserTraits = Record<string, UseroUserTraitValue>\nexport interface UseroUser {\n\tid: string\n\temail?: string\n\tdisplayName?: string\n\ttraits?: UseroUserTraits\n}\n\nexport type WidgetPosition = 'right' | 'left'\n\nexport interface WidgetTheme {\n\tprimary: string\n\tbackground: string\n\ttext: string\n\tborder: string\n\tshadow: string\n}\n\n// Forward-declared to avoid a circular import. The plugin module imports\n// FeedbackSubmission from this file, so we can't import UseroPlugin back\n// up here without a cycle. Keeping the prop typed as an opaque object with\n// the minimal shape the widget actually inspects works fine for consumers.\nexport interface FeedbackWidgetProps {\n\tclientId: string\n\tposition?: WidgetPosition\n\ttheme?: Partial<WidgetTheme>\n\ttitle?: string\n\tplaceholder?: string\n\tshowEmailOption?: boolean\n\tshowScreenshotOption?: boolean\n\tenvironment?: string\n\tbaseUrl?: string\n\tmetadata?: Record<string, unknown>\n\tplugins?: ReadonlyArray<import('./plugin').UseroPlugin>\n\t// Declarative identity. React: pass the current user (or null on\n\t// logout) and the SDK auto-fires identify when the resolved id\n\t// transitions. Vanilla: pass a getter so the SDK can resolve user at\n\t// session start / chunk boundaries. Pass at most one. The SDK never\n\t// invokes both.\n\tuser?: UseroUser | null\n\tgetUser?: () => UseroUser | null | undefined\n\tonSubmit?: (feedback: FeedbackData) => void\n\tonError?: (error: Error) => void\n\tonOpen?: () => void\n\tonClose?: () => void\n}\n\nexport interface SubmissionResponse {\n\tsuccess: boolean\n\terror?: string\n\tid?: string\n\tmessage?: string\n\tdata?: unknown\n}\n\nexport const EMOJI_MAP: Record<FeedbackRating, string> = {\n\t1: '😞',\n\t2: '😐',\n\t3: '😊',\n\t4: '🤩',\n}\n\nexport const RATING_LABELS: Record<FeedbackRating, string> = {\n\t1: 'Needs work',\n\t2: \"It's okay\",\n\t3: 'Pretty good',\n\t4: 'Amazing!',\n}\n\nexport const EMOJI_BACKGROUNDS: Record<FeedbackRating, string> = {\n\t1: 'linear-gradient(135deg,#ff6b6b14,#ff6b6b1f)',\n\t2: 'linear-gradient(135deg,#9ca3af0f,#9ca3af1a)',\n\t3: 'linear-gradient(135deg,#3b82f614,#3b82f61f)',\n\t4: 'linear-gradient(135deg,#f59e0b14,#f59e0b1f)',\n}\n\nexport const DEFAULT_API_URL = 'https://usero.io'\n\nexport const DEFAULT_THEME: WidgetTheme = {\n\tprimary: '#2563eb',\n\tbackground: '#ffffff',\n\ttext: '#374151',\n\tborder: '#e5e7eb',\n\tshadow:\n\t\t'0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',\n}\n\nexport const DARK_THEME: WidgetTheme = {\n\tprimary: '#2563eb',\n\tbackground: '#1f2937',\n\ttext: '#f9fafb',\n\tborder: '#374151',\n\tshadow:\n\t\t'0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2)',\n}\n\nexport function mergeTheme(customTheme: Partial<WidgetTheme> = {}): WidgetTheme {\n\treturn { ...DEFAULT_THEME, ...customTheme }\n}\n","// User-testing-mode plugin for the Usero widget.\n//\n// Activates ONLY when the host page URL carries `?usero_test=<slug>`. When\n// active, it:\n// 1. Creates a UserTestSession on the SaaS side via\n// POST /api/user-test-sessions\n// 2. Starts a MediaRecorder on the user's microphone (Opus in WebM) and\n// ships chunks every `chunkSeconds` to\n// PUT /api/user-test-sessions/:id/chunk?index=N\n// 3. Renders a small floating \"recording\" indicator with a Finish button\n// bottom-center so the tester can stop on their own.\n// 4. On Finish (or `pagehide`), flushes any buffered chunks and calls\n// POST /api/user-test-sessions/:id/finalise.\n//\n// Mic-permission denied is non-fatal: the session is still created (the\n// session-replay plugin keeps recording in parallel) and the indicator\n// shows a \"no audio\" hint. The SaaS side detects audio presence from the\n// first chunk; if no chunks land, hasAudio stays false there too.\n//\n// Bundle hygiene: this module is a subpath export and does NOT depend on\n// rrweb or any other heavy dep — just the native MediaRecorder API. The\n// floating UI is shadow-DOM scoped so host page CSS can't leak in.\n//\n// Privacy: only the microphone is recorded. We never read DOM text, never\n// touch the camera, and never persist audio in the browser past the\n// IndexedDB fallback (cleared on successful upload).\n\nimport type { UseroPlugin, PluginContext } from '../plugin'\nimport { DEFAULT_API_URL } from '../types'\n\nexport interface UserTestOptions {\n\t// URL query param the welcome page appends to redirect testers back.\n\t// Default `usero_test`. Match SaaS side if you ever change it.\n\tqueryParam?: string\n\t// Audio chunk length in seconds. Smaller = more partial-data resilience\n\t// but more requests. Default 30.\n\tchunkSeconds?: number\n\t// API origin. Override for self-hosted or local dev. Defaults to the\n\t// shared SDK constant (https://usero.io).\n\tapiUrl?: string\n\t// Override the tester-name shown on the SaaS side. Normally the welcome\n\t// page collects this and stores it in localStorage; the plugin reads it\n\t// from there. This option lets a host site bypass that.\n\ttesterName?: string\n\t// Hide the floating indicator. The plugin still records and finalises\n\t// on `pagehide`, but the tester gets no on-page UI. Useful if the host\n\t// page wants to render its own.\n\thideIndicator?: boolean\n}\n\ninterface RecorderStore {\n\tcancelled: boolean\n\tslug: string\n\tsessionId: string | null\n\tclientId: string | null\n\trecorder: MediaRecorder | null\n\tstream: MediaStream | null\n\tchunkIndex: number\n\tuploadQueue: Promise<void>\n\tpendingUploads: number\n\tstartedAt: number\n\tindicator: HTMLElement | null\n\tindicatorRoot: ShadowRoot | null\n\tindicatorState: 'recording' | 'finishing' | 'done' | 'no-audio' | 'error'\n\tpageHideHandler: (() => void) | null\n\toptions: Required<UserTestOptions>\n}\n\nconst DEFAULT_OPTIONS: Required<Omit<UserTestOptions, 'testerName' | 'apiUrl'>> & {\n\ttesterName: string\n\tapiUrl: string\n} = {\n\tqueryParam: 'usero_test',\n\tchunkSeconds: 30,\n\tapiUrl: DEFAULT_API_URL,\n\ttesterName: '',\n\thideIndicator: false,\n}\n\nconst TESTER_NAME_STORAGE_KEY = 'usero:user-test:tester-name'\nconst IDB_NAME = 'usero-user-test'\nconst IDB_STORE = 'pending-chunks'\n\ninterface PendingChunk {\n\tid: string\n\tsessionId: string\n\tapiUrl: string\n\tchunkIndex: number\n\tblob: Blob\n\tcreatedAt: number\n}\n\nfunction readTesterName(override: string): string | undefined {\n\tif (override) return override\n\ttry {\n\t\tconst stored = window.localStorage?.getItem(TESTER_NAME_STORAGE_KEY)\n\t\tif (stored && stored.trim()) return stored.trim().slice(0, 120)\n\t} catch {\n\t\t// Storage access can throw in some sandboxed iframes — ignore.\n\t}\n\treturn undefined\n}\n\nfunction getTestSlug(queryParam: string): string | null {\n\tif (typeof window === 'undefined' || typeof window.location === 'undefined') return null\n\ttry {\n\t\tconst params = new URLSearchParams(window.location.search)\n\t\tconst slug = params.get(queryParam)\n\t\tif (!slug) return null\n\t\tconst cleaned = slug.trim().slice(0, 64)\n\t\tif (!/^[a-z0-9-]+$/i.test(cleaned)) return null\n\t\treturn cleaned\n\t} catch {\n\t\treturn null\n\t}\n}\n\nfunction isMediaRecorderSupported(): boolean {\n\treturn typeof window !== 'undefined' && typeof window.MediaRecorder !== 'undefined' && typeof navigator !== 'undefined' && !!navigator.mediaDevices?.getUserMedia\n}\n\nfunction pickMimeType(): string | undefined {\n\tconst candidates = ['audio/webm;codecs=opus', 'audio/webm', 'audio/ogg;codecs=opus', 'audio/mp4']\n\tfor (const candidate of candidates) {\n\t\tif (typeof MediaRecorder !== 'undefined' && MediaRecorder.isTypeSupported?.(candidate)) {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn undefined\n}\n\n// IndexedDB helpers. Best-effort, never throw upstream — if IDB is missing\n// or quota-blown we just lose offline resilience but the live upload path\n// still runs.\nfunction idbOpen(): Promise<IDBDatabase | null> {\n\treturn new Promise(resolve => {\n\t\tif (typeof indexedDB === 'undefined') {\n\t\t\tresolve(null)\n\t\t\treturn\n\t\t}\n\t\ttry {\n\t\t\tconst req = indexedDB.open(IDB_NAME, 1)\n\t\t\treq.onupgradeneeded = (): void => {\n\t\t\t\tconst db = req.result\n\t\t\t\tif (!db.objectStoreNames.contains(IDB_STORE)) {\n\t\t\t\t\tdb.createObjectStore(IDB_STORE, { keyPath: 'id' })\n\t\t\t\t}\n\t\t\t}\n\t\t\treq.onsuccess = (): void => resolve(req.result)\n\t\t\treq.onerror = (): void => resolve(null)\n\t\t} catch {\n\t\t\tresolve(null)\n\t\t}\n\t})\n}\n\nasync function idbStashChunk(chunk: PendingChunk): Promise<void> {\n\tconst db = await idbOpen()\n\tif (!db) return\n\tawait new Promise<void>(resolve => {\n\t\ttry {\n\t\t\tconst tx = db.transaction(IDB_STORE, 'readwrite')\n\t\t\ttx.objectStore(IDB_STORE).put(chunk)\n\t\t\ttx.oncomplete = (): void => resolve()\n\t\t\ttx.onerror = (): void => resolve()\n\t\t\ttx.onabort = (): void => resolve()\n\t\t} catch {\n\t\t\tresolve()\n\t\t}\n\t})\n\tdb.close()\n}\n\nasync function idbDeleteChunk(id: string): Promise<void> {\n\tconst db = await idbOpen()\n\tif (!db) return\n\tawait new Promise<void>(resolve => {\n\t\ttry {\n\t\t\tconst tx = db.transaction(IDB_STORE, 'readwrite')\n\t\t\ttx.objectStore(IDB_STORE).delete(id)\n\t\t\ttx.oncomplete = (): void => resolve()\n\t\t\ttx.onerror = (): void => resolve()\n\t\t\ttx.onabort = (): void => resolve()\n\t\t} catch {\n\t\t\tresolve()\n\t\t}\n\t})\n\tdb.close()\n}\n\nasync function idbListChunks(sessionId: string): Promise<PendingChunk[]> {\n\tconst db = await idbOpen()\n\tif (!db) return []\n\tconst items = await new Promise<PendingChunk[]>(resolve => {\n\t\ttry {\n\t\t\tconst tx = db.transaction(IDB_STORE, 'readonly')\n\t\t\tconst req = tx.objectStore(IDB_STORE).getAll()\n\t\t\treq.onsuccess = (): void => {\n\t\t\t\tconst all = (req.result as PendingChunk[]) ?? []\n\t\t\t\tresolve(all.filter(c => c.sessionId === sessionId))\n\t\t\t}\n\t\t\treq.onerror = (): void => resolve([])\n\t\t} catch {\n\t\t\tresolve([])\n\t\t}\n\t})\n\tdb.close()\n\treturn items\n}\n\nasync function uploadChunkWithRetry(\n\tapiUrl: string,\n\tsessionId: string,\n\tindex: number,\n\tblob: Blob,\n\tlogger: PluginContext['logger'],\n\tmaxAttempts = 5,\n): Promise<boolean> {\n\tconst url = `${apiUrl.replace(/\\/$/, '')}/api/user-test-sessions/${encodeURIComponent(sessionId)}/chunk?index=${index}`\n\tlet attempt = 0\n\twhile (attempt < maxAttempts) {\n\t\ttry {\n\t\t\tconst res = await fetch(url, {\n\t\t\t\tmethod: 'PUT',\n\t\t\t\tbody: blob,\n\t\t\t\theaders: { 'Content-Type': blob.type || 'audio/webm' },\n\t\t\t\tkeepalive: blob.size <= 60 * 1024, // browsers cap keepalive bodies\n\t\t\t})\n\t\t\tif (res.ok) return true\n\t\t\t// 4xx (other than 413) won't get better with retries; bail early.\n\t\t\tif (res.status >= 400 && res.status < 500 && res.status !== 408 && res.status !== 429) {\n\t\t\t\tlogger.error(`chunk ${index} rejected with ${res.status}`)\n\t\t\t\treturn false\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tlogger.warn(`chunk ${index} upload attempt ${attempt + 1} failed`, err)\n\t\t}\n\t\tattempt += 1\n\t\tconst backoff = Math.min(15000, 500 * 2 ** attempt) + Math.floor(Math.random() * 250)\n\t\tawait new Promise(resolve => setTimeout(resolve, backoff))\n\t}\n\treturn false\n}\n\nfunction buildIndicator(host: HTMLElement, store: RecorderStore, onFinish: () => void): ShadowRoot {\n\tconst root = host.attachShadow({ mode: 'closed' })\n\tconst style = document.createElement('style')\n\t// Keep this CSS small. Pulse is the only animation. Bottom-center,\n\t// safe-area aware, semi-transparent so it doesn't block the page.\n\tstyle.textContent = `\n\t\t:host { all: initial; }\n\t\t.bar {\n\t\t\tposition: fixed;\n\t\t\tbottom: calc(env(safe-area-inset-bottom, 0px) + 16px);\n\t\t\tleft: 50%;\n\t\t\ttransform: translateX(-50%);\n\t\t\tdisplay: inline-flex;\n\t\t\talign-items: center;\n\t\t\tgap: 10px;\n\t\t\tpadding: 8px 14px 8px 12px;\n\t\t\tbackground: rgba(17, 17, 17, 0.78);\n\t\t\tcolor: #fff;\n\t\t\tborder-radius: 999px;\n\t\t\tfont-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", system-ui, sans-serif;\n\t\t\tfont-size: 13px;\n\t\t\tline-height: 1;\n\t\t\tbox-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);\n\t\t\tbackdrop-filter: blur(8px);\n\t\t\t-webkit-backdrop-filter: blur(8px);\n\t\t\tz-index: 2147483646;\n\t\t\tmax-width: calc(100vw - 32px);\n\t\t}\n\t\t.dot {\n\t\t\twidth: 8px; height: 8px; border-radius: 50%;\n\t\t\tbackground: #ef4444;\n\t\t\tbox-shadow: 0 0 0 0 rgba(239, 68, 68, 0.6);\n\t\t\tanimation: pulse 1.6s ease-out infinite;\n\t\t}\n\t\t.dot[data-state=\"no-audio\"] { background: #fbbf24; animation: none; }\n\t\t.dot[data-state=\"finishing\"] { background: #fbbf24; animation: none; }\n\t\t.dot[data-state=\"done\"] { background: #10b981; animation: none; }\n\t\t.dot[data-state=\"error\"] { background: #ef4444; animation: none; }\n\t\t.label { font-weight: 500; letter-spacing: 0.01em; }\n\t\t.spacer { width: 1px; height: 16px; background: rgba(255,255,255,0.18); margin: 0 2px; }\n\t\t.btn {\n\t\t\tappearance: none; border: 0; background: rgba(255,255,255,0.12);\n\t\t\tcolor: #fff; font: inherit; font-weight: 600;\n\t\t\tpadding: 6px 12px; border-radius: 999px; cursor: pointer;\n\t\t\ttransition: background 0.15s ease;\n\t\t}\n\t\t.btn:hover { background: rgba(255,255,255,0.22); }\n\t\t.btn:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }\n\t\t.btn[disabled] { opacity: 0.5; cursor: progress; }\n\t\t.thanks {\n\t\t\tposition: fixed; inset: 0;\n\t\t\tdisplay: grid; place-items: center;\n\t\t\tbackground: rgba(15, 15, 17, 0.78);\n\t\t\tbackdrop-filter: blur(6px);\n\t\t\t-webkit-backdrop-filter: blur(6px);\n\t\t\tcolor: #fff;\n\t\t\tfont-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", system-ui, sans-serif;\n\t\t\tz-index: 2147483647;\n\t\t\tpadding: 24px;\n\t\t\ttext-align: center;\n\t\t}\n\t\t.thanks-card {\n\t\t\tbackground: #fff; color: #111;\n\t\t\tborder-radius: 16px; padding: 28px 24px;\n\t\t\tmax-width: 360px; width: 100%;\n\t\t\tbox-shadow: 0 20px 50px rgba(0,0,0,0.25);\n\t\t}\n\t\t.thanks h2 { margin: 0 0 8px; font-size: 20px; }\n\t\t.thanks p { margin: 0; font-size: 14px; line-height: 1.45; color: #4b5563; }\n\t\t.thanks .check {\n\t\t\twidth: 44px; height: 44px; border-radius: 50%;\n\t\t\tbackground: #10b981; color: #fff;\n\t\t\tdisplay: grid; place-items: center;\n\t\t\tmargin: 0 auto 12px;\n\t\t\tfont-size: 22px;\n\t\t}\n\t\t@keyframes pulse {\n\t\t\t0% { box-shadow: 0 0 0 0 rgba(239,68,68,0.55); }\n\t\t\t70% { box-shadow: 0 0 0 10px rgba(239,68,68,0); }\n\t\t\t100% { box-shadow: 0 0 0 0 rgba(239,68,68,0); }\n\t\t}\n\t\t@media (prefers-reduced-motion: reduce) {\n\t\t\t.dot { animation: none; }\n\t\t}\n\t`\n\tconst bar = document.createElement('div')\n\tbar.className = 'bar'\n\tbar.setAttribute('role', 'status')\n\tbar.setAttribute('aria-live', 'polite')\n\n\tconst dot = document.createElement('span')\n\tdot.className = 'dot'\n\tdot.setAttribute('data-state', store.indicatorState)\n\n\tconst label = document.createElement('span')\n\tlabel.className = 'label'\n\tlabel.textContent = 'Recording'\n\n\tconst spacer = document.createElement('span')\n\tspacer.className = 'spacer'\n\n\tconst btn = document.createElement('button')\n\tbtn.type = 'button'\n\tbtn.className = 'btn'\n\tbtn.textContent = 'Finish'\n\tbtn.addEventListener('click', onFinish)\n\n\tbar.appendChild(dot)\n\tbar.appendChild(label)\n\tbar.appendChild(spacer)\n\tbar.appendChild(btn)\n\n\troot.appendChild(style)\n\troot.appendChild(bar)\n\treturn root\n}\n\nfunction renderIndicatorState(store: RecorderStore): void {\n\tconst root = store.indicatorRoot\n\tif (!root) return\n\tconst dot = root.querySelector('.dot')\n\tconst label = root.querySelector('.label')\n\tconst btn = root.querySelector<HTMLButtonElement>('.btn')\n\tif (!(dot instanceof HTMLElement) || !(label instanceof HTMLElement) || !btn) return\n\tdot.setAttribute('data-state', store.indicatorState)\n\tswitch (store.indicatorState) {\n\t\tcase 'recording':\n\t\t\tlabel.textContent = 'Recording'\n\t\t\tbtn.textContent = 'Finish'\n\t\t\tbtn.disabled = false\n\t\t\tbreak\n\t\tcase 'no-audio':\n\t\t\tlabel.textContent = 'No mic, replay only'\n\t\t\tbtn.textContent = 'Finish'\n\t\t\tbtn.disabled = false\n\t\t\tbreak\n\t\tcase 'finishing':\n\t\t\tlabel.textContent = 'Saving'\n\t\t\tbtn.textContent = 'Saving'\n\t\t\tbtn.disabled = true\n\t\t\tbreak\n\t\tcase 'done':\n\t\t\tlabel.textContent = 'Saved'\n\t\t\tbtn.textContent = 'Done'\n\t\t\tbtn.disabled = true\n\t\t\tbreak\n\t\tcase 'error':\n\t\t\tlabel.textContent = 'Save failed'\n\t\t\tbtn.textContent = 'Retry'\n\t\t\tbtn.disabled = false\n\t\t\tbreak\n\t}\n}\n\nfunction showThanksScreen(root: ShadowRoot): void {\n\tconst overlay = document.createElement('div')\n\toverlay.className = 'thanks'\n\toverlay.innerHTML = `\n\t\t<div class=\"thanks-card\">\n\t\t\t<div class=\"check\" aria-hidden=\"true\">✓</div>\n\t\t\t<h2>Thanks for testing</h2>\n\t\t\t<p>Your session was saved. You can close this tab.</p>\n\t\t</div>\n\t`\n\troot.appendChild(overlay)\n}\n\nasync function createSession(\n\tapiUrl: string,\n\tslug: string,\n\ttesterName: string | undefined,\n): Promise<{ sessionId: string; clientId: string } | null> {\n\ttry {\n\t\tconst res = await fetch(`${apiUrl.replace(/\\/$/, '')}/api/user-test-sessions`, {\n\t\t\tmethod: 'POST',\n\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\tbody: JSON.stringify({ slug, ...(testerName ? { testerName } : {}) }),\n\t\t})\n\t\tif (!res.ok) return null\n\t\tconst json = (await res.json()) as { sessionId?: unknown; clientId?: unknown }\n\t\tif (typeof json.sessionId !== 'string' || typeof json.clientId !== 'string') return null\n\t\treturn { sessionId: json.sessionId, clientId: json.clientId }\n\t} catch {\n\t\treturn null\n\t}\n}\n\nasync function finaliseSession(\n\tapiUrl: string,\n\tsessionId: string,\n\tdurationSeconds: number,\n): Promise<boolean> {\n\ttry {\n\t\tconst res = await fetch(`${apiUrl.replace(/\\/$/, '')}/api/user-test-sessions/${encodeURIComponent(sessionId)}/finalise`, {\n\t\t\tmethod: 'POST',\n\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\tbody: JSON.stringify({ durationSeconds: Math.max(0, Math.round(durationSeconds)) }),\n\t\t\tkeepalive: true,\n\t\t})\n\t\treturn res.ok\n\t} catch {\n\t\treturn false\n\t}\n}\n\nasync function flushPendingFromIdb(store: RecorderStore, ctx: PluginContext): Promise<void> {\n\tif (!store.sessionId) return\n\tconst pending = await idbListChunks(store.sessionId)\n\tfor (const chunk of pending) {\n\t\tconst ok = await uploadChunkWithRetry(chunk.apiUrl, chunk.sessionId, chunk.chunkIndex, chunk.blob, ctx.logger, 3)\n\t\tif (ok) await idbDeleteChunk(chunk.id)\n\t}\n}\n\nfunction enqueueChunk(store: RecorderStore, ctx: PluginContext, blob: Blob): void {\n\tif (store.cancelled || !store.sessionId || blob.size === 0) return\n\tconst index = store.chunkIndex\n\tstore.chunkIndex += 1\n\tstore.pendingUploads += 1\n\tconst sessionId = store.sessionId\n\tconst apiUrl = store.options.apiUrl\n\n\tstore.uploadQueue = store.uploadQueue.then(async () => {\n\t\tconst ok = await uploadChunkWithRetry(apiUrl, sessionId, index, blob, ctx.logger)\n\t\tif (!ok) {\n\t\t\tctx.logger.warn(`chunk ${index} stashed for offline retry`)\n\t\t\tawait idbStashChunk({\n\t\t\t\tid: `${sessionId}:${index}:${Date.now()}`,\n\t\t\t\tsessionId,\n\t\t\t\tapiUrl,\n\t\t\t\tchunkIndex: index,\n\t\t\t\tblob,\n\t\t\t\tcreatedAt: Date.now(),\n\t\t\t})\n\t\t}\n\t\tstore.pendingUploads -= 1\n\t})\n}\n\nasync function startRecording(store: RecorderStore, ctx: PluginContext): Promise<void> {\n\tif (!isMediaRecorderSupported()) {\n\t\tctx.logger.warn('MediaRecorder not supported, continuing without audio')\n\t\tstore.indicatorState = 'no-audio'\n\t\trenderIndicatorState(store)\n\t\treturn\n\t}\n\tlet stream: MediaStream\n\ttry {\n\t\tstream = await navigator.mediaDevices.getUserMedia({ audio: true })\n\t} catch (err) {\n\t\tctx.logger.warn('mic permission denied or unavailable', err)\n\t\tstore.indicatorState = 'no-audio'\n\t\trenderIndicatorState(store)\n\t\treturn\n\t}\n\tstore.stream = stream\n\tconst mimeType = pickMimeType()\n\tlet recorder: MediaRecorder\n\ttry {\n\t\trecorder = mimeType ? new MediaRecorder(stream, { mimeType }) : new MediaRecorder(stream)\n\t} catch (err) {\n\t\tctx.logger.error('MediaRecorder construction failed', err)\n\t\tstream.getTracks().forEach(t => t.stop())\n\t\tstore.stream = null\n\t\tstore.indicatorState = 'no-audio'\n\t\trenderIndicatorState(store)\n\t\treturn\n\t}\n\tstore.recorder = recorder\n\trecorder.addEventListener('dataavailable', event => {\n\t\tif (event.data && event.data.size > 0) {\n\t\t\tenqueueChunk(store, ctx, event.data)\n\t\t}\n\t})\n\trecorder.addEventListener('error', event => {\n\t\tctx.logger.error('MediaRecorder error', event)\n\t})\n\t// `timeslice` makes the recorder emit a self-contained chunk every N ms.\n\trecorder.start(store.options.chunkSeconds * 1000)\n}\n\nfunction stopRecording(store: RecorderStore): void {\n\tconst recorder = store.recorder\n\tif (recorder && recorder.state !== 'inactive') {\n\t\ttry {\n\t\t\trecorder.requestData()\n\t\t} catch {\n\t\t\t// requestData throws if state is invalid; ignore.\n\t\t}\n\t\ttry {\n\t\t\trecorder.stop()\n\t\t} catch {\n\t\t\t// already stopped\n\t\t}\n\t}\n\tstore.recorder = null\n\tif (store.stream) {\n\t\tstore.stream.getTracks().forEach(t => t.stop())\n\t\tstore.stream = null\n\t}\n}\n\nasync function finishFlow(store: RecorderStore, ctx: PluginContext, opts: { showThanks: boolean }): Promise<void> {\n\tif (store.cancelled) return\n\tif (store.indicatorState === 'finishing' || store.indicatorState === 'done') return\n\tstore.indicatorState = 'finishing'\n\trenderIndicatorState(store)\n\n\tstopRecording(store)\n\t// Wait for the queued uploads to drain. Each upload already has its own\n\t// retry/backoff; this just lets them finish before finalise fires.\n\tawait store.uploadQueue\n\tawait flushPendingFromIdb(store, ctx)\n\n\tconst durationSeconds = (Date.now() - store.startedAt) / 1000\n\tif (store.sessionId) {\n\t\tconst ok = await finaliseSession(store.options.apiUrl, store.sessionId, durationSeconds)\n\t\tstore.indicatorState = ok ? 'done' : 'error'\n\t} else {\n\t\tstore.indicatorState = 'error'\n\t}\n\trenderIndicatorState(store)\n\n\tif (opts.showThanks && store.indicatorRoot && store.indicatorState === 'done') {\n\t\tshowThanksScreen(store.indicatorRoot)\n\t}\n}\n\nexport function userTest(options: UserTestOptions = {}): UseroPlugin {\n\tconst merged: Required<UserTestOptions> = {\n\t\tqueryParam: options.queryParam ?? DEFAULT_OPTIONS.queryParam,\n\t\tchunkSeconds: options.chunkSeconds ?? DEFAULT_OPTIONS.chunkSeconds,\n\t\tapiUrl: options.apiUrl ?? DEFAULT_OPTIONS.apiUrl,\n\t\ttesterName: options.testerName ?? DEFAULT_OPTIONS.testerName,\n\t\thideIndicator: options.hideIndicator ?? DEFAULT_OPTIONS.hideIndicator,\n\t}\n\n\treturn {\n\t\tname: 'user-test',\n\t\tonInit(ctx) {\n\t\t\tif (typeof window === 'undefined' || typeof document === 'undefined') return\n\t\t\tconst slug = getTestSlug(merged.queryParam)\n\t\t\tif (!slug) return\n\n\t\t\tconst apiUrl = merged.apiUrl || ctx.baseUrl || DEFAULT_API_URL\n\t\t\tconst store: RecorderStore = {\n\t\t\t\tcancelled: false,\n\t\t\t\tslug,\n\t\t\t\tsessionId: null,\n\t\t\t\tclientId: null,\n\t\t\t\trecorder: null,\n\t\t\t\tstream: null,\n\t\t\t\tchunkIndex: 0,\n\t\t\t\tuploadQueue: Promise.resolve(),\n\t\t\t\tpendingUploads: 0,\n\t\t\t\tstartedAt: Date.now(),\n\t\t\t\tindicator: null,\n\t\t\t\tindicatorRoot: null,\n\t\t\t\tindicatorState: 'recording',\n\t\t\t\tpageHideHandler: null,\n\t\t\t\toptions: { ...merged, apiUrl },\n\t\t\t}\n\t\t\tctx.setStore(store)\n\n\t\t\tconst onFinish = (): void => {\n\t\t\t\tvoid finishFlow(store, ctx, { showThanks: true })\n\t\t\t}\n\n\t\t\tif (!merged.hideIndicator) {\n\t\t\t\tconst host = document.createElement('div')\n\t\t\t\thost.setAttribute('data-usero-user-test', 'true')\n\t\t\t\tdocument.body.appendChild(host)\n\t\t\t\tstore.indicator = host\n\t\t\t\tstore.indicatorRoot = buildIndicator(host, store, onFinish)\n\t\t\t\trenderIndicatorState(store)\n\t\t\t}\n\n\t\t\tconst pageHide = (): void => {\n\t\t\t\t// Best-effort flush + finalise. We don't await here; the browser\n\t\t\t\t// is shutting the page down. `keepalive: true` on finalise lets\n\t\t\t\t// the request race the unload.\n\t\t\t\tvoid finishFlow(store, ctx, { showThanks: false })\n\t\t\t}\n\t\t\tstore.pageHideHandler = pageHide\n\t\t\twindow.addEventListener('pagehide', pageHide)\n\n\t\t\tvoid (async (): Promise<void> => {\n\t\t\t\tconst created = await createSession(apiUrl, slug, readTesterName(merged.testerName))\n\t\t\t\tif (store.cancelled) return\n\t\t\t\tif (!created) {\n\t\t\t\t\tctx.logger.error('failed to create user-test session')\n\t\t\t\t\tstore.indicatorState = 'error'\n\t\t\t\t\trenderIndicatorState(store)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tstore.sessionId = created.sessionId\n\t\t\t\tstore.clientId = created.clientId\n\t\t\t\tawait startRecording(store, ctx)\n\t\t\t\trenderIndicatorState(store)\n\t\t\t})()\n\t\t},\n\t\tonDestroy(ctx) {\n\t\t\tconst store = ctx.getStore<RecorderStore>()\n\t\t\tif (!store) return\n\t\t\tstore.cancelled = true\n\t\t\tif (store.pageHideHandler) {\n\t\t\t\twindow.removeEventListener('pagehide', store.pageHideHandler)\n\t\t\t\tstore.pageHideHandler = null\n\t\t\t}\n\t\t\tstopRecording(store)\n\t\t\tif (store.indicator && store.indicator.parentNode) {\n\t\t\t\tstore.indicator.parentNode.removeChild(store.indicator)\n\t\t\t}\n\t\t\tstore.indicator = null\n\t\t\tstore.indicatorRoot = null\n\t\t},\n\t}\n}\n\n// Internal helpers exposed for tests only. Not part of the public API.\nexport const __test__ = { getTestSlug, pickMimeType, isMediaRecorderSupported }\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/types.ts","../../src/plugins/user-test.ts"],"names":[],"mappings":"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","file":"user-test.cjs","sourcesContent":["// Shared types used by both the vanilla and React entry points.\n// Keep this file framework-free so the vanilla bundle never pulls react.\n\nexport type FeedbackRating = 1 | 2 | 3 | 4\n\nexport interface FeedbackMetadata {\n\tpageUrl: string\n\tpageTitle: string\n\treferrer?: string\n\ttimestamp: number\n}\n\nexport interface ScreenshotData {\n\tfileName: string\n\turl: string\n\tfileSize: number\n\twidth?: number\n\theight?: number\n\tmimeType: string\n}\n\nexport interface FeedbackSubmission {\n\tclientId: string\n\trating?: FeedbackRating\n\tcomment?: string\n\tuserEmail?: string\n\tpageUrl: string\n\tpageTitle: string\n\treferrer?: string\n\tenvironment?: string\n\tscreenshots?: ScreenshotData[]\n\tmetadata?: Record<string, unknown>\n\t// Legacy gzipped + base64-encoded rrweb event stream. The pre-chunked\n\t// session-replay plugin attached this on submit. The chunked-upload\n\t// plugin (>= 0.4.0) does NOT set this — it ships events out-of-band\n\t// via the chunk endpoints and points at the resulting session replay\n\t// via `sessionReplayId` + `replayOffsetMs`. Kept on the wire one\n\t// release for backward compat with old SaaS deployments that only\n\t// know how to ingest the legacy field.\n\treplayEvents?: string\n\t// Pointer to a SessionReplay row created by the session-replay\n\t// plugin's chunked-upload pipeline, plus the offset within that\n\t// recording at submit time so the dashboard can deep-link.\n\tsessionReplayId?: string\n\treplayOffsetMs?: number\n}\n\nexport interface FeedbackData {\n\trating?: FeedbackRating\n\tcomment?: string\n\tuserEmail?: string\n\tscreenshots?: ScreenshotData[]\n\tmetadata: FeedbackMetadata\n}\n\n// Customer-supplied identity for the session-replay / identify pipeline.\n// Use the declarative form: pass `user` on the React widget (the SDK\n// diffs and auto-fires identify), or supply `getUser` on the vanilla\n// init (the SDK polls it at session start). An imperative `identify()`\n// call exists as an escape hatch only and is intentionally not\n// documented as the headline API.\nexport type UseroUserTraitValue = string | number | boolean | null\nexport type UseroUserTraits = Record<string, UseroUserTraitValue>\nexport interface UseroUser {\n\tid: string\n\temail?: string\n\tdisplayName?: string\n\ttraits?: UseroUserTraits\n}\n\nexport type WidgetPosition = 'right' | 'left'\n\nexport interface WidgetTheme {\n\tprimary: string\n\tbackground: string\n\ttext: string\n\tborder: string\n\tshadow: string\n}\n\n// Forward-declared to avoid a circular import. The plugin module imports\n// FeedbackSubmission from this file, so we can't import UseroPlugin back\n// up here without a cycle. Keeping the prop typed as an opaque object with\n// the minimal shape the widget actually inspects works fine for consumers.\nexport interface FeedbackWidgetProps {\n\tclientId: string\n\tposition?: WidgetPosition\n\ttheme?: Partial<WidgetTheme>\n\ttitle?: string\n\tplaceholder?: string\n\tshowEmailOption?: boolean\n\tshowScreenshotOption?: boolean\n\tenvironment?: string\n\tbaseUrl?: string\n\tmetadata?: Record<string, unknown>\n\tplugins?: ReadonlyArray<import('./plugin').UseroPlugin>\n\t// Declarative identity. React: pass the current user (or null on\n\t// logout) and the SDK auto-fires identify when the resolved id\n\t// transitions. Vanilla: pass a getter so the SDK can resolve user at\n\t// session start / chunk boundaries. Pass at most one. The SDK never\n\t// invokes both.\n\tuser?: UseroUser | null\n\tgetUser?: () => UseroUser | null | undefined\n\tonSubmit?: (feedback: FeedbackData) => void\n\tonError?: (error: Error) => void\n\tonOpen?: () => void\n\tonClose?: () => void\n}\n\nexport interface SubmissionResponse {\n\tsuccess: boolean\n\terror?: string\n\tid?: string\n\tmessage?: string\n\tdata?: unknown\n}\n\nexport const EMOJI_MAP: Record<FeedbackRating, string> = {\n\t1: '😞',\n\t2: '😐',\n\t3: '😊',\n\t4: '🤩',\n}\n\nexport const RATING_LABELS: Record<FeedbackRating, string> = {\n\t1: 'Needs work',\n\t2: \"It's okay\",\n\t3: 'Pretty good',\n\t4: 'Amazing!',\n}\n\nexport const EMOJI_BACKGROUNDS: Record<FeedbackRating, string> = {\n\t1: 'linear-gradient(135deg,#ff6b6b14,#ff6b6b1f)',\n\t2: 'linear-gradient(135deg,#9ca3af0f,#9ca3af1a)',\n\t3: 'linear-gradient(135deg,#3b82f614,#3b82f61f)',\n\t4: 'linear-gradient(135deg,#f59e0b14,#f59e0b1f)',\n}\n\nexport const DEFAULT_API_URL = 'https://usero.io'\n\nexport const DEFAULT_THEME: WidgetTheme = {\n\tprimary: '#2563eb',\n\tbackground: '#ffffff',\n\ttext: '#374151',\n\tborder: '#e5e7eb',\n\tshadow:\n\t\t'0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',\n}\n\nexport const DARK_THEME: WidgetTheme = {\n\tprimary: '#2563eb',\n\tbackground: '#1f2937',\n\ttext: '#f9fafb',\n\tborder: '#374151',\n\tshadow:\n\t\t'0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2)',\n}\n\nexport function mergeTheme(customTheme: Partial<WidgetTheme> = {}): WidgetTheme {\n\treturn { ...DEFAULT_THEME, ...customTheme }\n}\n","// User-testing-mode plugin for the Usero widget.\n//\n// Activates ONLY when the host page URL carries `?usero_test=<slug>`. When\n// active, it:\n// 1. Creates a UserTestSession on the SaaS side via\n// POST /api/user-test-sessions\n// 2. Starts a MediaRecorder on the user's microphone (Opus in WebM) and\n// ships chunks every `chunkSeconds` to\n// PUT /api/user-test-sessions/:id/chunk?index=N\n// 3. Renders a small floating \"recording\" indicator with a Finish button\n// bottom-center so the tester can stop on their own.\n// 4. On Finish (or `pagehide`), flushes any buffered chunks and calls\n// POST /api/user-test-sessions/:id/finalise.\n//\n// Mic-permission denied is non-fatal: the session is still created (the\n// session-replay plugin keeps recording in parallel) and the indicator\n// shows a \"no audio\" hint. The SaaS side detects audio presence from the\n// first chunk; if no chunks land, hasAudio stays false there too.\n//\n// Bundle hygiene: this module is a subpath export and does NOT depend on\n// rrweb or any other heavy dep — just the native MediaRecorder API. The\n// floating UI is shadow-DOM scoped so host page CSS can't leak in.\n//\n// Privacy: only the microphone is recorded. We never read DOM text, never\n// touch the camera, and never persist audio in the browser past the\n// IndexedDB fallback (cleared on successful upload).\n\nimport type { UseroPlugin, PluginContext } from '../plugin'\nimport { DEFAULT_API_URL } from '../types'\n\nexport interface UserTestOptions {\n\t// URL query param the welcome page appends to redirect testers back.\n\t// Default `usero_test`. Match SaaS side if you ever change it.\n\tqueryParam?: string\n\t// Audio chunk length in seconds. Smaller = more partial-data resilience\n\t// but more requests. Default 30.\n\tchunkSeconds?: number\n\t// API origin. Override for self-hosted or local dev. Defaults to the\n\t// shared SDK constant (https://usero.io).\n\tapiUrl?: string\n\t// Override the tester-name shown on the SaaS side. Normally the welcome\n\t// page collects this and stores it in localStorage; the plugin reads it\n\t// from there. This option lets a host site bypass that.\n\ttesterName?: string\n\t// Hide the floating indicator. The plugin still records and finalises\n\t// on `pagehide`, but the tester gets no on-page UI. Useful if the host\n\t// page wants to render its own.\n\thideIndicator?: boolean\n}\n\ninterface UserTestTask {\n\tid: string\n\tprompt: string\n\tsortOrder: number\n}\n\ninterface RecorderStore {\n\tcancelled: boolean\n\tslug: string\n\tsessionId: string | null\n\tclientId: string | null\n\trecorder: MediaRecorder | null\n\tstream: MediaStream | null\n\tchunkIndex: number\n\tuploadQueue: Promise<void>\n\tpendingUploads: number\n\tstartedAt: number\n\tindicator: HTMLElement | null\n\tindicatorRoot: ShadowRoot | null\n\tindicatorState: 'recording' | 'finishing' | 'done' | 'no-audio' | 'error'\n\tpageHideHandler: (() => void) | null\n\toptions: Required<UserTestOptions>\n\ttasks: UserTestTask[]\n\ttasksPanelOpen: boolean\n\toutsidePointerHandler: ((event: PointerEvent) => void) | null\n\tkeydownHandler: ((event: KeyboardEvent) => void) | null\n}\n\nconst DEFAULT_OPTIONS: Required<Omit<UserTestOptions, 'testerName' | 'apiUrl'>> & {\n\ttesterName: string\n\tapiUrl: string\n} = {\n\tqueryParam: 'usero_test',\n\tchunkSeconds: 30,\n\tapiUrl: DEFAULT_API_URL,\n\ttesterName: '',\n\thideIndicator: false,\n}\n\nconst TESTER_NAME_STORAGE_KEY = 'usero:user-test:tester-name'\nconst TASKS_PANEL_OPEN_STORAGE_KEY = 'usero:user-test:tasks-panel-open'\nconst IDB_NAME = 'usero-user-test'\nconst IDB_STORE = 'pending-chunks'\n\ninterface PendingChunk {\n\tid: string\n\tsessionId: string\n\tapiUrl: string\n\tchunkIndex: number\n\tblob: Blob\n\tcreatedAt: number\n}\n\nfunction readTesterName(override: string): string | undefined {\n\tif (override) return override\n\ttry {\n\t\tconst stored = window.localStorage?.getItem(TESTER_NAME_STORAGE_KEY)\n\t\tif (stored && stored.trim()) return stored.trim().slice(0, 120)\n\t} catch {\n\t\t// Storage access can throw in some sandboxed iframes — ignore.\n\t}\n\treturn undefined\n}\n\nfunction getTestSlug(queryParam: string): string | null {\n\tif (typeof window === 'undefined' || typeof window.location === 'undefined') return null\n\ttry {\n\t\tconst params = new URLSearchParams(window.location.search)\n\t\tconst slug = params.get(queryParam)\n\t\tif (!slug) return null\n\t\tconst cleaned = slug.trim().slice(0, 64)\n\t\tif (!/^[a-z0-9-]+$/i.test(cleaned)) return null\n\t\treturn cleaned\n\t} catch {\n\t\treturn null\n\t}\n}\n\nfunction isMediaRecorderSupported(): boolean {\n\treturn typeof window !== 'undefined' && typeof window.MediaRecorder !== 'undefined' && typeof navigator !== 'undefined' && !!navigator.mediaDevices?.getUserMedia\n}\n\nfunction pickMimeType(): string | undefined {\n\tconst candidates = ['audio/webm;codecs=opus', 'audio/webm', 'audio/ogg;codecs=opus', 'audio/mp4']\n\tfor (const candidate of candidates) {\n\t\tif (typeof MediaRecorder !== 'undefined' && MediaRecorder.isTypeSupported?.(candidate)) {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn undefined\n}\n\n// IndexedDB helpers. Best-effort, never throw upstream — if IDB is missing\n// or quota-blown we just lose offline resilience but the live upload path\n// still runs.\nfunction idbOpen(): Promise<IDBDatabase | null> {\n\treturn new Promise(resolve => {\n\t\tif (typeof indexedDB === 'undefined') {\n\t\t\tresolve(null)\n\t\t\treturn\n\t\t}\n\t\ttry {\n\t\t\tconst req = indexedDB.open(IDB_NAME, 1)\n\t\t\treq.onupgradeneeded = (): void => {\n\t\t\t\tconst db = req.result\n\t\t\t\tif (!db.objectStoreNames.contains(IDB_STORE)) {\n\t\t\t\t\tdb.createObjectStore(IDB_STORE, { keyPath: 'id' })\n\t\t\t\t}\n\t\t\t}\n\t\t\treq.onsuccess = (): void => resolve(req.result)\n\t\t\treq.onerror = (): void => resolve(null)\n\t\t} catch {\n\t\t\tresolve(null)\n\t\t}\n\t})\n}\n\nasync function idbStashChunk(chunk: PendingChunk): Promise<void> {\n\tconst db = await idbOpen()\n\tif (!db) return\n\tawait new Promise<void>(resolve => {\n\t\ttry {\n\t\t\tconst tx = db.transaction(IDB_STORE, 'readwrite')\n\t\t\ttx.objectStore(IDB_STORE).put(chunk)\n\t\t\ttx.oncomplete = (): void => resolve()\n\t\t\ttx.onerror = (): void => resolve()\n\t\t\ttx.onabort = (): void => resolve()\n\t\t} catch {\n\t\t\tresolve()\n\t\t}\n\t})\n\tdb.close()\n}\n\nasync function idbDeleteChunk(id: string): Promise<void> {\n\tconst db = await idbOpen()\n\tif (!db) return\n\tawait new Promise<void>(resolve => {\n\t\ttry {\n\t\t\tconst tx = db.transaction(IDB_STORE, 'readwrite')\n\t\t\ttx.objectStore(IDB_STORE).delete(id)\n\t\t\ttx.oncomplete = (): void => resolve()\n\t\t\ttx.onerror = (): void => resolve()\n\t\t\ttx.onabort = (): void => resolve()\n\t\t} catch {\n\t\t\tresolve()\n\t\t}\n\t})\n\tdb.close()\n}\n\nasync function idbListChunks(sessionId: string): Promise<PendingChunk[]> {\n\tconst db = await idbOpen()\n\tif (!db) return []\n\tconst items = await new Promise<PendingChunk[]>(resolve => {\n\t\ttry {\n\t\t\tconst tx = db.transaction(IDB_STORE, 'readonly')\n\t\t\tconst req = tx.objectStore(IDB_STORE).getAll()\n\t\t\treq.onsuccess = (): void => {\n\t\t\t\tconst all = (req.result as PendingChunk[]) ?? []\n\t\t\t\tresolve(all.filter(c => c.sessionId === sessionId))\n\t\t\t}\n\t\t\treq.onerror = (): void => resolve([])\n\t\t} catch {\n\t\t\tresolve([])\n\t\t}\n\t})\n\tdb.close()\n\treturn items\n}\n\nasync function uploadChunkWithRetry(\n\tapiUrl: string,\n\tsessionId: string,\n\tindex: number,\n\tblob: Blob,\n\tlogger: PluginContext['logger'],\n\tmaxAttempts = 5,\n): Promise<boolean> {\n\tconst url = `${apiUrl.replace(/\\/$/, '')}/api/user-test-sessions/${encodeURIComponent(sessionId)}/chunk?index=${index}`\n\tlet attempt = 0\n\twhile (attempt < maxAttempts) {\n\t\ttry {\n\t\t\tconst res = await fetch(url, {\n\t\t\t\tmethod: 'PUT',\n\t\t\t\tbody: blob,\n\t\t\t\theaders: { 'Content-Type': blob.type || 'audio/webm' },\n\t\t\t\tkeepalive: blob.size <= 60 * 1024, // browsers cap keepalive bodies\n\t\t\t})\n\t\t\tif (res.ok) return true\n\t\t\t// 4xx (other than 413) won't get better with retries; bail early.\n\t\t\tif (res.status >= 400 && res.status < 500 && res.status !== 408 && res.status !== 429) {\n\t\t\t\tlogger.error(`chunk ${index} rejected with ${res.status}`)\n\t\t\t\treturn false\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tlogger.warn(`chunk ${index} upload attempt ${attempt + 1} failed`, err)\n\t\t}\n\t\tattempt += 1\n\t\tconst backoff = Math.min(15000, 500 * 2 ** attempt) + Math.floor(Math.random() * 250)\n\t\tawait new Promise(resolve => setTimeout(resolve, backoff))\n\t}\n\treturn false\n}\n\nfunction buildIndicator(host: HTMLElement, store: RecorderStore, onFinish: () => void, onToggleTasks: () => void): ShadowRoot {\n\tconst root = host.attachShadow({ mode: 'closed' })\n\tconst style = document.createElement('style')\n\t// Keep this CSS small. Pulse is the only animation. Bottom-center,\n\t// safe-area aware, semi-transparent so it doesn't block the page.\n\tstyle.textContent = `\n\t\t:host { all: initial; }\n\t\t.anchor {\n\t\t\tposition: fixed;\n\t\t\tbottom: calc(env(safe-area-inset-bottom, 0px) + 16px);\n\t\t\tleft: 50%; transform: translateX(-50%);\n\t\t\tdisplay: flex; flex-direction: column; align-items: center; gap: 8px;\n\t\t\tz-index: 2147483646; max-width: calc(100vw - 32px);\n\t\t\tfont: 13px/1 -apple-system, BlinkMacSystemFont, \"Segoe UI\", system-ui, sans-serif;\n\t\t\tcolor: #fff;\n\t\t}\n\t\t.bar {\n\t\t\tdisplay: inline-flex; align-items: center; gap: 10px;\n\t\t\tpadding: 8px 14px 8px 12px;\n\t\t\tbackground: rgba(17,17,17,0.78);\n\t\t\tborder-radius: 999px;\n\t\t\tbox-shadow: 0 8px 24px rgba(0,0,0,0.18);\n\t\t\tbackdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);\n\t\t\tmax-width: 100%;\n\t\t}\n\t\t.panel {\n\t\t\tbackground: rgba(17,17,17,0.88);\n\t\t\tborder-radius: 14px; padding: 12px 14px 12px 8px;\n\t\t\tline-height: 1.45;\n\t\t\tbox-shadow: 0 12px 32px rgba(0,0,0,0.28);\n\t\t\tmax-height: min(60vh, 480px);\n\t\t\tmax-width: min(420px, calc(100vw - 32px));\n\t\t\twidth: max-content; overflow-y: auto;\n\t\t}\n\t\t.panel[hidden] { display: none; }\n\t\t.panel ol { margin: 0; padding-left: 26px; }\n\t\t.panel li { margin: 0 0 8px; }\n\t\t.panel li:last-child { margin: 0; }\n\t\t.dot {\n\t\t\twidth: 8px; height: 8px; border-radius: 50%;\n\t\t\tbackground: #ef4444;\n\t\t\tbox-shadow: 0 0 0 0 rgba(239, 68, 68, 0.6);\n\t\t\tanimation: pulse 1.6s ease-out infinite;\n\t\t}\n\t\t.dot[data-state=\"no-audio\"] { background: #fbbf24; animation: none; }\n\t\t.dot[data-state=\"finishing\"] { background: #fbbf24; animation: none; }\n\t\t.dot[data-state=\"done\"] { background: #10b981; animation: none; }\n\t\t.dot[data-state=\"error\"] { background: #ef4444; animation: none; }\n\t\t.label { font-weight: 500; letter-spacing: 0.01em; }\n\t\t.spacer { width: 1px; height: 16px; background: rgba(255,255,255,0.18); margin: 0 2px; }\n\t\t.btn {\n\t\t\tappearance: none; border: 0; background: rgba(255,255,255,0.12);\n\t\t\tcolor: #fff; font: inherit; font-weight: 600;\n\t\t\tpadding: 6px 12px; border-radius: 999px; cursor: pointer;\n\t\t\ttransition: background 0.15s ease;\n\t\t}\n\t\t.btn:hover { background: rgba(255,255,255,0.22); }\n\t\t.btn:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }\n\t\t.btn[disabled] { opacity: 0.5; cursor: progress; }\n\t\t.tasks-btn[aria-expanded=\"true\"] { background: rgba(255,255,255,0.24); }\n\t\t@media (max-width: 420px) { .btn { padding: 9px 14px; min-height: 40px; } }\n\t\t.thanks {\n\t\t\tposition: fixed; inset: 0;\n\t\t\tdisplay: grid; place-items: center;\n\t\t\tbackground: rgba(15, 15, 17, 0.78);\n\t\t\tbackdrop-filter: blur(6px);\n\t\t\t-webkit-backdrop-filter: blur(6px);\n\t\t\tcolor: #fff;\n\t\t\tfont-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", system-ui, sans-serif;\n\t\t\tz-index: 2147483647;\n\t\t\tpadding: 24px;\n\t\t\ttext-align: center;\n\t\t}\n\t\t.thanks-card {\n\t\t\tbackground: #fff; color: #111;\n\t\t\tborder-radius: 16px; padding: 28px 24px;\n\t\t\tmax-width: 360px; width: 100%;\n\t\t\tbox-shadow: 0 20px 50px rgba(0,0,0,0.25);\n\t\t}\n\t\t.thanks h2 { margin: 0 0 8px; font-size: 20px; }\n\t\t.thanks p { margin: 0; font-size: 14px; line-height: 1.45; color: #4b5563; }\n\t\t.thanks .check {\n\t\t\twidth: 44px; height: 44px; border-radius: 50%;\n\t\t\tbackground: #10b981; color: #fff;\n\t\t\tdisplay: grid; place-items: center;\n\t\t\tmargin: 0 auto 12px;\n\t\t\tfont-size: 22px;\n\t\t}\n\t\t@keyframes pulse {\n\t\t\t0% { box-shadow: 0 0 0 0 rgba(239,68,68,0.55); }\n\t\t\t70% { box-shadow: 0 0 0 10px rgba(239,68,68,0); }\n\t\t\t100% { box-shadow: 0 0 0 0 rgba(239,68,68,0); }\n\t\t}\n\t\t@media (prefers-reduced-motion: reduce) {\n\t\t\t.dot { animation: none; }\n\t\t}\n\t`\n\tconst anchor = document.createElement('div')\n\tanchor.className = 'anchor'\n\n\tconst panel = document.createElement('div')\n\tpanel.className = 'panel'\n\tpanel.hidden = true\n\n\tconst bar = document.createElement('div')\n\tbar.className = 'bar'\n\tbar.setAttribute('role', 'status')\n\tbar.setAttribute('aria-live', 'polite')\n\n\tconst dot = document.createElement('span')\n\tdot.className = 'dot'\n\tdot.setAttribute('data-state', store.indicatorState)\n\n\tconst label = document.createElement('span')\n\tlabel.className = 'label'\n\tlabel.textContent = 'Recording'\n\n\tconst spacer = document.createElement('span')\n\tspacer.className = 'spacer'\n\n\tbar.appendChild(dot)\n\tbar.appendChild(label)\n\tbar.appendChild(spacer)\n\n\tconst btn = document.createElement('button')\n\tbtn.type = 'button'\n\tbtn.className = 'btn finish-btn'\n\tbtn.textContent = 'Finish'\n\tbtn.addEventListener('click', onFinish)\n\tbar.appendChild(btn)\n\n\tif (store.tasks.length > 0) installTasksToggle(bar, btn, store, onToggleTasks)\n\n\tanchor.appendChild(panel)\n\tanchor.appendChild(bar)\n\n\troot.appendChild(style)\n\troot.appendChild(anchor)\n\treturn root\n}\n\nfunction installTasksToggle(bar: HTMLElement, finishBtn: HTMLElement, store: RecorderStore, onToggleTasks: () => void): void {\n\tconst tasksBtn = document.createElement('button')\n\ttasksBtn.type = 'button'\n\ttasksBtn.className = 'btn tasks-btn'\n\ttasksBtn.textContent = `Tasks (${store.tasks.length})`\n\ttasksBtn.setAttribute('aria-expanded', store.tasksPanelOpen ? 'true' : 'false')\n\ttasksBtn.addEventListener('click', onToggleTasks)\n\tbar.insertBefore(tasksBtn, finishBtn)\n}\n\nfunction renderTasksPanel(store: RecorderStore): void {\n\tconst root = store.indicatorRoot\n\tif (!root) return\n\tconst panel = root.querySelector('.panel')\n\tif (!(panel instanceof HTMLElement)) return\n\t// Build content once.\n\tif (!panel.firstChild && store.tasks.length > 0) {\n\t\tconst ol = document.createElement('ol')\n\t\tfor (const task of store.tasks) {\n\t\t\tconst li = document.createElement('li')\n\t\t\tli.textContent = task.prompt\n\t\t\tol.appendChild(li)\n\t\t}\n\t\tpanel.appendChild(ol)\n\t}\n\tpanel.hidden = !store.tasksPanelOpen\n\tconst tasksBtn = root.querySelector('.tasks-btn')\n\tif (tasksBtn instanceof HTMLElement) {\n\t\ttasksBtn.setAttribute('aria-expanded', store.tasksPanelOpen ? 'true' : 'false')\n\t}\n}\n\nfunction readTasksPanelOpen(): boolean {\n\ttry { return window.sessionStorage?.getItem(TASKS_PANEL_OPEN_STORAGE_KEY) === '1' } catch { return false }\n}\nfunction writeTasksPanelOpen(open: boolean): void {\n\ttry { window.sessionStorage?.setItem(TASKS_PANEL_OPEN_STORAGE_KEY, open ? '1' : '0') } catch { /* ignore */ }\n}\n\nfunction renderIndicatorState(store: RecorderStore): void {\n\tconst root = store.indicatorRoot\n\tif (!root) return\n\tconst dot = root.querySelector('.dot')\n\tconst label = root.querySelector('.label')\n\tconst btn = root.querySelector<HTMLButtonElement>('.finish-btn')\n\tif (!(dot instanceof HTMLElement) || !(label instanceof HTMLElement) || !btn) return\n\tdot.setAttribute('data-state', store.indicatorState)\n\tswitch (store.indicatorState) {\n\t\tcase 'recording':\n\t\t\tlabel.textContent = 'Recording'\n\t\t\tbtn.textContent = 'Finish'\n\t\t\tbtn.disabled = false\n\t\t\tbreak\n\t\tcase 'no-audio':\n\t\t\tlabel.textContent = 'No mic, replay only'\n\t\t\tbtn.textContent = 'Finish'\n\t\t\tbtn.disabled = false\n\t\t\tbreak\n\t\tcase 'finishing':\n\t\t\tlabel.textContent = 'Saving'\n\t\t\tbtn.textContent = 'Saving'\n\t\t\tbtn.disabled = true\n\t\t\tbreak\n\t\tcase 'done':\n\t\t\tlabel.textContent = 'Saved'\n\t\t\tbtn.textContent = 'Done'\n\t\t\tbtn.disabled = true\n\t\t\tbreak\n\t\tcase 'error':\n\t\t\tlabel.textContent = 'Save failed'\n\t\t\tbtn.textContent = 'Retry'\n\t\t\tbtn.disabled = false\n\t\t\tbreak\n\t}\n}\n\nfunction showThanksScreen(root: ShadowRoot): void {\n\tconst overlay = document.createElement('div')\n\toverlay.className = 'thanks'\n\toverlay.innerHTML = `\n\t\t<div class=\"thanks-card\">\n\t\t\t<div class=\"check\" aria-hidden=\"true\">✓</div>\n\t\t\t<h2>Thanks for testing</h2>\n\t\t\t<p>Your session was saved. You can close this tab.</p>\n\t\t</div>\n\t`\n\troot.appendChild(overlay)\n}\n\nfunction parseTasks(raw: unknown): UserTestTask[] {\n\tif (!Array.isArray(raw)) return []\n\tconst out = raw.flatMap((item: unknown): UserTestTask[] => {\n\t\tconst t = item as { id?: unknown; prompt?: unknown; sortOrder?: unknown }\n\t\tif (!t || typeof t.id !== 'string' || typeof t.prompt !== 'string' || typeof t.sortOrder !== 'number') return []\n\t\treturn [{ id: t.id, prompt: t.prompt, sortOrder: t.sortOrder }]\n\t})\n\tout.sort((a, b) => a.sortOrder - b.sortOrder)\n\treturn out\n}\n\nasync function createSession(\n\tapiUrl: string,\n\tslug: string,\n\ttesterName: string | undefined,\n): Promise<{ sessionId: string; clientId: string; tasks: UserTestTask[] } | null> {\n\ttry {\n\t\tconst res = await fetch(`${apiUrl.replace(/\\/$/, '')}/api/user-test-sessions`, {\n\t\t\tmethod: 'POST',\n\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\tbody: JSON.stringify({ slug, ...(testerName ? { testerName } : {}) }),\n\t\t})\n\t\tif (!res.ok) return null\n\t\tconst json = (await res.json()) as { sessionId?: unknown; clientId?: unknown; tasks?: unknown }\n\t\tif (typeof json.sessionId !== 'string' || typeof json.clientId !== 'string') return null\n\t\treturn { sessionId: json.sessionId, clientId: json.clientId, tasks: parseTasks(json.tasks) }\n\t} catch {\n\t\treturn null\n\t}\n}\n\nasync function finaliseSession(\n\tapiUrl: string,\n\tsessionId: string,\n\tdurationSeconds: number,\n): Promise<boolean> {\n\ttry {\n\t\tconst res = await fetch(`${apiUrl.replace(/\\/$/, '')}/api/user-test-sessions/${encodeURIComponent(sessionId)}/finalise`, {\n\t\t\tmethod: 'POST',\n\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\tbody: JSON.stringify({ durationSeconds: Math.max(0, Math.round(durationSeconds)) }),\n\t\t\tkeepalive: true,\n\t\t})\n\t\treturn res.ok\n\t} catch {\n\t\treturn false\n\t}\n}\n\nasync function flushPendingFromIdb(store: RecorderStore, ctx: PluginContext): Promise<void> {\n\tif (!store.sessionId) return\n\tconst pending = await idbListChunks(store.sessionId)\n\tfor (const chunk of pending) {\n\t\tconst ok = await uploadChunkWithRetry(chunk.apiUrl, chunk.sessionId, chunk.chunkIndex, chunk.blob, ctx.logger, 3)\n\t\tif (ok) await idbDeleteChunk(chunk.id)\n\t}\n}\n\nfunction enqueueChunk(store: RecorderStore, ctx: PluginContext, blob: Blob): void {\n\tif (store.cancelled || !store.sessionId || blob.size === 0) return\n\tconst index = store.chunkIndex\n\tstore.chunkIndex += 1\n\tstore.pendingUploads += 1\n\tconst sessionId = store.sessionId\n\tconst apiUrl = store.options.apiUrl\n\n\tstore.uploadQueue = store.uploadQueue.then(async () => {\n\t\tconst ok = await uploadChunkWithRetry(apiUrl, sessionId, index, blob, ctx.logger)\n\t\tif (!ok) {\n\t\t\tctx.logger.warn(`chunk ${index} stashed for offline retry`)\n\t\t\tawait idbStashChunk({\n\t\t\t\tid: `${sessionId}:${index}:${Date.now()}`,\n\t\t\t\tsessionId,\n\t\t\t\tapiUrl,\n\t\t\t\tchunkIndex: index,\n\t\t\t\tblob,\n\t\t\t\tcreatedAt: Date.now(),\n\t\t\t})\n\t\t}\n\t\tstore.pendingUploads -= 1\n\t})\n}\n\nasync function startRecording(store: RecorderStore, ctx: PluginContext): Promise<void> {\n\tif (!isMediaRecorderSupported()) {\n\t\tctx.logger.warn('MediaRecorder not supported, continuing without audio')\n\t\tstore.indicatorState = 'no-audio'\n\t\trenderIndicatorState(store)\n\t\treturn\n\t}\n\tlet stream: MediaStream\n\ttry {\n\t\tstream = await navigator.mediaDevices.getUserMedia({ audio: true })\n\t} catch (err) {\n\t\tctx.logger.warn('mic permission denied or unavailable', err)\n\t\tstore.indicatorState = 'no-audio'\n\t\trenderIndicatorState(store)\n\t\treturn\n\t}\n\tstore.stream = stream\n\tconst mimeType = pickMimeType()\n\tlet recorder: MediaRecorder\n\ttry {\n\t\trecorder = mimeType ? new MediaRecorder(stream, { mimeType }) : new MediaRecorder(stream)\n\t} catch (err) {\n\t\tctx.logger.error('MediaRecorder construction failed', err)\n\t\tstream.getTracks().forEach(t => t.stop())\n\t\tstore.stream = null\n\t\tstore.indicatorState = 'no-audio'\n\t\trenderIndicatorState(store)\n\t\treturn\n\t}\n\tstore.recorder = recorder\n\trecorder.addEventListener('dataavailable', event => {\n\t\tif (event.data && event.data.size > 0) {\n\t\t\tenqueueChunk(store, ctx, event.data)\n\t\t}\n\t})\n\trecorder.addEventListener('error', event => {\n\t\tctx.logger.error('MediaRecorder error', event)\n\t})\n\t// `timeslice` makes the recorder emit a self-contained chunk every N ms.\n\trecorder.start(store.options.chunkSeconds * 1000)\n}\n\nfunction stopRecording(store: RecorderStore): void {\n\tconst recorder = store.recorder\n\tif (recorder && recorder.state !== 'inactive') {\n\t\ttry {\n\t\t\trecorder.requestData()\n\t\t} catch {\n\t\t\t// requestData throws if state is invalid; ignore.\n\t\t}\n\t\ttry {\n\t\t\trecorder.stop()\n\t\t} catch {\n\t\t\t// already stopped\n\t\t}\n\t}\n\tstore.recorder = null\n\tif (store.stream) {\n\t\tstore.stream.getTracks().forEach(t => t.stop())\n\t\tstore.stream = null\n\t}\n}\n\nasync function finishFlow(store: RecorderStore, ctx: PluginContext, opts: { showThanks: boolean }): Promise<void> {\n\tif (store.cancelled) return\n\tif (store.indicatorState === 'finishing' || store.indicatorState === 'done') return\n\tstore.indicatorState = 'finishing'\n\trenderIndicatorState(store)\n\n\tstopRecording(store)\n\t// Wait for the queued uploads to drain. Each upload already has its own\n\t// retry/backoff; this just lets them finish before finalise fires.\n\tawait store.uploadQueue\n\tawait flushPendingFromIdb(store, ctx)\n\n\tconst durationSeconds = (Date.now() - store.startedAt) / 1000\n\tif (store.sessionId) {\n\t\tconst ok = await finaliseSession(store.options.apiUrl, store.sessionId, durationSeconds)\n\t\tstore.indicatorState = ok ? 'done' : 'error'\n\t} else {\n\t\tstore.indicatorState = 'error'\n\t}\n\trenderIndicatorState(store)\n\n\tif (opts.showThanks && store.indicatorRoot && store.indicatorState === 'done') {\n\t\tshowThanksScreen(store.indicatorRoot)\n\t}\n}\n\nexport function userTest(options: UserTestOptions = {}): UseroPlugin {\n\tconst merged: Required<UserTestOptions> = {\n\t\tqueryParam: options.queryParam ?? DEFAULT_OPTIONS.queryParam,\n\t\tchunkSeconds: options.chunkSeconds ?? DEFAULT_OPTIONS.chunkSeconds,\n\t\tapiUrl: options.apiUrl ?? DEFAULT_OPTIONS.apiUrl,\n\t\ttesterName: options.testerName ?? DEFAULT_OPTIONS.testerName,\n\t\thideIndicator: options.hideIndicator ?? DEFAULT_OPTIONS.hideIndicator,\n\t}\n\n\treturn {\n\t\tname: 'user-test',\n\t\tonInit(ctx) {\n\t\t\tif (typeof window === 'undefined' || typeof document === 'undefined') return\n\t\t\tconst slug = getTestSlug(merged.queryParam)\n\t\t\tif (!slug) return\n\n\t\t\tconst apiUrl = merged.apiUrl || ctx.baseUrl || DEFAULT_API_URL\n\t\t\tconst store: RecorderStore = {\n\t\t\t\tcancelled: false,\n\t\t\t\tslug,\n\t\t\t\tsessionId: null,\n\t\t\t\tclientId: null,\n\t\t\t\trecorder: null,\n\t\t\t\tstream: null,\n\t\t\t\tchunkIndex: 0,\n\t\t\t\tuploadQueue: Promise.resolve(),\n\t\t\t\tpendingUploads: 0,\n\t\t\t\tstartedAt: Date.now(),\n\t\t\t\tindicator: null,\n\t\t\t\tindicatorRoot: null,\n\t\t\t\tindicatorState: 'recording',\n\t\t\t\tpageHideHandler: null,\n\t\t\t\toptions: { ...merged, apiUrl },\n\t\t\t\ttasks: [],\n\t\t\t\ttasksPanelOpen: readTasksPanelOpen(),\n\t\t\t\toutsidePointerHandler: null,\n\t\t\t\tkeydownHandler: null,\n\t\t\t}\n\t\t\tctx.setStore(store)\n\n\t\t\tconst onFinish = (): void => {\n\t\t\t\tvoid finishFlow(store, ctx, { showThanks: true })\n\t\t\t}\n\n\t\t\tconst setPanelOpen = (open: boolean): void => {\n\t\t\t\tif (store.tasksPanelOpen === open) return\n\t\t\t\tstore.tasksPanelOpen = open\n\t\t\t\twriteTasksPanelOpen(open)\n\t\t\t\trenderTasksPanel(store)\n\t\t\t}\n\n\t\t\tconst onToggleTasks = (): void => setPanelOpen(!store.tasksPanelOpen)\n\n\t\t\tif (!merged.hideIndicator) {\n\t\t\t\tconst host = document.createElement('div')\n\t\t\t\thost.setAttribute('data-usero-user-test', 'true')\n\t\t\t\tdocument.body.appendChild(host)\n\t\t\t\tstore.indicator = host\n\t\t\t\tstore.indicatorRoot = buildIndicator(host, store, onFinish, onToggleTasks)\n\t\t\t\trenderIndicatorState(store)\n\t\t\t}\n\n\t\t\t// Outside-click + Escape close the tasks panel. Listen on document\n\t\t\t// (composedPath checks shadow ancestry so taps on the panel/pill\n\t\t\t// itself don't dismiss).\n\t\t\tconst outsidePointer = (event: PointerEvent): void => {\n\t\t\t\tif (!store.tasksPanelOpen) return\n\t\t\t\tconst host = store.indicator\n\t\t\t\tif (!host) return\n\t\t\t\tconst path = event.composedPath()\n\t\t\t\tif (!path.includes(host)) setPanelOpen(false)\n\t\t\t}\n\t\t\tconst onKeydown = (event: KeyboardEvent): void => {\n\t\t\t\tif (event.key === 'Escape' && store.tasksPanelOpen) setPanelOpen(false)\n\t\t\t}\n\t\t\tstore.outsidePointerHandler = outsidePointer\n\t\t\tstore.keydownHandler = onKeydown\n\t\t\tdocument.addEventListener('pointerdown', outsidePointer, true)\n\t\t\tdocument.addEventListener('keydown', onKeydown)\n\n\t\t\tconst pageHide = (): void => {\n\t\t\t\t// Best-effort flush + finalise. We don't await here; the browser\n\t\t\t\t// is shutting the page down. `keepalive: true` on finalise lets\n\t\t\t\t// the request race the unload.\n\t\t\t\tvoid finishFlow(store, ctx, { showThanks: false })\n\t\t\t}\n\t\t\tstore.pageHideHandler = pageHide\n\t\t\twindow.addEventListener('pagehide', pageHide)\n\n\t\t\tvoid (async (): Promise<void> => {\n\t\t\t\tconst created = await createSession(apiUrl, slug, readTesterName(merged.testerName))\n\t\t\t\tif (store.cancelled) return\n\t\t\t\tif (!created) {\n\t\t\t\t\tctx.logger.error('failed to create user-test session')\n\t\t\t\t\tstore.indicatorState = 'error'\n\t\t\t\t\trenderIndicatorState(store)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tstore.sessionId = created.sessionId\n\t\t\t\tstore.clientId = created.clientId\n\t\t\t\tstore.tasks = created.tasks\n\t\t\t\tif (store.tasks.length > 0 && store.indicatorRoot && !merged.hideIndicator) {\n\t\t\t\t\tconst bar = store.indicatorRoot.querySelector('.bar')\n\t\t\t\t\tconst finishBtn = bar?.querySelector('.finish-btn')\n\t\t\t\t\tif (bar instanceof HTMLElement && finishBtn instanceof HTMLElement && !bar.querySelector('.tasks-btn')) {\n\t\t\t\t\t\tinstallTasksToggle(bar, finishBtn, store, onToggleTasks)\n\t\t\t\t\t}\n\t\t\t\t\trenderTasksPanel(store)\n\t\t\t\t}\n\t\t\t\tawait startRecording(store, ctx)\n\t\t\t\trenderIndicatorState(store)\n\t\t\t})()\n\t\t},\n\t\tonDestroy(ctx) {\n\t\t\tconst store = ctx.getStore<RecorderStore>()\n\t\t\tif (!store) return\n\t\t\tstore.cancelled = true\n\t\t\tif (store.pageHideHandler) {\n\t\t\t\twindow.removeEventListener('pagehide', store.pageHideHandler)\n\t\t\t\tstore.pageHideHandler = null\n\t\t\t}\n\t\t\tstopRecording(store)\n\t\t\tif (store.outsidePointerHandler) {\n\t\t\t\tdocument.removeEventListener('pointerdown', store.outsidePointerHandler, true)\n\t\t\t\tstore.outsidePointerHandler = null\n\t\t\t}\n\t\t\tif (store.keydownHandler) {\n\t\t\t\tdocument.removeEventListener('keydown', store.keydownHandler)\n\t\t\t\tstore.keydownHandler = null\n\t\t\t}\n\t\t\tif (store.indicator && store.indicator.parentNode) {\n\t\t\t\tstore.indicator.parentNode.removeChild(store.indicator)\n\t\t\t}\n\t\t\tstore.indicator = null\n\t\t\tstore.indicatorRoot = null\n\t\t},\n\t}\n}\n\n// Internal helpers exposed for tests only. Not part of the public API.\nexport const __test__ = { getTestSlug, pickMimeType, isMediaRecorderSupported }\n"]}
|
|
@@ -10,6 +10,7 @@ var DEFAULT_OPTIONS = {
|
|
|
10
10
|
hideIndicator: false
|
|
11
11
|
};
|
|
12
12
|
var TESTER_NAME_STORAGE_KEY = "usero:user-test:tester-name";
|
|
13
|
+
var TASKS_PANEL_OPEN_STORAGE_KEY = "usero:user-test:tasks-panel-open";
|
|
13
14
|
var IDB_NAME = "usero-user-test";
|
|
14
15
|
var IDB_STORE = "pending-chunks";
|
|
15
16
|
function readTesterName(override) {
|
|
@@ -144,32 +145,42 @@ async function uploadChunkWithRetry(apiUrl, sessionId, index, blob, logger, maxA
|
|
|
144
145
|
}
|
|
145
146
|
return false;
|
|
146
147
|
}
|
|
147
|
-
function buildIndicator(host, store, onFinish) {
|
|
148
|
+
function buildIndicator(host, store, onFinish, onToggleTasks) {
|
|
148
149
|
const root = host.attachShadow({ mode: "closed" });
|
|
149
150
|
const style = document.createElement("style");
|
|
150
151
|
style.textContent = `
|
|
151
152
|
:host { all: initial; }
|
|
152
|
-
.
|
|
153
|
+
.anchor {
|
|
153
154
|
position: fixed;
|
|
154
155
|
bottom: calc(env(safe-area-inset-bottom, 0px) + 16px);
|
|
155
|
-
left: 50%;
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
gap: 10px;
|
|
160
|
-
padding: 8px 14px 8px 12px;
|
|
161
|
-
background: rgba(17, 17, 17, 0.78);
|
|
156
|
+
left: 50%; transform: translateX(-50%);
|
|
157
|
+
display: flex; flex-direction: column; align-items: center; gap: 8px;
|
|
158
|
+
z-index: 2147483646; max-width: calc(100vw - 32px);
|
|
159
|
+
font: 13px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
|
162
160
|
color: #fff;
|
|
161
|
+
}
|
|
162
|
+
.bar {
|
|
163
|
+
display: inline-flex; align-items: center; gap: 10px;
|
|
164
|
+
padding: 8px 14px 8px 12px;
|
|
165
|
+
background: rgba(17,17,17,0.78);
|
|
163
166
|
border-radius: 999px;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
167
|
+
box-shadow: 0 8px 24px rgba(0,0,0,0.18);
|
|
168
|
+
backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
|
|
169
|
+
max-width: 100%;
|
|
170
|
+
}
|
|
171
|
+
.panel {
|
|
172
|
+
background: rgba(17,17,17,0.88);
|
|
173
|
+
border-radius: 14px; padding: 12px 14px 12px 8px;
|
|
174
|
+
line-height: 1.45;
|
|
175
|
+
box-shadow: 0 12px 32px rgba(0,0,0,0.28);
|
|
176
|
+
max-height: min(60vh, 480px);
|
|
177
|
+
max-width: min(420px, calc(100vw - 32px));
|
|
178
|
+
width: max-content; overflow-y: auto;
|
|
172
179
|
}
|
|
180
|
+
.panel[hidden] { display: none; }
|
|
181
|
+
.panel ol { margin: 0; padding-left: 26px; }
|
|
182
|
+
.panel li { margin: 0 0 8px; }
|
|
183
|
+
.panel li:last-child { margin: 0; }
|
|
173
184
|
.dot {
|
|
174
185
|
width: 8px; height: 8px; border-radius: 50%;
|
|
175
186
|
background: #ef4444;
|
|
@@ -191,6 +202,8 @@ function buildIndicator(host, store, onFinish) {
|
|
|
191
202
|
.btn:hover { background: rgba(255,255,255,0.22); }
|
|
192
203
|
.btn:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
|
|
193
204
|
.btn[disabled] { opacity: 0.5; cursor: progress; }
|
|
205
|
+
.tasks-btn[aria-expanded="true"] { background: rgba(255,255,255,0.24); }
|
|
206
|
+
@media (max-width: 420px) { .btn { padding: 9px 14px; min-height: 40px; } }
|
|
194
207
|
.thanks {
|
|
195
208
|
position: fixed; inset: 0;
|
|
196
209
|
display: grid; place-items: center;
|
|
@@ -227,6 +240,11 @@ function buildIndicator(host, store, onFinish) {
|
|
|
227
240
|
.dot { animation: none; }
|
|
228
241
|
}
|
|
229
242
|
`;
|
|
243
|
+
const anchor = document.createElement("div");
|
|
244
|
+
anchor.className = "anchor";
|
|
245
|
+
const panel = document.createElement("div");
|
|
246
|
+
panel.className = "panel";
|
|
247
|
+
panel.hidden = true;
|
|
230
248
|
const bar = document.createElement("div");
|
|
231
249
|
bar.className = "bar";
|
|
232
250
|
bar.setAttribute("role", "status");
|
|
@@ -239,25 +257,70 @@ function buildIndicator(host, store, onFinish) {
|
|
|
239
257
|
label.textContent = "Recording";
|
|
240
258
|
const spacer = document.createElement("span");
|
|
241
259
|
spacer.className = "spacer";
|
|
260
|
+
bar.appendChild(dot);
|
|
261
|
+
bar.appendChild(label);
|
|
262
|
+
bar.appendChild(spacer);
|
|
242
263
|
const btn = document.createElement("button");
|
|
243
264
|
btn.type = "button";
|
|
244
|
-
btn.className = "btn";
|
|
265
|
+
btn.className = "btn finish-btn";
|
|
245
266
|
btn.textContent = "Finish";
|
|
246
267
|
btn.addEventListener("click", onFinish);
|
|
247
|
-
bar.appendChild(dot);
|
|
248
|
-
bar.appendChild(label);
|
|
249
|
-
bar.appendChild(spacer);
|
|
250
268
|
bar.appendChild(btn);
|
|
269
|
+
if (store.tasks.length > 0) installTasksToggle(bar, btn, store, onToggleTasks);
|
|
270
|
+
anchor.appendChild(panel);
|
|
271
|
+
anchor.appendChild(bar);
|
|
251
272
|
root.appendChild(style);
|
|
252
|
-
root.appendChild(
|
|
273
|
+
root.appendChild(anchor);
|
|
253
274
|
return root;
|
|
254
275
|
}
|
|
276
|
+
function installTasksToggle(bar, finishBtn, store, onToggleTasks) {
|
|
277
|
+
const tasksBtn = document.createElement("button");
|
|
278
|
+
tasksBtn.type = "button";
|
|
279
|
+
tasksBtn.className = "btn tasks-btn";
|
|
280
|
+
tasksBtn.textContent = `Tasks (${store.tasks.length})`;
|
|
281
|
+
tasksBtn.setAttribute("aria-expanded", store.tasksPanelOpen ? "true" : "false");
|
|
282
|
+
tasksBtn.addEventListener("click", onToggleTasks);
|
|
283
|
+
bar.insertBefore(tasksBtn, finishBtn);
|
|
284
|
+
}
|
|
285
|
+
function renderTasksPanel(store) {
|
|
286
|
+
const root = store.indicatorRoot;
|
|
287
|
+
if (!root) return;
|
|
288
|
+
const panel = root.querySelector(".panel");
|
|
289
|
+
if (!(panel instanceof HTMLElement)) return;
|
|
290
|
+
if (!panel.firstChild && store.tasks.length > 0) {
|
|
291
|
+
const ol = document.createElement("ol");
|
|
292
|
+
for (const task of store.tasks) {
|
|
293
|
+
const li = document.createElement("li");
|
|
294
|
+
li.textContent = task.prompt;
|
|
295
|
+
ol.appendChild(li);
|
|
296
|
+
}
|
|
297
|
+
panel.appendChild(ol);
|
|
298
|
+
}
|
|
299
|
+
panel.hidden = !store.tasksPanelOpen;
|
|
300
|
+
const tasksBtn = root.querySelector(".tasks-btn");
|
|
301
|
+
if (tasksBtn instanceof HTMLElement) {
|
|
302
|
+
tasksBtn.setAttribute("aria-expanded", store.tasksPanelOpen ? "true" : "false");
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function readTasksPanelOpen() {
|
|
306
|
+
try {
|
|
307
|
+
return window.sessionStorage?.getItem(TASKS_PANEL_OPEN_STORAGE_KEY) === "1";
|
|
308
|
+
} catch {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function writeTasksPanelOpen(open) {
|
|
313
|
+
try {
|
|
314
|
+
window.sessionStorage?.setItem(TASKS_PANEL_OPEN_STORAGE_KEY, open ? "1" : "0");
|
|
315
|
+
} catch {
|
|
316
|
+
}
|
|
317
|
+
}
|
|
255
318
|
function renderIndicatorState(store) {
|
|
256
319
|
const root = store.indicatorRoot;
|
|
257
320
|
if (!root) return;
|
|
258
321
|
const dot = root.querySelector(".dot");
|
|
259
322
|
const label = root.querySelector(".label");
|
|
260
|
-
const btn = root.querySelector(".btn");
|
|
323
|
+
const btn = root.querySelector(".finish-btn");
|
|
261
324
|
if (!(dot instanceof HTMLElement) || !(label instanceof HTMLElement) || !btn) return;
|
|
262
325
|
dot.setAttribute("data-state", store.indicatorState);
|
|
263
326
|
switch (store.indicatorState) {
|
|
@@ -300,6 +363,16 @@ function showThanksScreen(root) {
|
|
|
300
363
|
`;
|
|
301
364
|
root.appendChild(overlay);
|
|
302
365
|
}
|
|
366
|
+
function parseTasks(raw) {
|
|
367
|
+
if (!Array.isArray(raw)) return [];
|
|
368
|
+
const out = raw.flatMap((item) => {
|
|
369
|
+
const t = item;
|
|
370
|
+
if (!t || typeof t.id !== "string" || typeof t.prompt !== "string" || typeof t.sortOrder !== "number") return [];
|
|
371
|
+
return [{ id: t.id, prompt: t.prompt, sortOrder: t.sortOrder }];
|
|
372
|
+
});
|
|
373
|
+
out.sort((a, b) => a.sortOrder - b.sortOrder);
|
|
374
|
+
return out;
|
|
375
|
+
}
|
|
303
376
|
async function createSession(apiUrl, slug, testerName) {
|
|
304
377
|
try {
|
|
305
378
|
const res = await fetch(`${apiUrl.replace(/\/$/, "")}/api/user-test-sessions`, {
|
|
@@ -310,7 +383,7 @@ async function createSession(apiUrl, slug, testerName) {
|
|
|
310
383
|
if (!res.ok) return null;
|
|
311
384
|
const json = await res.json();
|
|
312
385
|
if (typeof json.sessionId !== "string" || typeof json.clientId !== "string") return null;
|
|
313
|
-
return { sessionId: json.sessionId, clientId: json.clientId };
|
|
386
|
+
return { sessionId: json.sessionId, clientId: json.clientId, tasks: parseTasks(json.tasks) };
|
|
314
387
|
} catch {
|
|
315
388
|
return null;
|
|
316
389
|
}
|
|
@@ -467,20 +540,45 @@ function userTest(options = {}) {
|
|
|
467
540
|
indicatorRoot: null,
|
|
468
541
|
indicatorState: "recording",
|
|
469
542
|
pageHideHandler: null,
|
|
470
|
-
options: { ...merged, apiUrl }
|
|
543
|
+
options: { ...merged, apiUrl },
|
|
544
|
+
tasks: [],
|
|
545
|
+
tasksPanelOpen: readTasksPanelOpen(),
|
|
546
|
+
outsidePointerHandler: null,
|
|
547
|
+
keydownHandler: null
|
|
471
548
|
};
|
|
472
549
|
ctx.setStore(store);
|
|
473
550
|
const onFinish = () => {
|
|
474
551
|
void finishFlow(store, ctx, { showThanks: true });
|
|
475
552
|
};
|
|
553
|
+
const setPanelOpen = (open) => {
|
|
554
|
+
if (store.tasksPanelOpen === open) return;
|
|
555
|
+
store.tasksPanelOpen = open;
|
|
556
|
+
writeTasksPanelOpen(open);
|
|
557
|
+
renderTasksPanel(store);
|
|
558
|
+
};
|
|
559
|
+
const onToggleTasks = () => setPanelOpen(!store.tasksPanelOpen);
|
|
476
560
|
if (!merged.hideIndicator) {
|
|
477
561
|
const host = document.createElement("div");
|
|
478
562
|
host.setAttribute("data-usero-user-test", "true");
|
|
479
563
|
document.body.appendChild(host);
|
|
480
564
|
store.indicator = host;
|
|
481
|
-
store.indicatorRoot = buildIndicator(host, store, onFinish);
|
|
565
|
+
store.indicatorRoot = buildIndicator(host, store, onFinish, onToggleTasks);
|
|
482
566
|
renderIndicatorState(store);
|
|
483
567
|
}
|
|
568
|
+
const outsidePointer = (event) => {
|
|
569
|
+
if (!store.tasksPanelOpen) return;
|
|
570
|
+
const host = store.indicator;
|
|
571
|
+
if (!host) return;
|
|
572
|
+
const path = event.composedPath();
|
|
573
|
+
if (!path.includes(host)) setPanelOpen(false);
|
|
574
|
+
};
|
|
575
|
+
const onKeydown = (event) => {
|
|
576
|
+
if (event.key === "Escape" && store.tasksPanelOpen) setPanelOpen(false);
|
|
577
|
+
};
|
|
578
|
+
store.outsidePointerHandler = outsidePointer;
|
|
579
|
+
store.keydownHandler = onKeydown;
|
|
580
|
+
document.addEventListener("pointerdown", outsidePointer, true);
|
|
581
|
+
document.addEventListener("keydown", onKeydown);
|
|
484
582
|
const pageHide = () => {
|
|
485
583
|
void finishFlow(store, ctx, { showThanks: false });
|
|
486
584
|
};
|
|
@@ -497,6 +595,15 @@ function userTest(options = {}) {
|
|
|
497
595
|
}
|
|
498
596
|
store.sessionId = created.sessionId;
|
|
499
597
|
store.clientId = created.clientId;
|
|
598
|
+
store.tasks = created.tasks;
|
|
599
|
+
if (store.tasks.length > 0 && store.indicatorRoot && !merged.hideIndicator) {
|
|
600
|
+
const bar = store.indicatorRoot.querySelector(".bar");
|
|
601
|
+
const finishBtn = bar?.querySelector(".finish-btn");
|
|
602
|
+
if (bar instanceof HTMLElement && finishBtn instanceof HTMLElement && !bar.querySelector(".tasks-btn")) {
|
|
603
|
+
installTasksToggle(bar, finishBtn, store, onToggleTasks);
|
|
604
|
+
}
|
|
605
|
+
renderTasksPanel(store);
|
|
606
|
+
}
|
|
500
607
|
await startRecording(store, ctx);
|
|
501
608
|
renderIndicatorState(store);
|
|
502
609
|
})();
|
|
@@ -510,6 +617,14 @@ function userTest(options = {}) {
|
|
|
510
617
|
store.pageHideHandler = null;
|
|
511
618
|
}
|
|
512
619
|
stopRecording(store);
|
|
620
|
+
if (store.outsidePointerHandler) {
|
|
621
|
+
document.removeEventListener("pointerdown", store.outsidePointerHandler, true);
|
|
622
|
+
store.outsidePointerHandler = null;
|
|
623
|
+
}
|
|
624
|
+
if (store.keydownHandler) {
|
|
625
|
+
document.removeEventListener("keydown", store.keydownHandler);
|
|
626
|
+
store.keydownHandler = null;
|
|
627
|
+
}
|
|
513
628
|
if (store.indicator && store.indicator.parentNode) {
|
|
514
629
|
store.indicator.parentNode.removeChild(store.indicator);
|
|
515
630
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/types.ts","../../src/plugins/user-test.ts"],"names":[],"mappings":"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","file":"user-test.js","sourcesContent":["// Shared types used by both the vanilla and React entry points.\n// Keep this file framework-free so the vanilla bundle never pulls react.\n\nexport type FeedbackRating = 1 | 2 | 3 | 4\n\nexport interface FeedbackMetadata {\n\tpageUrl: string\n\tpageTitle: string\n\treferrer?: string\n\ttimestamp: number\n}\n\nexport interface ScreenshotData {\n\tfileName: string\n\turl: string\n\tfileSize: number\n\twidth?: number\n\theight?: number\n\tmimeType: string\n}\n\nexport interface FeedbackSubmission {\n\tclientId: string\n\trating?: FeedbackRating\n\tcomment?: string\n\tuserEmail?: string\n\tpageUrl: string\n\tpageTitle: string\n\treferrer?: string\n\tenvironment?: string\n\tscreenshots?: ScreenshotData[]\n\tmetadata?: Record<string, unknown>\n\t// Legacy gzipped + base64-encoded rrweb event stream. The pre-chunked\n\t// session-replay plugin attached this on submit. The chunked-upload\n\t// plugin (>= 0.4.0) does NOT set this — it ships events out-of-band\n\t// via the chunk endpoints and points at the resulting session replay\n\t// via `sessionReplayId` + `replayOffsetMs`. Kept on the wire one\n\t// release for backward compat with old SaaS deployments that only\n\t// know how to ingest the legacy field.\n\treplayEvents?: string\n\t// Pointer to a SessionReplay row created by the session-replay\n\t// plugin's chunked-upload pipeline, plus the offset within that\n\t// recording at submit time so the dashboard can deep-link.\n\tsessionReplayId?: string\n\treplayOffsetMs?: number\n}\n\nexport interface FeedbackData {\n\trating?: FeedbackRating\n\tcomment?: string\n\tuserEmail?: string\n\tscreenshots?: ScreenshotData[]\n\tmetadata: FeedbackMetadata\n}\n\n// Customer-supplied identity for the session-replay / identify pipeline.\n// Use the declarative form: pass `user` on the React widget (the SDK\n// diffs and auto-fires identify), or supply `getUser` on the vanilla\n// init (the SDK polls it at session start). An imperative `identify()`\n// call exists as an escape hatch only and is intentionally not\n// documented as the headline API.\nexport type UseroUserTraitValue = string | number | boolean | null\nexport type UseroUserTraits = Record<string, UseroUserTraitValue>\nexport interface UseroUser {\n\tid: string\n\temail?: string\n\tdisplayName?: string\n\ttraits?: UseroUserTraits\n}\n\nexport type WidgetPosition = 'right' | 'left'\n\nexport interface WidgetTheme {\n\tprimary: string\n\tbackground: string\n\ttext: string\n\tborder: string\n\tshadow: string\n}\n\n// Forward-declared to avoid a circular import. The plugin module imports\n// FeedbackSubmission from this file, so we can't import UseroPlugin back\n// up here without a cycle. Keeping the prop typed as an opaque object with\n// the minimal shape the widget actually inspects works fine for consumers.\nexport interface FeedbackWidgetProps {\n\tclientId: string\n\tposition?: WidgetPosition\n\ttheme?: Partial<WidgetTheme>\n\ttitle?: string\n\tplaceholder?: string\n\tshowEmailOption?: boolean\n\tshowScreenshotOption?: boolean\n\tenvironment?: string\n\tbaseUrl?: string\n\tmetadata?: Record<string, unknown>\n\tplugins?: ReadonlyArray<import('./plugin').UseroPlugin>\n\t// Declarative identity. React: pass the current user (or null on\n\t// logout) and the SDK auto-fires identify when the resolved id\n\t// transitions. Vanilla: pass a getter so the SDK can resolve user at\n\t// session start / chunk boundaries. Pass at most one. The SDK never\n\t// invokes both.\n\tuser?: UseroUser | null\n\tgetUser?: () => UseroUser | null | undefined\n\tonSubmit?: (feedback: FeedbackData) => void\n\tonError?: (error: Error) => void\n\tonOpen?: () => void\n\tonClose?: () => void\n}\n\nexport interface SubmissionResponse {\n\tsuccess: boolean\n\terror?: string\n\tid?: string\n\tmessage?: string\n\tdata?: unknown\n}\n\nexport const EMOJI_MAP: Record<FeedbackRating, string> = {\n\t1: '😞',\n\t2: '😐',\n\t3: '😊',\n\t4: '🤩',\n}\n\nexport const RATING_LABELS: Record<FeedbackRating, string> = {\n\t1: 'Needs work',\n\t2: \"It's okay\",\n\t3: 'Pretty good',\n\t4: 'Amazing!',\n}\n\nexport const EMOJI_BACKGROUNDS: Record<FeedbackRating, string> = {\n\t1: 'linear-gradient(135deg,#ff6b6b14,#ff6b6b1f)',\n\t2: 'linear-gradient(135deg,#9ca3af0f,#9ca3af1a)',\n\t3: 'linear-gradient(135deg,#3b82f614,#3b82f61f)',\n\t4: 'linear-gradient(135deg,#f59e0b14,#f59e0b1f)',\n}\n\nexport const DEFAULT_API_URL = 'https://usero.io'\n\nexport const DEFAULT_THEME: WidgetTheme = {\n\tprimary: '#2563eb',\n\tbackground: '#ffffff',\n\ttext: '#374151',\n\tborder: '#e5e7eb',\n\tshadow:\n\t\t'0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',\n}\n\nexport const DARK_THEME: WidgetTheme = {\n\tprimary: '#2563eb',\n\tbackground: '#1f2937',\n\ttext: '#f9fafb',\n\tborder: '#374151',\n\tshadow:\n\t\t'0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2)',\n}\n\nexport function mergeTheme(customTheme: Partial<WidgetTheme> = {}): WidgetTheme {\n\treturn { ...DEFAULT_THEME, ...customTheme }\n}\n","// User-testing-mode plugin for the Usero widget.\n//\n// Activates ONLY when the host page URL carries `?usero_test=<slug>`. When\n// active, it:\n// 1. Creates a UserTestSession on the SaaS side via\n// POST /api/user-test-sessions\n// 2. Starts a MediaRecorder on the user's microphone (Opus in WebM) and\n// ships chunks every `chunkSeconds` to\n// PUT /api/user-test-sessions/:id/chunk?index=N\n// 3. Renders a small floating \"recording\" indicator with a Finish button\n// bottom-center so the tester can stop on their own.\n// 4. On Finish (or `pagehide`), flushes any buffered chunks and calls\n// POST /api/user-test-sessions/:id/finalise.\n//\n// Mic-permission denied is non-fatal: the session is still created (the\n// session-replay plugin keeps recording in parallel) and the indicator\n// shows a \"no audio\" hint. The SaaS side detects audio presence from the\n// first chunk; if no chunks land, hasAudio stays false there too.\n//\n// Bundle hygiene: this module is a subpath export and does NOT depend on\n// rrweb or any other heavy dep — just the native MediaRecorder API. The\n// floating UI is shadow-DOM scoped so host page CSS can't leak in.\n//\n// Privacy: only the microphone is recorded. We never read DOM text, never\n// touch the camera, and never persist audio in the browser past the\n// IndexedDB fallback (cleared on successful upload).\n\nimport type { UseroPlugin, PluginContext } from '../plugin'\nimport { DEFAULT_API_URL } from '../types'\n\nexport interface UserTestOptions {\n\t// URL query param the welcome page appends to redirect testers back.\n\t// Default `usero_test`. Match SaaS side if you ever change it.\n\tqueryParam?: string\n\t// Audio chunk length in seconds. Smaller = more partial-data resilience\n\t// but more requests. Default 30.\n\tchunkSeconds?: number\n\t// API origin. Override for self-hosted or local dev. Defaults to the\n\t// shared SDK constant (https://usero.io).\n\tapiUrl?: string\n\t// Override the tester-name shown on the SaaS side. Normally the welcome\n\t// page collects this and stores it in localStorage; the plugin reads it\n\t// from there. This option lets a host site bypass that.\n\ttesterName?: string\n\t// Hide the floating indicator. The plugin still records and finalises\n\t// on `pagehide`, but the tester gets no on-page UI. Useful if the host\n\t// page wants to render its own.\n\thideIndicator?: boolean\n}\n\ninterface RecorderStore {\n\tcancelled: boolean\n\tslug: string\n\tsessionId: string | null\n\tclientId: string | null\n\trecorder: MediaRecorder | null\n\tstream: MediaStream | null\n\tchunkIndex: number\n\tuploadQueue: Promise<void>\n\tpendingUploads: number\n\tstartedAt: number\n\tindicator: HTMLElement | null\n\tindicatorRoot: ShadowRoot | null\n\tindicatorState: 'recording' | 'finishing' | 'done' | 'no-audio' | 'error'\n\tpageHideHandler: (() => void) | null\n\toptions: Required<UserTestOptions>\n}\n\nconst DEFAULT_OPTIONS: Required<Omit<UserTestOptions, 'testerName' | 'apiUrl'>> & {\n\ttesterName: string\n\tapiUrl: string\n} = {\n\tqueryParam: 'usero_test',\n\tchunkSeconds: 30,\n\tapiUrl: DEFAULT_API_URL,\n\ttesterName: '',\n\thideIndicator: false,\n}\n\nconst TESTER_NAME_STORAGE_KEY = 'usero:user-test:tester-name'\nconst IDB_NAME = 'usero-user-test'\nconst IDB_STORE = 'pending-chunks'\n\ninterface PendingChunk {\n\tid: string\n\tsessionId: string\n\tapiUrl: string\n\tchunkIndex: number\n\tblob: Blob\n\tcreatedAt: number\n}\n\nfunction readTesterName(override: string): string | undefined {\n\tif (override) return override\n\ttry {\n\t\tconst stored = window.localStorage?.getItem(TESTER_NAME_STORAGE_KEY)\n\t\tif (stored && stored.trim()) return stored.trim().slice(0, 120)\n\t} catch {\n\t\t// Storage access can throw in some sandboxed iframes — ignore.\n\t}\n\treturn undefined\n}\n\nfunction getTestSlug(queryParam: string): string | null {\n\tif (typeof window === 'undefined' || typeof window.location === 'undefined') return null\n\ttry {\n\t\tconst params = new URLSearchParams(window.location.search)\n\t\tconst slug = params.get(queryParam)\n\t\tif (!slug) return null\n\t\tconst cleaned = slug.trim().slice(0, 64)\n\t\tif (!/^[a-z0-9-]+$/i.test(cleaned)) return null\n\t\treturn cleaned\n\t} catch {\n\t\treturn null\n\t}\n}\n\nfunction isMediaRecorderSupported(): boolean {\n\treturn typeof window !== 'undefined' && typeof window.MediaRecorder !== 'undefined' && typeof navigator !== 'undefined' && !!navigator.mediaDevices?.getUserMedia\n}\n\nfunction pickMimeType(): string | undefined {\n\tconst candidates = ['audio/webm;codecs=opus', 'audio/webm', 'audio/ogg;codecs=opus', 'audio/mp4']\n\tfor (const candidate of candidates) {\n\t\tif (typeof MediaRecorder !== 'undefined' && MediaRecorder.isTypeSupported?.(candidate)) {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn undefined\n}\n\n// IndexedDB helpers. Best-effort, never throw upstream — if IDB is missing\n// or quota-blown we just lose offline resilience but the live upload path\n// still runs.\nfunction idbOpen(): Promise<IDBDatabase | null> {\n\treturn new Promise(resolve => {\n\t\tif (typeof indexedDB === 'undefined') {\n\t\t\tresolve(null)\n\t\t\treturn\n\t\t}\n\t\ttry {\n\t\t\tconst req = indexedDB.open(IDB_NAME, 1)\n\t\t\treq.onupgradeneeded = (): void => {\n\t\t\t\tconst db = req.result\n\t\t\t\tif (!db.objectStoreNames.contains(IDB_STORE)) {\n\t\t\t\t\tdb.createObjectStore(IDB_STORE, { keyPath: 'id' })\n\t\t\t\t}\n\t\t\t}\n\t\t\treq.onsuccess = (): void => resolve(req.result)\n\t\t\treq.onerror = (): void => resolve(null)\n\t\t} catch {\n\t\t\tresolve(null)\n\t\t}\n\t})\n}\n\nasync function idbStashChunk(chunk: PendingChunk): Promise<void> {\n\tconst db = await idbOpen()\n\tif (!db) return\n\tawait new Promise<void>(resolve => {\n\t\ttry {\n\t\t\tconst tx = db.transaction(IDB_STORE, 'readwrite')\n\t\t\ttx.objectStore(IDB_STORE).put(chunk)\n\t\t\ttx.oncomplete = (): void => resolve()\n\t\t\ttx.onerror = (): void => resolve()\n\t\t\ttx.onabort = (): void => resolve()\n\t\t} catch {\n\t\t\tresolve()\n\t\t}\n\t})\n\tdb.close()\n}\n\nasync function idbDeleteChunk(id: string): Promise<void> {\n\tconst db = await idbOpen()\n\tif (!db) return\n\tawait new Promise<void>(resolve => {\n\t\ttry {\n\t\t\tconst tx = db.transaction(IDB_STORE, 'readwrite')\n\t\t\ttx.objectStore(IDB_STORE).delete(id)\n\t\t\ttx.oncomplete = (): void => resolve()\n\t\t\ttx.onerror = (): void => resolve()\n\t\t\ttx.onabort = (): void => resolve()\n\t\t} catch {\n\t\t\tresolve()\n\t\t}\n\t})\n\tdb.close()\n}\n\nasync function idbListChunks(sessionId: string): Promise<PendingChunk[]> {\n\tconst db = await idbOpen()\n\tif (!db) return []\n\tconst items = await new Promise<PendingChunk[]>(resolve => {\n\t\ttry {\n\t\t\tconst tx = db.transaction(IDB_STORE, 'readonly')\n\t\t\tconst req = tx.objectStore(IDB_STORE).getAll()\n\t\t\treq.onsuccess = (): void => {\n\t\t\t\tconst all = (req.result as PendingChunk[]) ?? []\n\t\t\t\tresolve(all.filter(c => c.sessionId === sessionId))\n\t\t\t}\n\t\t\treq.onerror = (): void => resolve([])\n\t\t} catch {\n\t\t\tresolve([])\n\t\t}\n\t})\n\tdb.close()\n\treturn items\n}\n\nasync function uploadChunkWithRetry(\n\tapiUrl: string,\n\tsessionId: string,\n\tindex: number,\n\tblob: Blob,\n\tlogger: PluginContext['logger'],\n\tmaxAttempts = 5,\n): Promise<boolean> {\n\tconst url = `${apiUrl.replace(/\\/$/, '')}/api/user-test-sessions/${encodeURIComponent(sessionId)}/chunk?index=${index}`\n\tlet attempt = 0\n\twhile (attempt < maxAttempts) {\n\t\ttry {\n\t\t\tconst res = await fetch(url, {\n\t\t\t\tmethod: 'PUT',\n\t\t\t\tbody: blob,\n\t\t\t\theaders: { 'Content-Type': blob.type || 'audio/webm' },\n\t\t\t\tkeepalive: blob.size <= 60 * 1024, // browsers cap keepalive bodies\n\t\t\t})\n\t\t\tif (res.ok) return true\n\t\t\t// 4xx (other than 413) won't get better with retries; bail early.\n\t\t\tif (res.status >= 400 && res.status < 500 && res.status !== 408 && res.status !== 429) {\n\t\t\t\tlogger.error(`chunk ${index} rejected with ${res.status}`)\n\t\t\t\treturn false\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tlogger.warn(`chunk ${index} upload attempt ${attempt + 1} failed`, err)\n\t\t}\n\t\tattempt += 1\n\t\tconst backoff = Math.min(15000, 500 * 2 ** attempt) + Math.floor(Math.random() * 250)\n\t\tawait new Promise(resolve => setTimeout(resolve, backoff))\n\t}\n\treturn false\n}\n\nfunction buildIndicator(host: HTMLElement, store: RecorderStore, onFinish: () => void): ShadowRoot {\n\tconst root = host.attachShadow({ mode: 'closed' })\n\tconst style = document.createElement('style')\n\t// Keep this CSS small. Pulse is the only animation. Bottom-center,\n\t// safe-area aware, semi-transparent so it doesn't block the page.\n\tstyle.textContent = `\n\t\t:host { all: initial; }\n\t\t.bar {\n\t\t\tposition: fixed;\n\t\t\tbottom: calc(env(safe-area-inset-bottom, 0px) + 16px);\n\t\t\tleft: 50%;\n\t\t\ttransform: translateX(-50%);\n\t\t\tdisplay: inline-flex;\n\t\t\talign-items: center;\n\t\t\tgap: 10px;\n\t\t\tpadding: 8px 14px 8px 12px;\n\t\t\tbackground: rgba(17, 17, 17, 0.78);\n\t\t\tcolor: #fff;\n\t\t\tborder-radius: 999px;\n\t\t\tfont-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", system-ui, sans-serif;\n\t\t\tfont-size: 13px;\n\t\t\tline-height: 1;\n\t\t\tbox-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);\n\t\t\tbackdrop-filter: blur(8px);\n\t\t\t-webkit-backdrop-filter: blur(8px);\n\t\t\tz-index: 2147483646;\n\t\t\tmax-width: calc(100vw - 32px);\n\t\t}\n\t\t.dot {\n\t\t\twidth: 8px; height: 8px; border-radius: 50%;\n\t\t\tbackground: #ef4444;\n\t\t\tbox-shadow: 0 0 0 0 rgba(239, 68, 68, 0.6);\n\t\t\tanimation: pulse 1.6s ease-out infinite;\n\t\t}\n\t\t.dot[data-state=\"no-audio\"] { background: #fbbf24; animation: none; }\n\t\t.dot[data-state=\"finishing\"] { background: #fbbf24; animation: none; }\n\t\t.dot[data-state=\"done\"] { background: #10b981; animation: none; }\n\t\t.dot[data-state=\"error\"] { background: #ef4444; animation: none; }\n\t\t.label { font-weight: 500; letter-spacing: 0.01em; }\n\t\t.spacer { width: 1px; height: 16px; background: rgba(255,255,255,0.18); margin: 0 2px; }\n\t\t.btn {\n\t\t\tappearance: none; border: 0; background: rgba(255,255,255,0.12);\n\t\t\tcolor: #fff; font: inherit; font-weight: 600;\n\t\t\tpadding: 6px 12px; border-radius: 999px; cursor: pointer;\n\t\t\ttransition: background 0.15s ease;\n\t\t}\n\t\t.btn:hover { background: rgba(255,255,255,0.22); }\n\t\t.btn:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }\n\t\t.btn[disabled] { opacity: 0.5; cursor: progress; }\n\t\t.thanks {\n\t\t\tposition: fixed; inset: 0;\n\t\t\tdisplay: grid; place-items: center;\n\t\t\tbackground: rgba(15, 15, 17, 0.78);\n\t\t\tbackdrop-filter: blur(6px);\n\t\t\t-webkit-backdrop-filter: blur(6px);\n\t\t\tcolor: #fff;\n\t\t\tfont-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", system-ui, sans-serif;\n\t\t\tz-index: 2147483647;\n\t\t\tpadding: 24px;\n\t\t\ttext-align: center;\n\t\t}\n\t\t.thanks-card {\n\t\t\tbackground: #fff; color: #111;\n\t\t\tborder-radius: 16px; padding: 28px 24px;\n\t\t\tmax-width: 360px; width: 100%;\n\t\t\tbox-shadow: 0 20px 50px rgba(0,0,0,0.25);\n\t\t}\n\t\t.thanks h2 { margin: 0 0 8px; font-size: 20px; }\n\t\t.thanks p { margin: 0; font-size: 14px; line-height: 1.45; color: #4b5563; }\n\t\t.thanks .check {\n\t\t\twidth: 44px; height: 44px; border-radius: 50%;\n\t\t\tbackground: #10b981; color: #fff;\n\t\t\tdisplay: grid; place-items: center;\n\t\t\tmargin: 0 auto 12px;\n\t\t\tfont-size: 22px;\n\t\t}\n\t\t@keyframes pulse {\n\t\t\t0% { box-shadow: 0 0 0 0 rgba(239,68,68,0.55); }\n\t\t\t70% { box-shadow: 0 0 0 10px rgba(239,68,68,0); }\n\t\t\t100% { box-shadow: 0 0 0 0 rgba(239,68,68,0); }\n\t\t}\n\t\t@media (prefers-reduced-motion: reduce) {\n\t\t\t.dot { animation: none; }\n\t\t}\n\t`\n\tconst bar = document.createElement('div')\n\tbar.className = 'bar'\n\tbar.setAttribute('role', 'status')\n\tbar.setAttribute('aria-live', 'polite')\n\n\tconst dot = document.createElement('span')\n\tdot.className = 'dot'\n\tdot.setAttribute('data-state', store.indicatorState)\n\n\tconst label = document.createElement('span')\n\tlabel.className = 'label'\n\tlabel.textContent = 'Recording'\n\n\tconst spacer = document.createElement('span')\n\tspacer.className = 'spacer'\n\n\tconst btn = document.createElement('button')\n\tbtn.type = 'button'\n\tbtn.className = 'btn'\n\tbtn.textContent = 'Finish'\n\tbtn.addEventListener('click', onFinish)\n\n\tbar.appendChild(dot)\n\tbar.appendChild(label)\n\tbar.appendChild(spacer)\n\tbar.appendChild(btn)\n\n\troot.appendChild(style)\n\troot.appendChild(bar)\n\treturn root\n}\n\nfunction renderIndicatorState(store: RecorderStore): void {\n\tconst root = store.indicatorRoot\n\tif (!root) return\n\tconst dot = root.querySelector('.dot')\n\tconst label = root.querySelector('.label')\n\tconst btn = root.querySelector<HTMLButtonElement>('.btn')\n\tif (!(dot instanceof HTMLElement) || !(label instanceof HTMLElement) || !btn) return\n\tdot.setAttribute('data-state', store.indicatorState)\n\tswitch (store.indicatorState) {\n\t\tcase 'recording':\n\t\t\tlabel.textContent = 'Recording'\n\t\t\tbtn.textContent = 'Finish'\n\t\t\tbtn.disabled = false\n\t\t\tbreak\n\t\tcase 'no-audio':\n\t\t\tlabel.textContent = 'No mic, replay only'\n\t\t\tbtn.textContent = 'Finish'\n\t\t\tbtn.disabled = false\n\t\t\tbreak\n\t\tcase 'finishing':\n\t\t\tlabel.textContent = 'Saving'\n\t\t\tbtn.textContent = 'Saving'\n\t\t\tbtn.disabled = true\n\t\t\tbreak\n\t\tcase 'done':\n\t\t\tlabel.textContent = 'Saved'\n\t\t\tbtn.textContent = 'Done'\n\t\t\tbtn.disabled = true\n\t\t\tbreak\n\t\tcase 'error':\n\t\t\tlabel.textContent = 'Save failed'\n\t\t\tbtn.textContent = 'Retry'\n\t\t\tbtn.disabled = false\n\t\t\tbreak\n\t}\n}\n\nfunction showThanksScreen(root: ShadowRoot): void {\n\tconst overlay = document.createElement('div')\n\toverlay.className = 'thanks'\n\toverlay.innerHTML = `\n\t\t<div class=\"thanks-card\">\n\t\t\t<div class=\"check\" aria-hidden=\"true\">✓</div>\n\t\t\t<h2>Thanks for testing</h2>\n\t\t\t<p>Your session was saved. You can close this tab.</p>\n\t\t</div>\n\t`\n\troot.appendChild(overlay)\n}\n\nasync function createSession(\n\tapiUrl: string,\n\tslug: string,\n\ttesterName: string | undefined,\n): Promise<{ sessionId: string; clientId: string } | null> {\n\ttry {\n\t\tconst res = await fetch(`${apiUrl.replace(/\\/$/, '')}/api/user-test-sessions`, {\n\t\t\tmethod: 'POST',\n\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\tbody: JSON.stringify({ slug, ...(testerName ? { testerName } : {}) }),\n\t\t})\n\t\tif (!res.ok) return null\n\t\tconst json = (await res.json()) as { sessionId?: unknown; clientId?: unknown }\n\t\tif (typeof json.sessionId !== 'string' || typeof json.clientId !== 'string') return null\n\t\treturn { sessionId: json.sessionId, clientId: json.clientId }\n\t} catch {\n\t\treturn null\n\t}\n}\n\nasync function finaliseSession(\n\tapiUrl: string,\n\tsessionId: string,\n\tdurationSeconds: number,\n): Promise<boolean> {\n\ttry {\n\t\tconst res = await fetch(`${apiUrl.replace(/\\/$/, '')}/api/user-test-sessions/${encodeURIComponent(sessionId)}/finalise`, {\n\t\t\tmethod: 'POST',\n\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\tbody: JSON.stringify({ durationSeconds: Math.max(0, Math.round(durationSeconds)) }),\n\t\t\tkeepalive: true,\n\t\t})\n\t\treturn res.ok\n\t} catch {\n\t\treturn false\n\t}\n}\n\nasync function flushPendingFromIdb(store: RecorderStore, ctx: PluginContext): Promise<void> {\n\tif (!store.sessionId) return\n\tconst pending = await idbListChunks(store.sessionId)\n\tfor (const chunk of pending) {\n\t\tconst ok = await uploadChunkWithRetry(chunk.apiUrl, chunk.sessionId, chunk.chunkIndex, chunk.blob, ctx.logger, 3)\n\t\tif (ok) await idbDeleteChunk(chunk.id)\n\t}\n}\n\nfunction enqueueChunk(store: RecorderStore, ctx: PluginContext, blob: Blob): void {\n\tif (store.cancelled || !store.sessionId || blob.size === 0) return\n\tconst index = store.chunkIndex\n\tstore.chunkIndex += 1\n\tstore.pendingUploads += 1\n\tconst sessionId = store.sessionId\n\tconst apiUrl = store.options.apiUrl\n\n\tstore.uploadQueue = store.uploadQueue.then(async () => {\n\t\tconst ok = await uploadChunkWithRetry(apiUrl, sessionId, index, blob, ctx.logger)\n\t\tif (!ok) {\n\t\t\tctx.logger.warn(`chunk ${index} stashed for offline retry`)\n\t\t\tawait idbStashChunk({\n\t\t\t\tid: `${sessionId}:${index}:${Date.now()}`,\n\t\t\t\tsessionId,\n\t\t\t\tapiUrl,\n\t\t\t\tchunkIndex: index,\n\t\t\t\tblob,\n\t\t\t\tcreatedAt: Date.now(),\n\t\t\t})\n\t\t}\n\t\tstore.pendingUploads -= 1\n\t})\n}\n\nasync function startRecording(store: RecorderStore, ctx: PluginContext): Promise<void> {\n\tif (!isMediaRecorderSupported()) {\n\t\tctx.logger.warn('MediaRecorder not supported, continuing without audio')\n\t\tstore.indicatorState = 'no-audio'\n\t\trenderIndicatorState(store)\n\t\treturn\n\t}\n\tlet stream: MediaStream\n\ttry {\n\t\tstream = await navigator.mediaDevices.getUserMedia({ audio: true })\n\t} catch (err) {\n\t\tctx.logger.warn('mic permission denied or unavailable', err)\n\t\tstore.indicatorState = 'no-audio'\n\t\trenderIndicatorState(store)\n\t\treturn\n\t}\n\tstore.stream = stream\n\tconst mimeType = pickMimeType()\n\tlet recorder: MediaRecorder\n\ttry {\n\t\trecorder = mimeType ? new MediaRecorder(stream, { mimeType }) : new MediaRecorder(stream)\n\t} catch (err) {\n\t\tctx.logger.error('MediaRecorder construction failed', err)\n\t\tstream.getTracks().forEach(t => t.stop())\n\t\tstore.stream = null\n\t\tstore.indicatorState = 'no-audio'\n\t\trenderIndicatorState(store)\n\t\treturn\n\t}\n\tstore.recorder = recorder\n\trecorder.addEventListener('dataavailable', event => {\n\t\tif (event.data && event.data.size > 0) {\n\t\t\tenqueueChunk(store, ctx, event.data)\n\t\t}\n\t})\n\trecorder.addEventListener('error', event => {\n\t\tctx.logger.error('MediaRecorder error', event)\n\t})\n\t// `timeslice` makes the recorder emit a self-contained chunk every N ms.\n\trecorder.start(store.options.chunkSeconds * 1000)\n}\n\nfunction stopRecording(store: RecorderStore): void {\n\tconst recorder = store.recorder\n\tif (recorder && recorder.state !== 'inactive') {\n\t\ttry {\n\t\t\trecorder.requestData()\n\t\t} catch {\n\t\t\t// requestData throws if state is invalid; ignore.\n\t\t}\n\t\ttry {\n\t\t\trecorder.stop()\n\t\t} catch {\n\t\t\t// already stopped\n\t\t}\n\t}\n\tstore.recorder = null\n\tif (store.stream) {\n\t\tstore.stream.getTracks().forEach(t => t.stop())\n\t\tstore.stream = null\n\t}\n}\n\nasync function finishFlow(store: RecorderStore, ctx: PluginContext, opts: { showThanks: boolean }): Promise<void> {\n\tif (store.cancelled) return\n\tif (store.indicatorState === 'finishing' || store.indicatorState === 'done') return\n\tstore.indicatorState = 'finishing'\n\trenderIndicatorState(store)\n\n\tstopRecording(store)\n\t// Wait for the queued uploads to drain. Each upload already has its own\n\t// retry/backoff; this just lets them finish before finalise fires.\n\tawait store.uploadQueue\n\tawait flushPendingFromIdb(store, ctx)\n\n\tconst durationSeconds = (Date.now() - store.startedAt) / 1000\n\tif (store.sessionId) {\n\t\tconst ok = await finaliseSession(store.options.apiUrl, store.sessionId, durationSeconds)\n\t\tstore.indicatorState = ok ? 'done' : 'error'\n\t} else {\n\t\tstore.indicatorState = 'error'\n\t}\n\trenderIndicatorState(store)\n\n\tif (opts.showThanks && store.indicatorRoot && store.indicatorState === 'done') {\n\t\tshowThanksScreen(store.indicatorRoot)\n\t}\n}\n\nexport function userTest(options: UserTestOptions = {}): UseroPlugin {\n\tconst merged: Required<UserTestOptions> = {\n\t\tqueryParam: options.queryParam ?? DEFAULT_OPTIONS.queryParam,\n\t\tchunkSeconds: options.chunkSeconds ?? DEFAULT_OPTIONS.chunkSeconds,\n\t\tapiUrl: options.apiUrl ?? DEFAULT_OPTIONS.apiUrl,\n\t\ttesterName: options.testerName ?? DEFAULT_OPTIONS.testerName,\n\t\thideIndicator: options.hideIndicator ?? DEFAULT_OPTIONS.hideIndicator,\n\t}\n\n\treturn {\n\t\tname: 'user-test',\n\t\tonInit(ctx) {\n\t\t\tif (typeof window === 'undefined' || typeof document === 'undefined') return\n\t\t\tconst slug = getTestSlug(merged.queryParam)\n\t\t\tif (!slug) return\n\n\t\t\tconst apiUrl = merged.apiUrl || ctx.baseUrl || DEFAULT_API_URL\n\t\t\tconst store: RecorderStore = {\n\t\t\t\tcancelled: false,\n\t\t\t\tslug,\n\t\t\t\tsessionId: null,\n\t\t\t\tclientId: null,\n\t\t\t\trecorder: null,\n\t\t\t\tstream: null,\n\t\t\t\tchunkIndex: 0,\n\t\t\t\tuploadQueue: Promise.resolve(),\n\t\t\t\tpendingUploads: 0,\n\t\t\t\tstartedAt: Date.now(),\n\t\t\t\tindicator: null,\n\t\t\t\tindicatorRoot: null,\n\t\t\t\tindicatorState: 'recording',\n\t\t\t\tpageHideHandler: null,\n\t\t\t\toptions: { ...merged, apiUrl },\n\t\t\t}\n\t\t\tctx.setStore(store)\n\n\t\t\tconst onFinish = (): void => {\n\t\t\t\tvoid finishFlow(store, ctx, { showThanks: true })\n\t\t\t}\n\n\t\t\tif (!merged.hideIndicator) {\n\t\t\t\tconst host = document.createElement('div')\n\t\t\t\thost.setAttribute('data-usero-user-test', 'true')\n\t\t\t\tdocument.body.appendChild(host)\n\t\t\t\tstore.indicator = host\n\t\t\t\tstore.indicatorRoot = buildIndicator(host, store, onFinish)\n\t\t\t\trenderIndicatorState(store)\n\t\t\t}\n\n\t\t\tconst pageHide = (): void => {\n\t\t\t\t// Best-effort flush + finalise. We don't await here; the browser\n\t\t\t\t// is shutting the page down. `keepalive: true` on finalise lets\n\t\t\t\t// the request race the unload.\n\t\t\t\tvoid finishFlow(store, ctx, { showThanks: false })\n\t\t\t}\n\t\t\tstore.pageHideHandler = pageHide\n\t\t\twindow.addEventListener('pagehide', pageHide)\n\n\t\t\tvoid (async (): Promise<void> => {\n\t\t\t\tconst created = await createSession(apiUrl, slug, readTesterName(merged.testerName))\n\t\t\t\tif (store.cancelled) return\n\t\t\t\tif (!created) {\n\t\t\t\t\tctx.logger.error('failed to create user-test session')\n\t\t\t\t\tstore.indicatorState = 'error'\n\t\t\t\t\trenderIndicatorState(store)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tstore.sessionId = created.sessionId\n\t\t\t\tstore.clientId = created.clientId\n\t\t\t\tawait startRecording(store, ctx)\n\t\t\t\trenderIndicatorState(store)\n\t\t\t})()\n\t\t},\n\t\tonDestroy(ctx) {\n\t\t\tconst store = ctx.getStore<RecorderStore>()\n\t\t\tif (!store) return\n\t\t\tstore.cancelled = true\n\t\t\tif (store.pageHideHandler) {\n\t\t\t\twindow.removeEventListener('pagehide', store.pageHideHandler)\n\t\t\t\tstore.pageHideHandler = null\n\t\t\t}\n\t\t\tstopRecording(store)\n\t\t\tif (store.indicator && store.indicator.parentNode) {\n\t\t\t\tstore.indicator.parentNode.removeChild(store.indicator)\n\t\t\t}\n\t\t\tstore.indicator = null\n\t\t\tstore.indicatorRoot = null\n\t\t},\n\t}\n}\n\n// Internal helpers exposed for tests only. Not part of the public API.\nexport const __test__ = { getTestSlug, pickMimeType, isMediaRecorderSupported }\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/types.ts","../../src/plugins/user-test.ts"],"names":[],"mappings":"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","file":"user-test.js","sourcesContent":["// Shared types used by both the vanilla and React entry points.\n// Keep this file framework-free so the vanilla bundle never pulls react.\n\nexport type FeedbackRating = 1 | 2 | 3 | 4\n\nexport interface FeedbackMetadata {\n\tpageUrl: string\n\tpageTitle: string\n\treferrer?: string\n\ttimestamp: number\n}\n\nexport interface ScreenshotData {\n\tfileName: string\n\turl: string\n\tfileSize: number\n\twidth?: number\n\theight?: number\n\tmimeType: string\n}\n\nexport interface FeedbackSubmission {\n\tclientId: string\n\trating?: FeedbackRating\n\tcomment?: string\n\tuserEmail?: string\n\tpageUrl: string\n\tpageTitle: string\n\treferrer?: string\n\tenvironment?: string\n\tscreenshots?: ScreenshotData[]\n\tmetadata?: Record<string, unknown>\n\t// Legacy gzipped + base64-encoded rrweb event stream. The pre-chunked\n\t// session-replay plugin attached this on submit. The chunked-upload\n\t// plugin (>= 0.4.0) does NOT set this — it ships events out-of-band\n\t// via the chunk endpoints and points at the resulting session replay\n\t// via `sessionReplayId` + `replayOffsetMs`. Kept on the wire one\n\t// release for backward compat with old SaaS deployments that only\n\t// know how to ingest the legacy field.\n\treplayEvents?: string\n\t// Pointer to a SessionReplay row created by the session-replay\n\t// plugin's chunked-upload pipeline, plus the offset within that\n\t// recording at submit time so the dashboard can deep-link.\n\tsessionReplayId?: string\n\treplayOffsetMs?: number\n}\n\nexport interface FeedbackData {\n\trating?: FeedbackRating\n\tcomment?: string\n\tuserEmail?: string\n\tscreenshots?: ScreenshotData[]\n\tmetadata: FeedbackMetadata\n}\n\n// Customer-supplied identity for the session-replay / identify pipeline.\n// Use the declarative form: pass `user` on the React widget (the SDK\n// diffs and auto-fires identify), or supply `getUser` on the vanilla\n// init (the SDK polls it at session start). An imperative `identify()`\n// call exists as an escape hatch only and is intentionally not\n// documented as the headline API.\nexport type UseroUserTraitValue = string | number | boolean | null\nexport type UseroUserTraits = Record<string, UseroUserTraitValue>\nexport interface UseroUser {\n\tid: string\n\temail?: string\n\tdisplayName?: string\n\ttraits?: UseroUserTraits\n}\n\nexport type WidgetPosition = 'right' | 'left'\n\nexport interface WidgetTheme {\n\tprimary: string\n\tbackground: string\n\ttext: string\n\tborder: string\n\tshadow: string\n}\n\n// Forward-declared to avoid a circular import. The plugin module imports\n// FeedbackSubmission from this file, so we can't import UseroPlugin back\n// up here without a cycle. Keeping the prop typed as an opaque object with\n// the minimal shape the widget actually inspects works fine for consumers.\nexport interface FeedbackWidgetProps {\n\tclientId: string\n\tposition?: WidgetPosition\n\ttheme?: Partial<WidgetTheme>\n\ttitle?: string\n\tplaceholder?: string\n\tshowEmailOption?: boolean\n\tshowScreenshotOption?: boolean\n\tenvironment?: string\n\tbaseUrl?: string\n\tmetadata?: Record<string, unknown>\n\tplugins?: ReadonlyArray<import('./plugin').UseroPlugin>\n\t// Declarative identity. React: pass the current user (or null on\n\t// logout) and the SDK auto-fires identify when the resolved id\n\t// transitions. Vanilla: pass a getter so the SDK can resolve user at\n\t// session start / chunk boundaries. Pass at most one. The SDK never\n\t// invokes both.\n\tuser?: UseroUser | null\n\tgetUser?: () => UseroUser | null | undefined\n\tonSubmit?: (feedback: FeedbackData) => void\n\tonError?: (error: Error) => void\n\tonOpen?: () => void\n\tonClose?: () => void\n}\n\nexport interface SubmissionResponse {\n\tsuccess: boolean\n\terror?: string\n\tid?: string\n\tmessage?: string\n\tdata?: unknown\n}\n\nexport const EMOJI_MAP: Record<FeedbackRating, string> = {\n\t1: '😞',\n\t2: '😐',\n\t3: '😊',\n\t4: '🤩',\n}\n\nexport const RATING_LABELS: Record<FeedbackRating, string> = {\n\t1: 'Needs work',\n\t2: \"It's okay\",\n\t3: 'Pretty good',\n\t4: 'Amazing!',\n}\n\nexport const EMOJI_BACKGROUNDS: Record<FeedbackRating, string> = {\n\t1: 'linear-gradient(135deg,#ff6b6b14,#ff6b6b1f)',\n\t2: 'linear-gradient(135deg,#9ca3af0f,#9ca3af1a)',\n\t3: 'linear-gradient(135deg,#3b82f614,#3b82f61f)',\n\t4: 'linear-gradient(135deg,#f59e0b14,#f59e0b1f)',\n}\n\nexport const DEFAULT_API_URL = 'https://usero.io'\n\nexport const DEFAULT_THEME: WidgetTheme = {\n\tprimary: '#2563eb',\n\tbackground: '#ffffff',\n\ttext: '#374151',\n\tborder: '#e5e7eb',\n\tshadow:\n\t\t'0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',\n}\n\nexport const DARK_THEME: WidgetTheme = {\n\tprimary: '#2563eb',\n\tbackground: '#1f2937',\n\ttext: '#f9fafb',\n\tborder: '#374151',\n\tshadow:\n\t\t'0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2)',\n}\n\nexport function mergeTheme(customTheme: Partial<WidgetTheme> = {}): WidgetTheme {\n\treturn { ...DEFAULT_THEME, ...customTheme }\n}\n","// User-testing-mode plugin for the Usero widget.\n//\n// Activates ONLY when the host page URL carries `?usero_test=<slug>`. When\n// active, it:\n// 1. Creates a UserTestSession on the SaaS side via\n// POST /api/user-test-sessions\n// 2. Starts a MediaRecorder on the user's microphone (Opus in WebM) and\n// ships chunks every `chunkSeconds` to\n// PUT /api/user-test-sessions/:id/chunk?index=N\n// 3. Renders a small floating \"recording\" indicator with a Finish button\n// bottom-center so the tester can stop on their own.\n// 4. On Finish (or `pagehide`), flushes any buffered chunks and calls\n// POST /api/user-test-sessions/:id/finalise.\n//\n// Mic-permission denied is non-fatal: the session is still created (the\n// session-replay plugin keeps recording in parallel) and the indicator\n// shows a \"no audio\" hint. The SaaS side detects audio presence from the\n// first chunk; if no chunks land, hasAudio stays false there too.\n//\n// Bundle hygiene: this module is a subpath export and does NOT depend on\n// rrweb or any other heavy dep — just the native MediaRecorder API. The\n// floating UI is shadow-DOM scoped so host page CSS can't leak in.\n//\n// Privacy: only the microphone is recorded. We never read DOM text, never\n// touch the camera, and never persist audio in the browser past the\n// IndexedDB fallback (cleared on successful upload).\n\nimport type { UseroPlugin, PluginContext } from '../plugin'\nimport { DEFAULT_API_URL } from '../types'\n\nexport interface UserTestOptions {\n\t// URL query param the welcome page appends to redirect testers back.\n\t// Default `usero_test`. Match SaaS side if you ever change it.\n\tqueryParam?: string\n\t// Audio chunk length in seconds. Smaller = more partial-data resilience\n\t// but more requests. Default 30.\n\tchunkSeconds?: number\n\t// API origin. Override for self-hosted or local dev. Defaults to the\n\t// shared SDK constant (https://usero.io).\n\tapiUrl?: string\n\t// Override the tester-name shown on the SaaS side. Normally the welcome\n\t// page collects this and stores it in localStorage; the plugin reads it\n\t// from there. This option lets a host site bypass that.\n\ttesterName?: string\n\t// Hide the floating indicator. The plugin still records and finalises\n\t// on `pagehide`, but the tester gets no on-page UI. Useful if the host\n\t// page wants to render its own.\n\thideIndicator?: boolean\n}\n\ninterface UserTestTask {\n\tid: string\n\tprompt: string\n\tsortOrder: number\n}\n\ninterface RecorderStore {\n\tcancelled: boolean\n\tslug: string\n\tsessionId: string | null\n\tclientId: string | null\n\trecorder: MediaRecorder | null\n\tstream: MediaStream | null\n\tchunkIndex: number\n\tuploadQueue: Promise<void>\n\tpendingUploads: number\n\tstartedAt: number\n\tindicator: HTMLElement | null\n\tindicatorRoot: ShadowRoot | null\n\tindicatorState: 'recording' | 'finishing' | 'done' | 'no-audio' | 'error'\n\tpageHideHandler: (() => void) | null\n\toptions: Required<UserTestOptions>\n\ttasks: UserTestTask[]\n\ttasksPanelOpen: boolean\n\toutsidePointerHandler: ((event: PointerEvent) => void) | null\n\tkeydownHandler: ((event: KeyboardEvent) => void) | null\n}\n\nconst DEFAULT_OPTIONS: Required<Omit<UserTestOptions, 'testerName' | 'apiUrl'>> & {\n\ttesterName: string\n\tapiUrl: string\n} = {\n\tqueryParam: 'usero_test',\n\tchunkSeconds: 30,\n\tapiUrl: DEFAULT_API_URL,\n\ttesterName: '',\n\thideIndicator: false,\n}\n\nconst TESTER_NAME_STORAGE_KEY = 'usero:user-test:tester-name'\nconst TASKS_PANEL_OPEN_STORAGE_KEY = 'usero:user-test:tasks-panel-open'\nconst IDB_NAME = 'usero-user-test'\nconst IDB_STORE = 'pending-chunks'\n\ninterface PendingChunk {\n\tid: string\n\tsessionId: string\n\tapiUrl: string\n\tchunkIndex: number\n\tblob: Blob\n\tcreatedAt: number\n}\n\nfunction readTesterName(override: string): string | undefined {\n\tif (override) return override\n\ttry {\n\t\tconst stored = window.localStorage?.getItem(TESTER_NAME_STORAGE_KEY)\n\t\tif (stored && stored.trim()) return stored.trim().slice(0, 120)\n\t} catch {\n\t\t// Storage access can throw in some sandboxed iframes — ignore.\n\t}\n\treturn undefined\n}\n\nfunction getTestSlug(queryParam: string): string | null {\n\tif (typeof window === 'undefined' || typeof window.location === 'undefined') return null\n\ttry {\n\t\tconst params = new URLSearchParams(window.location.search)\n\t\tconst slug = params.get(queryParam)\n\t\tif (!slug) return null\n\t\tconst cleaned = slug.trim().slice(0, 64)\n\t\tif (!/^[a-z0-9-]+$/i.test(cleaned)) return null\n\t\treturn cleaned\n\t} catch {\n\t\treturn null\n\t}\n}\n\nfunction isMediaRecorderSupported(): boolean {\n\treturn typeof window !== 'undefined' && typeof window.MediaRecorder !== 'undefined' && typeof navigator !== 'undefined' && !!navigator.mediaDevices?.getUserMedia\n}\n\nfunction pickMimeType(): string | undefined {\n\tconst candidates = ['audio/webm;codecs=opus', 'audio/webm', 'audio/ogg;codecs=opus', 'audio/mp4']\n\tfor (const candidate of candidates) {\n\t\tif (typeof MediaRecorder !== 'undefined' && MediaRecorder.isTypeSupported?.(candidate)) {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn undefined\n}\n\n// IndexedDB helpers. Best-effort, never throw upstream — if IDB is missing\n// or quota-blown we just lose offline resilience but the live upload path\n// still runs.\nfunction idbOpen(): Promise<IDBDatabase | null> {\n\treturn new Promise(resolve => {\n\t\tif (typeof indexedDB === 'undefined') {\n\t\t\tresolve(null)\n\t\t\treturn\n\t\t}\n\t\ttry {\n\t\t\tconst req = indexedDB.open(IDB_NAME, 1)\n\t\t\treq.onupgradeneeded = (): void => {\n\t\t\t\tconst db = req.result\n\t\t\t\tif (!db.objectStoreNames.contains(IDB_STORE)) {\n\t\t\t\t\tdb.createObjectStore(IDB_STORE, { keyPath: 'id' })\n\t\t\t\t}\n\t\t\t}\n\t\t\treq.onsuccess = (): void => resolve(req.result)\n\t\t\treq.onerror = (): void => resolve(null)\n\t\t} catch {\n\t\t\tresolve(null)\n\t\t}\n\t})\n}\n\nasync function idbStashChunk(chunk: PendingChunk): Promise<void> {\n\tconst db = await idbOpen()\n\tif (!db) return\n\tawait new Promise<void>(resolve => {\n\t\ttry {\n\t\t\tconst tx = db.transaction(IDB_STORE, 'readwrite')\n\t\t\ttx.objectStore(IDB_STORE).put(chunk)\n\t\t\ttx.oncomplete = (): void => resolve()\n\t\t\ttx.onerror = (): void => resolve()\n\t\t\ttx.onabort = (): void => resolve()\n\t\t} catch {\n\t\t\tresolve()\n\t\t}\n\t})\n\tdb.close()\n}\n\nasync function idbDeleteChunk(id: string): Promise<void> {\n\tconst db = await idbOpen()\n\tif (!db) return\n\tawait new Promise<void>(resolve => {\n\t\ttry {\n\t\t\tconst tx = db.transaction(IDB_STORE, 'readwrite')\n\t\t\ttx.objectStore(IDB_STORE).delete(id)\n\t\t\ttx.oncomplete = (): void => resolve()\n\t\t\ttx.onerror = (): void => resolve()\n\t\t\ttx.onabort = (): void => resolve()\n\t\t} catch {\n\t\t\tresolve()\n\t\t}\n\t})\n\tdb.close()\n}\n\nasync function idbListChunks(sessionId: string): Promise<PendingChunk[]> {\n\tconst db = await idbOpen()\n\tif (!db) return []\n\tconst items = await new Promise<PendingChunk[]>(resolve => {\n\t\ttry {\n\t\t\tconst tx = db.transaction(IDB_STORE, 'readonly')\n\t\t\tconst req = tx.objectStore(IDB_STORE).getAll()\n\t\t\treq.onsuccess = (): void => {\n\t\t\t\tconst all = (req.result as PendingChunk[]) ?? []\n\t\t\t\tresolve(all.filter(c => c.sessionId === sessionId))\n\t\t\t}\n\t\t\treq.onerror = (): void => resolve([])\n\t\t} catch {\n\t\t\tresolve([])\n\t\t}\n\t})\n\tdb.close()\n\treturn items\n}\n\nasync function uploadChunkWithRetry(\n\tapiUrl: string,\n\tsessionId: string,\n\tindex: number,\n\tblob: Blob,\n\tlogger: PluginContext['logger'],\n\tmaxAttempts = 5,\n): Promise<boolean> {\n\tconst url = `${apiUrl.replace(/\\/$/, '')}/api/user-test-sessions/${encodeURIComponent(sessionId)}/chunk?index=${index}`\n\tlet attempt = 0\n\twhile (attempt < maxAttempts) {\n\t\ttry {\n\t\t\tconst res = await fetch(url, {\n\t\t\t\tmethod: 'PUT',\n\t\t\t\tbody: blob,\n\t\t\t\theaders: { 'Content-Type': blob.type || 'audio/webm' },\n\t\t\t\tkeepalive: blob.size <= 60 * 1024, // browsers cap keepalive bodies\n\t\t\t})\n\t\t\tif (res.ok) return true\n\t\t\t// 4xx (other than 413) won't get better with retries; bail early.\n\t\t\tif (res.status >= 400 && res.status < 500 && res.status !== 408 && res.status !== 429) {\n\t\t\t\tlogger.error(`chunk ${index} rejected with ${res.status}`)\n\t\t\t\treturn false\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tlogger.warn(`chunk ${index} upload attempt ${attempt + 1} failed`, err)\n\t\t}\n\t\tattempt += 1\n\t\tconst backoff = Math.min(15000, 500 * 2 ** attempt) + Math.floor(Math.random() * 250)\n\t\tawait new Promise(resolve => setTimeout(resolve, backoff))\n\t}\n\treturn false\n}\n\nfunction buildIndicator(host: HTMLElement, store: RecorderStore, onFinish: () => void, onToggleTasks: () => void): ShadowRoot {\n\tconst root = host.attachShadow({ mode: 'closed' })\n\tconst style = document.createElement('style')\n\t// Keep this CSS small. Pulse is the only animation. Bottom-center,\n\t// safe-area aware, semi-transparent so it doesn't block the page.\n\tstyle.textContent = `\n\t\t:host { all: initial; }\n\t\t.anchor {\n\t\t\tposition: fixed;\n\t\t\tbottom: calc(env(safe-area-inset-bottom, 0px) + 16px);\n\t\t\tleft: 50%; transform: translateX(-50%);\n\t\t\tdisplay: flex; flex-direction: column; align-items: center; gap: 8px;\n\t\t\tz-index: 2147483646; max-width: calc(100vw - 32px);\n\t\t\tfont: 13px/1 -apple-system, BlinkMacSystemFont, \"Segoe UI\", system-ui, sans-serif;\n\t\t\tcolor: #fff;\n\t\t}\n\t\t.bar {\n\t\t\tdisplay: inline-flex; align-items: center; gap: 10px;\n\t\t\tpadding: 8px 14px 8px 12px;\n\t\t\tbackground: rgba(17,17,17,0.78);\n\t\t\tborder-radius: 999px;\n\t\t\tbox-shadow: 0 8px 24px rgba(0,0,0,0.18);\n\t\t\tbackdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);\n\t\t\tmax-width: 100%;\n\t\t}\n\t\t.panel {\n\t\t\tbackground: rgba(17,17,17,0.88);\n\t\t\tborder-radius: 14px; padding: 12px 14px 12px 8px;\n\t\t\tline-height: 1.45;\n\t\t\tbox-shadow: 0 12px 32px rgba(0,0,0,0.28);\n\t\t\tmax-height: min(60vh, 480px);\n\t\t\tmax-width: min(420px, calc(100vw - 32px));\n\t\t\twidth: max-content; overflow-y: auto;\n\t\t}\n\t\t.panel[hidden] { display: none; }\n\t\t.panel ol { margin: 0; padding-left: 26px; }\n\t\t.panel li { margin: 0 0 8px; }\n\t\t.panel li:last-child { margin: 0; }\n\t\t.dot {\n\t\t\twidth: 8px; height: 8px; border-radius: 50%;\n\t\t\tbackground: #ef4444;\n\t\t\tbox-shadow: 0 0 0 0 rgba(239, 68, 68, 0.6);\n\t\t\tanimation: pulse 1.6s ease-out infinite;\n\t\t}\n\t\t.dot[data-state=\"no-audio\"] { background: #fbbf24; animation: none; }\n\t\t.dot[data-state=\"finishing\"] { background: #fbbf24; animation: none; }\n\t\t.dot[data-state=\"done\"] { background: #10b981; animation: none; }\n\t\t.dot[data-state=\"error\"] { background: #ef4444; animation: none; }\n\t\t.label { font-weight: 500; letter-spacing: 0.01em; }\n\t\t.spacer { width: 1px; height: 16px; background: rgba(255,255,255,0.18); margin: 0 2px; }\n\t\t.btn {\n\t\t\tappearance: none; border: 0; background: rgba(255,255,255,0.12);\n\t\t\tcolor: #fff; font: inherit; font-weight: 600;\n\t\t\tpadding: 6px 12px; border-radius: 999px; cursor: pointer;\n\t\t\ttransition: background 0.15s ease;\n\t\t}\n\t\t.btn:hover { background: rgba(255,255,255,0.22); }\n\t\t.btn:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }\n\t\t.btn[disabled] { opacity: 0.5; cursor: progress; }\n\t\t.tasks-btn[aria-expanded=\"true\"] { background: rgba(255,255,255,0.24); }\n\t\t@media (max-width: 420px) { .btn { padding: 9px 14px; min-height: 40px; } }\n\t\t.thanks {\n\t\t\tposition: fixed; inset: 0;\n\t\t\tdisplay: grid; place-items: center;\n\t\t\tbackground: rgba(15, 15, 17, 0.78);\n\t\t\tbackdrop-filter: blur(6px);\n\t\t\t-webkit-backdrop-filter: blur(6px);\n\t\t\tcolor: #fff;\n\t\t\tfont-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", system-ui, sans-serif;\n\t\t\tz-index: 2147483647;\n\t\t\tpadding: 24px;\n\t\t\ttext-align: center;\n\t\t}\n\t\t.thanks-card {\n\t\t\tbackground: #fff; color: #111;\n\t\t\tborder-radius: 16px; padding: 28px 24px;\n\t\t\tmax-width: 360px; width: 100%;\n\t\t\tbox-shadow: 0 20px 50px rgba(0,0,0,0.25);\n\t\t}\n\t\t.thanks h2 { margin: 0 0 8px; font-size: 20px; }\n\t\t.thanks p { margin: 0; font-size: 14px; line-height: 1.45; color: #4b5563; }\n\t\t.thanks .check {\n\t\t\twidth: 44px; height: 44px; border-radius: 50%;\n\t\t\tbackground: #10b981; color: #fff;\n\t\t\tdisplay: grid; place-items: center;\n\t\t\tmargin: 0 auto 12px;\n\t\t\tfont-size: 22px;\n\t\t}\n\t\t@keyframes pulse {\n\t\t\t0% { box-shadow: 0 0 0 0 rgba(239,68,68,0.55); }\n\t\t\t70% { box-shadow: 0 0 0 10px rgba(239,68,68,0); }\n\t\t\t100% { box-shadow: 0 0 0 0 rgba(239,68,68,0); }\n\t\t}\n\t\t@media (prefers-reduced-motion: reduce) {\n\t\t\t.dot { animation: none; }\n\t\t}\n\t`\n\tconst anchor = document.createElement('div')\n\tanchor.className = 'anchor'\n\n\tconst panel = document.createElement('div')\n\tpanel.className = 'panel'\n\tpanel.hidden = true\n\n\tconst bar = document.createElement('div')\n\tbar.className = 'bar'\n\tbar.setAttribute('role', 'status')\n\tbar.setAttribute('aria-live', 'polite')\n\n\tconst dot = document.createElement('span')\n\tdot.className = 'dot'\n\tdot.setAttribute('data-state', store.indicatorState)\n\n\tconst label = document.createElement('span')\n\tlabel.className = 'label'\n\tlabel.textContent = 'Recording'\n\n\tconst spacer = document.createElement('span')\n\tspacer.className = 'spacer'\n\n\tbar.appendChild(dot)\n\tbar.appendChild(label)\n\tbar.appendChild(spacer)\n\n\tconst btn = document.createElement('button')\n\tbtn.type = 'button'\n\tbtn.className = 'btn finish-btn'\n\tbtn.textContent = 'Finish'\n\tbtn.addEventListener('click', onFinish)\n\tbar.appendChild(btn)\n\n\tif (store.tasks.length > 0) installTasksToggle(bar, btn, store, onToggleTasks)\n\n\tanchor.appendChild(panel)\n\tanchor.appendChild(bar)\n\n\troot.appendChild(style)\n\troot.appendChild(anchor)\n\treturn root\n}\n\nfunction installTasksToggle(bar: HTMLElement, finishBtn: HTMLElement, store: RecorderStore, onToggleTasks: () => void): void {\n\tconst tasksBtn = document.createElement('button')\n\ttasksBtn.type = 'button'\n\ttasksBtn.className = 'btn tasks-btn'\n\ttasksBtn.textContent = `Tasks (${store.tasks.length})`\n\ttasksBtn.setAttribute('aria-expanded', store.tasksPanelOpen ? 'true' : 'false')\n\ttasksBtn.addEventListener('click', onToggleTasks)\n\tbar.insertBefore(tasksBtn, finishBtn)\n}\n\nfunction renderTasksPanel(store: RecorderStore): void {\n\tconst root = store.indicatorRoot\n\tif (!root) return\n\tconst panel = root.querySelector('.panel')\n\tif (!(panel instanceof HTMLElement)) return\n\t// Build content once.\n\tif (!panel.firstChild && store.tasks.length > 0) {\n\t\tconst ol = document.createElement('ol')\n\t\tfor (const task of store.tasks) {\n\t\t\tconst li = document.createElement('li')\n\t\t\tli.textContent = task.prompt\n\t\t\tol.appendChild(li)\n\t\t}\n\t\tpanel.appendChild(ol)\n\t}\n\tpanel.hidden = !store.tasksPanelOpen\n\tconst tasksBtn = root.querySelector('.tasks-btn')\n\tif (tasksBtn instanceof HTMLElement) {\n\t\ttasksBtn.setAttribute('aria-expanded', store.tasksPanelOpen ? 'true' : 'false')\n\t}\n}\n\nfunction readTasksPanelOpen(): boolean {\n\ttry { return window.sessionStorage?.getItem(TASKS_PANEL_OPEN_STORAGE_KEY) === '1' } catch { return false }\n}\nfunction writeTasksPanelOpen(open: boolean): void {\n\ttry { window.sessionStorage?.setItem(TASKS_PANEL_OPEN_STORAGE_KEY, open ? '1' : '0') } catch { /* ignore */ }\n}\n\nfunction renderIndicatorState(store: RecorderStore): void {\n\tconst root = store.indicatorRoot\n\tif (!root) return\n\tconst dot = root.querySelector('.dot')\n\tconst label = root.querySelector('.label')\n\tconst btn = root.querySelector<HTMLButtonElement>('.finish-btn')\n\tif (!(dot instanceof HTMLElement) || !(label instanceof HTMLElement) || !btn) return\n\tdot.setAttribute('data-state', store.indicatorState)\n\tswitch (store.indicatorState) {\n\t\tcase 'recording':\n\t\t\tlabel.textContent = 'Recording'\n\t\t\tbtn.textContent = 'Finish'\n\t\t\tbtn.disabled = false\n\t\t\tbreak\n\t\tcase 'no-audio':\n\t\t\tlabel.textContent = 'No mic, replay only'\n\t\t\tbtn.textContent = 'Finish'\n\t\t\tbtn.disabled = false\n\t\t\tbreak\n\t\tcase 'finishing':\n\t\t\tlabel.textContent = 'Saving'\n\t\t\tbtn.textContent = 'Saving'\n\t\t\tbtn.disabled = true\n\t\t\tbreak\n\t\tcase 'done':\n\t\t\tlabel.textContent = 'Saved'\n\t\t\tbtn.textContent = 'Done'\n\t\t\tbtn.disabled = true\n\t\t\tbreak\n\t\tcase 'error':\n\t\t\tlabel.textContent = 'Save failed'\n\t\t\tbtn.textContent = 'Retry'\n\t\t\tbtn.disabled = false\n\t\t\tbreak\n\t}\n}\n\nfunction showThanksScreen(root: ShadowRoot): void {\n\tconst overlay = document.createElement('div')\n\toverlay.className = 'thanks'\n\toverlay.innerHTML = `\n\t\t<div class=\"thanks-card\">\n\t\t\t<div class=\"check\" aria-hidden=\"true\">✓</div>\n\t\t\t<h2>Thanks for testing</h2>\n\t\t\t<p>Your session was saved. You can close this tab.</p>\n\t\t</div>\n\t`\n\troot.appendChild(overlay)\n}\n\nfunction parseTasks(raw: unknown): UserTestTask[] {\n\tif (!Array.isArray(raw)) return []\n\tconst out = raw.flatMap((item: unknown): UserTestTask[] => {\n\t\tconst t = item as { id?: unknown; prompt?: unknown; sortOrder?: unknown }\n\t\tif (!t || typeof t.id !== 'string' || typeof t.prompt !== 'string' || typeof t.sortOrder !== 'number') return []\n\t\treturn [{ id: t.id, prompt: t.prompt, sortOrder: t.sortOrder }]\n\t})\n\tout.sort((a, b) => a.sortOrder - b.sortOrder)\n\treturn out\n}\n\nasync function createSession(\n\tapiUrl: string,\n\tslug: string,\n\ttesterName: string | undefined,\n): Promise<{ sessionId: string; clientId: string; tasks: UserTestTask[] } | null> {\n\ttry {\n\t\tconst res = await fetch(`${apiUrl.replace(/\\/$/, '')}/api/user-test-sessions`, {\n\t\t\tmethod: 'POST',\n\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\tbody: JSON.stringify({ slug, ...(testerName ? { testerName } : {}) }),\n\t\t})\n\t\tif (!res.ok) return null\n\t\tconst json = (await res.json()) as { sessionId?: unknown; clientId?: unknown; tasks?: unknown }\n\t\tif (typeof json.sessionId !== 'string' || typeof json.clientId !== 'string') return null\n\t\treturn { sessionId: json.sessionId, clientId: json.clientId, tasks: parseTasks(json.tasks) }\n\t} catch {\n\t\treturn null\n\t}\n}\n\nasync function finaliseSession(\n\tapiUrl: string,\n\tsessionId: string,\n\tdurationSeconds: number,\n): Promise<boolean> {\n\ttry {\n\t\tconst res = await fetch(`${apiUrl.replace(/\\/$/, '')}/api/user-test-sessions/${encodeURIComponent(sessionId)}/finalise`, {\n\t\t\tmethod: 'POST',\n\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\tbody: JSON.stringify({ durationSeconds: Math.max(0, Math.round(durationSeconds)) }),\n\t\t\tkeepalive: true,\n\t\t})\n\t\treturn res.ok\n\t} catch {\n\t\treturn false\n\t}\n}\n\nasync function flushPendingFromIdb(store: RecorderStore, ctx: PluginContext): Promise<void> {\n\tif (!store.sessionId) return\n\tconst pending = await idbListChunks(store.sessionId)\n\tfor (const chunk of pending) {\n\t\tconst ok = await uploadChunkWithRetry(chunk.apiUrl, chunk.sessionId, chunk.chunkIndex, chunk.blob, ctx.logger, 3)\n\t\tif (ok) await idbDeleteChunk(chunk.id)\n\t}\n}\n\nfunction enqueueChunk(store: RecorderStore, ctx: PluginContext, blob: Blob): void {\n\tif (store.cancelled || !store.sessionId || blob.size === 0) return\n\tconst index = store.chunkIndex\n\tstore.chunkIndex += 1\n\tstore.pendingUploads += 1\n\tconst sessionId = store.sessionId\n\tconst apiUrl = store.options.apiUrl\n\n\tstore.uploadQueue = store.uploadQueue.then(async () => {\n\t\tconst ok = await uploadChunkWithRetry(apiUrl, sessionId, index, blob, ctx.logger)\n\t\tif (!ok) {\n\t\t\tctx.logger.warn(`chunk ${index} stashed for offline retry`)\n\t\t\tawait idbStashChunk({\n\t\t\t\tid: `${sessionId}:${index}:${Date.now()}`,\n\t\t\t\tsessionId,\n\t\t\t\tapiUrl,\n\t\t\t\tchunkIndex: index,\n\t\t\t\tblob,\n\t\t\t\tcreatedAt: Date.now(),\n\t\t\t})\n\t\t}\n\t\tstore.pendingUploads -= 1\n\t})\n}\n\nasync function startRecording(store: RecorderStore, ctx: PluginContext): Promise<void> {\n\tif (!isMediaRecorderSupported()) {\n\t\tctx.logger.warn('MediaRecorder not supported, continuing without audio')\n\t\tstore.indicatorState = 'no-audio'\n\t\trenderIndicatorState(store)\n\t\treturn\n\t}\n\tlet stream: MediaStream\n\ttry {\n\t\tstream = await navigator.mediaDevices.getUserMedia({ audio: true })\n\t} catch (err) {\n\t\tctx.logger.warn('mic permission denied or unavailable', err)\n\t\tstore.indicatorState = 'no-audio'\n\t\trenderIndicatorState(store)\n\t\treturn\n\t}\n\tstore.stream = stream\n\tconst mimeType = pickMimeType()\n\tlet recorder: MediaRecorder\n\ttry {\n\t\trecorder = mimeType ? new MediaRecorder(stream, { mimeType }) : new MediaRecorder(stream)\n\t} catch (err) {\n\t\tctx.logger.error('MediaRecorder construction failed', err)\n\t\tstream.getTracks().forEach(t => t.stop())\n\t\tstore.stream = null\n\t\tstore.indicatorState = 'no-audio'\n\t\trenderIndicatorState(store)\n\t\treturn\n\t}\n\tstore.recorder = recorder\n\trecorder.addEventListener('dataavailable', event => {\n\t\tif (event.data && event.data.size > 0) {\n\t\t\tenqueueChunk(store, ctx, event.data)\n\t\t}\n\t})\n\trecorder.addEventListener('error', event => {\n\t\tctx.logger.error('MediaRecorder error', event)\n\t})\n\t// `timeslice` makes the recorder emit a self-contained chunk every N ms.\n\trecorder.start(store.options.chunkSeconds * 1000)\n}\n\nfunction stopRecording(store: RecorderStore): void {\n\tconst recorder = store.recorder\n\tif (recorder && recorder.state !== 'inactive') {\n\t\ttry {\n\t\t\trecorder.requestData()\n\t\t} catch {\n\t\t\t// requestData throws if state is invalid; ignore.\n\t\t}\n\t\ttry {\n\t\t\trecorder.stop()\n\t\t} catch {\n\t\t\t// already stopped\n\t\t}\n\t}\n\tstore.recorder = null\n\tif (store.stream) {\n\t\tstore.stream.getTracks().forEach(t => t.stop())\n\t\tstore.stream = null\n\t}\n}\n\nasync function finishFlow(store: RecorderStore, ctx: PluginContext, opts: { showThanks: boolean }): Promise<void> {\n\tif (store.cancelled) return\n\tif (store.indicatorState === 'finishing' || store.indicatorState === 'done') return\n\tstore.indicatorState = 'finishing'\n\trenderIndicatorState(store)\n\n\tstopRecording(store)\n\t// Wait for the queued uploads to drain. Each upload already has its own\n\t// retry/backoff; this just lets them finish before finalise fires.\n\tawait store.uploadQueue\n\tawait flushPendingFromIdb(store, ctx)\n\n\tconst durationSeconds = (Date.now() - store.startedAt) / 1000\n\tif (store.sessionId) {\n\t\tconst ok = await finaliseSession(store.options.apiUrl, store.sessionId, durationSeconds)\n\t\tstore.indicatorState = ok ? 'done' : 'error'\n\t} else {\n\t\tstore.indicatorState = 'error'\n\t}\n\trenderIndicatorState(store)\n\n\tif (opts.showThanks && store.indicatorRoot && store.indicatorState === 'done') {\n\t\tshowThanksScreen(store.indicatorRoot)\n\t}\n}\n\nexport function userTest(options: UserTestOptions = {}): UseroPlugin {\n\tconst merged: Required<UserTestOptions> = {\n\t\tqueryParam: options.queryParam ?? DEFAULT_OPTIONS.queryParam,\n\t\tchunkSeconds: options.chunkSeconds ?? DEFAULT_OPTIONS.chunkSeconds,\n\t\tapiUrl: options.apiUrl ?? DEFAULT_OPTIONS.apiUrl,\n\t\ttesterName: options.testerName ?? DEFAULT_OPTIONS.testerName,\n\t\thideIndicator: options.hideIndicator ?? DEFAULT_OPTIONS.hideIndicator,\n\t}\n\n\treturn {\n\t\tname: 'user-test',\n\t\tonInit(ctx) {\n\t\t\tif (typeof window === 'undefined' || typeof document === 'undefined') return\n\t\t\tconst slug = getTestSlug(merged.queryParam)\n\t\t\tif (!slug) return\n\n\t\t\tconst apiUrl = merged.apiUrl || ctx.baseUrl || DEFAULT_API_URL\n\t\t\tconst store: RecorderStore = {\n\t\t\t\tcancelled: false,\n\t\t\t\tslug,\n\t\t\t\tsessionId: null,\n\t\t\t\tclientId: null,\n\t\t\t\trecorder: null,\n\t\t\t\tstream: null,\n\t\t\t\tchunkIndex: 0,\n\t\t\t\tuploadQueue: Promise.resolve(),\n\t\t\t\tpendingUploads: 0,\n\t\t\t\tstartedAt: Date.now(),\n\t\t\t\tindicator: null,\n\t\t\t\tindicatorRoot: null,\n\t\t\t\tindicatorState: 'recording',\n\t\t\t\tpageHideHandler: null,\n\t\t\t\toptions: { ...merged, apiUrl },\n\t\t\t\ttasks: [],\n\t\t\t\ttasksPanelOpen: readTasksPanelOpen(),\n\t\t\t\toutsidePointerHandler: null,\n\t\t\t\tkeydownHandler: null,\n\t\t\t}\n\t\t\tctx.setStore(store)\n\n\t\t\tconst onFinish = (): void => {\n\t\t\t\tvoid finishFlow(store, ctx, { showThanks: true })\n\t\t\t}\n\n\t\t\tconst setPanelOpen = (open: boolean): void => {\n\t\t\t\tif (store.tasksPanelOpen === open) return\n\t\t\t\tstore.tasksPanelOpen = open\n\t\t\t\twriteTasksPanelOpen(open)\n\t\t\t\trenderTasksPanel(store)\n\t\t\t}\n\n\t\t\tconst onToggleTasks = (): void => setPanelOpen(!store.tasksPanelOpen)\n\n\t\t\tif (!merged.hideIndicator) {\n\t\t\t\tconst host = document.createElement('div')\n\t\t\t\thost.setAttribute('data-usero-user-test', 'true')\n\t\t\t\tdocument.body.appendChild(host)\n\t\t\t\tstore.indicator = host\n\t\t\t\tstore.indicatorRoot = buildIndicator(host, store, onFinish, onToggleTasks)\n\t\t\t\trenderIndicatorState(store)\n\t\t\t}\n\n\t\t\t// Outside-click + Escape close the tasks panel. Listen on document\n\t\t\t// (composedPath checks shadow ancestry so taps on the panel/pill\n\t\t\t// itself don't dismiss).\n\t\t\tconst outsidePointer = (event: PointerEvent): void => {\n\t\t\t\tif (!store.tasksPanelOpen) return\n\t\t\t\tconst host = store.indicator\n\t\t\t\tif (!host) return\n\t\t\t\tconst path = event.composedPath()\n\t\t\t\tif (!path.includes(host)) setPanelOpen(false)\n\t\t\t}\n\t\t\tconst onKeydown = (event: KeyboardEvent): void => {\n\t\t\t\tif (event.key === 'Escape' && store.tasksPanelOpen) setPanelOpen(false)\n\t\t\t}\n\t\t\tstore.outsidePointerHandler = outsidePointer\n\t\t\tstore.keydownHandler = onKeydown\n\t\t\tdocument.addEventListener('pointerdown', outsidePointer, true)\n\t\t\tdocument.addEventListener('keydown', onKeydown)\n\n\t\t\tconst pageHide = (): void => {\n\t\t\t\t// Best-effort flush + finalise. We don't await here; the browser\n\t\t\t\t// is shutting the page down. `keepalive: true` on finalise lets\n\t\t\t\t// the request race the unload.\n\t\t\t\tvoid finishFlow(store, ctx, { showThanks: false })\n\t\t\t}\n\t\t\tstore.pageHideHandler = pageHide\n\t\t\twindow.addEventListener('pagehide', pageHide)\n\n\t\t\tvoid (async (): Promise<void> => {\n\t\t\t\tconst created = await createSession(apiUrl, slug, readTesterName(merged.testerName))\n\t\t\t\tif (store.cancelled) return\n\t\t\t\tif (!created) {\n\t\t\t\t\tctx.logger.error('failed to create user-test session')\n\t\t\t\t\tstore.indicatorState = 'error'\n\t\t\t\t\trenderIndicatorState(store)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tstore.sessionId = created.sessionId\n\t\t\t\tstore.clientId = created.clientId\n\t\t\t\tstore.tasks = created.tasks\n\t\t\t\tif (store.tasks.length > 0 && store.indicatorRoot && !merged.hideIndicator) {\n\t\t\t\t\tconst bar = store.indicatorRoot.querySelector('.bar')\n\t\t\t\t\tconst finishBtn = bar?.querySelector('.finish-btn')\n\t\t\t\t\tif (bar instanceof HTMLElement && finishBtn instanceof HTMLElement && !bar.querySelector('.tasks-btn')) {\n\t\t\t\t\t\tinstallTasksToggle(bar, finishBtn, store, onToggleTasks)\n\t\t\t\t\t}\n\t\t\t\t\trenderTasksPanel(store)\n\t\t\t\t}\n\t\t\t\tawait startRecording(store, ctx)\n\t\t\t\trenderIndicatorState(store)\n\t\t\t})()\n\t\t},\n\t\tonDestroy(ctx) {\n\t\t\tconst store = ctx.getStore<RecorderStore>()\n\t\t\tif (!store) return\n\t\t\tstore.cancelled = true\n\t\t\tif (store.pageHideHandler) {\n\t\t\t\twindow.removeEventListener('pagehide', store.pageHideHandler)\n\t\t\t\tstore.pageHideHandler = null\n\t\t\t}\n\t\t\tstopRecording(store)\n\t\t\tif (store.outsidePointerHandler) {\n\t\t\t\tdocument.removeEventListener('pointerdown', store.outsidePointerHandler, true)\n\t\t\t\tstore.outsidePointerHandler = null\n\t\t\t}\n\t\t\tif (store.keydownHandler) {\n\t\t\t\tdocument.removeEventListener('keydown', store.keydownHandler)\n\t\t\t\tstore.keydownHandler = null\n\t\t\t}\n\t\t\tif (store.indicator && store.indicator.parentNode) {\n\t\t\t\tstore.indicator.parentNode.removeChild(store.indicator)\n\t\t\t}\n\t\t\tstore.indicator = null\n\t\t\tstore.indicatorRoot = null\n\t\t},\n\t}\n}\n\n// Internal helpers exposed for tests only. Not part of the public API.\nexport const __test__ = { getTestSlug, pickMimeType, isMediaRecorderSupported }\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usero/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Usero SDK. Drop-in feedback widget (vanilla JS, React, or script tag) plus future plugins like session replay. Zero config, framework-free, tiny.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Will Smith <will@willsmithte.com>",
|