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