userlens-session-recorder 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs.js CHANGED
@@ -12188,6 +12188,8 @@ var n$1;
12188
12188
  !function(t2) {
12189
12189
  t2[t2.NotStarted = 0] = "NotStarted", t2[t2.Running = 1] = "Running", t2[t2.Stopped = 2] = "Stopped";
12190
12190
  }(n$1 || (n$1 = {}));
12191
+ const { addCustomEvent } = record;
12192
+ const { freezePage } = record;
12191
12193
  const { takeFullSnapshot } = record;
12192
12194
 
12193
12195
  var __defProp = Object.defineProperty;
@@ -20592,7 +20594,7 @@ function generateUuid() {
20592
20594
  function saveWriteCode(writeCode) {
20593
20595
  window.localStorage.setItem("$ul_WRITE_CODE", btoa(`${writeCode}:`));
20594
20596
  }
20595
- const getWriteCode = () => {
20597
+ function getWriteCode() {
20596
20598
  try {
20597
20599
  const raw = window.localStorage.getItem("$ul_WRITE_CODE");
20598
20600
  if (raw == null)
@@ -20608,37 +20610,6 @@ const getWriteCode = () => {
20608
20610
  catch {
20609
20611
  return null;
20610
20612
  }
20611
- };
20612
- const STATE_KEY = "userlensSessionRec";
20613
- function readSessionState() {
20614
- try {
20615
- const raw = window.sessionStorage.getItem(STATE_KEY);
20616
- if (!raw)
20617
- return null;
20618
- const parsed = JSON.parse(raw);
20619
- if (typeof parsed !== "object" || parsed === null ||
20620
- typeof parsed.session_uuid !== "string" ||
20621
- typeof parsed.last_active !== "number" ||
20622
- typeof parsed.chunk_seq !== "number" ||
20623
- typeof parsed.initial_url !== "string")
20624
- return null;
20625
- return parsed;
20626
- }
20627
- catch {
20628
- return null;
20629
- }
20630
- }
20631
- function writeSessionState(state) {
20632
- try {
20633
- window.sessionStorage.setItem(STATE_KEY, JSON.stringify(state));
20634
- }
20635
- catch { }
20636
- }
20637
- function clearSessionState() {
20638
- try {
20639
- window.sessionStorage.removeItem(STATE_KEY);
20640
- }
20641
- catch { }
20642
20613
  }
20643
20614
 
20644
20615
  const INGEST_BASE_URL = "https://ul-ingest.userlens.io";
@@ -20650,15 +20621,13 @@ const uploadSessionEvents = async (args) => {
20650
20621
  }
20651
20622
  const body = {
20652
20623
  session_uuid: args.session_uuid,
20624
+ window_id: args.window_id,
20653
20625
  chunk_seq: args.chunk_seq,
20654
20626
  chunk_start_ts: args.chunk_start_ts,
20655
20627
  chunk_end_ts: args.chunk_end_ts,
20656
20628
  user_id: args.user_id,
20657
20629
  events: args.events,
20658
20630
  };
20659
- if (args.initial_url !== undefined) {
20660
- body.initial_url = args.initial_url;
20661
- }
20662
20631
  const res = await fetch(`${INGEST_BASE_URL}/session-recording`, {
20663
20632
  method: "POST",
20664
20633
  headers: {
@@ -20673,7 +20642,224 @@ const uploadSessionEvents = async (args) => {
20673
20642
  return "ok";
20674
20643
  };
20675
20644
 
20676
- var _SessionRecorder_instances, _SessionRecorder_uploading, _SessionRecorder_uploadingMaxTs, _SessionRecorder_bufferTimer, _SessionRecorder_log, _SessionRecorder_initRecorder, _SessionRecorder_isUserInteraction, _SessionRecorder_handleEvent, _SessionRecorder_armBufferTimer, _SessionRecorder_flushBuffer, _SessionRecorder_resetSession, _SessionRecorder_createSession, _SessionRecorder_handlePageHide, _SessionRecorder_initListeners, _SessionRecorder_trackEvents, _SessionRecorder_clearEvents;
20645
+ const SHARED_KEY = "userlensSession";
20646
+ const WINDOW_KEY = "userlensWindow";
20647
+ // Set in sessionStorage on construct, cleared on pagehide. If we read it on
20648
+ // init and it's already set, sessionStorage was cloned (Cmd+click, duplicate,
20649
+ // window.open) — discard inherited window_id and mint a fresh one.
20650
+ const PRIMARY_WINDOW_KEY = "userlensPrimaryWindow";
20651
+ function readSharedSessionState() {
20652
+ try {
20653
+ const raw = window.localStorage.getItem(SHARED_KEY);
20654
+ if (!raw)
20655
+ return null;
20656
+ const parsed = JSON.parse(raw);
20657
+ if (typeof parsed !== "object" || parsed === null ||
20658
+ typeof parsed.session_uuid !== "string" ||
20659
+ typeof parsed.last_active !== "number")
20660
+ return null;
20661
+ return parsed;
20662
+ }
20663
+ catch {
20664
+ return null;
20665
+ }
20666
+ }
20667
+ function writeSharedSessionState(state) {
20668
+ try {
20669
+ window.localStorage.setItem(SHARED_KEY, JSON.stringify(state));
20670
+ }
20671
+ catch { }
20672
+ }
20673
+ function clearSharedSessionState() {
20674
+ try {
20675
+ window.localStorage.removeItem(SHARED_KEY);
20676
+ }
20677
+ catch { }
20678
+ }
20679
+ function readWindowState() {
20680
+ try {
20681
+ const raw = window.sessionStorage.getItem(WINDOW_KEY);
20682
+ if (!raw)
20683
+ return null;
20684
+ const parsed = JSON.parse(raw);
20685
+ if (typeof parsed !== "object" || parsed === null ||
20686
+ typeof parsed.window_id !== "string" ||
20687
+ typeof parsed.chunk_seq !== "number")
20688
+ return null;
20689
+ return parsed;
20690
+ }
20691
+ catch {
20692
+ return null;
20693
+ }
20694
+ }
20695
+ function writeWindowState(state) {
20696
+ try {
20697
+ window.sessionStorage.setItem(WINDOW_KEY, JSON.stringify(state));
20698
+ }
20699
+ catch { }
20700
+ }
20701
+ function clearWindowState() {
20702
+ try {
20703
+ window.sessionStorage.removeItem(WINDOW_KEY);
20704
+ }
20705
+ catch { }
20706
+ }
20707
+ function wasPrimaryWindowFlagAlreadySet() {
20708
+ try {
20709
+ return window.sessionStorage.getItem(PRIMARY_WINDOW_KEY) === "1";
20710
+ }
20711
+ catch {
20712
+ return false;
20713
+ }
20714
+ }
20715
+ function markPrimaryWindow() {
20716
+ try {
20717
+ window.sessionStorage.setItem(PRIMARY_WINDOW_KEY, "1");
20718
+ }
20719
+ catch { }
20720
+ }
20721
+ function clearPrimaryWindowFlag() {
20722
+ try {
20723
+ window.sessionStorage.removeItem(PRIMARY_WINDOW_KEY);
20724
+ }
20725
+ catch { }
20726
+ }
20727
+
20728
+ var _SessionManager_instances, _SessionManager_initWindow, _SessionManager_joinOrStartSession, _SessionManager_startFresh;
20729
+ /**
20730
+ * Owns the (session_uuid, window_id, chunk_seq, last_active) lifecycle.
20731
+ *
20732
+ * Two storage scopes:
20733
+ * - shared (localStorage): session_uuid + last_active — visible to every tab
20734
+ * of the same origin so tabs can adopt one logical session and a single
20735
+ * idle timeout governs the whole user.
20736
+ * - per-tab (sessionStorage): window_id + chunk_seq — independent per tab so
20737
+ * concurrent tabs can upload chunks without colliding on chunk_seq.
20738
+ *
20739
+ * The recorder talks to this class instead of touching storage directly.
20740
+ */
20741
+ class SessionManager {
20742
+ constructor(timeoutMs) {
20743
+ _SessionManager_instances.add(this);
20744
+ this.timeoutMs = timeoutMs;
20745
+ }
20746
+ initialize() {
20747
+ __classPrivateFieldGet(this, _SessionManager_instances, "m", _SessionManager_initWindow).call(this);
20748
+ __classPrivateFieldGet(this, _SessionManager_instances, "m", _SessionManager_joinOrStartSession).call(this);
20749
+ }
20750
+ get sessionUuid() {
20751
+ return this._sessionUuid;
20752
+ }
20753
+ get windowId() {
20754
+ return this._windowId;
20755
+ }
20756
+ /**
20757
+ * Reconciles in-memory session_uuid with whatever shared storage says.
20758
+ * Called on every event so cross-tab session changes are picked up quickly.
20759
+ *
20760
+ * Returns:
20761
+ * "expired" — shared session has been idle past timeout; we rotated.
20762
+ * "rotated" — another tab rotated the session_uuid; we adopted it.
20763
+ * "none" — no change, business as usual.
20764
+ *
20765
+ * Caller takes a fresh full snapshot on "expired" or "rotated" so this
20766
+ * window's first chunk in the new session is replayable on its own.
20767
+ */
20768
+ syncWithSharedState(now) {
20769
+ const shared = readSharedSessionState();
20770
+ if (shared && now - shared.last_active > this.timeoutMs) {
20771
+ __classPrivateFieldGet(this, _SessionManager_instances, "m", _SessionManager_startFresh).call(this, now);
20772
+ return "expired";
20773
+ }
20774
+ if (shared && shared.session_uuid !== this._sessionUuid) {
20775
+ this._sessionUuid = shared.session_uuid;
20776
+ writeWindowState({ window_id: this._windowId, chunk_seq: 0 });
20777
+ return "rotated";
20778
+ }
20779
+ return "none";
20780
+ }
20781
+ /** Bump shared last_active. Call only on real user interactions. */
20782
+ bumpActivity(now) {
20783
+ const latest = readSharedSessionState();
20784
+ if (latest) {
20785
+ writeSharedSessionState({ ...latest, last_active: now });
20786
+ }
20787
+ }
20788
+ /** Read this window's current chunk_seq for a pending upload. */
20789
+ currentChunkSeq() {
20790
+ var _a, _b;
20791
+ return (_b = (_a = readWindowState()) === null || _a === void 0 ? void 0 : _a.chunk_seq) !== null && _b !== void 0 ? _b : null;
20792
+ }
20793
+ /**
20794
+ * Mark a chunk_seq as committed after a successful upload. No-ops if the
20795
+ * stored seq has moved on (e.g. a session rotation reset it to 0 mid-flight).
20796
+ */
20797
+ commitChunkSeq(uploadedSeq) {
20798
+ const after = readWindowState();
20799
+ if (after && after.window_id === this._windowId && after.chunk_seq === uploadedSeq) {
20800
+ writeWindowState({ window_id: this._windowId, chunk_seq: uploadedSeq + 1 });
20801
+ }
20802
+ }
20803
+ /**
20804
+ * Reserve and return the current chunk_seq for a pagehide flush, advancing
20805
+ * storage so the next normal upload uses seq+1. Returns null if window state
20806
+ * is missing.
20807
+ */
20808
+ reserveChunkSeq() {
20809
+ const win = readWindowState();
20810
+ if (!win)
20811
+ return null;
20812
+ writeWindowState({ window_id: this._windowId, chunk_seq: win.chunk_seq + 1 });
20813
+ return win.chunk_seq;
20814
+ }
20815
+ /** Hard reset: wipe shared session and start a fresh one. */
20816
+ reset() {
20817
+ clearSharedSessionState();
20818
+ __classPrivateFieldGet(this, _SessionManager_instances, "m", _SessionManager_joinOrStartSession).call(this);
20819
+ }
20820
+ /**
20821
+ * Called from pagehide. Lets a future reload of the same tab reuse the
20822
+ * window_id (because the flag is gone), while a cloned tab still sees the
20823
+ * flag set in its inherited sessionStorage and treats itself as a duplicate.
20824
+ */
20825
+ notifyPageHide() {
20826
+ clearPrimaryWindowFlag();
20827
+ }
20828
+ /** Full teardown — call on stop(). */
20829
+ dispose() {
20830
+ clearSharedSessionState();
20831
+ clearWindowState();
20832
+ clearPrimaryWindowFlag();
20833
+ }
20834
+ }
20835
+ _SessionManager_instances = new WeakSet(), _SessionManager_initWindow = function _SessionManager_initWindow() {
20836
+ const cloned = wasPrimaryWindowFlagAlreadySet();
20837
+ const existing = readWindowState();
20838
+ if (existing && !cloned) {
20839
+ this._windowId = existing.window_id;
20840
+ }
20841
+ else {
20842
+ this._windowId = generateUuid();
20843
+ writeWindowState({ window_id: this._windowId, chunk_seq: 0 });
20844
+ }
20845
+ markPrimaryWindow();
20846
+ }, _SessionManager_joinOrStartSession = function _SessionManager_joinOrStartSession() {
20847
+ const now = Date.now();
20848
+ const shared = readSharedSessionState();
20849
+ if (!shared || now - shared.last_active > this.timeoutMs) {
20850
+ __classPrivateFieldGet(this, _SessionManager_instances, "m", _SessionManager_startFresh).call(this, now);
20851
+ }
20852
+ else {
20853
+ this._sessionUuid = shared.session_uuid;
20854
+ writeSharedSessionState({ ...shared, last_active: now });
20855
+ }
20856
+ }, _SessionManager_startFresh = function _SessionManager_startFresh(now) {
20857
+ this._sessionUuid = generateUuid();
20858
+ writeSharedSessionState({ session_uuid: this._sessionUuid, last_active: now });
20859
+ writeWindowState({ window_id: this._windowId, chunk_seq: 0 });
20860
+ };
20861
+
20862
+ var _SessionRecorder_instances, _SessionRecorder_uploading, _SessionRecorder_uploadingMaxTs, _SessionRecorder_bufferTimer, _SessionRecorder_log, _SessionRecorder_initRecorder, _SessionRecorder_isUserInteraction, _SessionRecorder_handleEvent, _SessionRecorder_armBufferTimer, _SessionRecorder_flushBuffer, _SessionRecorder_handlePageHide, _SessionRecorder_handleVisibilityChange, _SessionRecorder_initListeners, _SessionRecorder_trackEvents, _SessionRecorder_clearEvents;
20677
20863
  const BUFFER_TIMEOUT_MS = 2000;
20678
20864
  const MAX_BUFFER_BYTES = 900 * 1024;
20679
20865
  function estimateEventSize(event) {
@@ -20697,6 +20883,7 @@ class SessionRecorder {
20697
20883
  _SessionRecorder_bufferTimer.set(this, null);
20698
20884
  _SessionRecorder_handlePageHide.set(this, () => {
20699
20885
  try {
20886
+ this.sessionManager.notifyPageHide();
20700
20887
  if (this.sessionEvents.length === 0)
20701
20888
  return;
20702
20889
  let events;
@@ -20708,24 +20895,21 @@ class SessionRecorder {
20708
20895
  }
20709
20896
  if (events.length === 0)
20710
20897
  return;
20711
- const state = readSessionState();
20712
- if (!state)
20898
+ const chunk_seq = this.sessionManager.reserveChunkSeq();
20899
+ if (chunk_seq === null)
20713
20900
  return;
20714
- const chunk_seq = __classPrivateFieldGet(this, _SessionRecorder_uploading, "f") ? state.chunk_seq + 1 : state.chunk_seq;
20715
- writeSessionState({ ...state, chunk_seq: chunk_seq + 1 });
20716
20901
  const start_ts_ms = events[0].timestamp;
20717
20902
  const end_ts_ms = events[events.length - 1].timestamp;
20718
- const initial_url = chunk_seq === 0 ? state.initial_url : undefined;
20719
20903
  const chunk_start_ts = new Date(start_ts_ms).toISOString();
20720
20904
  const chunk_end_ts = new Date(end_ts_ms).toISOString();
20721
20905
  if (this.mode === "manual") {
20722
20906
  try {
20723
20907
  void this.onEvents({
20724
- session_uuid: this.sessionUuid,
20908
+ session_uuid: this.sessionManager.sessionUuid,
20909
+ window_id: this.sessionManager.windowId,
20725
20910
  chunk_seq,
20726
20911
  chunk_start_ts,
20727
20912
  chunk_end_ts,
20728
- initial_url,
20729
20913
  events,
20730
20914
  keepalive: true,
20731
20915
  });
@@ -20737,11 +20921,11 @@ class SessionRecorder {
20737
20921
  else {
20738
20922
  uploadSessionEvents({
20739
20923
  user_id: this.userId,
20740
- session_uuid: this.sessionUuid,
20924
+ session_uuid: this.sessionManager.sessionUuid,
20925
+ window_id: this.sessionManager.windowId,
20741
20926
  chunk_seq,
20742
20927
  chunk_start_ts,
20743
20928
  chunk_end_ts,
20744
- initial_url,
20745
20929
  events,
20746
20930
  keepalive: true,
20747
20931
  }).catch(() => { });
@@ -20753,6 +20937,19 @@ class SessionRecorder {
20753
20937
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "Page hide handling failed", err);
20754
20938
  }
20755
20939
  });
20940
+ // Player groups multi-tab recordings into segments by which tab was visible
20941
+ // when. Without these markers it has to guess from chunk timestamps and
20942
+ // active-source heuristics, which is unreliable when two tabs overlap.
20943
+ _SessionRecorder_handleVisibilityChange.set(this, () => {
20944
+ try {
20945
+ if (typeof document === "undefined" || !document.visibilityState)
20946
+ return;
20947
+ record.addCustomEvent("window " + document.visibilityState, {});
20948
+ }
20949
+ catch (err) {
20950
+ __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "Visibility change handling failed", err);
20951
+ }
20952
+ });
20756
20953
  try {
20757
20954
  if (typeof window === "undefined")
20758
20955
  return;
@@ -20797,6 +20994,7 @@ class SessionRecorder {
20797
20994
  this.maskingOptions = maskingOptions;
20798
20995
  this.recordCrossOriginIframes = recordCrossOriginIframes;
20799
20996
  this.sessionEvents = [];
20997
+ this.sessionManager = new SessionManager(this.TIMEOUT);
20800
20998
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_initRecorder).call(this);
20801
20999
  }
20802
21000
  catch (err) {
@@ -20805,7 +21003,8 @@ class SessionRecorder {
20805
21003
  }
20806
21004
  }
20807
21005
  getSessionId() {
20808
- return this.sessionUuid;
21006
+ var _a;
21007
+ return (_a = this.sessionManager) === null || _a === void 0 ? void 0 : _a.sessionUuid;
20809
21008
  }
20810
21009
  stop() {
20811
21010
  try {
@@ -20815,15 +21014,16 @@ class SessionRecorder {
20815
21014
  this.rrwebStop();
20816
21015
  this.rrwebStop = null;
20817
21016
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_clearEvents).call(this);
20818
- clearSessionState();
21017
+ this.sessionManager.dispose();
20819
21018
  window.removeEventListener("pagehide", __classPrivateFieldGet(this, _SessionRecorder_handlePageHide, "f"));
21019
+ document.removeEventListener("visibilitychange", __classPrivateFieldGet(this, _SessionRecorder_handleVisibilityChange, "f"));
20820
21020
  }
20821
21021
  catch (err) {
20822
21022
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "Stop failed", err);
20823
21023
  }
20824
21024
  }
20825
21025
  }
20826
- _SessionRecorder_uploading = new WeakMap(), _SessionRecorder_uploadingMaxTs = new WeakMap(), _SessionRecorder_bufferTimer = new WeakMap(), _SessionRecorder_handlePageHide = new WeakMap(), _SessionRecorder_instances = new WeakSet(), _SessionRecorder_log = function _SessionRecorder_log(message, error) {
21026
+ _SessionRecorder_uploading = new WeakMap(), _SessionRecorder_uploadingMaxTs = new WeakMap(), _SessionRecorder_bufferTimer = new WeakMap(), _SessionRecorder_handlePageHide = new WeakMap(), _SessionRecorder_handleVisibilityChange = new WeakMap(), _SessionRecorder_instances = new WeakSet(), _SessionRecorder_log = function _SessionRecorder_log(message, error) {
20827
21027
  if (!this.debug)
20828
21028
  return;
20829
21029
  if (error) {
@@ -20835,7 +21035,7 @@ _SessionRecorder_uploading = new WeakMap(), _SessionRecorder_uploadingMaxTs = ne
20835
21035
  }, _SessionRecorder_initRecorder = function _SessionRecorder_initRecorder() {
20836
21036
  if (this.rrwebStop)
20837
21037
  return;
20838
- __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_createSession).call(this);
21038
+ this.sessionManager.initialize();
20839
21039
  this.rrwebStop = record({
20840
21040
  emit: (event, isCheckout) => {
20841
21041
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_handleEvent).call(this, event, isCheckout);
@@ -20859,16 +21059,16 @@ _SessionRecorder_uploading = new WeakMap(), _SessionRecorder_uploadingMaxTs = ne
20859
21059
  }, _SessionRecorder_handleEvent = function _SessionRecorder_handleEvent(event, _isCheckout) {
20860
21060
  try {
20861
21061
  const now = Date.now();
20862
- const state = readSessionState();
20863
- if (state && now - state.last_active > this.TIMEOUT) {
20864
- __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_resetSession).call(this);
21062
+ const change = this.sessionManager.syncWithSharedState(now);
21063
+ if (change === "expired") {
21064
+ __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_clearEvents).call(this);
21065
+ takeFullSnapshot(true);
21066
+ }
21067
+ else if (change === "rotated") {
20865
21068
  takeFullSnapshot(true);
20866
21069
  }
20867
21070
  if (__classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_isUserInteraction).call(this, event)) {
20868
- const latest = readSessionState();
20869
- if (latest) {
20870
- writeSessionState({ ...latest, last_active: now });
20871
- }
21071
+ this.sessionManager.bumpActivity(now);
20872
21072
  }
20873
21073
  this.sessionEvents.push(event);
20874
21074
  this.bufferBytes += estimateEventSize(event);
@@ -20894,30 +21094,13 @@ _SessionRecorder_uploading = new WeakMap(), _SessionRecorder_uploadingMaxTs = ne
20894
21094
  __classPrivateFieldSet(this, _SessionRecorder_bufferTimer, null, "f");
20895
21095
  }
20896
21096
  void __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_trackEvents).call(this);
20897
- }, _SessionRecorder_resetSession = function _SessionRecorder_resetSession() {
20898
- clearSessionState();
20899
- __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_clearEvents).call(this);
20900
- __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_createSession).call(this);
20901
- }, _SessionRecorder_createSession = function _SessionRecorder_createSession() {
20902
- const now = Date.now();
20903
- const state = readSessionState();
20904
- const isExpired = !state || now - state.last_active > this.TIMEOUT;
20905
- if (!state || isExpired) {
20906
- this.sessionUuid = generateUuid();
20907
- const initial_url = typeof window !== "undefined" ? window.location.href : "";
20908
- writeSessionState({
20909
- session_uuid: this.sessionUuid,
20910
- last_active: now,
20911
- chunk_seq: 0,
20912
- initial_url,
20913
- });
20914
- }
20915
- else {
20916
- this.sessionUuid = state.session_uuid;
20917
- writeSessionState({ ...state, last_active: now });
20918
- }
20919
21097
  }, _SessionRecorder_initListeners = function _SessionRecorder_initListeners() {
20920
21098
  window.addEventListener("pagehide", __classPrivateFieldGet(this, _SessionRecorder_handlePageHide, "f"));
21099
+ document.addEventListener("visibilitychange", __classPrivateFieldGet(this, _SessionRecorder_handleVisibilityChange, "f"));
21100
+ // Emit the initial state so the player knows whether this tab started
21101
+ // visible or hidden (a tab opened in the background never fires
21102
+ // visibilitychange until the user focuses it).
21103
+ __classPrivateFieldGet(this, _SessionRecorder_handleVisibilityChange, "f").call(this);
20921
21104
  }, _SessionRecorder_trackEvents = async function _SessionRecorder_trackEvents() {
20922
21105
  if (__classPrivateFieldGet(this, _SessionRecorder_uploading, "f"))
20923
21106
  return;
@@ -20930,45 +21113,38 @@ _SessionRecorder_uploading = new WeakMap(), _SessionRecorder_uploadingMaxTs = ne
20930
21113
  __classPrivateFieldSet(this, _SessionRecorder_uploadingMaxTs, events[events.length - 1].timestamp, "f");
20931
21114
  const start_ts_ms = events[0].timestamp;
20932
21115
  const end_ts_ms = events[events.length - 1].timestamp;
20933
- const state = readSessionState();
20934
- if (!state) {
20935
- __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "No session state during upload — skipping chunk");
21116
+ const chunk_seq = this.sessionManager.currentChunkSeq();
21117
+ if (chunk_seq === null) {
21118
+ __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "No window state during upload — skipping chunk");
20936
21119
  return;
20937
21120
  }
20938
- const chunk_seq = state.chunk_seq;
20939
- const initial_url = chunk_seq === 0 ? state.initial_url : undefined;
20940
21121
  const chunk_start_ts = new Date(start_ts_ms).toISOString();
20941
21122
  const chunk_end_ts = new Date(end_ts_ms).toISOString();
20942
21123
  if (this.mode === "manual") {
20943
21124
  await this.onEvents({
20944
- session_uuid: this.sessionUuid,
21125
+ session_uuid: this.sessionManager.sessionUuid,
21126
+ window_id: this.sessionManager.windowId,
20945
21127
  chunk_seq,
20946
21128
  chunk_start_ts,
20947
21129
  chunk_end_ts,
20948
- initial_url,
20949
21130
  events,
20950
21131
  });
20951
21132
  }
20952
21133
  else {
20953
21134
  await uploadSessionEvents({
20954
21135
  user_id: this.userId,
20955
- session_uuid: this.sessionUuid,
21136
+ session_uuid: this.sessionManager.sessionUuid,
21137
+ window_id: this.sessionManager.windowId,
20956
21138
  chunk_seq,
20957
21139
  chunk_start_ts,
20958
21140
  chunk_end_ts,
20959
- initial_url,
20960
21141
  events,
20961
21142
  });
20962
21143
  }
20963
21144
  const removedBytes = events.reduce((sum, e) => sum + estimateEventSize(e), 0);
20964
21145
  this.sessionEvents = this.sessionEvents.slice(snapshot_count);
20965
21146
  this.bufferBytes = Math.max(0, this.bufferBytes - removedBytes);
20966
- const after = readSessionState();
20967
- if (after &&
20968
- after.session_uuid === state.session_uuid &&
20969
- after.chunk_seq === chunk_seq) {
20970
- writeSessionState({ ...after, chunk_seq: chunk_seq + 1 });
20971
- }
21147
+ this.sessionManager.commitChunkSeq(chunk_seq);
20972
21148
  }
20973
21149
  catch (err) {
20974
21150
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "Event tracking failed", err);