@usero/sdk 1.1.10 → 1.1.12

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.
@@ -44,6 +44,9 @@ function getOrMintAnonymousId() {
44
44
  cachedAnonymousId = id;
45
45
  return id;
46
46
  }
47
+ function isValidSdkSessionId(id) {
48
+ return /^[a-z0-9-]{8,}$/i.test(id);
49
+ }
47
50
 
48
51
  // src/plugins/session-replay.ts
49
52
  var DEFAULTS = {
@@ -101,7 +104,7 @@ function generateRandomId2() {
101
104
  function mintSdkSessionId() {
102
105
  try {
103
106
  const existing = window.sessionStorage?.getItem(SDK_SESSION_STORAGE_KEY);
104
- if (existing && /^[a-z0-9-]{8,}$/i.test(existing)) return existing;
107
+ if (existing && isValidSdkSessionId(existing)) return existing;
105
108
  } catch {
106
109
  }
107
110
  const id = generateRandomId2();
@@ -302,6 +305,7 @@ function flushPendingChunk(store, ctx) {
302
305
  scheduleChunkUpload(store, ctx);
303
306
  }
304
307
  function stopRrweb(store) {
308
+ stopUrlChangeTracking(store);
305
309
  if (store.stopRecording) {
306
310
  try {
307
311
  store.stopRecording();
@@ -314,6 +318,61 @@ function stopRrweb(store) {
314
318
  store.chunkFlushTimer = null;
315
319
  }
316
320
  }
321
+ var URL_CHANGE_TAG = "url-change";
322
+ function startUrlChangeTracking(store, ctx) {
323
+ if (typeof window === "undefined") return;
324
+ if (store.stopUrlTracking) return;
325
+ const record = store.record;
326
+ const addCustomEvent = record?.addCustomEvent;
327
+ if (!record || typeof addCustomEvent !== "function") return;
328
+ const emitUrl = () => {
329
+ if (store.stopped || store.cancelled) return;
330
+ try {
331
+ addCustomEvent.call(record, URL_CHANGE_TAG, { href: window.location.href });
332
+ } catch (err) {
333
+ ctx.logger.warn("url-change addCustomEvent threw", err);
334
+ }
335
+ };
336
+ try {
337
+ const history = window.history;
338
+ const originalPushState = history.pushState;
339
+ const originalReplaceState = history.replaceState;
340
+ const patchedPushState = function patchedPushState2(...args) {
341
+ const result = originalPushState.apply(this, args);
342
+ emitUrl();
343
+ return result;
344
+ };
345
+ const patchedReplaceState = function patchedReplaceState2(...args) {
346
+ const result = originalReplaceState.apply(this, args);
347
+ emitUrl();
348
+ return result;
349
+ };
350
+ history.pushState = patchedPushState;
351
+ history.replaceState = patchedReplaceState;
352
+ const onPopState = () => emitUrl();
353
+ window.addEventListener("popstate", onPopState);
354
+ store.stopUrlTracking = () => {
355
+ try {
356
+ if (history.pushState === patchedPushState) history.pushState = originalPushState;
357
+ if (history.replaceState === patchedReplaceState) {
358
+ history.replaceState = originalReplaceState;
359
+ }
360
+ window.removeEventListener("popstate", onPopState);
361
+ } catch (err) {
362
+ ctx.logger.warn("url-change teardown threw", err);
363
+ }
364
+ };
365
+ emitUrl();
366
+ } catch (err) {
367
+ ctx.logger.warn("url-change tracking setup threw", err);
368
+ }
369
+ }
370
+ function stopUrlChangeTracking(store) {
371
+ if (store.stopUrlTracking) {
372
+ store.stopUrlTracking();
373
+ store.stopUrlTracking = null;
374
+ }
375
+ }
317
376
  function startRecording(store, ctx) {
318
377
  if (store.cancelled || store.stopped || store.stopRecording || store.loadInProgress) return;
319
378
  store.loadInProgress = true;
@@ -348,6 +407,7 @@ function startRecording(store, ctx) {
348
407
  });
349
408
  store.stopRecording = stop;
350
409
  store.record = record;
410
+ startUrlChangeTracking(store, ctx);
351
411
  scheduleShadowSnapshot(store, ctx);
352
412
  store.chunkFlushTimer = setInterval(
353
413
  () => flushPendingChunk(store, ctx),
@@ -411,12 +471,14 @@ function sessionReplay(options = {}) {
411
471
  ctx.logger.error("session-replay needs an apiUrl (via options or PluginContext)");
412
472
  return;
413
473
  }
414
- const sdkSessionId = ctx.getSdkSessionId ? ctx.getSdkSessionId() : mintSdkSessionId();
474
+ const resolveSdkSessionId = () => ctx.getSdkSessionId ? ctx.getSdkSessionId() : mintSdkSessionId();
415
475
  const anonymousId = getOrMintAnonymousId();
416
476
  const store = {
417
477
  options: { ...merged, apiUrl },
418
478
  clientId: ctx.clientId,
419
- sdkSessionId,
479
+ // Filled in lazily at createSession time (see resolveSdkSessionId)
480
+ // so a user-test re-seat during a later onInit is honoured.
481
+ sdkSessionId: "",
420
482
  sessionReplayId: null,
421
483
  recordingStartedAt: null,
422
484
  pendingEvents: [],
@@ -436,6 +498,7 @@ function sessionReplay(options = {}) {
436
498
  shadowUpdateHandler: null,
437
499
  record: null,
438
500
  stopRecording: null,
501
+ stopUrlTracking: null,
439
502
  loadInProgress: false,
440
503
  cancelled: false,
441
504
  stopped: false
@@ -465,6 +528,8 @@ function sessionReplay(options = {}) {
465
528
  } catch (err) {
466
529
  ctx.logger.warn("resolveUser threw at session start", err);
467
530
  }
531
+ const sdkSessionId = resolveSdkSessionId();
532
+ store.sdkSessionId = sdkSessionId;
468
533
  const created = await createSession(apiUrl, ctx.clientId, sdkSessionId, anonymousId);
469
534
  if (!created) {
470
535
  ctx.logger.warn("session create failed, replay disabled");
@@ -556,6 +621,9 @@ var __test__ = {
556
621
  joinUrl,
557
622
  scheduleChunkUpload,
558
623
  maybeIsolateSnapshot,
624
+ startUrlChangeTracking,
625
+ stopUrlChangeTracking,
626
+ URL_CHANGE_TAG,
559
627
  RRWEB_EVENT_TYPE_FULL_SNAPSHOT,
560
628
  SNAPSHOT_ISOLATION_MIN_GAP_MS,
561
629
  HARD_CHUNK_BYTE_CAP,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/identity.ts","../../src/plugins/session-replay.ts"],"names":["generateRandomId"],"mappings":";;;AAeA,IAAM,gBAAA,GAAmB,oBAAA;AASzB,IAAI,iBAAA,GAAmC,IAAA;AA0BvC,SAAS,gBAAA,GAA2B;AACnC,EAAA,IAAI,OAAO,MAAA,KAAW,WAAA,IAAe,OAAO,MAAA,CAAO,eAAe,UAAA,EAAY;AAC7E,IAAA,OAAO,OAAO,UAAA,EAAW;AAAA,EAC1B;AACA,EAAA,MAAM,KAAA,GAAQ,IAAI,UAAA,CAAW,EAAE,CAAA;AAC/B,EAAA,IAAI,OAAO,MAAA,KAAW,WAAA,IAAe,OAAO,MAAA,CAAO,oBAAoB,UAAA,EAAY;AAClF,IAAA,MAAA,CAAO,gBAAgB,KAAK,CAAA;AAAA,EAC7B,CAAA,MAAO;AACN,IAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,QAAQ,CAAA,IAAK,CAAA,EAAG,KAAA,CAAM,CAAC,IAAI,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,MAAA,KAAW,GAAG,CAAA;AAAA,EACpF;AACA,EAAA,IAAI,GAAA,GAAM,EAAA;AACV,EAAA,KAAA,MAAW,CAAA,IAAK,OAAO,GAAA,IAAO,CAAA,CAAE,SAAS,EAAE,CAAA,CAAE,QAAA,CAAS,CAAA,EAAG,GAAG,CAAA;AAC5D,EAAA,OAAO,GAAA;AACR;AAEA,SAAS,qBAAqB,GAAA,EAA4B;AACzD,EAAA,IAAI,OAAO,MAAA,KAAW,WAAA,EAAa,OAAO,IAAA;AAC1C,EAAA,IAAI;AACH,IAAA,OAAO,MAAA,CAAO,YAAA,EAAc,OAAA,CAAQ,GAAG,CAAA,IAAK,IAAA;AAAA,EAC7C,CAAA,CAAA,MAAQ;AACP,IAAA,OAAO,IAAA;AAAA,EACR;AACD;AAEA,SAAS,qBAAA,CAAsB,KAAa,KAAA,EAAqB;AAChE,EAAA,IAAI,OAAO,WAAW,WAAA,EAAa;AACnC,EAAA,IAAI;AACH,IAAA,MAAA,CAAO,YAAA,EAAc,OAAA,CAAQ,GAAA,EAAK,KAAK,CAAA;AAAA,EACxC,CAAA,CAAA,MAAQ;AAAA,EAER;AACD;AAyBO,SAAS,oBAAA,GAA+B;AAC9C,EAAA,IAAI,mBAAmB,OAAO,iBAAA;AAC9B,EAAA,MAAM,QAAA,GAAW,qBAAqB,gBAAgB,CAAA;AAOtD,EAAA,IAAI,QAAA,IAAY,kBAAA,CAAmB,IAAA,CAAK,QAAQ,CAAA,EAAG;AAClD,IAAA,iBAAA,GAAoB,QAAA;AACpB,IAAA,OAAO,QAAA;AAAA,EACR;AACA,EAAA,MAAM,KAAK,gBAAA,EAAiB;AAC5B,EAAA,qBAAA,CAAsB,kBAAkB,EAAE,CAAA;AAC1C,EAAA,iBAAA,GAAoB,EAAA;AACpB,EAAA,OAAO,EAAA;AACR;;;ACwCA,IAAM,QAAA,GAA4B;AAAA,EACjC,YAAA,EAAc,CAAA;AAAA,EACd,UAAA,EAAY,CAAA;AAAA,EACZ,QAAA,EAAU,EAAE,SAAA,EAAW,EAAA,EAAI,QAAQ,GAAA,EAAI;AAAA,EACvC,aAAA,EAAe,IAAA;AAAA,EACf,gBAAA,EAAkB,mBAAA;AAAA,EAClB,gBAAA,EAAkB,IAAA;AAAA,EAClB,aAAA,EAAe,oBAAA;AAAA,EACf,YAAA,EAAc,CAAA;AAAA,EACd,cAAA,EAAgB,GAAA;AAAA,EAChB,aAAA,EAAe,KAAA;AAAA,EACf,gBAAA,EAAkB,CAAA;AAAA,EAClB,eAAA,EAAiB,GAAA;AAAA,EACjB,MAAA,EAAQ;AACT,CAAA;AAEA,IAAM,uBAAA,GAA0B,qCAAA;AAChC,IAAM,mBAAA,GAAsB,IAAI,IAAA,GAAO,IAAA;AACvC,IAAM,mBAAA,GAAsB,CAAA;AAC5B,IAAM,4BAAA,GAA+B,GAAA;AAKrC,IAAM,8BAAA,GAAiC,CAAA;AAOvC,IAAM,6BAAA,GAAgC,IAAA;AAEtC,SAAS,cAAc,KAAA,EAA2B;AACjD,EAAA,IAAI,MAAA,GAAS,EAAA;AACb,EAAA,MAAM,SAAA,GAAY,KAAA;AAClB,EAAA,KAAA,IAAS,IAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,MAAA,EAAQ,KAAK,SAAA,EAAW;AACjD,IAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,QAAA,CAAS,CAAA,EAAG,IAAI,SAAS,CAAA;AAC7C,IAAA,MAAA,IAAU,OAAO,YAAA,CAAa,KAAA,CAAM,MAAM,KAAA,CAAM,IAAA,CAAK,KAAK,CAAC,CAAA;AAAA,EAC5D;AACA,EAAA,OAAO,OAAO,IAAA,KAAS,UAAA,GAAa,IAAA,CAAK,MAAM,CAAA,GAAI,EAAA;AACpD;AAEA,eAAe,UAAU,KAAA,EAAoC;AAC5D,EAAA,IAAI,OAAO,sBAAsB,WAAA,EAAa;AAG7C,IAAA,OAAO,IAAI,WAAA,EAAY,CAAE,MAAA,CAAO,KAAK,CAAA;AAAA,EACtC;AACA,EAAA,MAAM,MAAA,GAAS,IAAI,IAAA,CAAK,CAAC,KAAK,CAAC,CAAA,CAAE,MAAA,EAAO,CAAE,WAAA,CAAY,IAAI,iBAAA,CAAkB,MAAM,CAAC,CAAA;AACnF,EAAA,MAAM,MAAM,MAAM,IAAI,QAAA,CAAS,MAAM,EAAE,WAAA,EAAY;AACnD,EAAA,OAAO,IAAI,WAAW,GAAG,CAAA;AAC1B;AAEA,SAASA,iBAAAA,GAA2B;AACnC,EAAA,IAAI,OAAO,MAAA,KAAW,WAAA,IAAe,OAAO,MAAA,CAAO,eAAe,UAAA,EAAY;AAC7E,IAAA,OAAO,OAAO,UAAA,EAAW;AAAA,EAC1B;AACA,EAAA,MAAM,KAAA,GAAQ,IAAI,UAAA,CAAW,EAAE,CAAA;AAC/B,EAAA,IAAI,OAAO,MAAA,KAAW,WAAA,IAAe,OAAO,MAAA,CAAO,oBAAoB,UAAA,EAAY;AAClF,IAAA,MAAA,CAAO,gBAAgB,KAAK,CAAA;AAAA,EAC7B,CAAA,MAAO;AACN,IAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,QAAQ,CAAA,IAAK,CAAA,EAAG,KAAA,CAAM,CAAC,IAAI,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,MAAA,KAAW,GAAG,CAAA;AAAA,EACpF;AACA,EAAA,IAAI,GAAA,GAAM,EAAA;AACV,EAAA,KAAA,MAAW,CAAA,IAAK,OAAO,GAAA,IAAO,CAAA,CAAE,SAAS,EAAE,CAAA,CAAE,QAAA,CAAS,CAAA,EAAG,GAAG,CAAA;AAC5D,EAAA,OAAO,GAAA;AACR;AAEA,SAAS,gBAAA,GAA2B;AACnC,EAAA,IAAI;AACH,IAAA,MAAM,QAAA,GAAW,MAAA,CAAO,cAAA,EAAgB,OAAA,CAAQ,uBAAuB,CAAA;AACvE,IAAA,IAAI,QAAA,IAAY,kBAAA,CAAmB,IAAA,CAAK,QAAQ,GAAG,OAAO,QAAA;AAAA,EAC3D,CAAA,CAAA,MAAQ;AAAA,EAER;AACA,EAAA,MAAM,KAAKA,iBAAAA,EAAiB;AAC5B,EAAA,IAAI;AACH,IAAA,MAAA,CAAO,cAAA,EAAgB,OAAA,CAAQ,uBAAA,EAAyB,EAAE,CAAA;AAAA,EAC3D,CAAA,CAAA,MAAQ;AAAA,EAER;AACA,EAAA,OAAO,EAAA;AACR;AAEA,SAAS,OAAA,CAAQ,QAAgB,IAAA,EAAsB;AACtD,EAAA,OAAO,GAAG,MAAA,CAAO,OAAA,CAAQ,OAAO,EAAE,CAAC,GAAG,IAAI,CAAA,CAAA;AAC3C;AAOA,SAAS,mBAAmB,KAAA,EAA2B;AACtD,EAAA,IAAI,KAAA,CAAM,IAAA,KAAS,CAAA,EAAG,OAAO,GAAA;AAC7B,EAAA,IAAI,KAAA,CAAM,IAAA,KAAS,CAAA,EAAG,OAAO,GAAA;AAC7B,EAAA,OAAO,GAAA;AACR;AAQA,eAAe,aAAA,CACd,MAAA,EACA,QAAA,EACA,YAAA,EACA,WAAA,EACsC;AACtC,EAAA,IAAI;AACH,IAAA,MAAM,QAAA,GACL,OAAO,MAAA,KAAW,WAAA,IAAe,OAAO,QAAA,GAAW,MAAA,CAAO,SAAS,IAAA,GAAO,KAAA,CAAA;AAC3E,IAAA,MAAM,YACL,OAAO,SAAA,KAAc,eAAe,SAAA,CAAU,SAAA,GAAY,UAAU,SAAA,GAAY,KAAA,CAAA;AACjF,IAAA,MAAM,MAAM,MAAM,KAAA,CAAM,OAAA,CAAQ,MAAA,EAAQ,sBAAsB,CAAA,EAAG;AAAA,MAChE,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA,EAAmB;AAAA,MAC9C,IAAA,EAAM,KAAK,SAAA,CAAU;AAAA,QACpB,QAAA;AAAA,QACA,YAAA;AAAA,QACA,WAAA;AAAA,QACA,QAAA;AAAA,QACA,SAAA;AAAA,QACA,SAAA,EAAA,iBAAW,IAAI,IAAA,EAAK,EAAE,WAAA;AAAY,OAClC;AAAA,KACD,CAAA;AACD,IAAA,IAAI,CAAC,GAAA,CAAI,EAAA,EAAI,OAAO,IAAA;AACpB,IAAA,MAAM,IAAA,GAAQ,MAAM,GAAA,CAAI,IAAA,EAAK;AAK7B,IAAA,IAAI,OAAO,IAAA,CAAK,QAAA,KAAa,SAAA,EAAW,OAAO,IAAA;AAC/C,IAAA,MAAM,MAAA,GAA8B,EAAE,QAAA,EAAU,IAAA,CAAK,QAAA,EAAS;AAC9D,IAAA,IAAI,OAAO,IAAA,CAAK,eAAA,KAAoB,QAAA,EAAU,MAAA,CAAO,kBAAkB,IAAA,CAAK,eAAA;AAC5E,IAAA,IAAI,OAAO,IAAA,CAAK,UAAA,KAAe,QAAA,EAAU,MAAA,CAAO,aAAa,IAAA,CAAK,UAAA;AAClE,IAAA,OAAO,MAAA;AAAA,EACR,CAAA,CAAA,MAAQ;AACP,IAAA,OAAO,IAAA;AAAA,EACR;AACD;AAOA,eAAe,WAAA,CACd,MAAA,EACA,eAAA,EACA,QAAA,EACA,GAAA,EACA,OACA,UAAA,EACA,UAAA,EACA,MAAA,EACA,WAAA,EACA,aAAA,EAC6B;AAC7B,EAAA,MAAM,GAAA,GAAM,OAAA;AAAA,IACX,MAAA;AAAA,IACA,CAAA,qBAAA,EAAwB,kBAAA,CAAmB,eAAe,CAAC,WAAW,GAAG,CAAA;AAAA,GAC1E;AACA,EAAA,IAAI,OAAA,GAAU,CAAA;AACd,EAAA,OAAO,UAAU,WAAA,EAAa;AAC7B,IAAA,IAAI;AAMH,MAAA,MAAM,MAAA,GAAS,MAAM,MAAA,CAAO,KAAA;AAAA,QAC3B,KAAA,CAAM,UAAA;AAAA,QACN,KAAA,CAAM,aAAa,KAAA,CAAM;AAAA,OAC1B;AACA,MAAA,MAAM,IAAA,GAAO,IAAI,IAAA,CAAK,CAAC,MAAM,CAAA,EAAG,EAAE,IAAA,EAAM,0BAAA,EAA4B,CAAA;AACpE,MAAA,MAAM,OAAA,GAAkC;AAAA,QACvC,cAAA,EAAgB,0BAAA;AAAA,QAChB,mBAAA,EAAqB,QAAA;AAAA,QACrB,qBAAA,EAAuB,OAAO,UAAU,CAAA;AAAA,QACxC,qBAAA,EAAuB,OAAO,IAAA,CAAK,GAAA,CAAI,GAAG,IAAA,CAAK,KAAA,CAAM,UAAU,CAAC,CAAC;AAAA,OAClE;AAIA,MAAA,IAAI,gBAAgB,CAAA,EAAG,OAAA,CAAQ,wBAAwB,CAAA,GAAI,OAAO,aAAa,CAAA;AAC/E,MAAA,MAAM,GAAA,GAAM,MAAM,KAAA,CAAM,GAAA,EAAK;AAAA,QAC5B,MAAA,EAAQ,KAAA;AAAA,QACR,IAAA,EAAM,IAAA;AAAA,QACN;AAAA,OACA,CAAA;AACD,MAAA,IAAI,IAAI,EAAA,EAAI,OAAO,EAAE,EAAA,EAAI,IAAA,EAAM,aAAa,KAAA,EAAM;AAGlD,MAAA,IAAI,GAAA,CAAI,WAAW,GAAA,EAAK;AACvB,QAAA,MAAA,CAAO,IAAA,CAAK,CAAA,MAAA,EAAS,GAAG,CAAA,oCAAA,CAAsC,CAAA;AAC9D,QAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,WAAA,EAAa,IAAA,EAAK;AAAA,MACvC;AAEA,MAAA,IAAI,GAAA,CAAI,MAAA,IAAU,GAAA,IAAO,GAAA,CAAI,MAAA,GAAS,GAAA,IAAO,GAAA,CAAI,MAAA,KAAW,GAAA,IAAO,GAAA,CAAI,MAAA,KAAW,GAAA,EAAK;AACtF,QAAA,MAAA,CAAO,MAAM,CAAA,MAAA,EAAS,GAAG,CAAA,eAAA,EAAkB,GAAA,CAAI,MAAM,CAAA,CAAE,CAAA;AACvD,QAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,WAAA,EAAa,KAAA,EAAM;AAAA,MACxC;AAAA,IACD,SAAS,GAAA,EAAK;AACb,MAAA,MAAA,CAAO,KAAK,CAAA,MAAA,EAAS,GAAG,YAAY,OAAA,GAAU,CAAC,WAAW,GAAG,CAAA;AAAA,IAC9D;AACA,IAAA,OAAA,IAAW,CAAA;AACX,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,GAAA,CAAI,IAAA,EAAQ,GAAA,GAAM,CAAA,IAAK,OAAO,CAAA,GAAI,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,MAAA,KAAW,GAAG,CAAA;AACrF,IAAA,MAAM,IAAI,OAAA,CAAQ,CAAA,OAAA,KAAW,UAAA,CAAW,OAAA,EAAS,OAAO,CAAC,CAAA;AAAA,EAC1D;AACA,EAAA,MAAA,CAAO,KAAA,CAAM,CAAA,MAAA,EAAS,GAAG,CAAA,eAAA,EAAkB,WAAW,CAAA,SAAA,CAAW,CAAA;AACjE,EAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,WAAA,EAAa,KAAA,EAAM;AACxC;AAEA,eAAe,eAAA,GAA+C;AAC7D,EAAA,IAAI;AACH,IAAA,MAAM,MAAe,MAAM;AAAA;AAAA,MAAuC;AAAA,KAAO;AACzE,IAAA,IACC,GAAA,IACA,OAAO,GAAA,KAAQ,QAAA,IACf,YAAY,GAAA,IACZ,OAAQ,GAAA,CAA4B,MAAA,KAAW,UAAA,EAC9C;AACD,MAAA,OAAQ,GAAA,CAAgC,MAAA;AAAA,IACzC;AACA,IAAA,OAAO,IAAA;AAAA,EACR,CAAA,CAAA,MAAQ;AACP,IAAA,OAAO,IAAA;AAAA,EACR;AACD;AA2BO,SAAS,oBAAA,CACf,KAAA,EACA,GAAA,EACA,KAAA,EACA,GAAA,EAC0B;AAC1B,EAAA,IAAI,MAAM,IAAA,KAAS,8BAAA,EAAgC,OAAO,EAAE,YAAY,KAAA,EAAM;AAC9E,EAAA,IAAI,GAAA,GAAM,KAAA,CAAM,mBAAA,GAAsB,6BAAA,EAA+B;AACpE,IAAA,OAAO,EAAE,YAAY,KAAA,EAAM;AAAA,EAC5B;AACA,EAAA,IAAI,KAAA,CAAM,aAAA,CAAc,MAAA,GAAS,CAAA,EAAG;AACnC,IAAA,mBAAA,CAAoB,OAAO,GAAG,CAAA;AAAA,EAC/B;AACA,EAAA,KAAA,CAAM,mBAAA,GAAsB,GAAA;AAC5B,EAAA,OAAO,EAAE,YAAY,IAAA,EAAK;AAC3B;AAEA,SAAS,mBAAA,CAAoB,OAAoB,GAAA,EAA0B;AAC1E,EAAA,IAAI,CAAC,MAAM,eAAA,EAAiB;AAC5B,EAAA,IAAI,KAAA,CAAM,aAAA,CAAc,MAAA,KAAW,CAAA,EAAG;AACtC,EAAA,IAAI,KAAA,CAAM,kBAAkB,mBAAA,EAAqB;AAChD,IAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AACrB,IAAA,IAAI,GAAA,GAAM,KAAA,CAAM,oBAAA,GAAuB,4BAAA,EAA8B;AACpE,MAAA,KAAA,CAAM,oBAAA,GAAuB,GAAA;AAC7B,MAAA,GAAA,CAAI,MAAA,CAAO,IAAA;AAAA,QACV,CAAA,mBAAA,EAAsB,MAAM,cAAc,CAAA,2CAAA;AAAA,OAC3C;AAAA,IACD;AACA,IAAA,KAAA,CAAM,gBAAgB,EAAC;AACvB,IAAA,KAAA,CAAM,YAAA,GAAe,CAAA;AACrB,IAAA,KAAA,CAAM,cAAA,GAAiB,IAAA;AACvB,IAAA,KAAA,CAAM,aAAA,GAAgB,IAAA;AAEtB,IAAA,KAAA,CAAM,sBAAA,IAA0B,CAAA;AAChC,IAAA;AAAA,EACD;AAIA,EAAA,IAAI;AACH,IAAA,GAAA,CAAI,WAAA,IAAc;AAAA,EACnB,SAAS,GAAA,EAAK;AACb,IAAA,GAAA,CAAI,MAAA,CAAO,IAAA,CAAK,qCAAA,EAAuC,GAAG,CAAA;AAAA,EAC3D;AACA,EAAA,MAAM,SAAS,KAAA,CAAM,aAAA;AACrB,EAAA,MAAM,aAAa,MAAA,CAAO,MAAA;AAC1B,EAAA,MAAM,OAAA,GAAU,MAAM,cAAA,IAAkB,CAAA;AACxC,EAAA,MAAM,MAAA,GAAS,MAAM,aAAA,IAAiB,OAAA;AACtC,EAAA,MAAM,UAAA,GAAa,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,SAAS,OAAO,CAAA;AAC/C,EAAA,MAAM,MAAM,KAAA,CAAM,YAAA;AAClB,EAAA,KAAA,CAAM,YAAA,IAAgB,CAAA;AACtB,EAAA,KAAA,CAAM,gBAAgB,EAAC;AACvB,EAAA,KAAA,CAAM,YAAA,GAAe,CAAA;AACrB,EAAA,KAAA,CAAM,cAAA,GAAiB,IAAA;AACvB,EAAA,KAAA,CAAM,aAAA,GAAgB,IAAA;AAEtB,EAAA,MAAM,kBAAkB,KAAA,CAAM,eAAA;AAC9B,EAAA,MAAM,MAAA,GAAS,MAAM,OAAA,CAAQ,MAAA;AAC7B,EAAA,MAAM,WAAW,KAAA,CAAM,QAAA;AACvB,EAAA,MAAM,WAAA,GAAc,MAAM,OAAA,CAAQ,gBAAA;AAElC,EAAA,MAAM,gBAAgB,KAAA,CAAM,sBAAA;AAC5B,EAAA,KAAA,CAAM,cAAA,IAAkB,CAAA;AACxB,EAAA,KAAA,CAAM,WAAA,GAAc,KAAA,CAAM,WAAA,CAAY,IAAA,CAAK,YAAY;AACtD,IAAA,IAAI;AACH,MAAA,IAAI,MAAM,SAAA,EAAW;AACrB,MAAA,MAAM,IAAA,GAAO,IAAA,CAAK,SAAA,CAAU,MAAM,CAAA;AAClC,MAAA,MAAM,KAAA,GAAQ,MAAM,SAAA,CAAU,IAAI,CAAA;AAClC,MAAA,IAAI,KAAA,CAAM,aAAa,mBAAA,EAAqB;AAC3C,QAAA,GAAA,CAAI,MAAA,CAAO,KAAA;AAAA,UACV,CAAA,MAAA,EAAS,GAAG,CAAA,uBAAA,EAA0B,KAAA,CAAM,UAAU,CAAA,iBAAA;AAAA,SACvD;AAIA,QAAA,KAAA,CAAM,sBAAA,IAA0B,CAAA;AAChC,QAAA;AAAA,MACD;AACA,MAAA,MAAM,SAAS,MAAM,WAAA;AAAA,QACpB,MAAA;AAAA,QACA,eAAA;AAAA,QACA,QAAA;AAAA,QACA,GAAA;AAAA,QACA,KAAA;AAAA,QACA,UAAA;AAAA,QACA,UAAA;AAAA,QACA,GAAA,CAAI,MAAA;AAAA,QACJ,WAAA;AAAA,QACA;AAAA,OACD;AACA,MAAA,IAAI,MAAA,CAAO,EAAA,IAAM,aAAA,GAAgB,CAAA,EAAG;AAInC,QAAA,KAAA,CAAM,yBAAyB,IAAA,CAAK,GAAA;AAAA,UACnC,CAAA;AAAA,UACA,MAAM,sBAAA,GAAyB;AAAA,SAChC;AAAA,MACD;AACA,MAAA,IAAI,OAAO,WAAA,EAAa;AACvB,QAAA,KAAA,CAAM,OAAA,GAAU,IAAA;AAChB,QAAA,SAAA,CAAU,KAAK,CAAA;AAAA,MAChB;AAAA,IACD,SAAS,GAAA,EAAK;AACb,MAAA,GAAA,CAAI,MAAA,CAAO,KAAA,CAAM,CAAA,MAAA,EAAS,GAAG,kBAAkB,GAAG,CAAA;AAAA,IACnD,CAAA,SAAE;AACD,MAAA,KAAA,CAAM,cAAA,IAAkB,CAAA;AAAA,IACzB;AAAA,EACD,CAAC,CAAA;AACF;AAEA,SAAS,iBAAA,CAAkB,OAAoB,GAAA,EAA0B;AACxE,EAAA,IAAI,KAAA,CAAM,OAAA,IAAW,KAAA,CAAM,SAAA,EAAW;AACtC,EAAA,IAAI,KAAA,CAAM,aAAA,CAAc,MAAA,KAAW,CAAA,EAAG;AACtC,EAAA,mBAAA,CAAoB,OAAO,GAAG,CAAA;AAC/B;AAEA,SAAS,UAAU,KAAA,EAA0B;AAC5C,EAAA,IAAI,MAAM,aAAA,EAAe;AACxB,IAAA,IAAI;AACH,MAAA,KAAA,CAAM,aAAA,EAAc;AAAA,IACrB,CAAA,CAAA,MAAQ;AAAA,IAER;AACA,IAAA,KAAA,CAAM,aAAA,GAAgB,IAAA;AAAA,EACvB;AACA,EAAA,IAAI,MAAM,eAAA,EAAiB;AAC1B,IAAA,aAAA,CAAc,MAAM,eAAe,CAAA;AACnC,IAAA,KAAA,CAAM,eAAA,GAAkB,IAAA;AAAA,EACzB;AACD;AAEA,SAAS,cAAA,CAAe,OAAoB,GAAA,EAA0B;AACrE,EAAA,IAAI,MAAM,SAAA,IAAa,KAAA,CAAM,WAAW,KAAA,CAAM,aAAA,IAAiB,MAAM,cAAA,EAAgB;AACrF,EAAA,KAAA,CAAM,cAAA,GAAiB,IAAA;AACvB,EAAA,KAAK,eAAA,EAAgB,CAAE,IAAA,CAAK,CAAA,MAAA,KAAU;AACrC,IAAA,KAAA,CAAM,cAAA,GAAiB,KAAA;AACvB,IAAA,IAAI,KAAA,CAAM,SAAA,IAAa,KAAA,CAAM,OAAA,IAAW,CAAC,MAAA,EAAQ;AAChD,MAAA,IAAI,CAAC,MAAA,EAAQ,GAAA,CAAI,MAAA,CAAO,KAAK,uCAAuC,CAAA;AACpE,MAAA;AAAA,IACD;AACA,IAAA,IAAI;AACH,MAAA,MAAM,OAAO,MAAA,CAAO;AAAA,QACnB,MAAM,CAAA,KAAA,KAAS;AACd,UAAA,IAAI,KAAA,CAAM,OAAA,IAAW,KAAA,CAAM,SAAA,EAAW;AACtC,UAAA,IAAI,KAAA,CAAM,kBAAA,KAAuB,IAAA,EAAM,KAAA,CAAM,qBAAqB,KAAA,CAAM,SAAA;AAOxE,UAAA,MAAM,EAAE,YAAW,GAAI,oBAAA,CAAqB,OAAO,GAAA,EAAK,KAAA,EAAO,IAAA,CAAK,GAAA,EAAK,CAAA;AACzE,UAAA,KAAA,CAAM,aAAA,CAAc,KAAK,KAAK,CAAA;AAO9B,UAAA,KAAA,CAAM,YAAA,IAAgB,mBAAmB,KAAK,CAAA;AAC9C,UAAA,IAAI,KAAA,CAAM,cAAA,KAAmB,IAAA,EAAM,KAAA,CAAM,iBAAiB,KAAA,CAAM,SAAA;AAChE,UAAA,KAAA,CAAM,gBAAgB,KAAA,CAAM,SAAA;AAC5B,UAAA,IAAI,UAAA,EAAY;AAIf,YAAA,mBAAA,CAAoB,OAAO,GAAG,CAAA;AAAA,UAC/B,CAAA,MAAA,IACC,KAAA,CAAM,aAAA,CAAc,MAAA,IAAU,KAAA,CAAM,OAAA,CAAQ,cAAA,IAC5C,KAAA,CAAM,YAAA,IAAgB,KAAA,CAAM,OAAA,CAAQ,aAAA,EACnC;AACD,YAAA,mBAAA,CAAoB,OAAO,GAAG,CAAA;AAAA,UAC/B;AAAA,QACD,CAAA;AAAA,QACA,aAAA,EAAe,MAAM,OAAA,CAAQ,aAAA;AAAA,QAC7B,gBAAA,EAAkB,KAAA,CAAM,OAAA,CAAQ,gBAAA,IAAoB,KAAA,CAAA;AAAA,QACpD,gBAAA,EAAkB,MAAM,OAAA,CAAQ,gBAAA;AAAA,QAChC,aAAA,EAAe,MAAM,OAAA,CAAQ,aAAA;AAAA,QAC7B,QAAA,EAAU,MAAM,OAAA,CAAQ,QAAA;AAAA,QACxB,gBAAA,EAAkB,MAAM,OAAA,CAAQ;AAAA,OAChC,CAAA;AACD,MAAA,KAAA,CAAM,aAAA,GAAgB,IAAA;AACtB,MAAA,KAAA,CAAM,MAAA,GAAS,MAAA;AACf,MAAA,sBAAA,CAAuB,OAAO,GAAG,CAAA;AAEjC,MAAA,KAAA,CAAM,eAAA,GAAkB,WAAA;AAAA,QACvB,MAAM,iBAAA,CAAkB,KAAA,EAAO,GAAG,CAAA;AAAA,QAClC,KAAA,CAAM,QAAQ,YAAA,GAAe;AAAA,OAC9B;AAAA,IACD,SAAS,GAAA,EAAK;AACb,MAAA,GAAA,CAAI,MAAA,CAAO,KAAA,CAAM,sBAAA,EAAwB,GAAG,CAAA;AAAA,IAC7C;AAAA,EACD,CAAC,CAAA;AACF;AAEA,SAAS,sBAAA,CAAuB,OAAoB,GAAA,EAA0B;AAC7E,EAAA,IAAI,KAAA,CAAM,aAAa,KAAA,CAAM,OAAA,IAAW,CAAC,KAAA,CAAM,MAAA,IAAU,CAAC,KAAA,CAAM,aAAA,EAAe;AAC/E,EAAA,MAAM,EAAA,GAAK,MAAM,MAAA,CAAO,gBAAA;AACxB,EAAA,IAAI,OAAO,OAAO,UAAA,EAAY;AAC9B,EAAA,IAAI;AACH,IAAA,EAAA,CAAG,IAAI,CAAA;AAAA,EACR,SAAS,GAAA,EAAK;AACb,IAAA,GAAA,CAAI,MAAA,CAAO,IAAA,CAAK,wBAAA,EAA0B,GAAG,CAAA;AAAA,EAC9C;AACD;AAEA,SAAS,QAAA,CAAS,KAAA,EAAoB,GAAA,EAAoB,IAAA,EAAoC;AAC7F,EAAA,IAAI,CAAC,MAAM,eAAA,EAAiB;AAC5B,EAAA,IAAI,MAAM,aAAA,CAAc,MAAA,GAAS,CAAA,EAAG,iBAAA,CAAkB,OAAO,GAAG,CAAA;AAChE,EAAA,MAAM,GAAA,GAAM,OAAA;AAAA,IACX,MAAM,OAAA,CAAQ,MAAA;AAAA,IACd,CAAA,qBAAA,EAAwB,kBAAA,CAAmB,KAAA,CAAM,eAAe,CAAC,CAAA,SAAA;AAAA,GAClE;AACA,EAAA,MAAM,IAAA,GAAO,IAAA,CAAK,SAAA,CAAU,EAAE,QAAA,EAAU,KAAA,CAAM,QAAA,EAAU,OAAA,EAAA,iBAAS,IAAI,IAAA,EAAK,EAAE,WAAA,IAAe,CAAA;AAC3F,EAAA,IAAI,KAAK,SAAA,IAAa,OAAO,SAAA,KAAc,WAAA,IAAe,UAAU,UAAA,EAAY;AAC/E,IAAA,IAAI;AACH,MAAA,MAAM,IAAA,GAAO,IAAI,IAAA,CAAK,CAAC,IAAI,CAAA,EAAG,EAAE,IAAA,EAAM,kBAAA,EAAoB,CAAA;AAC1D,MAAA,SAAA,CAAU,UAAA,CAAW,KAAK,IAAI,CAAA;AAC9B,MAAA;AAAA,IACD,SAAS,GAAA,EAAK;AACb,MAAA,GAAA,CAAI,MAAA,CAAO,IAAA,CAAK,2BAAA,EAA6B,GAAG,CAAA;AAAA,IACjD;AAAA,EACD;AACA,EAAA,KAAK,MAAM,GAAA,EAAK;AAAA,IACf,MAAA,EAAQ,MAAA;AAAA,IACR,IAAA;AAAA,IACA,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA,EAAmB;AAAA,IAC9C,SAAA,EAAW;AAAA,GACX,EAAE,KAAA,CAAM,CAAA,GAAA,KAAO,IAAI,MAAA,CAAO,IAAA,CAAK,uBAAA,EAAyB,GAAG,CAAC,CAAA;AAC9D;AAOO,SAAS,aAAA,CAAc,OAAA,GAAgC,EAAC,EAAgB;AAC9E,EAAA,MAAM,MAAA,GAA0B;AAAA,IAC/B,GAAG,QAAA;AAAA,IACH,GAAG,OAAA;AAAA,IACH,QAAA,EAAU,EAAE,GAAG,QAAA,CAAS,UAAU,GAAI,OAAA,CAAQ,QAAA,IAAY,EAAC;AAAG,GAC/D;AAEA,EAAA,OAAO;AAAA,IACN,IAAA,EAAM,gBAAA;AAAA,IACN,OAAO,GAAA,EAAK;AACX,MAAA,IAAI,OAAO,WAAW,WAAA,EAAa;AACnC,MAAA,IAAI,OAAO,UAAA,GAAa,CAAA,IAAK,KAAK,MAAA,EAAO,IAAK,OAAO,UAAA,EAAY;AAChE,QAAA,GAAA,CAAI,MAAA,CAAO,MAAM,uBAAuB,CAAA;AACxC,QAAA;AAAA,MACD;AAEA,MAAA,MAAM,MAAA,GAAS,MAAA,CAAO,MAAA,IAAU,GAAA,CAAI,OAAA;AACpC,MAAA,IAAI,CAAC,MAAA,EAAQ;AACZ,QAAA,GAAA,CAAI,MAAA,CAAO,MAAM,+DAA+D,CAAA;AAChF,QAAA;AAAA,MACD;AAMA,MAAA,MAAM,eAAe,GAAA,CAAI,eAAA,GAAkB,GAAA,CAAI,eAAA,KAAoB,gBAAA,EAAiB;AAGpF,MAAA,MAAM,cAAc,oBAAA,EAAqB;AAEzC,MAAA,MAAM,KAAA,GAAqB;AAAA,QAC1B,OAAA,EAAS,EAAE,GAAG,MAAA,EAAQ,MAAA,EAAO;AAAA,QAC7B,UAAU,GAAA,CAAI,QAAA;AAAA,QACd,YAAA;AAAA,QACA,eAAA,EAAiB,IAAA;AAAA,QACjB,kBAAA,EAAoB,IAAA;AAAA,QACpB,eAAe,EAAC;AAAA,QAChB,YAAA,EAAc,CAAA;AAAA,QACd,cAAA,EAAgB,IAAA;AAAA,QAChB,aAAA,EAAe,IAAA;AAAA,QACf,oBAAA,EAAsB,CAAA;AAAA,QACtB,sBAAA,EAAwB,CAAA;AAAA,QACxB,mBAAA,EAAqB,CAAA;AAAA,QACrB,YAAA,EAAc,CAAA;AAAA,QACd,WAAA,EAAa,QAAQ,OAAA,EAAQ;AAAA,QAC7B,cAAA,EAAgB,CAAA;AAAA,QAChB,eAAA,EAAiB,IAAA;AAAA,QACjB,UAAA,EAAY,IAAA;AAAA,QACZ,eAAA,EAAiB,IAAA;AAAA,QACjB,iBAAA,EAAmB,IAAA;AAAA,QACnB,mBAAA,EAAqB,IAAA;AAAA,QACrB,MAAA,EAAQ,IAAA;AAAA,QACR,aAAA,EAAe,IAAA;AAAA,QACf,cAAA,EAAgB,KAAA;AAAA,QAChB,SAAA,EAAW,KAAA;AAAA,QACX,OAAA,EAAS;AAAA,OACV;AACA,MAAA,GAAA,CAAI,SAAS,KAAK,CAAA;AAElB,MAAA,MAAM,cAAA,GAAiB,MAAY,sBAAA,CAAuB,KAAA,EAAO,GAAG,CAAA;AACpE,MAAA,KAAA,CAAM,mBAAA,GAAsB,cAAA;AAC5B,MAAA,MAAA,CAAO,gBAAA,CAAiB,uBAAuB,cAAc,CAAA;AAM7D,MAAA,MAAM,eAAe,MAAY;AAChC,QAAA,IAAI,MAAM,OAAA,EAAS;AACnB,QAAA,QAAA,CAAS,KAAA,EAAO,GAAA,EAAK,EAAE,SAAA,EAAW,MAAM,CAAA;AACxC,QAAA,KAAA,CAAM,OAAA,GAAU,IAAA;AAChB,QAAA,SAAA,CAAU,KAAK,CAAA;AAAA,MAChB,CAAA;AACA,MAAA,KAAA,CAAM,eAAA,GAAkB,YAAA;AACxB,MAAA,MAAA,CAAO,gBAAA,CAAiB,YAAY,YAAY,CAAA;AAQhD,MAAA,MAAM,qBAAqB,MAAY;AACtC,QAAA,IAAI,QAAA,CAAS,oBAAoB,QAAA,EAAU;AAC3C,QAAA,YAAA,EAAa;AAAA,MACd,CAAA;AACA,MAAA,KAAA,CAAM,iBAAA,GAAoB,kBAAA;AAC1B,MAAA,QAAA,CAAS,gBAAA,CAAiB,oBAAoB,kBAAkB,CAAA;AAEhE,MAAA,MAAM,QAAQ,YAA2B;AACxC,QAAA,IAAI,MAAM,SAAA,EAAW;AAQrB,QAAA,IAAI;AACH,UAAA,GAAA,CAAI,WAAA,IAAc;AAAA,QACnB,SAAS,GAAA,EAAK;AACb,UAAA,GAAA,CAAI,MAAA,CAAO,IAAA,CAAK,oCAAA,EAAsC,GAAG,CAAA;AAAA,QAC1D;AACA,QAAA,MAAM,UAAU,MAAM,aAAA,CAAc,QAAQ,GAAA,CAAI,QAAA,EAAU,cAAc,WAAW,CAAA;AACnF,QAAA,IAAI,CAAC,OAAA,EAAS;AACb,UAAA,GAAA,CAAI,MAAA,CAAO,KAAK,wCAAwC,CAAA;AACxD,UAAA,KAAA,CAAM,OAAA,GAAU,IAAA;AAChB,UAAA;AAAA,QACD;AACA,QAAA,IAAI,CAAC,QAAQ,QAAA,EAAU;AACtB,UAAA,GAAA,CAAI,OAAO,IAAA,CAAK,CAAA,yBAAA,EAA4B,OAAA,CAAQ,UAAA,IAAc,SAAS,CAAA,CAAE,CAAA;AAC7E,UAAA,KAAA,CAAM,OAAA,GAAU,IAAA;AAChB,UAAA;AAAA,QACD;AACA,QAAA,IAAI,CAAC,QAAQ,eAAA,EAAiB;AAC7B,UAAA,GAAA,CAAI,MAAA,CAAO,MAAM,iDAAiD,CAAA;AAClE,UAAA,KAAA,CAAM,OAAA,GAAU,IAAA;AAChB,UAAA;AAAA,QACD;AACA,QAAA,KAAA,CAAM,kBAAkB,OAAA,CAAQ,eAAA;AAChC,QAAA,KAAA,CAAM,kBAAA,GAAqB,KAAK,GAAA,EAAI;AAKpC,QAAA,GAAA,CAAI,oBAAA,GAAuB,MAAM,kBAAkB,CAAA;AACnD,QAAA,cAAA,CAAe,OAAO,GAAG,CAAA;AAAA,MAC1B,CAAA;AAEA,MAAA,IAAI,MAAA,CAAO,eAAe,CAAA,EAAG;AAC5B,QAAA,MAAM,eAAe,MAAY;AAChC,UAAA,KAAA,CAAM,SAAA,GAAY,IAAA;AAClB,UAAA,IAAI,MAAM,UAAA,EAAY;AACrB,YAAA,YAAA,CAAa,MAAM,UAAU,CAAA;AAC7B,YAAA,KAAA,CAAM,UAAA,GAAa,IAAA;AAAA,UACpB;AAAA,QACD,CAAA;AACA,QAAA,MAAA,CAAO,iBAAiB,UAAA,EAAY,YAAA,EAAc,EAAE,IAAA,EAAM,MAAM,CAAA;AAChE,QAAA,MAAA,CAAO,iBAAiB,cAAA,EAAgB,YAAA,EAAc,EAAE,IAAA,EAAM,MAAM,CAAA;AACpE,QAAA,KAAA,CAAM,UAAA,GAAa,WAAW,MAAM;AACnC,UAAA,KAAK,KAAA,EAAM;AAAA,QACZ,CAAA,EAAG,OAAO,YAAY,CAAA;AAAA,MACvB,CAAA,MAAO;AACN,QAAA,KAAK,KAAA,EAAM;AAAA,MACZ;AAAA,IACD,CAAA;AAAA,IACA,iBAAiB,GAAA,EAAK;AACrB,MAAA,MAAM,KAAA,GAAQ,IAAI,QAAA,EAAsB;AACxC,MAAA,IAAI,CAAC,KAAA,IAAS,KAAA,CAAM,SAAA,IAAa,KAAA,CAAM,SAAS,OAAO,MAAA;AACvD,MAAA,IAAI,CAAC,KAAA,CAAM,eAAA,EAAiB,OAAO,MAAA;AACnC,MAAA,MAAM,QAAA,GACL,KAAA,CAAM,kBAAA,KAAuB,IAAA,GAC1B,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,GAAA,EAAI,GAAI,KAAA,CAAM,kBAAkB,CAAA,GACjD,CAAA;AACJ,MAAA,OAAO,EAAE,eAAA,EAAiB,KAAA,CAAM,eAAA,EAAiB,gBAAgB,QAAA,EAAS;AAAA,IAC3E,CAAA;AAAA,IACA,UAAU,GAAA,EAAK;AACd,MAAA,MAAM,KAAA,GAAQ,IAAI,QAAA,EAAsB;AACxC,MAAA,IAAI,CAAC,KAAA,EAAO;AACZ,MAAA,KAAA,CAAM,SAAA,GAAY,IAAA;AAClB,MAAA,IAAI,MAAM,UAAA,EAAY;AACrB,QAAA,YAAA,CAAa,MAAM,UAAU,CAAA;AAC7B,QAAA,KAAA,CAAM,UAAA,GAAa,IAAA;AAAA,MACpB;AACA,MAAA,IAAI,MAAM,eAAA,EAAiB;AAC1B,QAAA,MAAA,CAAO,mBAAA,CAAoB,UAAA,EAAY,KAAA,CAAM,eAAe,CAAA;AAC5D,QAAA,KAAA,CAAM,eAAA,GAAkB,IAAA;AAAA,MACzB;AACA,MAAA,IAAI,MAAM,iBAAA,EAAmB;AAC5B,QAAA,QAAA,CAAS,mBAAA,CAAoB,kBAAA,EAAoB,KAAA,CAAM,iBAAiB,CAAA;AACxE,QAAA,KAAA,CAAM,iBAAA,GAAoB,IAAA;AAAA,MAC3B;AACA,MAAA,IAAI,MAAM,mBAAA,EAAqB;AAC9B,QAAA,MAAA,CAAO,mBAAA,CAAoB,qBAAA,EAAuB,KAAA,CAAM,mBAAmB,CAAA;AAC3E,QAAA,KAAA,CAAM,mBAAA,GAAsB,IAAA;AAAA,MAC7B;AAIA,MAAA,IAAI,KAAA,CAAM,eAAA,IAAmB,CAAC,KAAA,CAAM,OAAA,EAAS;AAC5C,QAAA,QAAA,CAAS,KAAA,EAAO,GAAA,EAAK,EAAE,SAAA,EAAW,OAAO,CAAA;AAAA,MAC1C;AACA,MAAA,KAAA,CAAM,OAAA,GAAU,IAAA;AAChB,MAAA,SAAA,CAAU,KAAK,CAAA;AACf,MAAA,KAAA,CAAM,cAAc,MAAA,GAAS,CAAA;AAC7B,MAAA,KAAA,CAAM,YAAA,GAAe,CAAA;AACrB,MAAA,KAAA,CAAM,MAAA,GAAS,IAAA;AAAA,IAChB;AAAA,GACD;AACD;AAMO,SAAS,kBAAkB,GAAA,EAAiD;AAClF,EAAA,MAAM,KAAA,GAAQ,IAAI,QAAA,EAAsB;AACxC,EAAA,IAAI,CAAC,SAAS,KAAA,CAAM,SAAA,IAAa,MAAM,OAAA,IAAW,CAAC,KAAA,CAAM,eAAA,EAAiB,OAAO,IAAA;AACjF,EAAA,MAAM,QAAA,GACL,KAAA,CAAM,kBAAA,KAAuB,IAAA,GAC1B,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,GAAA,EAAI,GAAI,KAAA,CAAM,kBAAkB,CAAA,GACjD,CAAA;AACJ,EAAA,OAAO,EAAE,EAAA,EAAI,KAAA,CAAM,eAAA,EAAiB,QAAA,EAAS;AAC9C;AAGO,IAAM,QAAA,GAAW;AAAA,EACvB,aAAA;AAAA,EACA,SAAA;AAAA,EACA,gBAAA;AAAA,EACA,WAAA;AAAA,EACA,aAAA;AAAA,EACA,OAAA;AAAA,EACA,mBAAA;AAAA,EACA,oBAAA;AAAA,EACA,8BAAA;AAAA,EACA,6BAAA;AAAA,EACA,mBAAA;AAAA,EACA,uBAAA;AAAA,EACA,mBAAA;AAAA,EACA,4BAAA;AAAA,EACA;AACD","file":"session-replay.cjs","sourcesContent":["// Identity layer for the Usero SDK.\n//\n// Two responsibilities:\n// 1. Mint and persist a stable per-browser `anonymousId` in localStorage\n// so cross-tab + cross-day replays from the same browser stitch\n// together server-side. Falls back to an in-memory id if storage is\n// blocked (sandboxed iframes, Safari Lockdown, full quota). Replay\n// still works in that case, you just lose stitching.\n// 2. Auto-fire POST /api/identify when the resolved user transitions\n// (null -> id, id -> id'). Deduped by an in-memory fingerprint so\n// re-renders with the same user are no-ops on the network.\n//\n// All storage access is wrapped in try/catch and gated behind a one-shot\n// init read. The hot path (replay chunk flush) never touches localStorage.\n\nconst ANON_STORAGE_KEY = 'usero:anonymous-id'\n// Reuse the EXACT key the session-replay plugin has always written\n// (`usero:session-replay:sdk-session-id`) so we adopt per-tab session ids\n// already living in customers' browsers instead of minting a fresh one.\n// Changing this would split a tab's existing replay from its user-test\n// linkage, so it must stay byte-for-byte identical to the replay plugin's\n// historical key.\nconst SDK_SESSION_STORAGE_KEY = 'usero:session-replay:sdk-session-id'\n\nlet cachedAnonymousId: string | null = null\nlet cachedSdkSessionId: string | null = null\n// Last resolved external user id, set by the identify pipeline so any\n// plugin can read the current identity without re-resolving the host's\n// `user` prop. Cleared on logout (anonymousId rotation).\nlet currentUserId: string | null = null\n// Wall-clock epoch (ms) when the session-replay recording started, as\n// published by the replay plugin. Lets other plugins (user-test) compute\n// an offset into the recording without importing the replay module. Null\n// until replay actually starts (or if replay is not loaded at all).\nlet replayStartMs: number | null = null\n// Fingerprint of the last identify we POSTed. Same SDK instance + same\n// resolved user + same traits = no-op. Cleared on logout (anonymousId\n// rotation).\nlet lastIdentifyFingerprint: string | null = null\n\nexport type UserTraitValue = string | number | boolean | null\nexport type UserTraits = Record<string, UserTraitValue>\n\nexport interface UseroUser {\n\tid: string\n\temail?: string\n\tdisplayName?: string\n\ttraits?: UserTraits\n}\n\nfunction generateRandomId(): string {\n\tif (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n\t\treturn crypto.randomUUID()\n\t}\n\tconst bytes = new Uint8Array(16)\n\tif (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {\n\t\tcrypto.getRandomValues(bytes)\n\t} else {\n\t\tfor (let i = 0; i < bytes.length; i += 1) bytes[i] = Math.floor(Math.random() * 256)\n\t}\n\tlet out = ''\n\tfor (const b of bytes) out += b.toString(16).padStart(2, '0')\n\treturn out\n}\n\nfunction safeReadLocalStorage(key: string): string | null {\n\tif (typeof window === 'undefined') return null\n\ttry {\n\t\treturn window.localStorage?.getItem(key) ?? null\n\t} catch {\n\t\treturn null\n\t}\n}\n\nfunction safeWriteLocalStorage(key: string, value: string): void {\n\tif (typeof window === 'undefined') return\n\ttry {\n\t\twindow.localStorage?.setItem(key, value)\n\t} catch {\n\t\t// Sandboxed iframe / Safari Lockdown / quota. Fall back to memory.\n\t}\n}\n\nfunction safeReadSessionStorage(key: string): string | null {\n\tif (typeof window === 'undefined') return null\n\ttry {\n\t\treturn window.sessionStorage?.getItem(key) ?? null\n\t} catch {\n\t\treturn null\n\t}\n}\n\nfunction safeWriteSessionStorage(key: string, value: string): void {\n\tif (typeof window === 'undefined') return\n\ttry {\n\t\twindow.sessionStorage?.setItem(key, value)\n\t} catch {\n\t\t// Sandboxed iframe / Safari Lockdown / quota. Fall back to memory.\n\t}\n}\n\n/**\n * Returns the stable per-browser anonymousId. Reads localStorage at most\n * once per SDK instance. Subsequent calls hit the in-memory cache, so\n * even hot paths (per-event in replay) are safe to call this.\n */\nexport function getOrMintAnonymousId(): string {\n\tif (cachedAnonymousId) return cachedAnonymousId\n\tconst existing = safeReadLocalStorage(ANON_STORAGE_KEY)\n\t// Sanity filter, not strict validation. We accept anything that looks\n\t// plausibly like an id (>=8 alphanumeric-or-hyphen) so older SDK\n\t// versions that wrote a slightly different shape still stitch. Fresh\n\t// mint is cheap, so we only reject obvious garbage; tightening this\n\t// would force rotation in customer browsers and split otherwise-good\n\t// sibling-session attribution.\n\tif (existing && /^[a-z0-9-]{8,}$/i.test(existing)) {\n\t\tcachedAnonymousId = existing\n\t\treturn existing\n\t}\n\tconst id = generateRandomId()\n\tsafeWriteLocalStorage(ANON_STORAGE_KEY, id)\n\tcachedAnonymousId = id\n\treturn id\n}\n\n/**\n * Rotate the anonymousId. Called on logout (user transitions from a\n * known id to null) so the next anonymous trail does not get auto-merged\n * into the previous person on the next identify().\n */\nexport function rotateAnonymousId(): string {\n\tconst id = generateRandomId()\n\tcachedAnonymousId = id\n\tsafeWriteLocalStorage(ANON_STORAGE_KEY, id)\n\tlastIdentifyFingerprint = null\n\tcurrentUserId = null\n\treturn id\n}\n\n/**\n * Returns the stable per-tab sdkSessionId. Core-owned so every plugin\n * (replay, user-test, future feedback linkage) reads the SAME id for a\n * given tab. Reads sessionStorage at most once per SDK instance; later\n * calls hit the in-memory cache.\n *\n * Backward compat: reuses the `usero:sdk-session-id` key the replay\n * plugin has always written, and the same loose sanity filter, so an\n * id minted by an older replay-only SDK build is adopted as-is rather\n * than rotated (which would split a tab's replay from its user-test).\n */\nexport function getOrMintSdkSessionId(): string {\n\tif (cachedSdkSessionId) return cachedSdkSessionId\n\tconst existing = safeReadSessionStorage(SDK_SESSION_STORAGE_KEY)\n\tif (existing && /^[a-z0-9-]{8,}$/i.test(existing)) {\n\t\tcachedSdkSessionId = existing\n\t\treturn existing\n\t}\n\tconst id = generateRandomId()\n\tsafeWriteSessionStorage(SDK_SESSION_STORAGE_KEY, id)\n\tcachedSdkSessionId = id\n\treturn id\n}\n\n/**\n * The current resolved external user id, or null if no identify has\n * succeeded this session (or after logout). Set by the identify\n * pipeline; read by plugins that want the current identity without\n * re-resolving the host's `user` prop.\n */\nexport function getCurrentUserId(): string | null {\n\treturn currentUserId\n}\n\n/**\n * Published by the session-replay plugin when its recording starts.\n * Other plugins read it via `getReplayStartMs()` to compute an offset\n * into the recording. No-op duplicate publishes keep the first value so\n * the offset stays anchored to the true recording start.\n */\nexport function publishReplayStartMs(epochMs: number): void {\n\tif (replayStartMs === null) replayStartMs = epochMs\n}\n\n/**\n * The wall-clock epoch (ms) when session-replay started, or null if\n * replay never started this session (plugin not loaded, or sampled out).\n * Consumers degrade gracefully when null: ship the sdkSessionId linkage\n * key anyway and omit the offset.\n */\nexport function getReplayStartMs(): number | null {\n\treturn replayStartMs\n}\n\nfunction fingerprintUser(anonymousId: string, user: UseroUser): string {\n\t// Stable across re-renders: keys sorted, traits canonicalised. Cheap\n\t// enough on the hot path (only runs when the SDK thinks the user might\n\t// have changed, never per-event).\n\tconst traits = user.traits ?? {}\n\tconst keys = Object.keys(traits).sort()\n\tconst canonical: Array<[string, UserTraitValue]> = keys.map(k => [k, traits[k] ?? null])\n\treturn JSON.stringify([anonymousId, user.id, user.email ?? null, user.displayName ?? null, canonical])\n}\n\nexport interface IdentifyTransport {\n\tapiUrl: string\n\tclientId: string\n}\n\n/**\n * POST to /api/identify if the (anonymousId, user) fingerprint differs\n * from the last call. Returns true if a network request actually fired.\n * Never throws; failures are best-effort and the caller (the widget /\n * provider) should not treat them as errors.\n *\n * Tab-unload safety: if the page is hidden when this fires (visibility\n * 'hidden' or a pagehide handler), we route the payload through\n * `navigator.sendBeacon` so the request survives unload. Otherwise we\n * use a normal fetch and only cache the fingerprint when the server\n * confirms `accepted: true`. A 200 `{ accepted: false }` (e.g.\n * `unknown_client` for a clientId that becomes valid mid-session) is\n * treated as retryable so the next call re-fires.\n */\nexport async function identifyIfChanged(transport: IdentifyTransport, user: UseroUser): Promise<boolean> {\n\tconst anonymousId = getOrMintAnonymousId()\n\t// Make the resolved id readable by other plugins immediately, even\n\t// before the network round-trip resolves. This is the identity the\n\t// host just told us about; the POST below is best-effort persistence.\n\tcurrentUserId = user.id\n\tconst fp = fingerprintUser(anonymousId, user)\n\tif (fp === lastIdentifyFingerprint) return false\n\n\tconst url = `${transport.apiUrl.replace(/\\/$/, '')}/api/identify`\n\t// Body must stay under the browser's keepalive / sendBeacon cap\n\t// (~64KB across most engines) when this fires on pagehide. That\n\t// transitively caps trait payload size; in practice traits should be\n\t// small typed scalars, not blobs.\n\tconst body = JSON.stringify({\n\t\tclientId: transport.clientId,\n\t\tanonymousId,\n\t\texternalUserId: user.id,\n\t\temail: user.email,\n\t\tdisplayName: user.displayName,\n\t\ttraits: user.traits,\n\t})\n\n\t// If the document is hidden (pagehide / tab close in flight), best-effort\n\t// hand off to sendBeacon. We don't get a response back, so we optimistically\n\t// cache the fingerprint to avoid re-firing on the next page; the server is\n\t// idempotent if the page reload re-runs identify with the same payload.\n\tif (\n\t\ttypeof document !== 'undefined' &&\n\t\tdocument.visibilityState === 'hidden' &&\n\t\ttypeof navigator !== 'undefined' &&\n\t\ttypeof navigator.sendBeacon === 'function'\n\t) {\n\t\ttry {\n\t\t\tconst blob = new Blob([body], { type: 'application/json' })\n\t\t\t// sendBeacon returns false when the user agent refuses to queue\n\t\t\t// the request (size cap, or historically Safari rejecting\n\t\t\t// non-CORS-simple content types). Modern Safari accepts\n\t\t\t// application/json, but we keep a keepalive-fetch fallback so an\n\t\t\t// older WebKit that rejects the beacon still ships the identify.\n\t\t\tif (navigator.sendBeacon(url, blob)) {\n\t\t\t\tlastIdentifyFingerprint = fp\n\t\t\t\treturn true\n\t\t\t}\n\t\t} catch {\n\t\t\t// fall through to keepalive fetch below\n\t\t}\n\t}\n\n\ttry {\n\t\tconst res = await fetch(url, {\n\t\t\tmethod: 'POST',\n\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\tbody,\n\t\t\t// keepalive lets the request survive a tab-close mid-flight on\n\t\t\t// browsers that support it; sendBeacon above is the primary path.\n\t\t\tkeepalive: true,\n\t\t})\n\t\tif (!res.ok) return true\n\t\t// Parse the response: a 200 with `accepted: false` (e.g. unknown\n\t\t// client) is retryable. Only cache the fingerprint when the server\n\t\t// confirmed it actually stored the identity.\n\t\ttry {\n\t\t\tconst json = (await res.json()) as { accepted?: unknown }\n\t\t\tif (json && json.accepted === true) lastIdentifyFingerprint = fp\n\t\t} catch {\n\t\t\t// Server returned 2xx but unparseable body: don't cache, let the\n\t\t\t// next call retry.\n\t\t}\n\t\treturn true\n\t} catch {\n\t\treturn false\n\t}\n}\n\n/**\n * Clear identify state and rotate anonymousId. Called when the resolved\n * user transitions from a known id to null (logout). The next anonymous\n * trail will get a fresh anonymousId so it does not merge into the\n * previous person.\n */\nexport function handleLogout(): void {\n\trotateAnonymousId()\n}\n\n// Test hooks (not exported from the package public surface).\nexport const __test__ = {\n\tANON_STORAGE_KEY,\n\tSDK_SESSION_STORAGE_KEY,\n\tresetIdentityState: (): void => {\n\t\tcachedAnonymousId = null\n\t\tcachedSdkSessionId = null\n\t\tcurrentUserId = null\n\t\treplayStartMs = null\n\t\tlastIdentifyFingerprint = null\n\t},\n}\n","// Session replay plugin for the Usero widget.\n//\n// Streams rrweb events to the SaaS side as gzipped chunks while the user\n// is on the page, instead of buffering in memory and attaching to a\n// feedback submission. This decouples session replay from feedback so we\n// capture every session (subject to bot-gate + sampling + engagement\n// gates), not just the ones that submit feedback.\n//\n// Lifecycle:\n// 1. onInit: dice-roll sample, optional engagement-time gate, mint a\n// stable per-tab `sdkSessionId` in sessionStorage, and POST to\n// /api/replay-sessions to create the row. If the server returns\n// `{accepted:false}` (bot-gated), the plugin no-ops the rest of the\n// session and getCurrentSession() returns null.\n// 2. Recording: lazy-load rrweb, append events to a buffer, flush a\n// chunk every `chunkSeconds` (or sooner if the buffer is large).\n// Each chunk is gzipped via CompressionStream and PUT to\n// /api/replay-sessions/:id/chunks/:seq with raw bytes + the three\n// X-Usero-* headers (Client-Id, Event-Count, Duration-Ms). Retries\n// with exponential backoff. R2 head-check makes retries idempotent\n// server-side. A chunk PUT returning 409 stops the session.\n// 3. onFeedbackSubmit: returns `{sessionReplayId, replayOffsetMs}` so\n// the feedback record can FK at the moment of submit. Does NOT\n// attach `replayEvents` (legacy field) — chunked uploads carry the\n// events out-of-band.\n// 4. onDestroy / pagehide / visibilitychange -> hidden: best-effort flush\n// remaining buffer, then sendBeacon to\n// /api/replay-sessions/:id/finalise with the end-timestamp. Idempotent\n// server-side and via a `stopped` guard client-side.\n//\n// Bundle hygiene: rrweb stays lazy via dynamic `import('rrweb')` behind\n// the engagement gate, so consumers who lose the dice roll or navigate\n// away inside the gate window pay zero rrweb bytes.\n\nimport { getOrMintAnonymousId } from '../identity'\nimport type { UseroPlugin, PluginContext } from '../plugin'\n\nexport interface ReplaySampling {\n\tmousemove?: number\n\tscroll?: number\n\tmedia?: number\n\tinput?: number | 'last'\n}\n\nexport interface SessionReplayOptions {\n\t// Wait this many ms after page load before loading rrweb and creating\n\t// the session row. If the user navigates away first, rrweb is never\n\t// loaded and no session row is created. Default 0 (start immediately).\n\tstartAfterMs?: number\n\t// Probability (0..1) that this session records at all. Decided once\n\t// at init via Math.random(). Default 1.\n\tsampleRate?: number\n\t// rrweb sampling rates per event type.\n\tsampling?: ReplaySampling\n\t// Mask all <input>/<textarea> values in the recording. Default true.\n\tmaskAllInputs?: boolean\n\t// CSS selector for nodes whose text content should be masked. Default\n\t// `[data-usero-mask]`.\n\tmaskTextSelector?: string\n\t// Inline external stylesheets so the replay viewer renders correctly\n\t// without network access. Default true.\n\tinlineStylesheet?: boolean\n\t// Block (entirely skip) DOM subtrees matching this selector. Default\n\t// `[data-usero-block]`.\n\tblockSelector?: string\n\t// Flush a chunk every N seconds. Default 3. Smaller = more PUTs but\n\t// less data lost on tab crash and less time event refs retain detached\n\t// DOM nodes in memory.\n\tchunkSeconds?: number\n\t// Soft cap on buffered events before forcing a flush, regardless of\n\t// time. Default 1000.\n\tchunkMaxEvents?: number\n\t// Soft cap on estimated buffered bytes before forcing a flush. Default\n\t// 512_000 (~500 KB pre-gzip). Keeps memory pressure bounded on event-heavy\n\t// pages even when chunkMaxEvents hasn't been hit.\n\tchunkMaxBytes?: number\n\t// Max attempts per chunk before giving up. Default 5.\n\tchunkMaxAttempts?: number\n\t// Force rrweb to take a fresh full snapshot every N ms. This resets\n\t// rrweb's internal mirror so detached DOM (e.g. SPA route changes)\n\t// becomes GC-eligible. Default 60_000.\n\tcheckoutEveryMs?: number\n\t// API origin. Override for self-hosted or local dev. Defaults to the\n\t// PluginContext baseUrl threaded through by the widget.\n\tapiUrl?: string\n}\n\ninterface RrwebEvent {\n\ttype: number\n\tdata: unknown\n\ttimestamp: number\n}\n\ninterface RrwebRecordOptions {\n\temit: (event: RrwebEvent) => void\n\tmaskAllInputs?: boolean\n\tmaskTextSelector?: string\n\tinlineStylesheet?: boolean\n\tblockSelector?: string\n\tsampling?: ReplaySampling\n\tcheckoutEveryNms?: number\n}\n\ninterface RrwebRecordFn {\n\t(opts: RrwebRecordOptions): () => void\n\ttakeFullSnapshot?: (isCheckout?: boolean) => void\n}\n\ntype RrwebRecord = RrwebRecordFn\n\ninterface ResolvedOptions {\n\tstartAfterMs: number\n\tsampleRate: number\n\tsampling: ReplaySampling\n\tmaskAllInputs: boolean\n\tmaskTextSelector: string\n\tinlineStylesheet: boolean\n\tblockSelector: string\n\tchunkSeconds: number\n\tchunkMaxEvents: number\n\tchunkMaxBytes: number\n\tchunkMaxAttempts: number\n\tcheckoutEveryMs: number\n\tapiUrl: string\n}\n\ninterface ReplayStore {\n\toptions: ResolvedOptions\n\tclientId: string\n\tsdkSessionId: string\n\tsessionReplayId: string | null\n\t// Wall-clock timestamp (ms) of the first event we ever recorded.\n\t// Used to compute replayOffsetMs at feedback-submit time.\n\trecordingStartedAt: number | null\n\tpendingEvents: RrwebEvent[]\n\tpendingBytes: number\n\tpendingFirstTs: number | null\n\tpendingLastTs: number | null\n\tlastUploadDropWarnAt: number\n\t// Count of chunks dropped (queue saturation) since the last successful\n\t// upload. Sent as a header on the next successful chunk PUT so the\n\t// viewer can show a \"gap here\" marker. Reset on success.\n\tdroppedSinceLastUpload: number\n\t// Wall-clock timestamp of the last snapshot-isolation flush. Used to\n\t// rate-limit pre-snapshot flushes so SPA route-change snapshot bursts\n\t// don't trigger a flush storm.\n\tlastSnapshotFlushAt: number\n\tnextChunkSeq: number\n\tuploadQueue: Promise<void>\n\tpendingUploads: number\n\tchunkFlushTimer: ReturnType<typeof setInterval> | null\n\tstartTimer: ReturnType<typeof setTimeout> | null\n\tpageHideHandler: (() => void) | null\n\tvisibilityHandler: (() => void) | null\n\tshadowUpdateHandler: ((event: Event) => void) | null\n\trecord: RrwebRecord | null\n\tstopRecording: (() => void) | null\n\tloadInProgress: boolean\n\tcancelled: boolean\n\t// True once the session is \"done\": bot-gated, finalised, or destroyed.\n\tstopped: boolean\n}\n\nconst DEFAULTS: ResolvedOptions = {\n\tstartAfterMs: 0,\n\tsampleRate: 1,\n\tsampling: { mousemove: 50, scroll: 100 },\n\tmaskAllInputs: true,\n\tmaskTextSelector: '[data-usero-mask]',\n\tinlineStylesheet: true,\n\tblockSelector: '[data-usero-block]',\n\tchunkSeconds: 3,\n\tchunkMaxEvents: 1000,\n\tchunkMaxBytes: 512_000,\n\tchunkMaxAttempts: 5,\n\tcheckoutEveryMs: 60_000,\n\tapiUrl: '',\n}\n\nconst SDK_SESSION_STORAGE_KEY = 'usero:session-replay:sdk-session-id'\nconst HARD_CHUNK_BYTE_CAP = 4 * 1024 * 1024\nconst MAX_PENDING_UPLOADS = 3\nconst UPLOAD_DROP_WARN_INTERVAL_MS = 5000\n// rrweb EventType.FullSnapshot. We don't import rrweb's enum because rrweb is\n// dynamically imported (bundle hygiene), so we'd have to pay the load cost\n// just to reference a constant. Magic number matches estimateEventBytes above\n// and rrweb's stable public event-type enum.\nconst RRWEB_EVENT_TYPE_FULL_SNAPSHOT = 2\n// Minimum gap between back-to-back snapshot-isolation flushes. Snapshots\n// normally fire every checkoutEveryMs (default 60s), but rrweb can emit\n// additional ones on SPA route changes via checkoutEveryNms. Keeping this\n// below chunkSeconds * 1000 / 2 of the default (3000ms) and well under\n// checkoutEveryMs ensures isolation still happens for back-to-back snapshots\n// while preventing pathological flush storms.\nconst SNAPSHOT_ISOLATION_MIN_GAP_MS = 1500\n\nfunction uint8ToBase64(bytes: Uint8Array): string {\n\tlet binary = ''\n\tconst chunkSize = 0x8000\n\tfor (let i = 0; i < bytes.length; i += chunkSize) {\n\t\tconst slice = bytes.subarray(i, i + chunkSize)\n\t\tbinary += String.fromCharCode.apply(null, Array.from(slice))\n\t}\n\treturn typeof btoa === 'function' ? btoa(binary) : ''\n}\n\nasync function gzipBytes(input: string): Promise<Uint8Array> {\n\tif (typeof CompressionStream === 'undefined') {\n\t\t// Old browsers: send uncompressed JSON. Acceptable degradation;\n\t\t// the server endpoint accepts raw application/octet-stream.\n\t\treturn new TextEncoder().encode(input)\n\t}\n\tconst stream = new Blob([input]).stream().pipeThrough(new CompressionStream('gzip'))\n\tconst buf = await new Response(stream).arrayBuffer()\n\treturn new Uint8Array(buf)\n}\n\nfunction generateRandomId(): string {\n\tif (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n\t\treturn crypto.randomUUID()\n\t}\n\tconst bytes = new Uint8Array(16)\n\tif (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {\n\t\tcrypto.getRandomValues(bytes)\n\t} else {\n\t\tfor (let i = 0; i < bytes.length; i += 1) bytes[i] = Math.floor(Math.random() * 256)\n\t}\n\tlet out = ''\n\tfor (const b of bytes) out += b.toString(16).padStart(2, '0')\n\treturn out\n}\n\nfunction mintSdkSessionId(): string {\n\ttry {\n\t\tconst existing = window.sessionStorage?.getItem(SDK_SESSION_STORAGE_KEY)\n\t\tif (existing && /^[a-z0-9-]{8,}$/i.test(existing)) return existing\n\t} catch {\n\t\t// sessionStorage can throw in sandboxed iframes — fall through.\n\t}\n\tconst id = generateRandomId()\n\ttry {\n\t\twindow.sessionStorage?.setItem(SDK_SESSION_STORAGE_KEY, id)\n\t} catch {\n\t\t// Ignore: we still return the freshly minted id.\n\t}\n\treturn id\n}\n\nfunction joinUrl(apiUrl: string, path: string): string {\n\treturn `${apiUrl.replace(/\\/$/, '')}${path}`\n}\n\n// Cheap per-event byte estimate. Avoids JSON.stringify on the hot emit path.\n// rrweb EventType: 0=DomContentLoaded, 1=Load, 2=FullSnapshot, 3=IncrementalSnapshot,\n// 4=Meta, 5=Custom, 6=Plugin. Full snapshots are the only event class that's\n// genuinely large; everything else is well under a KB on average. Numbers\n// chosen to over-estimate slightly so chunkMaxBytes stays a safety net.\nfunction estimateEventBytes(event: RrwebEvent): number {\n\tif (event.type === 2) return 50_000\n\tif (event.type === 3) return 256\n\treturn 128\n}\n\ninterface CreateSessionResult {\n\taccepted: boolean\n\tsessionReplayId?: string\n\tdropReason?: string\n}\n\nasync function createSession(\n\tapiUrl: string,\n\tclientId: string,\n\tsdkSessionId: string,\n\tanonymousId: string,\n): Promise<CreateSessionResult | null> {\n\ttry {\n\t\tconst startUrl =\n\t\t\ttypeof window !== 'undefined' && window.location ? window.location.href : undefined\n\t\tconst userAgent =\n\t\t\ttypeof navigator !== 'undefined' && navigator.userAgent ? navigator.userAgent : undefined\n\t\tconst res = await fetch(joinUrl(apiUrl, '/api/replay-sessions'), {\n\t\t\tmethod: 'POST',\n\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\tbody: JSON.stringify({\n\t\t\t\tclientId,\n\t\t\t\tsdkSessionId,\n\t\t\t\tanonymousId,\n\t\t\t\tstartUrl,\n\t\t\t\tuserAgent,\n\t\t\t\tstartedAt: new Date().toISOString(),\n\t\t\t}),\n\t\t})\n\t\tif (!res.ok) return null\n\t\tconst json = (await res.json()) as {\n\t\t\taccepted?: unknown\n\t\t\tsessionReplayId?: unknown\n\t\t\tdropReason?: unknown\n\t\t}\n\t\tif (typeof json.accepted !== 'boolean') return null\n\t\tconst result: CreateSessionResult = { accepted: json.accepted }\n\t\tif (typeof json.sessionReplayId === 'string') result.sessionReplayId = json.sessionReplayId\n\t\tif (typeof json.dropReason === 'string') result.dropReason = json.dropReason\n\t\treturn result\n\t} catch {\n\t\treturn null\n\t}\n}\n\ninterface ChunkUploadResult {\n\tok: boolean\n\tstopSession: boolean\n}\n\nasync function uploadChunk(\n\tapiUrl: string,\n\tsessionReplayId: string,\n\tclientId: string,\n\tseq: number,\n\tbytes: Uint8Array,\n\teventCount: number,\n\tdurationMs: number,\n\tlogger: PluginContext['logger'],\n\tmaxAttempts: number,\n\tdroppedBefore: number,\n): Promise<ChunkUploadResult> {\n\tconst url = joinUrl(\n\t\tapiUrl,\n\t\t`/api/replay-sessions/${encodeURIComponent(sessionReplayId)}/chunks/${seq}`,\n\t)\n\tlet attempt = 0\n\twhile (attempt < maxAttempts) {\n\t\ttry {\n\t\t\t// Wrap in a Blob so the body type is unambiguously BodyInit; some\n\t\t\t// TS lib targets reject raw Uint8Array as fetch body. Slice off\n\t\t\t// the buffer to satisfy the BlobPart ArrayBuffer constraint\n\t\t\t// (Uint8Array<SharedArrayBuffer> is the alternative the lib\n\t\t\t// admits, which we never produce here).\n\t\t\tconst buffer = bytes.buffer.slice(\n\t\t\t\tbytes.byteOffset,\n\t\t\t\tbytes.byteOffset + bytes.byteLength,\n\t\t\t) as ArrayBuffer\n\t\t\tconst blob = new Blob([buffer], { type: 'application/octet-stream' })\n\t\t\tconst headers: Record<string, string> = {\n\t\t\t\t'Content-Type': 'application/octet-stream',\n\t\t\t\t'X-Usero-Client-Id': clientId,\n\t\t\t\t'X-Usero-Event-Count': String(eventCount),\n\t\t\t\t'X-Usero-Duration-Ms': String(Math.max(0, Math.round(durationMs))),\n\t\t\t}\n\t\t\t// Signal a playback gap: how many chunks were dropped (queue\n\t\t\t// saturation) between the previous successful upload and this one.\n\t\t\t// Server-side viewer will use this to render a \"missing data\" marker.\n\t\t\tif (droppedBefore > 0) headers['X-Usero-Dropped-Before'] = String(droppedBefore)\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,\n\t\t\t})\n\t\t\tif (res.ok) return { ok: true, stopSession: false }\n\t\t\t// 409: server told us to stop (bot-dropped, or session already\n\t\t\t// finalised). Don't retry, don't upload further chunks.\n\t\t\tif (res.status === 409) {\n\t\t\t\tlogger.warn(`chunk ${seq} rejected with 409, stopping session`)\n\t\t\t\treturn { ok: false, stopSession: true }\n\t\t\t}\n\t\t\t// Other 4xx (besides 408/429) won't get better with retry.\n\t\t\tif (res.status >= 400 && res.status < 500 && res.status !== 408 && res.status !== 429) {\n\t\t\t\tlogger.error(`chunk ${seq} rejected with ${res.status}`)\n\t\t\t\treturn { ok: false, stopSession: false }\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tlogger.warn(`chunk ${seq} attempt ${attempt + 1} failed`, err)\n\t\t}\n\t\tattempt += 1\n\t\tconst backoff = Math.min(15_000, 500 * 2 ** attempt) + Math.floor(Math.random() * 250)\n\t\tawait new Promise(resolve => setTimeout(resolve, backoff))\n\t}\n\tlogger.error(`chunk ${seq} dropped after ${maxAttempts} attempts`)\n\treturn { ok: false, stopSession: false }\n}\n\nasync function loadRrwebRecord(): Promise<RrwebRecord | null> {\n\ttry {\n\t\tconst mod: unknown = await import(/* webpackChunkName: \"rrweb\" */ 'rrweb')\n\t\tif (\n\t\t\tmod &&\n\t\t\ttypeof mod === 'object' &&\n\t\t\t'record' in mod &&\n\t\t\ttypeof (mod as { record: unknown }).record === 'function'\n\t\t) {\n\t\t\treturn (mod as { record: RrwebRecord }).record\n\t\t}\n\t\treturn null\n\t} catch {\n\t\treturn null\n\t}\n}\n\n// Decides whether to isolate a FullSnapshot event into its own chunk.\n//\n// rrweb FullSnapshots are the playback anchor for every subsequent\n// incremental event in the same chunk. If a chunk crosses the 4MB gzipped\n// hard cap (HARD_CHUNK_BYTE_CAP) it's dropped wholesale, taking the anchor\n// with it and breaking playback for up to a full checkoutEveryMs window.\n// To mitigate, we ship the snapshot in a near-empty chunk:\n//\n// 1. Pre-flush: if `pendingEvents` is non-empty, flush it now so the\n// snapshot doesn't inherit the previous up-to-3s of incrementals.\n// 2. Caller pushes the snapshot event onto `pendingEvents`.\n// 3. Post-flush (if `didIsolate`): caller calls `scheduleChunkUpload`\n// again so the snapshot ships solo, not bundled with the next up-to-3s\n// of post-snapshot incrementals.\n//\n// Both pre- and post-flush share a single rate-limit window\n// (`lastSnapshotFlushAt` + `SNAPSHOT_ISOLATION_MIN_GAP_MS`) so SPA route-\n// change snapshot bursts can't trigger a flush storm. If the gate is\n// closed, neither flush fires; if open, both fire and the watermark is\n// updated. Pre-flush is conditional on a non-empty buffer (nothing to\n// flush otherwise); post-flush is unconditional once the gate is open\n// because the goal is to ship the snapshot solo regardless.\n//\n// Returns `{ didIsolate }` so the caller knows whether to do the\n// post-flush after pushing the event.\nexport function maybeIsolateSnapshot(\n\tstore: ReplayStore,\n\tctx: PluginContext,\n\tevent: { type: number },\n\tnow: number,\n): { didIsolate: boolean } {\n\tif (event.type !== RRWEB_EVENT_TYPE_FULL_SNAPSHOT) return { didIsolate: false }\n\tif (now - store.lastSnapshotFlushAt < SNAPSHOT_ISOLATION_MIN_GAP_MS) {\n\t\treturn { didIsolate: false }\n\t}\n\tif (store.pendingEvents.length > 0) {\n\t\tscheduleChunkUpload(store, ctx)\n\t}\n\tstore.lastSnapshotFlushAt = now\n\treturn { didIsolate: true }\n}\n\nfunction scheduleChunkUpload(store: ReplayStore, ctx: PluginContext): void {\n\tif (!store.sessionReplayId) return\n\tif (store.pendingEvents.length === 0) return\n\tif (store.pendingUploads >= MAX_PENDING_UPLOADS) {\n\t\tconst now = Date.now()\n\t\tif (now - store.lastUploadDropWarnAt > UPLOAD_DROP_WARN_INTERVAL_MS) {\n\t\t\tstore.lastUploadDropWarnAt = now\n\t\t\tctx.logger.warn(\n\t\t\t\t`upload queue full (${store.pendingUploads} in-flight), dropping chunk to bound memory`,\n\t\t\t)\n\t\t}\n\t\tstore.pendingEvents = []\n\t\tstore.pendingBytes = 0\n\t\tstore.pendingFirstTs = null\n\t\tstore.pendingLastTs = null\n\t\t// Track for the next successful chunk so the viewer can render a gap.\n\t\tstore.droppedSinceLastUpload += 1\n\t\treturn\n\t}\n\t// Chunk boundary: re-resolve the user. Captures mid-session login on\n\t// replay-only installs that never open the widget. No-op via fingerprint\n\t// dedupe if nothing changed.\n\ttry {\n\t\tctx.resolveUser?.()\n\t} catch (err) {\n\t\tctx.logger.warn('resolveUser threw at chunk boundary', err)\n\t}\n\tconst events = store.pendingEvents\n\tconst eventCount = events.length\n\tconst firstTs = store.pendingFirstTs ?? 0\n\tconst lastTs = store.pendingLastTs ?? firstTs\n\tconst durationMs = Math.max(0, lastTs - firstTs)\n\tconst seq = store.nextChunkSeq\n\tstore.nextChunkSeq += 1\n\tstore.pendingEvents = []\n\tstore.pendingBytes = 0\n\tstore.pendingFirstTs = null\n\tstore.pendingLastTs = null\n\n\tconst sessionReplayId = store.sessionReplayId\n\tconst apiUrl = store.options.apiUrl\n\tconst clientId = store.clientId\n\tconst maxAttempts = store.options.chunkMaxAttempts\n\n\tconst droppedBefore = store.droppedSinceLastUpload\n\tstore.pendingUploads += 1\n\tstore.uploadQueue = store.uploadQueue.then(async () => {\n\t\ttry {\n\t\t\tif (store.cancelled) return\n\t\t\tconst json = JSON.stringify(events)\n\t\t\tconst bytes = await gzipBytes(json)\n\t\t\tif (bytes.byteLength > HARD_CHUNK_BYTE_CAP) {\n\t\t\t\tctx.logger.error(\n\t\t\t\t\t`chunk ${seq} exceeds 4MB hard cap (${bytes.byteLength} bytes), dropping`,\n\t\t\t\t)\n\t\t\t\t// Surface the drop on the next successful chunk so the viewer\n\t\t\t\t// can render a gap marker. Without this, oversized chunks\n\t\t\t\t// vanish without trace server-side.\n\t\t\t\tstore.droppedSinceLastUpload += 1\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst result = await uploadChunk(\n\t\t\t\tapiUrl,\n\t\t\t\tsessionReplayId,\n\t\t\t\tclientId,\n\t\t\t\tseq,\n\t\t\t\tbytes,\n\t\t\t\teventCount,\n\t\t\t\tdurationMs,\n\t\t\t\tctx.logger,\n\t\t\t\tmaxAttempts,\n\t\t\t\tdroppedBefore,\n\t\t\t)\n\t\t\tif (result.ok && droppedBefore > 0) {\n\t\t\t\t// Subtract what we just reported, rather than zeroing, so any\n\t\t\t\t// drops that happened while this chunk was in flight still\n\t\t\t\t// surface on the next successful upload.\n\t\t\t\tstore.droppedSinceLastUpload = Math.max(\n\t\t\t\t\t0,\n\t\t\t\t\tstore.droppedSinceLastUpload - droppedBefore,\n\t\t\t\t)\n\t\t\t}\n\t\t\tif (result.stopSession) {\n\t\t\t\tstore.stopped = true\n\t\t\t\tstopRrweb(store)\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tctx.logger.error(`chunk ${seq} encode failed`, err)\n\t\t} finally {\n\t\t\tstore.pendingUploads -= 1\n\t\t}\n\t})\n}\n\nfunction flushPendingChunk(store: ReplayStore, ctx: PluginContext): void {\n\tif (store.stopped || store.cancelled) return\n\tif (store.pendingEvents.length === 0) return\n\tscheduleChunkUpload(store, ctx)\n}\n\nfunction stopRrweb(store: ReplayStore): void {\n\tif (store.stopRecording) {\n\t\ttry {\n\t\t\tstore.stopRecording()\n\t\t} catch {\n\t\t\t// Already stopped.\n\t\t}\n\t\tstore.stopRecording = null\n\t}\n\tif (store.chunkFlushTimer) {\n\t\tclearInterval(store.chunkFlushTimer)\n\t\tstore.chunkFlushTimer = null\n\t}\n}\n\nfunction startRecording(store: ReplayStore, ctx: PluginContext): void {\n\tif (store.cancelled || store.stopped || store.stopRecording || store.loadInProgress) return\n\tstore.loadInProgress = true\n\tvoid loadRrwebRecord().then(record => {\n\t\tstore.loadInProgress = false\n\t\tif (store.cancelled || store.stopped || !record) {\n\t\t\tif (!record) ctx.logger.warn('rrweb failed to load, replay disabled')\n\t\t\treturn\n\t\t}\n\t\ttry {\n\t\t\tconst stop = record({\n\t\t\t\temit: event => {\n\t\t\t\t\tif (store.stopped || store.cancelled) return\n\t\t\t\t\tif (store.recordingStartedAt === null) store.recordingStartedAt = event.timestamp\n\t\t\t\t\t// FullSnapshot isolation: ship the snapshot in a near-empty\n\t\t\t\t\t// chunk so the 4MB hard cap can't drop the playback anchor.\n\t\t\t\t\t// `maybeIsolateSnapshot` handles the pre-flush + rate-limit;\n\t\t\t\t\t// we do the post-flush below so the snapshot ships solo\n\t\t\t\t\t// instead of inheriting up to chunkSeconds of trailing\n\t\t\t\t\t// incrementals. See the helper's doc comment for details.\n\t\t\t\t\tconst { didIsolate } = maybeIsolateSnapshot(store, ctx, event, Date.now())\n\t\t\t\t\tstore.pendingEvents.push(event)\n\t\t\t\t\t// Hot path: rrweb fires hundreds of events/sec on busy SPAs.\n\t\t\t\t\t// JSON.stringify-per-event burns CPU we don't have, and .length\n\t\t\t\t\t// is UTF-16 units (under-counts non-ASCII by ~2x) so it was\n\t\t\t\t\t// never a real byte count anyway. Use a per-type heuristic:\n\t\t\t\t\t// full snapshots are huge, mutations are mid, everything else\n\t\t\t\t\t// is cheap. chunkMaxBytes is documented as approximate.\n\t\t\t\t\tstore.pendingBytes += estimateEventBytes(event)\n\t\t\t\t\tif (store.pendingFirstTs === null) store.pendingFirstTs = event.timestamp\n\t\t\t\t\tstore.pendingLastTs = event.timestamp\n\t\t\t\t\tif (didIsolate) {\n\t\t\t\t\t\t// Post-flush: the snapshot we just pushed ships in its\n\t\t\t\t\t\t// own chunk so it doesn't inherit the next chunkSeconds\n\t\t\t\t\t\t// of incremental mutations and risk crossing the 4MB cap.\n\t\t\t\t\t\tscheduleChunkUpload(store, ctx)\n\t\t\t\t\t} else if (\n\t\t\t\t\t\tstore.pendingEvents.length >= store.options.chunkMaxEvents ||\n\t\t\t\t\t\tstore.pendingBytes >= store.options.chunkMaxBytes\n\t\t\t\t\t) {\n\t\t\t\t\t\tscheduleChunkUpload(store, ctx)\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tmaskAllInputs: store.options.maskAllInputs,\n\t\t\t\tmaskTextSelector: store.options.maskTextSelector || undefined,\n\t\t\t\tinlineStylesheet: store.options.inlineStylesheet,\n\t\t\t\tblockSelector: store.options.blockSelector,\n\t\t\t\tsampling: store.options.sampling,\n\t\t\t\tcheckoutEveryNms: store.options.checkoutEveryMs,\n\t\t\t})\n\t\t\tstore.stopRecording = stop\n\t\t\tstore.record = record\n\t\t\tscheduleShadowSnapshot(store, ctx)\n\n\t\t\tstore.chunkFlushTimer = setInterval(\n\t\t\t\t() => flushPendingChunk(store, ctx),\n\t\t\t\tstore.options.chunkSeconds * 1000,\n\t\t\t)\n\t\t} catch (err) {\n\t\t\tctx.logger.error('rrweb record() threw', err)\n\t\t}\n\t})\n}\n\nfunction scheduleShadowSnapshot(store: ReplayStore, ctx: PluginContext): void {\n\tif (store.cancelled || store.stopped || !store.record || !store.stopRecording) return\n\tconst fn = store.record.takeFullSnapshot\n\tif (typeof fn !== 'function') return\n\ttry {\n\t\tfn(true)\n\t} catch (err) {\n\t\tctx.logger.warn('takeFullSnapshot threw', err)\n\t}\n}\n\nfunction finalise(store: ReplayStore, ctx: PluginContext, opts: { useBeacon: boolean }): void {\n\tif (!store.sessionReplayId) return\n\tif (store.pendingEvents.length > 0) flushPendingChunk(store, ctx)\n\tconst url = joinUrl(\n\t\tstore.options.apiUrl,\n\t\t`/api/replay-sessions/${encodeURIComponent(store.sessionReplayId)}/finalise`,\n\t)\n\tconst body = JSON.stringify({ clientId: store.clientId, endedAt: new Date().toISOString() })\n\tif (opts.useBeacon && typeof navigator !== 'undefined' && navigator.sendBeacon) {\n\t\ttry {\n\t\t\tconst blob = new Blob([body], { type: 'application/json' })\n\t\t\tnavigator.sendBeacon(url, blob)\n\t\t\treturn\n\t\t} catch (err) {\n\t\t\tctx.logger.warn('finalise sendBeacon threw', err)\n\t\t}\n\t}\n\tvoid fetch(url, {\n\t\tmethod: 'POST',\n\t\tbody,\n\t\theaders: { 'Content-Type': 'application/json' },\n\t\tkeepalive: true,\n\t}).catch(err => ctx.logger.warn('finalise fetch failed', err))\n}\n\nexport interface CurrentSessionHandle {\n\tid: string\n\toffsetMs: number\n}\n\nexport function sessionReplay(options: SessionReplayOptions = {}): UseroPlugin {\n\tconst merged: ResolvedOptions = {\n\t\t...DEFAULTS,\n\t\t...options,\n\t\tsampling: { ...DEFAULTS.sampling, ...(options.sampling ?? {}) },\n\t}\n\n\treturn {\n\t\tname: 'session-replay',\n\t\tonInit(ctx) {\n\t\t\tif (typeof window === 'undefined') return\n\t\t\tif (merged.sampleRate < 1 && Math.random() >= merged.sampleRate) {\n\t\t\t\tctx.logger.debug('skipped by sampleRate')\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst apiUrl = merged.apiUrl || ctx.baseUrl\n\t\t\tif (!apiUrl) {\n\t\t\t\tctx.logger.error('session-replay needs an apiUrl (via options or PluginContext)')\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// Prefer the core-owned per-tab id so user-test and replay share\n\t\t\t// the SAME sdkSessionId for this tab (the server resolves the\n\t\t\t// SessionReplay by clientId + sdkSessionId). Fall back to local\n\t\t\t// minting only when the host predates the core accessor (older\n\t\t\t// SDK embed); both paths read/write the same sessionStorage key.\n\t\t\tconst sdkSessionId = ctx.getSdkSessionId ? ctx.getSdkSessionId() : mintSdkSessionId()\n\t\t\t// Mint or read the cross-session anonymousId. Cached in module\n\t\t\t// scope after the first call, so this stays O(1) on hot paths.\n\t\t\tconst anonymousId = getOrMintAnonymousId()\n\n\t\t\tconst store: ReplayStore = {\n\t\t\t\toptions: { ...merged, apiUrl },\n\t\t\t\tclientId: ctx.clientId,\n\t\t\t\tsdkSessionId,\n\t\t\t\tsessionReplayId: null,\n\t\t\t\trecordingStartedAt: null,\n\t\t\t\tpendingEvents: [],\n\t\t\t\tpendingBytes: 0,\n\t\t\t\tpendingFirstTs: null,\n\t\t\t\tpendingLastTs: null,\n\t\t\t\tlastUploadDropWarnAt: 0,\n\t\t\t\tdroppedSinceLastUpload: 0,\n\t\t\t\tlastSnapshotFlushAt: 0,\n\t\t\t\tnextChunkSeq: 0,\n\t\t\t\tuploadQueue: Promise.resolve(),\n\t\t\t\tpendingUploads: 0,\n\t\t\t\tchunkFlushTimer: null,\n\t\t\t\tstartTimer: null,\n\t\t\t\tpageHideHandler: null,\n\t\t\t\tvisibilityHandler: null,\n\t\t\t\tshadowUpdateHandler: null,\n\t\t\t\trecord: null,\n\t\t\t\tstopRecording: null,\n\t\t\t\tloadInProgress: false,\n\t\t\t\tcancelled: false,\n\t\t\t\tstopped: false,\n\t\t\t}\n\t\t\tctx.setStore(store)\n\n\t\t\tconst onShadowUpdate = (): void => scheduleShadowSnapshot(store, ctx)\n\t\t\tstore.shadowUpdateHandler = onShadowUpdate\n\t\t\twindow.addEventListener('usero:shadow-update', onShadowUpdate)\n\n\t\t\t// Shared unload backstop for both pagehide and visibilitychange.\n\t\t\t// The `store.stopped` short-circuit makes it idempotent: whichever\n\t\t\t// of the two fires first finalises + stops rrweb, the other (and any\n\t\t\t// later onDestroy) becomes a no-op, so we never double-finalise.\n\t\t\tconst stopOnUnload = (): void => {\n\t\t\t\tif (store.stopped) return\n\t\t\t\tfinalise(store, ctx, { useBeacon: true })\n\t\t\t\tstore.stopped = true\n\t\t\t\tstopRrweb(store)\n\t\t\t}\n\t\t\tstore.pageHideHandler = stopOnUnload\n\t\t\twindow.addEventListener('pagehide', stopOnUnload)\n\n\t\t\t// visibilitychange -> hidden is the reliable mobile backstop:\n\t\t\t// iOS often tears down a backgrounded tab without ever firing\n\t\t\t// pagehide, which used to leave the replay un-finalised (no\n\t\t\t// endedAt). visibilitychange fires consistently on backgrounding, so\n\t\t\t// flushing + finalising here closes that gap. Guarded by the same\n\t\t\t// idempotent stopOnUnload.\n\t\t\tconst onVisibilityChange = (): void => {\n\t\t\t\tif (document.visibilityState !== 'hidden') return\n\t\t\t\tstopOnUnload()\n\t\t\t}\n\t\t\tstore.visibilityHandler = onVisibilityChange\n\t\t\tdocument.addEventListener('visibilitychange', onVisibilityChange)\n\n\t\t\tconst begin = async (): Promise<void> => {\n\t\t\t\tif (store.cancelled) return\n\t\t\t\t// Replay-only customers may never open the widget, so the host's\n\t\t\t\t// user state never gets polled by the widget's interaction\n\t\t\t\t// boundaries. Re-resolve here so a mid-session login that\n\t\t\t\t// happened before session start is visible server-side before\n\t\t\t\t// the first chunk lands. Fingerprint dedupe inside\n\t\t\t\t// identifyIfChanged makes this effectively free when nothing\n\t\t\t\t// changed.\n\t\t\t\ttry {\n\t\t\t\t\tctx.resolveUser?.()\n\t\t\t\t} catch (err) {\n\t\t\t\t\tctx.logger.warn('resolveUser threw at session start', err)\n\t\t\t\t}\n\t\t\t\tconst created = await createSession(apiUrl, ctx.clientId, sdkSessionId, anonymousId)\n\t\t\t\tif (!created) {\n\t\t\t\t\tctx.logger.warn('session create failed, replay disabled')\n\t\t\t\t\tstore.stopped = true\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (!created.accepted) {\n\t\t\t\t\tctx.logger.info(`session-replay declined: ${created.dropReason ?? 'unknown'}`)\n\t\t\t\t\tstore.stopped = true\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (!created.sessionReplayId) {\n\t\t\t\t\tctx.logger.error('server accepted but returned no sessionReplayId')\n\t\t\t\t\tstore.stopped = true\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tstore.sessionReplayId = created.sessionReplayId\n\t\t\t\tstore.recordingStartedAt = Date.now()\n\t\t\t\t// Publish the recording start epoch into the core so other\n\t\t\t\t// plugins (user-test) can compute their offset into this\n\t\t\t\t// recording without importing the replay module. No-op if the\n\t\t\t\t// host predates the accessor.\n\t\t\t\tctx.publishReplayStartMs?.(store.recordingStartedAt)\n\t\t\t\tstartRecording(store, ctx)\n\t\t\t}\n\n\t\t\tif (merged.startAfterMs > 0) {\n\t\t\t\tconst cancelOnExit = (): void => {\n\t\t\t\t\tstore.cancelled = true\n\t\t\t\t\tif (store.startTimer) {\n\t\t\t\t\t\tclearTimeout(store.startTimer)\n\t\t\t\t\t\tstore.startTimer = null\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\twindow.addEventListener('pagehide', cancelOnExit, { once: true })\n\t\t\t\twindow.addEventListener('beforeunload', cancelOnExit, { once: true })\n\t\t\t\tstore.startTimer = setTimeout(() => {\n\t\t\t\t\tvoid begin()\n\t\t\t\t}, merged.startAfterMs)\n\t\t\t} else {\n\t\t\t\tvoid begin()\n\t\t\t}\n\t\t},\n\t\tonFeedbackSubmit(ctx) {\n\t\t\tconst store = ctx.getStore<ReplayStore>()\n\t\t\tif (!store || store.cancelled || store.stopped) return undefined\n\t\t\tif (!store.sessionReplayId) return undefined\n\t\t\tconst offsetMs =\n\t\t\t\tstore.recordingStartedAt !== null\n\t\t\t\t\t? Math.max(0, Date.now() - store.recordingStartedAt)\n\t\t\t\t\t: 0\n\t\t\treturn { sessionReplayId: store.sessionReplayId, replayOffsetMs: offsetMs }\n\t\t},\n\t\tonDestroy(ctx) {\n\t\t\tconst store = ctx.getStore<ReplayStore>()\n\t\t\tif (!store) return\n\t\t\tstore.cancelled = true\n\t\t\tif (store.startTimer) {\n\t\t\t\tclearTimeout(store.startTimer)\n\t\t\t\tstore.startTimer = null\n\t\t\t}\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\tif (store.visibilityHandler) {\n\t\t\t\tdocument.removeEventListener('visibilitychange', store.visibilityHandler)\n\t\t\t\tstore.visibilityHandler = null\n\t\t\t}\n\t\t\tif (store.shadowUpdateHandler) {\n\t\t\t\twindow.removeEventListener('usero:shadow-update', store.shadowUpdateHandler)\n\t\t\t\tstore.shadowUpdateHandler = null\n\t\t\t}\n\t\t\t// SPA route change / React unmount: send a finalise so the\n\t\t\t// server stamps endedAt. fetch+keepalive is fine here since we\n\t\t\t// aren't necessarily in a pagehide path.\n\t\t\tif (store.sessionReplayId && !store.stopped) {\n\t\t\t\tfinalise(store, ctx, { useBeacon: false })\n\t\t\t}\n\t\t\tstore.stopped = true\n\t\t\tstopRrweb(store)\n\t\t\tstore.pendingEvents.length = 0\n\t\t\tstore.pendingBytes = 0\n\t\t\tstore.record = null\n\t\t},\n\t}\n}\n\n// Returns the live session-replay handle for a given plugin context, or\n// null if the session was bot-dropped, sample-skipped, or not yet\n// created. Other plugins (e.g. user-test) can call this to attach the\n// replay FK + offset to their own server-side records.\nexport function getCurrentSession(ctx: PluginContext): CurrentSessionHandle | null {\n\tconst store = ctx.getStore<ReplayStore>()\n\tif (!store || store.cancelled || store.stopped || !store.sessionReplayId) return null\n\tconst offsetMs =\n\t\tstore.recordingStartedAt !== null\n\t\t\t? Math.max(0, Date.now() - store.recordingStartedAt)\n\t\t\t: 0\n\treturn { id: store.sessionReplayId, offsetMs }\n}\n\n// Internal helper exports for testing only. Not part of the public API.\nexport const __test__ = {\n\tuint8ToBase64,\n\tgzipBytes,\n\tmintSdkSessionId,\n\tuploadChunk,\n\tcreateSession,\n\tjoinUrl,\n\tscheduleChunkUpload,\n\tmaybeIsolateSnapshot,\n\tRRWEB_EVENT_TYPE_FULL_SNAPSHOT,\n\tSNAPSHOT_ISOLATION_MIN_GAP_MS,\n\tHARD_CHUNK_BYTE_CAP,\n\tSDK_SESSION_STORAGE_KEY,\n\tMAX_PENDING_UPLOADS,\n\tUPLOAD_DROP_WARN_INTERVAL_MS,\n\tDEFAULTS,\n}\n"]}
1
+ {"version":3,"sources":["../../src/identity.ts","../../src/plugins/session-replay.ts"],"names":["generateRandomId","patchedPushState","patchedReplaceState"],"mappings":"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","file":"session-replay.cjs","sourcesContent":["// Identity layer for the Usero SDK.\n//\n// Two responsibilities:\n// 1. Mint and persist a stable per-browser `anonymousId` in localStorage\n// so cross-tab + cross-day replays from the same browser stitch\n// together server-side. Falls back to an in-memory id if storage is\n// blocked (sandboxed iframes, Safari Lockdown, full quota). Replay\n// still works in that case, you just lose stitching.\n// 2. Auto-fire POST /api/identify when the resolved user transitions\n// (null -> id, id -> id'). Deduped by an in-memory fingerprint so\n// re-renders with the same user are no-ops on the network.\n//\n// All storage access is wrapped in try/catch and gated behind a one-shot\n// init read. The hot path (replay chunk flush) never touches localStorage.\n\nconst ANON_STORAGE_KEY = 'usero:anonymous-id'\n// Reuse the EXACT key the session-replay plugin has always written\n// (`usero:session-replay:sdk-session-id`) so we adopt per-tab session ids\n// already living in customers' browsers instead of minting a fresh one.\n// Changing this would split a tab's existing replay from its user-test\n// linkage, so it must stay byte-for-byte identical to the replay plugin's\n// historical key.\nconst SDK_SESSION_STORAGE_KEY = 'usero:session-replay:sdk-session-id'\n\nlet cachedAnonymousId: string | null = null\nlet cachedSdkSessionId: string | null = null\n// Last resolved external user id, set by the identify pipeline so any\n// plugin can read the current identity without re-resolving the host's\n// `user` prop. Cleared on logout (anonymousId rotation).\nlet currentUserId: string | null = null\n// Wall-clock epoch (ms) when the session-replay recording started, as\n// published by the replay plugin. Lets other plugins (user-test) compute\n// an offset into the recording without importing the replay module. Null\n// until replay actually starts (or if replay is not loaded at all).\nlet replayStartMs: number | null = null\n// Fingerprint of the last identify we POSTed. Same SDK instance + same\n// resolved user + same traits = no-op. Cleared on logout (anonymousId\n// rotation).\nlet lastIdentifyFingerprint: string | null = null\n\nexport type UserTraitValue = string | number | boolean | null\nexport type UserTraits = Record<string, UserTraitValue>\n\nexport interface UseroUser {\n\tid: string\n\temail?: string\n\tdisplayName?: string\n\ttraits?: UserTraits\n}\n\nfunction generateRandomId(): string {\n\tif (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n\t\treturn crypto.randomUUID()\n\t}\n\tconst bytes = new Uint8Array(16)\n\tif (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {\n\t\tcrypto.getRandomValues(bytes)\n\t} else {\n\t\tfor (let i = 0; i < bytes.length; i += 1) bytes[i] = Math.floor(Math.random() * 256)\n\t}\n\tlet out = ''\n\tfor (const b of bytes) out += b.toString(16).padStart(2, '0')\n\treturn out\n}\n\nfunction safeReadLocalStorage(key: string): string | null {\n\tif (typeof window === 'undefined') return null\n\ttry {\n\t\treturn window.localStorage?.getItem(key) ?? null\n\t} catch {\n\t\treturn null\n\t}\n}\n\nfunction safeWriteLocalStorage(key: string, value: string): void {\n\tif (typeof window === 'undefined') return\n\ttry {\n\t\twindow.localStorage?.setItem(key, value)\n\t} catch {\n\t\t// Sandboxed iframe / Safari Lockdown / quota. Fall back to memory.\n\t}\n}\n\nfunction safeReadSessionStorage(key: string): string | null {\n\tif (typeof window === 'undefined') return null\n\ttry {\n\t\treturn window.sessionStorage?.getItem(key) ?? null\n\t} catch {\n\t\treturn null\n\t}\n}\n\nfunction safeWriteSessionStorage(key: string, value: string): void {\n\tif (typeof window === 'undefined') return\n\ttry {\n\t\twindow.sessionStorage?.setItem(key, value)\n\t} catch {\n\t\t// Sandboxed iframe / Safari Lockdown / quota. Fall back to memory.\n\t}\n}\n\n/**\n * Returns the stable per-browser anonymousId. Reads localStorage at most\n * once per SDK instance. Subsequent calls hit the in-memory cache, so\n * even hot paths (per-event in replay) are safe to call this.\n */\nexport function getOrMintAnonymousId(): string {\n\tif (cachedAnonymousId) return cachedAnonymousId\n\tconst existing = safeReadLocalStorage(ANON_STORAGE_KEY)\n\t// Sanity filter, not strict validation. We accept anything that looks\n\t// plausibly like an id (>=8 alphanumeric-or-hyphen) so older SDK\n\t// versions that wrote a slightly different shape still stitch. Fresh\n\t// mint is cheap, so we only reject obvious garbage; tightening this\n\t// would force rotation in customer browsers and split otherwise-good\n\t// sibling-session attribution.\n\tif (existing && /^[a-z0-9-]{8,}$/i.test(existing)) {\n\t\tcachedAnonymousId = existing\n\t\treturn existing\n\t}\n\tconst id = generateRandomId()\n\tsafeWriteLocalStorage(ANON_STORAGE_KEY, id)\n\tcachedAnonymousId = id\n\treturn id\n}\n\n/**\n * Rotate the anonymousId. Called on logout (user transitions from a\n * known id to null) so the next anonymous trail does not get auto-merged\n * into the previous person on the next identify().\n */\nexport function rotateAnonymousId(): string {\n\tconst id = generateRandomId()\n\tcachedAnonymousId = id\n\tsafeWriteLocalStorage(ANON_STORAGE_KEY, id)\n\tlastIdentifyFingerprint = null\n\tcurrentUserId = null\n\treturn id\n}\n\n/**\n * Loose sanity filter for a persisted/incoming sdkSessionId: >=8 chars of\n * alphanumerics or hyphens. NOT strict validation. We accept anything that\n * looks plausibly like an id so older SDK builds (and cross-plugin re-seats)\n * still stitch; a fresh mint is cheap, so we only reject obvious garbage.\n * Tightening this would force rotation in customer browsers and split\n * otherwise-good sibling-session attribution. Single source of truth so the\n * mint / read / re-seat / resume-persist paths can never drift apart.\n */\nexport function isValidSdkSessionId(id: string): boolean {\n\treturn /^[a-z0-9-]{8,}$/i.test(id)\n}\n\n/**\n * Returns the stable per-tab sdkSessionId. Core-owned so every plugin\n * (replay, user-test, future feedback linkage) reads the SAME id for a\n * given tab. Reads sessionStorage at most once per SDK instance; later\n * calls hit the in-memory cache.\n *\n * Backward compat: reuses the `usero:sdk-session-id` key the replay\n * plugin has always written, and the same loose sanity filter, so an\n * id minted by an older replay-only SDK build is adopted as-is rather\n * than rotated (which would split a tab's replay from its user-test).\n */\nexport function getOrMintSdkSessionId(): string {\n\tif (cachedSdkSessionId) return cachedSdkSessionId\n\tconst existing = safeReadSessionStorage(SDK_SESSION_STORAGE_KEY)\n\tif (existing && isValidSdkSessionId(existing)) {\n\t\tcachedSdkSessionId = existing\n\t\treturn existing\n\t}\n\tconst id = generateRandomId()\n\tsafeWriteSessionStorage(SDK_SESSION_STORAGE_KEY, id)\n\tcachedSdkSessionId = id\n\treturn id\n}\n\n/**\n * Re-seat the per-tab sdkSessionId to a specific value. Used ONLY by the\n * user-test resume path: a cross-origin hard navigation (e.g. an OAuth round\n * trip) wipes sessionStorage, so the post-nav document would mint a NEW\n * sdkSessionId and the post-nav SessionReplay row would no longer share an id\n * with the audio session, breaking the (clientId, sdkSessionId) finalise join.\n *\n * The user-test plugin durably persists the original id in its localStorage\n * resume state (which survives the round trip) and calls this synchronously at\n * the very top of its onInit, BEFORE the session-replay plugin reads the id,\n * so the resumed replay reuses the SAME id and the server stitches both replay\n * rows under it.\n *\n * No-ops on a malformed id (so a corrupted resume entry can never poison the\n * tab's id) and on an id identical to the current one. Writes sessionStorage\n * AND the in-memory cache so a replay onInit that already cached a fresh mint\n * is corrected (the cache is the read-through path).\n */\nexport function reseatSdkSessionId(id: string): void {\n\tif (!isValidSdkSessionId(id)) return\n\tif (cachedSdkSessionId === id) return\n\tcachedSdkSessionId = id\n\tsafeWriteSessionStorage(SDK_SESSION_STORAGE_KEY, id)\n}\n\n/**\n * The current resolved external user id, or null if no identify has\n * succeeded this session (or after logout). Set by the identify\n * pipeline; read by plugins that want the current identity without\n * re-resolving the host's `user` prop.\n */\nexport function getCurrentUserId(): string | null {\n\treturn currentUserId\n}\n\n/**\n * Published by the session-replay plugin when its recording starts.\n * Other plugins read it via `getReplayStartMs()` to compute an offset\n * into the recording. No-op duplicate publishes keep the first value so\n * the offset stays anchored to the true recording start.\n */\nexport function publishReplayStartMs(epochMs: number): void {\n\tif (replayStartMs === null) replayStartMs = epochMs\n}\n\n/**\n * The wall-clock epoch (ms) when session-replay started, or null if\n * replay never started this session (plugin not loaded, or sampled out).\n * Consumers degrade gracefully when null: ship the sdkSessionId linkage\n * key anyway and omit the offset.\n */\nexport function getReplayStartMs(): number | null {\n\treturn replayStartMs\n}\n\nfunction fingerprintUser(anonymousId: string, user: UseroUser): string {\n\t// Stable across re-renders: keys sorted, traits canonicalised. Cheap\n\t// enough on the hot path (only runs when the SDK thinks the user might\n\t// have changed, never per-event).\n\tconst traits = user.traits ?? {}\n\tconst keys = Object.keys(traits).sort()\n\tconst canonical: Array<[string, UserTraitValue]> = keys.map(k => [k, traits[k] ?? null])\n\treturn JSON.stringify([anonymousId, user.id, user.email ?? null, user.displayName ?? null, canonical])\n}\n\nexport interface IdentifyTransport {\n\tapiUrl: string\n\tclientId: string\n}\n\n/**\n * POST to /api/identify if the (anonymousId, user) fingerprint differs\n * from the last call. Returns true if a network request actually fired.\n * Never throws; failures are best-effort and the caller (the widget /\n * provider) should not treat them as errors.\n *\n * Tab-unload safety: if the page is hidden when this fires (visibility\n * 'hidden' or a pagehide handler), we route the payload through\n * `navigator.sendBeacon` so the request survives unload. Otherwise we\n * use a normal fetch and only cache the fingerprint when the server\n * confirms `accepted: true`. A 200 `{ accepted: false }` (e.g.\n * `unknown_client` for a clientId that becomes valid mid-session) is\n * treated as retryable so the next call re-fires.\n */\nexport async function identifyIfChanged(transport: IdentifyTransport, user: UseroUser): Promise<boolean> {\n\tconst anonymousId = getOrMintAnonymousId()\n\t// Make the resolved id readable by other plugins immediately, even\n\t// before the network round-trip resolves. This is the identity the\n\t// host just told us about; the POST below is best-effort persistence.\n\tcurrentUserId = user.id\n\tconst fp = fingerprintUser(anonymousId, user)\n\tif (fp === lastIdentifyFingerprint) return false\n\n\tconst url = `${transport.apiUrl.replace(/\\/$/, '')}/api/identify`\n\t// Body must stay under the browser's keepalive / sendBeacon cap\n\t// (~64KB across most engines) when this fires on pagehide. That\n\t// transitively caps trait payload size; in practice traits should be\n\t// small typed scalars, not blobs.\n\tconst body = JSON.stringify({\n\t\tclientId: transport.clientId,\n\t\tanonymousId,\n\t\texternalUserId: user.id,\n\t\temail: user.email,\n\t\tdisplayName: user.displayName,\n\t\ttraits: user.traits,\n\t})\n\n\t// If the document is hidden (pagehide / tab close in flight), best-effort\n\t// hand off to sendBeacon. We don't get a response back, so we optimistically\n\t// cache the fingerprint to avoid re-firing on the next page; the server is\n\t// idempotent if the page reload re-runs identify with the same payload.\n\tif (\n\t\ttypeof document !== 'undefined' &&\n\t\tdocument.visibilityState === 'hidden' &&\n\t\ttypeof navigator !== 'undefined' &&\n\t\ttypeof navigator.sendBeacon === 'function'\n\t) {\n\t\ttry {\n\t\t\tconst blob = new Blob([body], { type: 'application/json' })\n\t\t\t// sendBeacon returns false when the user agent refuses to queue\n\t\t\t// the request (size cap, or historically Safari rejecting\n\t\t\t// non-CORS-simple content types). Modern Safari accepts\n\t\t\t// application/json, but we keep a keepalive-fetch fallback so an\n\t\t\t// older WebKit that rejects the beacon still ships the identify.\n\t\t\tif (navigator.sendBeacon(url, blob)) {\n\t\t\t\tlastIdentifyFingerprint = fp\n\t\t\t\treturn true\n\t\t\t}\n\t\t} catch {\n\t\t\t// fall through to keepalive fetch below\n\t\t}\n\t}\n\n\ttry {\n\t\tconst res = await fetch(url, {\n\t\t\tmethod: 'POST',\n\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\tbody,\n\t\t\t// keepalive lets the request survive a tab-close mid-flight on\n\t\t\t// browsers that support it; sendBeacon above is the primary path.\n\t\t\tkeepalive: true,\n\t\t})\n\t\tif (!res.ok) return true\n\t\t// Parse the response: a 200 with `accepted: false` (e.g. unknown\n\t\t// client) is retryable. Only cache the fingerprint when the server\n\t\t// confirmed it actually stored the identity.\n\t\ttry {\n\t\t\tconst json = (await res.json()) as { accepted?: unknown }\n\t\t\tif (json && json.accepted === true) lastIdentifyFingerprint = fp\n\t\t} catch {\n\t\t\t// Server returned 2xx but unparseable body: don't cache, let the\n\t\t\t// next call retry.\n\t\t}\n\t\treturn true\n\t} catch {\n\t\treturn false\n\t}\n}\n\n/**\n * Clear identify state and rotate anonymousId. Called when the resolved\n * user transitions from a known id to null (logout). The next anonymous\n * trail will get a fresh anonymousId so it does not merge into the\n * previous person.\n */\nexport function handleLogout(): void {\n\trotateAnonymousId()\n}\n\n// Test hooks (not exported from the package public surface).\nexport const __test__ = {\n\tANON_STORAGE_KEY,\n\tSDK_SESSION_STORAGE_KEY,\n\treseatSdkSessionId,\n\tgetOrMintSdkSessionId,\n\tresetIdentityState: (): void => {\n\t\tcachedAnonymousId = null\n\t\tcachedSdkSessionId = null\n\t\tcurrentUserId = null\n\t\treplayStartMs = null\n\t\tlastIdentifyFingerprint = null\n\t},\n}\n","// Session replay plugin for the Usero widget.\n//\n// Streams rrweb events to the SaaS side as gzipped chunks while the user\n// is on the page, instead of buffering in memory and attaching to a\n// feedback submission. This decouples session replay from feedback so we\n// capture every session (subject to bot-gate + sampling + engagement\n// gates), not just the ones that submit feedback.\n//\n// Lifecycle:\n// 1. onInit: dice-roll sample, optional engagement-time gate, mint a\n// stable per-tab `sdkSessionId` in sessionStorage, and POST to\n// /api/replay-sessions to create the row. If the server returns\n// `{accepted:false}` (bot-gated), the plugin no-ops the rest of the\n// session and getCurrentSession() returns null.\n// 2. Recording: lazy-load rrweb, append events to a buffer, flush a\n// chunk every `chunkSeconds` (or sooner if the buffer is large).\n// Each chunk is gzipped via CompressionStream and PUT to\n// /api/replay-sessions/:id/chunks/:seq with raw bytes + the three\n// X-Usero-* headers (Client-Id, Event-Count, Duration-Ms). Retries\n// with exponential backoff. R2 head-check makes retries idempotent\n// server-side. A chunk PUT returning 409 stops the session.\n// 3. onFeedbackSubmit: returns `{sessionReplayId, replayOffsetMs}` so\n// the feedback record can FK at the moment of submit. Does NOT\n// attach `replayEvents` (legacy field) — chunked uploads carry the\n// events out-of-band.\n// 4. onDestroy / pagehide / visibilitychange -> hidden: best-effort flush\n// remaining buffer, then sendBeacon to\n// /api/replay-sessions/:id/finalise with the end-timestamp. Idempotent\n// server-side and via a `stopped` guard client-side.\n//\n// Bundle hygiene: rrweb stays lazy via dynamic `import('rrweb')` behind\n// the engagement gate, so consumers who lose the dice roll or navigate\n// away inside the gate window pay zero rrweb bytes.\n\nimport { getOrMintAnonymousId, isValidSdkSessionId } from '../identity'\nimport type { UseroPlugin, PluginContext } from '../plugin'\n\nexport interface ReplaySampling {\n\tmousemove?: number\n\tscroll?: number\n\tmedia?: number\n\tinput?: number | 'last'\n}\n\nexport interface SessionReplayOptions {\n\t// Wait this many ms after page load before loading rrweb and creating\n\t// the session row. If the user navigates away first, rrweb is never\n\t// loaded and no session row is created. Default 0 (start immediately).\n\tstartAfterMs?: number\n\t// Probability (0..1) that this session records at all. Decided once\n\t// at init via Math.random(). Default 1.\n\tsampleRate?: number\n\t// rrweb sampling rates per event type.\n\tsampling?: ReplaySampling\n\t// Mask all <input>/<textarea> values in the recording. Default true.\n\tmaskAllInputs?: boolean\n\t// CSS selector for nodes whose text content should be masked. Default\n\t// `[data-usero-mask]`.\n\tmaskTextSelector?: string\n\t// Inline external stylesheets so the replay viewer renders correctly\n\t// without network access. Default true.\n\tinlineStylesheet?: boolean\n\t// Block (entirely skip) DOM subtrees matching this selector. Default\n\t// `[data-usero-block]`.\n\tblockSelector?: string\n\t// Flush a chunk every N seconds. Default 3. Smaller = more PUTs but\n\t// less data lost on tab crash and less time event refs retain detached\n\t// DOM nodes in memory.\n\tchunkSeconds?: number\n\t// Soft cap on buffered events before forcing a flush, regardless of\n\t// time. Default 1000.\n\tchunkMaxEvents?: number\n\t// Soft cap on estimated buffered bytes before forcing a flush. Default\n\t// 512_000 (~500 KB pre-gzip). Keeps memory pressure bounded on event-heavy\n\t// pages even when chunkMaxEvents hasn't been hit.\n\tchunkMaxBytes?: number\n\t// Max attempts per chunk before giving up. Default 5.\n\tchunkMaxAttempts?: number\n\t// Force rrweb to take a fresh full snapshot every N ms. This resets\n\t// rrweb's internal mirror so detached DOM (e.g. SPA route changes)\n\t// becomes GC-eligible. Default 60_000.\n\tcheckoutEveryMs?: number\n\t// API origin. Override for self-hosted or local dev. Defaults to the\n\t// PluginContext baseUrl threaded through by the widget.\n\tapiUrl?: string\n}\n\ninterface RrwebEvent {\n\ttype: number\n\tdata: unknown\n\ttimestamp: number\n}\n\ninterface RrwebRecordOptions {\n\temit: (event: RrwebEvent) => void\n\tmaskAllInputs?: boolean\n\tmaskTextSelector?: string\n\tinlineStylesheet?: boolean\n\tblockSelector?: string\n\tsampling?: ReplaySampling\n\tcheckoutEveryNms?: number\n}\n\ninterface RrwebRecordFn {\n\t(opts: RrwebRecordOptions): () => void\n\ttakeFullSnapshot?: (isCheckout?: boolean) => void\n\t// rrweb wraps the payload as an EventType.Custom (type 5) event with\n\t// `data: { tag, payload }` and emits it through the same `emit` callback\n\t// as every other rrweb event, so it streams in our chunks alongside Meta\n\t// (type 4) and incremental (type 3) events. Consumers reading the stream\n\t// resolve the URL-at-moment from these `tag === 'url-change'` events.\n\taddCustomEvent?: (tag: string, payload: unknown) => void\n}\n\ntype RrwebRecord = RrwebRecordFn\n\ninterface ResolvedOptions {\n\tstartAfterMs: number\n\tsampleRate: number\n\tsampling: ReplaySampling\n\tmaskAllInputs: boolean\n\tmaskTextSelector: string\n\tinlineStylesheet: boolean\n\tblockSelector: string\n\tchunkSeconds: number\n\tchunkMaxEvents: number\n\tchunkMaxBytes: number\n\tchunkMaxAttempts: number\n\tcheckoutEveryMs: number\n\tapiUrl: string\n}\n\ninterface ReplayStore {\n\toptions: ResolvedOptions\n\tclientId: string\n\tsdkSessionId: string\n\tsessionReplayId: string | null\n\t// Wall-clock timestamp (ms) of the first event we ever recorded.\n\t// Used to compute replayOffsetMs at feedback-submit time.\n\trecordingStartedAt: number | null\n\tpendingEvents: RrwebEvent[]\n\tpendingBytes: number\n\tpendingFirstTs: number | null\n\tpendingLastTs: number | null\n\tlastUploadDropWarnAt: number\n\t// Count of chunks dropped (queue saturation) since the last successful\n\t// upload. Sent as a header on the next successful chunk PUT so the\n\t// viewer can show a \"gap here\" marker. Reset on success.\n\tdroppedSinceLastUpload: number\n\t// Wall-clock timestamp of the last snapshot-isolation flush. Used to\n\t// rate-limit pre-snapshot flushes so SPA route-change snapshot bursts\n\t// don't trigger a flush storm.\n\tlastSnapshotFlushAt: number\n\tnextChunkSeq: number\n\tuploadQueue: Promise<void>\n\tpendingUploads: number\n\tchunkFlushTimer: ReturnType<typeof setInterval> | null\n\tstartTimer: ReturnType<typeof setTimeout> | null\n\tpageHideHandler: (() => void) | null\n\tvisibilityHandler: (() => void) | null\n\tshadowUpdateHandler: ((event: Event) => void) | null\n\trecord: RrwebRecord | null\n\tstopRecording: (() => void) | null\n\t// Teardown for the SPA URL-change tracker: restores the patched\n\t// history.pushState / history.replaceState and removes the popstate\n\t// listener. Null until tracking is wired up, and after teardown.\n\tstopUrlTracking: (() => void) | null\n\tloadInProgress: boolean\n\tcancelled: boolean\n\t// True once the session is \"done\": bot-gated, finalised, or destroyed.\n\tstopped: boolean\n}\n\nconst DEFAULTS: ResolvedOptions = {\n\tstartAfterMs: 0,\n\tsampleRate: 1,\n\tsampling: { mousemove: 50, scroll: 100 },\n\tmaskAllInputs: true,\n\tmaskTextSelector: '[data-usero-mask]',\n\tinlineStylesheet: true,\n\tblockSelector: '[data-usero-block]',\n\tchunkSeconds: 3,\n\tchunkMaxEvents: 1000,\n\tchunkMaxBytes: 512_000,\n\tchunkMaxAttempts: 5,\n\tcheckoutEveryMs: 60_000,\n\tapiUrl: '',\n}\n\nconst SDK_SESSION_STORAGE_KEY = 'usero:session-replay:sdk-session-id'\nconst HARD_CHUNK_BYTE_CAP = 4 * 1024 * 1024\nconst MAX_PENDING_UPLOADS = 3\nconst UPLOAD_DROP_WARN_INTERVAL_MS = 5000\n// rrweb EventType.FullSnapshot. We don't import rrweb's enum because rrweb is\n// dynamically imported (bundle hygiene), so we'd have to pay the load cost\n// just to reference a constant. Magic number matches estimateEventBytes above\n// and rrweb's stable public event-type enum.\nconst RRWEB_EVENT_TYPE_FULL_SNAPSHOT = 2\n// Minimum gap between back-to-back snapshot-isolation flushes. Snapshots\n// normally fire every checkoutEveryMs (default 60s), but rrweb can emit\n// additional ones on SPA route changes via checkoutEveryNms. Keeping this\n// below chunkSeconds * 1000 / 2 of the default (3000ms) and well under\n// checkoutEveryMs ensures isolation still happens for back-to-back snapshots\n// while preventing pathological flush storms.\nconst SNAPSHOT_ISOLATION_MIN_GAP_MS = 1500\n\nfunction uint8ToBase64(bytes: Uint8Array): string {\n\tlet binary = ''\n\tconst chunkSize = 0x8000\n\tfor (let i = 0; i < bytes.length; i += chunkSize) {\n\t\tconst slice = bytes.subarray(i, i + chunkSize)\n\t\tbinary += String.fromCharCode.apply(null, Array.from(slice))\n\t}\n\treturn typeof btoa === 'function' ? btoa(binary) : ''\n}\n\nasync function gzipBytes(input: string): Promise<Uint8Array> {\n\tif (typeof CompressionStream === 'undefined') {\n\t\t// Old browsers: send uncompressed JSON. Acceptable degradation;\n\t\t// the server endpoint accepts raw application/octet-stream.\n\t\treturn new TextEncoder().encode(input)\n\t}\n\tconst stream = new Blob([input]).stream().pipeThrough(new CompressionStream('gzip'))\n\tconst buf = await new Response(stream).arrayBuffer()\n\treturn new Uint8Array(buf)\n}\n\nfunction generateRandomId(): string {\n\tif (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n\t\treturn crypto.randomUUID()\n\t}\n\tconst bytes = new Uint8Array(16)\n\tif (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {\n\t\tcrypto.getRandomValues(bytes)\n\t} else {\n\t\tfor (let i = 0; i < bytes.length; i += 1) bytes[i] = Math.floor(Math.random() * 256)\n\t}\n\tlet out = ''\n\tfor (const b of bytes) out += b.toString(16).padStart(2, '0')\n\treturn out\n}\n\nfunction mintSdkSessionId(): string {\n\ttry {\n\t\tconst existing = window.sessionStorage?.getItem(SDK_SESSION_STORAGE_KEY)\n\t\tif (existing && isValidSdkSessionId(existing)) return existing\n\t} catch {\n\t\t// sessionStorage can throw in sandboxed iframes — fall through.\n\t}\n\tconst id = generateRandomId()\n\ttry {\n\t\twindow.sessionStorage?.setItem(SDK_SESSION_STORAGE_KEY, id)\n\t} catch {\n\t\t// Ignore: we still return the freshly minted id.\n\t}\n\treturn id\n}\n\nfunction joinUrl(apiUrl: string, path: string): string {\n\treturn `${apiUrl.replace(/\\/$/, '')}${path}`\n}\n\n// Cheap per-event byte estimate. Avoids JSON.stringify on the hot emit path.\n// rrweb EventType: 0=DomContentLoaded, 1=Load, 2=FullSnapshot, 3=IncrementalSnapshot,\n// 4=Meta, 5=Custom, 6=Plugin. Full snapshots are the only event class that's\n// genuinely large; everything else is well under a KB on average. Numbers\n// chosen to over-estimate slightly so chunkMaxBytes stays a safety net.\nfunction estimateEventBytes(event: RrwebEvent): number {\n\tif (event.type === 2) return 50_000\n\tif (event.type === 3) return 256\n\treturn 128\n}\n\ninterface CreateSessionResult {\n\taccepted: boolean\n\tsessionReplayId?: string\n\tdropReason?: string\n}\n\nasync function createSession(\n\tapiUrl: string,\n\tclientId: string,\n\tsdkSessionId: string,\n\tanonymousId: string,\n): Promise<CreateSessionResult | null> {\n\ttry {\n\t\tconst startUrl =\n\t\t\ttypeof window !== 'undefined' && window.location ? window.location.href : undefined\n\t\tconst userAgent =\n\t\t\ttypeof navigator !== 'undefined' && navigator.userAgent ? navigator.userAgent : undefined\n\t\tconst res = await fetch(joinUrl(apiUrl, '/api/replay-sessions'), {\n\t\t\tmethod: 'POST',\n\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\tbody: JSON.stringify({\n\t\t\t\tclientId,\n\t\t\t\tsdkSessionId,\n\t\t\t\tanonymousId,\n\t\t\t\tstartUrl,\n\t\t\t\tuserAgent,\n\t\t\t\tstartedAt: new Date().toISOString(),\n\t\t\t}),\n\t\t})\n\t\tif (!res.ok) return null\n\t\tconst json = (await res.json()) as {\n\t\t\taccepted?: unknown\n\t\t\tsessionReplayId?: unknown\n\t\t\tdropReason?: unknown\n\t\t}\n\t\tif (typeof json.accepted !== 'boolean') return null\n\t\tconst result: CreateSessionResult = { accepted: json.accepted }\n\t\tif (typeof json.sessionReplayId === 'string') result.sessionReplayId = json.sessionReplayId\n\t\tif (typeof json.dropReason === 'string') result.dropReason = json.dropReason\n\t\treturn result\n\t} catch {\n\t\treturn null\n\t}\n}\n\ninterface ChunkUploadResult {\n\tok: boolean\n\tstopSession: boolean\n}\n\nasync function uploadChunk(\n\tapiUrl: string,\n\tsessionReplayId: string,\n\tclientId: string,\n\tseq: number,\n\tbytes: Uint8Array,\n\teventCount: number,\n\tdurationMs: number,\n\tlogger: PluginContext['logger'],\n\tmaxAttempts: number,\n\tdroppedBefore: number,\n): Promise<ChunkUploadResult> {\n\tconst url = joinUrl(\n\t\tapiUrl,\n\t\t`/api/replay-sessions/${encodeURIComponent(sessionReplayId)}/chunks/${seq}`,\n\t)\n\tlet attempt = 0\n\twhile (attempt < maxAttempts) {\n\t\ttry {\n\t\t\t// Wrap in a Blob so the body type is unambiguously BodyInit; some\n\t\t\t// TS lib targets reject raw Uint8Array as fetch body. Slice off\n\t\t\t// the buffer to satisfy the BlobPart ArrayBuffer constraint\n\t\t\t// (Uint8Array<SharedArrayBuffer> is the alternative the lib\n\t\t\t// admits, which we never produce here).\n\t\t\tconst buffer = bytes.buffer.slice(\n\t\t\t\tbytes.byteOffset,\n\t\t\t\tbytes.byteOffset + bytes.byteLength,\n\t\t\t) as ArrayBuffer\n\t\t\tconst blob = new Blob([buffer], { type: 'application/octet-stream' })\n\t\t\tconst headers: Record<string, string> = {\n\t\t\t\t'Content-Type': 'application/octet-stream',\n\t\t\t\t'X-Usero-Client-Id': clientId,\n\t\t\t\t'X-Usero-Event-Count': String(eventCount),\n\t\t\t\t'X-Usero-Duration-Ms': String(Math.max(0, Math.round(durationMs))),\n\t\t\t}\n\t\t\t// Signal a playback gap: how many chunks were dropped (queue\n\t\t\t// saturation) between the previous successful upload and this one.\n\t\t\t// Server-side viewer will use this to render a \"missing data\" marker.\n\t\t\tif (droppedBefore > 0) headers['X-Usero-Dropped-Before'] = String(droppedBefore)\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,\n\t\t\t})\n\t\t\tif (res.ok) return { ok: true, stopSession: false }\n\t\t\t// 409: server told us to stop (bot-dropped, or session already\n\t\t\t// finalised). Don't retry, don't upload further chunks.\n\t\t\tif (res.status === 409) {\n\t\t\t\tlogger.warn(`chunk ${seq} rejected with 409, stopping session`)\n\t\t\t\treturn { ok: false, stopSession: true }\n\t\t\t}\n\t\t\t// Other 4xx (besides 408/429) won't get better with retry.\n\t\t\tif (res.status >= 400 && res.status < 500 && res.status !== 408 && res.status !== 429) {\n\t\t\t\tlogger.error(`chunk ${seq} rejected with ${res.status}`)\n\t\t\t\treturn { ok: false, stopSession: false }\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tlogger.warn(`chunk ${seq} attempt ${attempt + 1} failed`, err)\n\t\t}\n\t\tattempt += 1\n\t\tconst backoff = Math.min(15_000, 500 * 2 ** attempt) + Math.floor(Math.random() * 250)\n\t\tawait new Promise(resolve => setTimeout(resolve, backoff))\n\t}\n\tlogger.error(`chunk ${seq} dropped after ${maxAttempts} attempts`)\n\treturn { ok: false, stopSession: false }\n}\n\nasync function loadRrwebRecord(): Promise<RrwebRecord | null> {\n\ttry {\n\t\tconst mod: unknown = await import(/* webpackChunkName: \"rrweb\" */ 'rrweb')\n\t\tif (\n\t\t\tmod &&\n\t\t\ttypeof mod === 'object' &&\n\t\t\t'record' in mod &&\n\t\t\ttypeof (mod as { record: unknown }).record === 'function'\n\t\t) {\n\t\t\treturn (mod as { record: RrwebRecord }).record\n\t\t}\n\t\treturn null\n\t} catch {\n\t\treturn null\n\t}\n}\n\n// Decides whether to isolate a FullSnapshot event into its own chunk.\n//\n// rrweb FullSnapshots are the playback anchor for every subsequent\n// incremental event in the same chunk. If a chunk crosses the 4MB gzipped\n// hard cap (HARD_CHUNK_BYTE_CAP) it's dropped wholesale, taking the anchor\n// with it and breaking playback for up to a full checkoutEveryMs window.\n// To mitigate, we ship the snapshot in a near-empty chunk:\n//\n// 1. Pre-flush: if `pendingEvents` is non-empty, flush it now so the\n// snapshot doesn't inherit the previous up-to-3s of incrementals.\n// 2. Caller pushes the snapshot event onto `pendingEvents`.\n// 3. Post-flush (if `didIsolate`): caller calls `scheduleChunkUpload`\n// again so the snapshot ships solo, not bundled with the next up-to-3s\n// of post-snapshot incrementals.\n//\n// Both pre- and post-flush share a single rate-limit window\n// (`lastSnapshotFlushAt` + `SNAPSHOT_ISOLATION_MIN_GAP_MS`) so SPA route-\n// change snapshot bursts can't trigger a flush storm. If the gate is\n// closed, neither flush fires; if open, both fire and the watermark is\n// updated. Pre-flush is conditional on a non-empty buffer (nothing to\n// flush otherwise); post-flush is unconditional once the gate is open\n// because the goal is to ship the snapshot solo regardless.\n//\n// Returns `{ didIsolate }` so the caller knows whether to do the\n// post-flush after pushing the event.\nexport function maybeIsolateSnapshot(\n\tstore: ReplayStore,\n\tctx: PluginContext,\n\tevent: { type: number },\n\tnow: number,\n): { didIsolate: boolean } {\n\tif (event.type !== RRWEB_EVENT_TYPE_FULL_SNAPSHOT) return { didIsolate: false }\n\tif (now - store.lastSnapshotFlushAt < SNAPSHOT_ISOLATION_MIN_GAP_MS) {\n\t\treturn { didIsolate: false }\n\t}\n\tif (store.pendingEvents.length > 0) {\n\t\tscheduleChunkUpload(store, ctx)\n\t}\n\tstore.lastSnapshotFlushAt = now\n\treturn { didIsolate: true }\n}\n\nfunction scheduleChunkUpload(store: ReplayStore, ctx: PluginContext): void {\n\tif (!store.sessionReplayId) return\n\tif (store.pendingEvents.length === 0) return\n\tif (store.pendingUploads >= MAX_PENDING_UPLOADS) {\n\t\tconst now = Date.now()\n\t\tif (now - store.lastUploadDropWarnAt > UPLOAD_DROP_WARN_INTERVAL_MS) {\n\t\t\tstore.lastUploadDropWarnAt = now\n\t\t\tctx.logger.warn(\n\t\t\t\t`upload queue full (${store.pendingUploads} in-flight), dropping chunk to bound memory`,\n\t\t\t)\n\t\t}\n\t\tstore.pendingEvents = []\n\t\tstore.pendingBytes = 0\n\t\tstore.pendingFirstTs = null\n\t\tstore.pendingLastTs = null\n\t\t// Track for the next successful chunk so the viewer can render a gap.\n\t\tstore.droppedSinceLastUpload += 1\n\t\treturn\n\t}\n\t// Chunk boundary: re-resolve the user. Captures mid-session login on\n\t// replay-only installs that never open the widget. No-op via fingerprint\n\t// dedupe if nothing changed.\n\ttry {\n\t\tctx.resolveUser?.()\n\t} catch (err) {\n\t\tctx.logger.warn('resolveUser threw at chunk boundary', err)\n\t}\n\tconst events = store.pendingEvents\n\tconst eventCount = events.length\n\tconst firstTs = store.pendingFirstTs ?? 0\n\tconst lastTs = store.pendingLastTs ?? firstTs\n\tconst durationMs = Math.max(0, lastTs - firstTs)\n\tconst seq = store.nextChunkSeq\n\tstore.nextChunkSeq += 1\n\tstore.pendingEvents = []\n\tstore.pendingBytes = 0\n\tstore.pendingFirstTs = null\n\tstore.pendingLastTs = null\n\n\tconst sessionReplayId = store.sessionReplayId\n\tconst apiUrl = store.options.apiUrl\n\tconst clientId = store.clientId\n\tconst maxAttempts = store.options.chunkMaxAttempts\n\n\tconst droppedBefore = store.droppedSinceLastUpload\n\tstore.pendingUploads += 1\n\tstore.uploadQueue = store.uploadQueue.then(async () => {\n\t\ttry {\n\t\t\tif (store.cancelled) return\n\t\t\tconst json = JSON.stringify(events)\n\t\t\tconst bytes = await gzipBytes(json)\n\t\t\tif (bytes.byteLength > HARD_CHUNK_BYTE_CAP) {\n\t\t\t\tctx.logger.error(\n\t\t\t\t\t`chunk ${seq} exceeds 4MB hard cap (${bytes.byteLength} bytes), dropping`,\n\t\t\t\t)\n\t\t\t\t// Surface the drop on the next successful chunk so the viewer\n\t\t\t\t// can render a gap marker. Without this, oversized chunks\n\t\t\t\t// vanish without trace server-side.\n\t\t\t\tstore.droppedSinceLastUpload += 1\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst result = await uploadChunk(\n\t\t\t\tapiUrl,\n\t\t\t\tsessionReplayId,\n\t\t\t\tclientId,\n\t\t\t\tseq,\n\t\t\t\tbytes,\n\t\t\t\teventCount,\n\t\t\t\tdurationMs,\n\t\t\t\tctx.logger,\n\t\t\t\tmaxAttempts,\n\t\t\t\tdroppedBefore,\n\t\t\t)\n\t\t\tif (result.ok && droppedBefore > 0) {\n\t\t\t\t// Subtract what we just reported, rather than zeroing, so any\n\t\t\t\t// drops that happened while this chunk was in flight still\n\t\t\t\t// surface on the next successful upload.\n\t\t\t\tstore.droppedSinceLastUpload = Math.max(\n\t\t\t\t\t0,\n\t\t\t\t\tstore.droppedSinceLastUpload - droppedBefore,\n\t\t\t\t)\n\t\t\t}\n\t\t\tif (result.stopSession) {\n\t\t\t\tstore.stopped = true\n\t\t\t\tstopRrweb(store)\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tctx.logger.error(`chunk ${seq} encode failed`, err)\n\t\t} finally {\n\t\t\tstore.pendingUploads -= 1\n\t\t}\n\t})\n}\n\nfunction flushPendingChunk(store: ReplayStore, ctx: PluginContext): void {\n\tif (store.stopped || store.cancelled) return\n\tif (store.pendingEvents.length === 0) return\n\tscheduleChunkUpload(store, ctx)\n}\n\nfunction stopRrweb(store: ReplayStore): void {\n\t// Restore patched history methods + remove popstate listener before we\n\t// drop the record reference, so the SPA URL-change patch never outlives\n\t// the recording.\n\tstopUrlChangeTracking(store)\n\tif (store.stopRecording) {\n\t\ttry {\n\t\t\tstore.stopRecording()\n\t\t} catch {\n\t\t\t// Already stopped.\n\t\t}\n\t\tstore.stopRecording = null\n\t}\n\tif (store.chunkFlushTimer) {\n\t\tclearInterval(store.chunkFlushTimer)\n\t\tstore.chunkFlushTimer = null\n\t}\n}\n\n// rrweb tag for SPA URL-change custom events. Consumers (replay viewer, AI\n// analysis) key off this exact string inside type-5 Custom events to resolve\n// the URL the user was on at any moment, not just the initial Meta (type 4)\n// href captured at recording start.\nconst URL_CHANGE_TAG = 'url-change'\n\n// Captures client-side route changes (history.pushState / replaceState /\n// popstate) as rrweb custom events so the replay stream carries the URL after\n// the initial page load. rrweb only emits a Meta event with the URL at\n// recording START, so without this every SPA navigation is invisible in the\n// recording.\n//\n// Patches the two history methods by wrapping the originals (call through,\n// then emit) and listens for popstate. Emits once immediately so the current\n// SPA URL is anchored. Returns a teardown that restores the original methods\n// and removes the listener, so the patch never leaks across the widget's\n// lifecycle. Re-entrancy is guarded by `store.stopUrlTracking`: if tracking is\n// already wired up, this is a no-op (prevents double-patching if the plugin\n// inits more than once on the same page).\n//\n// All work is wrapped so a history-patch failure (locked-down history object,\n// frozen prototype) never breaks recording: it logs and leaves recording\n// untouched.\nfunction startUrlChangeTracking(store: ReplayStore, ctx: PluginContext): void {\n\tif (typeof window === 'undefined') return\n\tif (store.stopUrlTracking) return\n\tconst record = store.record\n\tconst addCustomEvent = record?.addCustomEvent\n\tif (!record || typeof addCustomEvent !== 'function') return\n\n\tconst emitUrl = (): void => {\n\t\tif (store.stopped || store.cancelled) return\n\t\ttry {\n\t\t\taddCustomEvent.call(record, URL_CHANGE_TAG, { href: window.location.href })\n\t\t} catch (err) {\n\t\t\tctx.logger.warn('url-change addCustomEvent threw', err)\n\t\t}\n\t}\n\n\ttry {\n\t\tconst history = window.history\n\t\tconst originalPushState = history.pushState\n\t\tconst originalReplaceState = history.replaceState\n\n\t\tconst patchedPushState: History['pushState'] = function patchedPushState(\n\t\t\tthis: History,\n\t\t\t...args\n\t\t) {\n\t\t\tconst result = originalPushState.apply(this, args)\n\t\t\temitUrl()\n\t\t\treturn result\n\t\t}\n\t\tconst patchedReplaceState: History['replaceState'] = function patchedReplaceState(\n\t\t\tthis: History,\n\t\t\t...args\n\t\t) {\n\t\t\tconst result = originalReplaceState.apply(this, args)\n\t\t\temitUrl()\n\t\t\treturn result\n\t\t}\n\n\t\thistory.pushState = patchedPushState\n\t\thistory.replaceState = patchedReplaceState\n\n\t\tconst onPopState = (): void => emitUrl()\n\t\twindow.addEventListener('popstate', onPopState)\n\n\t\tstore.stopUrlTracking = (): void => {\n\t\t\ttry {\n\t\t\t\t// Only restore if nothing else re-patched on top of us, so we\n\t\t\t\t// don't clobber a later wrapper (e.g. the host's own router\n\t\t\t\t// instrumentation) by reverting to a stale original.\n\t\t\t\tif (history.pushState === patchedPushState) history.pushState = originalPushState\n\t\t\t\tif (history.replaceState === patchedReplaceState) {\n\t\t\t\t\thistory.replaceState = originalReplaceState\n\t\t\t\t}\n\t\t\t\twindow.removeEventListener('popstate', onPopState)\n\t\t\t} catch (err) {\n\t\t\t\tctx.logger.warn('url-change teardown threw', err)\n\t\t\t}\n\t\t}\n\n\t\t// Anchor the current SPA URL immediately. Meta also carries it at\n\t\t// start, but this keeps the resolver logic uniform (always read the\n\t\t// latest url-change event) and covers hosts that delay the first\n\t\t// route render.\n\t\temitUrl()\n\t} catch (err) {\n\t\tctx.logger.warn('url-change tracking setup threw', err)\n\t}\n}\n\nfunction stopUrlChangeTracking(store: ReplayStore): void {\n\tif (store.stopUrlTracking) {\n\t\tstore.stopUrlTracking()\n\t\tstore.stopUrlTracking = null\n\t}\n}\n\nfunction startRecording(store: ReplayStore, ctx: PluginContext): void {\n\tif (store.cancelled || store.stopped || store.stopRecording || store.loadInProgress) return\n\tstore.loadInProgress = true\n\tvoid loadRrwebRecord().then(record => {\n\t\tstore.loadInProgress = false\n\t\tif (store.cancelled || store.stopped || !record) {\n\t\t\tif (!record) ctx.logger.warn('rrweb failed to load, replay disabled')\n\t\t\treturn\n\t\t}\n\t\ttry {\n\t\t\tconst stop = record({\n\t\t\t\temit: event => {\n\t\t\t\t\tif (store.stopped || store.cancelled) return\n\t\t\t\t\tif (store.recordingStartedAt === null) store.recordingStartedAt = event.timestamp\n\t\t\t\t\t// FullSnapshot isolation: ship the snapshot in a near-empty\n\t\t\t\t\t// chunk so the 4MB hard cap can't drop the playback anchor.\n\t\t\t\t\t// `maybeIsolateSnapshot` handles the pre-flush + rate-limit;\n\t\t\t\t\t// we do the post-flush below so the snapshot ships solo\n\t\t\t\t\t// instead of inheriting up to chunkSeconds of trailing\n\t\t\t\t\t// incrementals. See the helper's doc comment for details.\n\t\t\t\t\tconst { didIsolate } = maybeIsolateSnapshot(store, ctx, event, Date.now())\n\t\t\t\t\tstore.pendingEvents.push(event)\n\t\t\t\t\t// Hot path: rrweb fires hundreds of events/sec on busy SPAs.\n\t\t\t\t\t// JSON.stringify-per-event burns CPU we don't have, and .length\n\t\t\t\t\t// is UTF-16 units (under-counts non-ASCII by ~2x) so it was\n\t\t\t\t\t// never a real byte count anyway. Use a per-type heuristic:\n\t\t\t\t\t// full snapshots are huge, mutations are mid, everything else\n\t\t\t\t\t// is cheap. chunkMaxBytes is documented as approximate.\n\t\t\t\t\tstore.pendingBytes += estimateEventBytes(event)\n\t\t\t\t\tif (store.pendingFirstTs === null) store.pendingFirstTs = event.timestamp\n\t\t\t\t\tstore.pendingLastTs = event.timestamp\n\t\t\t\t\tif (didIsolate) {\n\t\t\t\t\t\t// Post-flush: the snapshot we just pushed ships in its\n\t\t\t\t\t\t// own chunk so it doesn't inherit the next chunkSeconds\n\t\t\t\t\t\t// of incremental mutations and risk crossing the 4MB cap.\n\t\t\t\t\t\tscheduleChunkUpload(store, ctx)\n\t\t\t\t\t} else if (\n\t\t\t\t\t\tstore.pendingEvents.length >= store.options.chunkMaxEvents ||\n\t\t\t\t\t\tstore.pendingBytes >= store.options.chunkMaxBytes\n\t\t\t\t\t) {\n\t\t\t\t\t\tscheduleChunkUpload(store, ctx)\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tmaskAllInputs: store.options.maskAllInputs,\n\t\t\t\tmaskTextSelector: store.options.maskTextSelector || undefined,\n\t\t\t\tinlineStylesheet: store.options.inlineStylesheet,\n\t\t\t\tblockSelector: store.options.blockSelector,\n\t\t\t\tsampling: store.options.sampling,\n\t\t\t\tcheckoutEveryNms: store.options.checkoutEveryMs,\n\t\t\t})\n\t\t\tstore.stopRecording = stop\n\t\t\tstore.record = record\n\t\t\t// Capture SPA route changes as custom events so the replay stream\n\t\t\t// knows the URL after the first page load.\n\t\t\tstartUrlChangeTracking(store, ctx)\n\t\t\tscheduleShadowSnapshot(store, ctx)\n\n\t\t\tstore.chunkFlushTimer = setInterval(\n\t\t\t\t() => flushPendingChunk(store, ctx),\n\t\t\t\tstore.options.chunkSeconds * 1000,\n\t\t\t)\n\t\t} catch (err) {\n\t\t\tctx.logger.error('rrweb record() threw', err)\n\t\t}\n\t})\n}\n\nfunction scheduleShadowSnapshot(store: ReplayStore, ctx: PluginContext): void {\n\tif (store.cancelled || store.stopped || !store.record || !store.stopRecording) return\n\tconst fn = store.record.takeFullSnapshot\n\tif (typeof fn !== 'function') return\n\ttry {\n\t\tfn(true)\n\t} catch (err) {\n\t\tctx.logger.warn('takeFullSnapshot threw', err)\n\t}\n}\n\nfunction finalise(store: ReplayStore, ctx: PluginContext, opts: { useBeacon: boolean }): void {\n\tif (!store.sessionReplayId) return\n\tif (store.pendingEvents.length > 0) flushPendingChunk(store, ctx)\n\tconst url = joinUrl(\n\t\tstore.options.apiUrl,\n\t\t`/api/replay-sessions/${encodeURIComponent(store.sessionReplayId)}/finalise`,\n\t)\n\tconst body = JSON.stringify({ clientId: store.clientId, endedAt: new Date().toISOString() })\n\tif (opts.useBeacon && typeof navigator !== 'undefined' && navigator.sendBeacon) {\n\t\ttry {\n\t\t\tconst blob = new Blob([body], { type: 'application/json' })\n\t\t\tnavigator.sendBeacon(url, blob)\n\t\t\treturn\n\t\t} catch (err) {\n\t\t\tctx.logger.warn('finalise sendBeacon threw', err)\n\t\t}\n\t}\n\tvoid fetch(url, {\n\t\tmethod: 'POST',\n\t\tbody,\n\t\theaders: { 'Content-Type': 'application/json' },\n\t\tkeepalive: true,\n\t}).catch(err => ctx.logger.warn('finalise fetch failed', err))\n}\n\nexport interface CurrentSessionHandle {\n\tid: string\n\toffsetMs: number\n}\n\nexport function sessionReplay(options: SessionReplayOptions = {}): UseroPlugin {\n\tconst merged: ResolvedOptions = {\n\t\t...DEFAULTS,\n\t\t...options,\n\t\tsampling: { ...DEFAULTS.sampling, ...(options.sampling ?? {}) },\n\t}\n\n\treturn {\n\t\tname: 'session-replay',\n\t\tonInit(ctx) {\n\t\t\tif (typeof window === 'undefined') return\n\t\t\tif (merged.sampleRate < 1 && Math.random() >= merged.sampleRate) {\n\t\t\t\tctx.logger.debug('skipped by sampleRate')\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst apiUrl = merged.apiUrl || ctx.baseUrl\n\t\t\tif (!apiUrl) {\n\t\t\t\tctx.logger.error('session-replay needs an apiUrl (via options or PluginContext)')\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// Resolve the core-owned per-tab id LAZILY (at createSession time,\n\t\t\t// inside begin() below), NOT once here at onInit. user-test's resume\n\t\t\t// path calls reseatSdkSessionId() during ITS onInit; if a consumer\n\t\t\t// registers [sessionReplay(), userTest()], session-replay's onInit\n\t\t\t// runs FIRST and would capture the pre-reseat id, writing the live\n\t\t\t// replay row under the stale id and breaking the audio<->replay join\n\t\t\t// regardless of plugin order. getSdkSessionId() reads through the\n\t\t\t// in-memory cache that reseatSdkSessionId() updates, so reading it at\n\t\t\t// createSession time honours a later re-seat no matter the order.\n\t\t\tconst resolveSdkSessionId = (): string =>\n\t\t\t\tctx.getSdkSessionId ? ctx.getSdkSessionId() : mintSdkSessionId()\n\t\t\t// Mint or read the cross-session anonymousId. Cached in module\n\t\t\t// scope after the first call, so this stays O(1) on hot paths.\n\t\t\tconst anonymousId = getOrMintAnonymousId()\n\n\t\t\tconst store: ReplayStore = {\n\t\t\t\toptions: { ...merged, apiUrl },\n\t\t\t\tclientId: ctx.clientId,\n\t\t\t\t// Filled in lazily at createSession time (see resolveSdkSessionId)\n\t\t\t\t// so a user-test re-seat during a later onInit is honoured.\n\t\t\t\tsdkSessionId: '',\n\t\t\t\tsessionReplayId: null,\n\t\t\t\trecordingStartedAt: null,\n\t\t\t\tpendingEvents: [],\n\t\t\t\tpendingBytes: 0,\n\t\t\t\tpendingFirstTs: null,\n\t\t\t\tpendingLastTs: null,\n\t\t\t\tlastUploadDropWarnAt: 0,\n\t\t\t\tdroppedSinceLastUpload: 0,\n\t\t\t\tlastSnapshotFlushAt: 0,\n\t\t\t\tnextChunkSeq: 0,\n\t\t\t\tuploadQueue: Promise.resolve(),\n\t\t\t\tpendingUploads: 0,\n\t\t\t\tchunkFlushTimer: null,\n\t\t\t\tstartTimer: null,\n\t\t\t\tpageHideHandler: null,\n\t\t\t\tvisibilityHandler: null,\n\t\t\t\tshadowUpdateHandler: null,\n\t\t\t\trecord: null,\n\t\t\t\tstopRecording: null,\n\t\t\t\tstopUrlTracking: null,\n\t\t\t\tloadInProgress: false,\n\t\t\t\tcancelled: false,\n\t\t\t\tstopped: false,\n\t\t\t}\n\t\t\tctx.setStore(store)\n\n\t\t\tconst onShadowUpdate = (): void => scheduleShadowSnapshot(store, ctx)\n\t\t\tstore.shadowUpdateHandler = onShadowUpdate\n\t\t\twindow.addEventListener('usero:shadow-update', onShadowUpdate)\n\n\t\t\t// Shared unload backstop for both pagehide and visibilitychange.\n\t\t\t// The `store.stopped` short-circuit makes it idempotent: whichever\n\t\t\t// of the two fires first finalises + stops rrweb, the other (and any\n\t\t\t// later onDestroy) becomes a no-op, so we never double-finalise.\n\t\t\tconst stopOnUnload = (): void => {\n\t\t\t\tif (store.stopped) return\n\t\t\t\tfinalise(store, ctx, { useBeacon: true })\n\t\t\t\tstore.stopped = true\n\t\t\t\tstopRrweb(store)\n\t\t\t}\n\t\t\tstore.pageHideHandler = stopOnUnload\n\t\t\twindow.addEventListener('pagehide', stopOnUnload)\n\n\t\t\t// visibilitychange -> hidden is the reliable mobile backstop:\n\t\t\t// iOS often tears down a backgrounded tab without ever firing\n\t\t\t// pagehide, which used to leave the replay un-finalised (no\n\t\t\t// endedAt). visibilitychange fires consistently on backgrounding, so\n\t\t\t// flushing + finalising here closes that gap. Guarded by the same\n\t\t\t// idempotent stopOnUnload.\n\t\t\tconst onVisibilityChange = (): void => {\n\t\t\t\tif (document.visibilityState !== 'hidden') return\n\t\t\t\tstopOnUnload()\n\t\t\t}\n\t\t\tstore.visibilityHandler = onVisibilityChange\n\t\t\tdocument.addEventListener('visibilitychange', onVisibilityChange)\n\n\t\t\tconst begin = async (): Promise<void> => {\n\t\t\t\tif (store.cancelled) return\n\t\t\t\t// Replay-only customers may never open the widget, so the host's\n\t\t\t\t// user state never gets polled by the widget's interaction\n\t\t\t\t// boundaries. Re-resolve here so a mid-session login that\n\t\t\t\t// happened before session start is visible server-side before\n\t\t\t\t// the first chunk lands. Fingerprint dedupe inside\n\t\t\t\t// identifyIfChanged makes this effectively free when nothing\n\t\t\t\t// changed.\n\t\t\t\ttry {\n\t\t\t\t\tctx.resolveUser?.()\n\t\t\t\t} catch (err) {\n\t\t\t\t\tctx.logger.warn('resolveUser threw at session start', err)\n\t\t\t\t}\n\t\t\t\t// Read the id NOW (not at onInit) so a user-test resume re-seat\n\t\t\t\t// that ran in a later plugin's onInit is reflected here.\n\t\t\t\tconst sdkSessionId = resolveSdkSessionId()\n\t\t\t\tstore.sdkSessionId = sdkSessionId\n\t\t\t\tconst created = await createSession(apiUrl, ctx.clientId, sdkSessionId, anonymousId)\n\t\t\t\tif (!created) {\n\t\t\t\t\tctx.logger.warn('session create failed, replay disabled')\n\t\t\t\t\tstore.stopped = true\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (!created.accepted) {\n\t\t\t\t\tctx.logger.info(`session-replay declined: ${created.dropReason ?? 'unknown'}`)\n\t\t\t\t\tstore.stopped = true\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (!created.sessionReplayId) {\n\t\t\t\t\tctx.logger.error('server accepted but returned no sessionReplayId')\n\t\t\t\t\tstore.stopped = true\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tstore.sessionReplayId = created.sessionReplayId\n\t\t\t\tstore.recordingStartedAt = Date.now()\n\t\t\t\t// Publish the recording start epoch into the core so other\n\t\t\t\t// plugins (user-test) can compute their offset into this\n\t\t\t\t// recording without importing the replay module. No-op if the\n\t\t\t\t// host predates the accessor.\n\t\t\t\tctx.publishReplayStartMs?.(store.recordingStartedAt)\n\t\t\t\tstartRecording(store, ctx)\n\t\t\t}\n\n\t\t\tif (merged.startAfterMs > 0) {\n\t\t\t\tconst cancelOnExit = (): void => {\n\t\t\t\t\tstore.cancelled = true\n\t\t\t\t\tif (store.startTimer) {\n\t\t\t\t\t\tclearTimeout(store.startTimer)\n\t\t\t\t\t\tstore.startTimer = null\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\twindow.addEventListener('pagehide', cancelOnExit, { once: true })\n\t\t\t\twindow.addEventListener('beforeunload', cancelOnExit, { once: true })\n\t\t\t\tstore.startTimer = setTimeout(() => {\n\t\t\t\t\tvoid begin()\n\t\t\t\t}, merged.startAfterMs)\n\t\t\t} else {\n\t\t\t\tvoid begin()\n\t\t\t}\n\t\t},\n\t\tonFeedbackSubmit(ctx) {\n\t\t\tconst store = ctx.getStore<ReplayStore>()\n\t\t\tif (!store || store.cancelled || store.stopped) return undefined\n\t\t\tif (!store.sessionReplayId) return undefined\n\t\t\tconst offsetMs =\n\t\t\t\tstore.recordingStartedAt !== null\n\t\t\t\t\t? Math.max(0, Date.now() - store.recordingStartedAt)\n\t\t\t\t\t: 0\n\t\t\treturn { sessionReplayId: store.sessionReplayId, replayOffsetMs: offsetMs }\n\t\t},\n\t\tonDestroy(ctx) {\n\t\t\tconst store = ctx.getStore<ReplayStore>()\n\t\t\tif (!store) return\n\t\t\tstore.cancelled = true\n\t\t\tif (store.startTimer) {\n\t\t\t\tclearTimeout(store.startTimer)\n\t\t\t\tstore.startTimer = null\n\t\t\t}\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\tif (store.visibilityHandler) {\n\t\t\t\tdocument.removeEventListener('visibilitychange', store.visibilityHandler)\n\t\t\t\tstore.visibilityHandler = null\n\t\t\t}\n\t\t\tif (store.shadowUpdateHandler) {\n\t\t\t\twindow.removeEventListener('usero:shadow-update', store.shadowUpdateHandler)\n\t\t\t\tstore.shadowUpdateHandler = null\n\t\t\t}\n\t\t\t// SPA route change / React unmount: send a finalise so the\n\t\t\t// server stamps endedAt. fetch+keepalive is fine here since we\n\t\t\t// aren't necessarily in a pagehide path.\n\t\t\tif (store.sessionReplayId && !store.stopped) {\n\t\t\t\tfinalise(store, ctx, { useBeacon: false })\n\t\t\t}\n\t\t\tstore.stopped = true\n\t\t\tstopRrweb(store)\n\t\t\tstore.pendingEvents.length = 0\n\t\t\tstore.pendingBytes = 0\n\t\t\tstore.record = null\n\t\t},\n\t}\n}\n\n// Returns the live session-replay handle for a given plugin context, or\n// null if the session was bot-dropped, sample-skipped, or not yet\n// created. Other plugins (e.g. user-test) can call this to attach the\n// replay FK + offset to their own server-side records.\nexport function getCurrentSession(ctx: PluginContext): CurrentSessionHandle | null {\n\tconst store = ctx.getStore<ReplayStore>()\n\tif (!store || store.cancelled || store.stopped || !store.sessionReplayId) return null\n\tconst offsetMs =\n\t\tstore.recordingStartedAt !== null\n\t\t\t? Math.max(0, Date.now() - store.recordingStartedAt)\n\t\t\t: 0\n\treturn { id: store.sessionReplayId, offsetMs }\n}\n\n// Internal helper exports for testing only. Not part of the public API.\nexport const __test__ = {\n\tuint8ToBase64,\n\tgzipBytes,\n\tmintSdkSessionId,\n\tuploadChunk,\n\tcreateSession,\n\tjoinUrl,\n\tscheduleChunkUpload,\n\tmaybeIsolateSnapshot,\n\tstartUrlChangeTracking,\n\tstopUrlChangeTracking,\n\tURL_CHANGE_TAG,\n\tRRWEB_EVENT_TYPE_FULL_SNAPSHOT,\n\tSNAPSHOT_ISOLATION_MIN_GAP_MS,\n\tHARD_CHUNK_BYTE_CAP,\n\tSDK_SESSION_STORAGE_KEY,\n\tMAX_PENDING_UPLOADS,\n\tUPLOAD_DROP_WARN_INTERVAL_MS,\n\tDEFAULTS,\n}\n"]}
@@ -37,6 +37,7 @@ interface PluginContext {
37
37
  logger: PluginLogger;
38
38
  resolveUser?: () => void;
39
39
  getSdkSessionId?: () => string;
40
+ reseatSdkSessionId?: (id: string) => void;
40
41
  getAnonymousId?: () => string;
41
42
  getUserId?: () => string | null;
42
43
  getReplayStartMs?: () => number | null;
@@ -87,6 +88,7 @@ interface RrwebRecordOptions {
87
88
  interface RrwebRecordFn {
88
89
  (opts: RrwebRecordOptions): () => void;
89
90
  takeFullSnapshot?: (isCheckout?: boolean) => void;
91
+ addCustomEvent?: (tag: string, payload: unknown) => void;
90
92
  }
91
93
  type RrwebRecord = RrwebRecordFn;
92
94
  interface ResolvedOptions {
@@ -127,6 +129,7 @@ interface ReplayStore {
127
129
  shadowUpdateHandler: ((event: Event) => void) | null;
128
130
  record: RrwebRecord | null;
129
131
  stopRecording: (() => void) | null;
132
+ stopUrlTracking: (() => void) | null;
130
133
  loadInProgress: boolean;
131
134
  cancelled: boolean;
132
135
  stopped: boolean;
@@ -152,6 +155,8 @@ declare function maybeIsolateSnapshot(store: ReplayStore, ctx: PluginContext, ev
152
155
  didIsolate: boolean;
153
156
  };
154
157
  declare function scheduleChunkUpload(store: ReplayStore, ctx: PluginContext): void;
158
+ declare function startUrlChangeTracking(store: ReplayStore, ctx: PluginContext): void;
159
+ declare function stopUrlChangeTracking(store: ReplayStore): void;
155
160
  interface CurrentSessionHandle {
156
161
  id: string;
157
162
  offsetMs: number;
@@ -167,6 +172,9 @@ declare const __test__: {
167
172
  joinUrl: typeof joinUrl;
168
173
  scheduleChunkUpload: typeof scheduleChunkUpload;
169
174
  maybeIsolateSnapshot: typeof maybeIsolateSnapshot;
175
+ startUrlChangeTracking: typeof startUrlChangeTracking;
176
+ stopUrlChangeTracking: typeof stopUrlChangeTracking;
177
+ URL_CHANGE_TAG: string;
170
178
  RRWEB_EVENT_TYPE_FULL_SNAPSHOT: number;
171
179
  SNAPSHOT_ISOLATION_MIN_GAP_MS: number;
172
180
  HARD_CHUNK_BYTE_CAP: number;
@@ -37,6 +37,7 @@ interface PluginContext {
37
37
  logger: PluginLogger;
38
38
  resolveUser?: () => void;
39
39
  getSdkSessionId?: () => string;
40
+ reseatSdkSessionId?: (id: string) => void;
40
41
  getAnonymousId?: () => string;
41
42
  getUserId?: () => string | null;
42
43
  getReplayStartMs?: () => number | null;
@@ -87,6 +88,7 @@ interface RrwebRecordOptions {
87
88
  interface RrwebRecordFn {
88
89
  (opts: RrwebRecordOptions): () => void;
89
90
  takeFullSnapshot?: (isCheckout?: boolean) => void;
91
+ addCustomEvent?: (tag: string, payload: unknown) => void;
90
92
  }
91
93
  type RrwebRecord = RrwebRecordFn;
92
94
  interface ResolvedOptions {
@@ -127,6 +129,7 @@ interface ReplayStore {
127
129
  shadowUpdateHandler: ((event: Event) => void) | null;
128
130
  record: RrwebRecord | null;
129
131
  stopRecording: (() => void) | null;
132
+ stopUrlTracking: (() => void) | null;
130
133
  loadInProgress: boolean;
131
134
  cancelled: boolean;
132
135
  stopped: boolean;
@@ -152,6 +155,8 @@ declare function maybeIsolateSnapshot(store: ReplayStore, ctx: PluginContext, ev
152
155
  didIsolate: boolean;
153
156
  };
154
157
  declare function scheduleChunkUpload(store: ReplayStore, ctx: PluginContext): void;
158
+ declare function startUrlChangeTracking(store: ReplayStore, ctx: PluginContext): void;
159
+ declare function stopUrlChangeTracking(store: ReplayStore): void;
155
160
  interface CurrentSessionHandle {
156
161
  id: string;
157
162
  offsetMs: number;
@@ -167,6 +172,9 @@ declare const __test__: {
167
172
  joinUrl: typeof joinUrl;
168
173
  scheduleChunkUpload: typeof scheduleChunkUpload;
169
174
  maybeIsolateSnapshot: typeof maybeIsolateSnapshot;
175
+ startUrlChangeTracking: typeof startUrlChangeTracking;
176
+ stopUrlChangeTracking: typeof stopUrlChangeTracking;
177
+ URL_CHANGE_TAG: string;
170
178
  RRWEB_EVENT_TYPE_FULL_SNAPSHOT: number;
171
179
  SNAPSHOT_ISOLATION_MIN_GAP_MS: number;
172
180
  HARD_CHUNK_BYTE_CAP: number;