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