@usero/sdk 0.4.2 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
- .bar {
153
+ .anchor {
153
154
  position: fixed;
154
155
  bottom: calc(env(safe-area-inset-bottom, 0px) + 16px);
155
- left: 50%;
156
- transform: translateX(-50%);
157
- display: inline-flex;
158
- align-items: center;
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
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
165
- font-size: 13px;
166
- line-height: 1;
167
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
168
- backdrop-filter: blur(8px);
169
- -webkit-backdrop-filter: blur(8px);
170
- z-index: 2147483646;
171
- max-width: calc(100vw - 32px);
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(bar);
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\">&#10003;</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\">&#10003;</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/dist/react.cjs CHANGED
@@ -926,6 +926,7 @@ function initUseroFeedbackWidget(props) {
926
926
  const readyPromise = initPromises.length === 0 ? Promise.resolve() : Promise.all(initPromises).then(() => {
927
927
  });
928
928
  let isOpen = false;
929
+ let focusCommentNext = false;
929
930
  let selectedRating = void 0;
930
931
  let comment = "";
931
932
  let shareEmail = false;
@@ -972,6 +973,7 @@ function initUseroFeedbackWidget(props) {
972
973
  function open() {
973
974
  if (isOpen) return;
974
975
  isOpen = true;
976
+ focusCommentNext = true;
975
977
  selectedRating = void 0;
976
978
  comment = "";
977
979
  shareEmail = false;
@@ -1210,6 +1212,7 @@ function initUseroFeedbackWidget(props) {
1210
1212
  const value = btn.dataset.rating;
1211
1213
  if (value === "1" || value === "2" || value === "3" || value === "4") {
1212
1214
  selectedRating = Number(value);
1215
+ focusCommentNext = true;
1213
1216
  render();
1214
1217
  }
1215
1218
  });
@@ -1218,6 +1221,10 @@ function initUseroFeedbackWidget(props) {
1218
1221
  'textarea[data-role="comment"]'
1219
1222
  );
1220
1223
  if (textarea) {
1224
+ if (focusCommentNext) {
1225
+ focusCommentNext = false;
1226
+ requestAnimationFrame(() => textarea.focus({ preventScroll: true }));
1227
+ }
1221
1228
  textarea.addEventListener("input", () => {
1222
1229
  if (textarea.value.length <= 1e3) {
1223
1230
  comment = textarea.value;