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.
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,23 +20895,41 @@ 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
- uploadSessionEvents({
20719
- user_id: this.userId,
20720
- session_uuid: this.sessionUuid,
20721
- chunk_seq,
20722
- chunk_start_ts: new Date(start_ts_ms).toISOString(),
20723
- chunk_end_ts: new Date(end_ts_ms).toISOString(),
20724
- initial_url: chunk_seq === 0 ? state.initial_url : undefined,
20725
- events,
20726
- keepalive: true,
20727
- }).catch(() => { });
20903
+ const chunk_start_ts = new Date(start_ts_ms).toISOString();
20904
+ const chunk_end_ts = new Date(end_ts_ms).toISOString();
20905
+ if (this.mode === "manual") {
20906
+ try {
20907
+ void this.onEvents({
20908
+ session_uuid: this.sessionManager.sessionUuid,
20909
+ window_id: this.sessionManager.windowId,
20910
+ chunk_seq,
20911
+ chunk_start_ts,
20912
+ chunk_end_ts,
20913
+ events,
20914
+ keepalive: true,
20915
+ });
20916
+ }
20917
+ catch {
20918
+ // ignore
20919
+ }
20920
+ }
20921
+ else {
20922
+ uploadSessionEvents({
20923
+ user_id: this.userId,
20924
+ session_uuid: this.sessionManager.sessionUuid,
20925
+ window_id: this.sessionManager.windowId,
20926
+ chunk_seq,
20927
+ chunk_start_ts,
20928
+ chunk_end_ts,
20929
+ events,
20930
+ keepalive: true,
20931
+ }).catch(() => { });
20932
+ }
20728
20933
  this.sessionEvents = [];
20729
20934
  this.bufferBytes = 0;
20730
20935
  }
@@ -20732,6 +20937,19 @@ class SessionRecorder {
20732
20937
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "Page hide handling failed", err);
20733
20938
  }
20734
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
+ });
20735
20953
  try {
20736
20954
  if (typeof window === "undefined")
20737
20955
  return;
@@ -20753,11 +20971,22 @@ class SessionRecorder {
20753
20971
  sessionStorage.setItem(testKey, "1");
20754
20972
  sessionStorage.removeItem(testKey);
20755
20973
  this.debug = (_a = config.debug) !== null && _a !== void 0 ? _a : false;
20756
- if (!((_b = config.WRITE_CODE) === null || _b === void 0 ? void 0 : _b.trim()) || !((_c = config.userId) === null || _c === void 0 ? void 0 : _c.trim())) {
20757
- return;
20974
+ if (config.mode === "manual") {
20975
+ this.mode = "manual";
20976
+ if (!config.onEvents || typeof config.onEvents !== "function") {
20977
+ __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "onEvents callback is required in manual mode");
20978
+ return;
20979
+ }
20980
+ this.onEvents = config.onEvents;
20981
+ }
20982
+ else {
20983
+ this.mode = "auto";
20984
+ if (!((_b = config.WRITE_CODE) === null || _b === void 0 ? void 0 : _b.trim()) || !((_c = config.userId) === null || _c === void 0 ? void 0 : _c.trim())) {
20985
+ return;
20986
+ }
20987
+ saveWriteCode(config.WRITE_CODE);
20988
+ this.userId = config.userId;
20758
20989
  }
20759
- saveWriteCode(config.WRITE_CODE);
20760
- this.userId = config.userId;
20761
20990
  const { recordingOptions = {} } = config;
20762
20991
  const { TIMEOUT = 30 * 60 * 1000, BUFFER_SIZE = 30, maskingOptions = ["passwords"], recordCrossOriginIframes = false, } = recordingOptions;
20763
20992
  this.TIMEOUT = TIMEOUT;
@@ -20765,6 +20994,7 @@ class SessionRecorder {
20765
20994
  this.maskingOptions = maskingOptions;
20766
20995
  this.recordCrossOriginIframes = recordCrossOriginIframes;
20767
20996
  this.sessionEvents = [];
20997
+ this.sessionManager = new SessionManager(this.TIMEOUT);
20768
20998
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_initRecorder).call(this);
20769
20999
  }
20770
21000
  catch (err) {
@@ -20773,7 +21003,8 @@ class SessionRecorder {
20773
21003
  }
20774
21004
  }
20775
21005
  getSessionId() {
20776
- return this.sessionUuid;
21006
+ var _a;
21007
+ return (_a = this.sessionManager) === null || _a === void 0 ? void 0 : _a.sessionUuid;
20777
21008
  }
20778
21009
  stop() {
20779
21010
  try {
@@ -20783,15 +21014,16 @@ class SessionRecorder {
20783
21014
  this.rrwebStop();
20784
21015
  this.rrwebStop = null;
20785
21016
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_clearEvents).call(this);
20786
- clearSessionState();
21017
+ this.sessionManager.dispose();
20787
21018
  window.removeEventListener("pagehide", __classPrivateFieldGet(this, _SessionRecorder_handlePageHide, "f"));
21019
+ document.removeEventListener("visibilitychange", __classPrivateFieldGet(this, _SessionRecorder_handleVisibilityChange, "f"));
20788
21020
  }
20789
21021
  catch (err) {
20790
21022
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "Stop failed", err);
20791
21023
  }
20792
21024
  }
20793
21025
  }
20794
- _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) {
20795
21027
  if (!this.debug)
20796
21028
  return;
20797
21029
  if (error) {
@@ -20803,7 +21035,7 @@ _SessionRecorder_uploading = new WeakMap(), _SessionRecorder_uploadingMaxTs = ne
20803
21035
  }, _SessionRecorder_initRecorder = function _SessionRecorder_initRecorder() {
20804
21036
  if (this.rrwebStop)
20805
21037
  return;
20806
- __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_createSession).call(this);
21038
+ this.sessionManager.initialize();
20807
21039
  this.rrwebStop = record({
20808
21040
  emit: (event, isCheckout) => {
20809
21041
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_handleEvent).call(this, event, isCheckout);
@@ -20827,16 +21059,16 @@ _SessionRecorder_uploading = new WeakMap(), _SessionRecorder_uploadingMaxTs = ne
20827
21059
  }, _SessionRecorder_handleEvent = function _SessionRecorder_handleEvent(event, _isCheckout) {
20828
21060
  try {
20829
21061
  const now = Date.now();
20830
- const state = readSessionState();
20831
- if (state && now - state.last_active > this.TIMEOUT) {
20832
- __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") {
20833
21068
  takeFullSnapshot(true);
20834
21069
  }
20835
21070
  if (__classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_isUserInteraction).call(this, event)) {
20836
- const latest = readSessionState();
20837
- if (latest) {
20838
- writeSessionState({ ...latest, last_active: now });
20839
- }
21071
+ this.sessionManager.bumpActivity(now);
20840
21072
  }
20841
21073
  this.sessionEvents.push(event);
20842
21074
  this.bufferBytes += estimateEventSize(event);
@@ -20862,30 +21094,13 @@ _SessionRecorder_uploading = new WeakMap(), _SessionRecorder_uploadingMaxTs = ne
20862
21094
  __classPrivateFieldSet(this, _SessionRecorder_bufferTimer, null, "f");
20863
21095
  }
20864
21096
  void __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_trackEvents).call(this);
20865
- }, _SessionRecorder_resetSession = function _SessionRecorder_resetSession() {
20866
- clearSessionState();
20867
- __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_clearEvents).call(this);
20868
- __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_createSession).call(this);
20869
- }, _SessionRecorder_createSession = function _SessionRecorder_createSession() {
20870
- const now = Date.now();
20871
- const state = readSessionState();
20872
- const isExpired = !state || now - state.last_active > this.TIMEOUT;
20873
- if (!state || isExpired) {
20874
- this.sessionUuid = generateUuid();
20875
- const initial_url = typeof window !== "undefined" ? window.location.href : "";
20876
- writeSessionState({
20877
- session_uuid: this.sessionUuid,
20878
- last_active: now,
20879
- chunk_seq: 0,
20880
- initial_url,
20881
- });
20882
- }
20883
- else {
20884
- this.sessionUuid = state.session_uuid;
20885
- writeSessionState({ ...state, last_active: now });
20886
- }
20887
21097
  }, _SessionRecorder_initListeners = function _SessionRecorder_initListeners() {
20888
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);
20889
21104
  }, _SessionRecorder_trackEvents = async function _SessionRecorder_trackEvents() {
20890
21105
  if (__classPrivateFieldGet(this, _SessionRecorder_uploading, "f"))
20891
21106
  return;
@@ -20898,30 +21113,38 @@ _SessionRecorder_uploading = new WeakMap(), _SessionRecorder_uploadingMaxTs = ne
20898
21113
  __classPrivateFieldSet(this, _SessionRecorder_uploadingMaxTs, events[events.length - 1].timestamp, "f");
20899
21114
  const start_ts_ms = events[0].timestamp;
20900
21115
  const end_ts_ms = events[events.length - 1].timestamp;
20901
- const state = readSessionState();
20902
- if (!state) {
20903
- __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");
20904
21119
  return;
20905
21120
  }
20906
- const chunk_seq = state.chunk_seq;
20907
- await uploadSessionEvents({
20908
- user_id: this.userId,
20909
- session_uuid: this.sessionUuid,
20910
- chunk_seq,
20911
- chunk_start_ts: new Date(start_ts_ms).toISOString(),
20912
- chunk_end_ts: new Date(end_ts_ms).toISOString(),
20913
- initial_url: chunk_seq === 0 ? state.initial_url : undefined,
20914
- events,
20915
- });
21121
+ const chunk_start_ts = new Date(start_ts_ms).toISOString();
21122
+ const chunk_end_ts = new Date(end_ts_ms).toISOString();
21123
+ if (this.mode === "manual") {
21124
+ await this.onEvents({
21125
+ session_uuid: this.sessionManager.sessionUuid,
21126
+ window_id: this.sessionManager.windowId,
21127
+ chunk_seq,
21128
+ chunk_start_ts,
21129
+ chunk_end_ts,
21130
+ events,
21131
+ });
21132
+ }
21133
+ else {
21134
+ await uploadSessionEvents({
21135
+ user_id: this.userId,
21136
+ session_uuid: this.sessionManager.sessionUuid,
21137
+ window_id: this.sessionManager.windowId,
21138
+ chunk_seq,
21139
+ chunk_start_ts,
21140
+ chunk_end_ts,
21141
+ events,
21142
+ });
21143
+ }
20916
21144
  const removedBytes = events.reduce((sum, e) => sum + estimateEventSize(e), 0);
20917
21145
  this.sessionEvents = this.sessionEvents.slice(snapshot_count);
20918
21146
  this.bufferBytes = Math.max(0, this.bufferBytes - removedBytes);
20919
- const after = readSessionState();
20920
- if (after &&
20921
- after.session_uuid === state.session_uuid &&
20922
- after.chunk_seq === chunk_seq) {
20923
- writeSessionState({ ...after, chunk_seq: chunk_seq + 1 });
20924
- }
21147
+ this.sessionManager.commitChunkSeq(chunk_seq);
20925
21148
  }
20926
21149
  catch (err) {
20927
21150
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "Event tracking failed", err);