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.d.ts CHANGED
@@ -1,12 +1,14 @@
1
- import { MaskingOption, SessionRecorderConfig } from "./types";
2
- export type { SessionRecorderConfig, MaskingOption, };
1
+ import { MaskingOption, RecorderMode, EventBatch, OnEventsCallback, SessionRecorderConfig, AutoModeConfig, ManualModeConfig } from "./types";
2
+ export type { SessionRecorderConfig, AutoModeConfig, ManualModeConfig, RecorderMode, EventBatch, OnEventsCallback, MaskingOption, };
3
3
  export default class SessionRecorder {
4
4
  #private;
5
+ private mode;
5
6
  private userId?;
7
+ private onEvents?;
6
8
  private TIMEOUT;
7
9
  private BUFFER_SIZE;
8
10
  private maskingOptions;
9
- private sessionUuid;
11
+ private sessionManager;
10
12
  private sessionEvents;
11
13
  private bufferBytes;
12
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,23 +20891,41 @@ 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
- uploadSessionEvents({
20715
- user_id: this.userId,
20716
- session_uuid: this.sessionUuid,
20717
- chunk_seq,
20718
- chunk_start_ts: new Date(start_ts_ms).toISOString(),
20719
- chunk_end_ts: new Date(end_ts_ms).toISOString(),
20720
- initial_url: chunk_seq === 0 ? state.initial_url : undefined,
20721
- events,
20722
- keepalive: true,
20723
- }).catch(() => { });
20899
+ const chunk_start_ts = new Date(start_ts_ms).toISOString();
20900
+ const chunk_end_ts = new Date(end_ts_ms).toISOString();
20901
+ if (this.mode === "manual") {
20902
+ try {
20903
+ void this.onEvents({
20904
+ session_uuid: this.sessionManager.sessionUuid,
20905
+ window_id: this.sessionManager.windowId,
20906
+ chunk_seq,
20907
+ chunk_start_ts,
20908
+ chunk_end_ts,
20909
+ events,
20910
+ keepalive: true,
20911
+ });
20912
+ }
20913
+ catch {
20914
+ // ignore
20915
+ }
20916
+ }
20917
+ else {
20918
+ uploadSessionEvents({
20919
+ user_id: this.userId,
20920
+ session_uuid: this.sessionManager.sessionUuid,
20921
+ window_id: this.sessionManager.windowId,
20922
+ chunk_seq,
20923
+ chunk_start_ts,
20924
+ chunk_end_ts,
20925
+ events,
20926
+ keepalive: true,
20927
+ }).catch(() => { });
20928
+ }
20724
20929
  this.sessionEvents = [];
20725
20930
  this.bufferBytes = 0;
20726
20931
  }
@@ -20728,6 +20933,19 @@ class SessionRecorder {
20728
20933
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "Page hide handling failed", err);
20729
20934
  }
20730
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
+ });
20731
20949
  try {
20732
20950
  if (typeof window === "undefined")
20733
20951
  return;
@@ -20749,11 +20967,22 @@ class SessionRecorder {
20749
20967
  sessionStorage.setItem(testKey, "1");
20750
20968
  sessionStorage.removeItem(testKey);
20751
20969
  this.debug = (_a = config.debug) !== null && _a !== void 0 ? _a : false;
20752
- if (!((_b = config.WRITE_CODE) === null || _b === void 0 ? void 0 : _b.trim()) || !((_c = config.userId) === null || _c === void 0 ? void 0 : _c.trim())) {
20753
- return;
20970
+ if (config.mode === "manual") {
20971
+ this.mode = "manual";
20972
+ if (!config.onEvents || typeof config.onEvents !== "function") {
20973
+ __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "onEvents callback is required in manual mode");
20974
+ return;
20975
+ }
20976
+ this.onEvents = config.onEvents;
20977
+ }
20978
+ else {
20979
+ this.mode = "auto";
20980
+ if (!((_b = config.WRITE_CODE) === null || _b === void 0 ? void 0 : _b.trim()) || !((_c = config.userId) === null || _c === void 0 ? void 0 : _c.trim())) {
20981
+ return;
20982
+ }
20983
+ saveWriteCode(config.WRITE_CODE);
20984
+ this.userId = config.userId;
20754
20985
  }
20755
- saveWriteCode(config.WRITE_CODE);
20756
- this.userId = config.userId;
20757
20986
  const { recordingOptions = {} } = config;
20758
20987
  const { TIMEOUT = 30 * 60 * 1000, BUFFER_SIZE = 30, maskingOptions = ["passwords"], recordCrossOriginIframes = false, } = recordingOptions;
20759
20988
  this.TIMEOUT = TIMEOUT;
@@ -20761,6 +20990,7 @@ class SessionRecorder {
20761
20990
  this.maskingOptions = maskingOptions;
20762
20991
  this.recordCrossOriginIframes = recordCrossOriginIframes;
20763
20992
  this.sessionEvents = [];
20993
+ this.sessionManager = new SessionManager(this.TIMEOUT);
20764
20994
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_initRecorder).call(this);
20765
20995
  }
20766
20996
  catch (err) {
@@ -20769,7 +20999,8 @@ class SessionRecorder {
20769
20999
  }
20770
21000
  }
20771
21001
  getSessionId() {
20772
- return this.sessionUuid;
21002
+ var _a;
21003
+ return (_a = this.sessionManager) === null || _a === void 0 ? void 0 : _a.sessionUuid;
20773
21004
  }
20774
21005
  stop() {
20775
21006
  try {
@@ -20779,15 +21010,16 @@ class SessionRecorder {
20779
21010
  this.rrwebStop();
20780
21011
  this.rrwebStop = null;
20781
21012
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_clearEvents).call(this);
20782
- clearSessionState();
21013
+ this.sessionManager.dispose();
20783
21014
  window.removeEventListener("pagehide", __classPrivateFieldGet(this, _SessionRecorder_handlePageHide, "f"));
21015
+ document.removeEventListener("visibilitychange", __classPrivateFieldGet(this, _SessionRecorder_handleVisibilityChange, "f"));
20784
21016
  }
20785
21017
  catch (err) {
20786
21018
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "Stop failed", err);
20787
21019
  }
20788
21020
  }
20789
21021
  }
20790
- _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) {
20791
21023
  if (!this.debug)
20792
21024
  return;
20793
21025
  if (error) {
@@ -20799,7 +21031,7 @@ _SessionRecorder_uploading = new WeakMap(), _SessionRecorder_uploadingMaxTs = ne
20799
21031
  }, _SessionRecorder_initRecorder = function _SessionRecorder_initRecorder() {
20800
21032
  if (this.rrwebStop)
20801
21033
  return;
20802
- __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_createSession).call(this);
21034
+ this.sessionManager.initialize();
20803
21035
  this.rrwebStop = record({
20804
21036
  emit: (event, isCheckout) => {
20805
21037
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_handleEvent).call(this, event, isCheckout);
@@ -20823,16 +21055,16 @@ _SessionRecorder_uploading = new WeakMap(), _SessionRecorder_uploadingMaxTs = ne
20823
21055
  }, _SessionRecorder_handleEvent = function _SessionRecorder_handleEvent(event, _isCheckout) {
20824
21056
  try {
20825
21057
  const now = Date.now();
20826
- const state = readSessionState();
20827
- if (state && now - state.last_active > this.TIMEOUT) {
20828
- __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") {
20829
21064
  takeFullSnapshot(true);
20830
21065
  }
20831
21066
  if (__classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_isUserInteraction).call(this, event)) {
20832
- const latest = readSessionState();
20833
- if (latest) {
20834
- writeSessionState({ ...latest, last_active: now });
20835
- }
21067
+ this.sessionManager.bumpActivity(now);
20836
21068
  }
20837
21069
  this.sessionEvents.push(event);
20838
21070
  this.bufferBytes += estimateEventSize(event);
@@ -20858,30 +21090,13 @@ _SessionRecorder_uploading = new WeakMap(), _SessionRecorder_uploadingMaxTs = ne
20858
21090
  __classPrivateFieldSet(this, _SessionRecorder_bufferTimer, null, "f");
20859
21091
  }
20860
21092
  void __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_trackEvents).call(this);
20861
- }, _SessionRecorder_resetSession = function _SessionRecorder_resetSession() {
20862
- clearSessionState();
20863
- __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_clearEvents).call(this);
20864
- __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_createSession).call(this);
20865
- }, _SessionRecorder_createSession = function _SessionRecorder_createSession() {
20866
- const now = Date.now();
20867
- const state = readSessionState();
20868
- const isExpired = !state || now - state.last_active > this.TIMEOUT;
20869
- if (!state || isExpired) {
20870
- this.sessionUuid = generateUuid();
20871
- const initial_url = typeof window !== "undefined" ? window.location.href : "";
20872
- writeSessionState({
20873
- session_uuid: this.sessionUuid,
20874
- last_active: now,
20875
- chunk_seq: 0,
20876
- initial_url,
20877
- });
20878
- }
20879
- else {
20880
- this.sessionUuid = state.session_uuid;
20881
- writeSessionState({ ...state, last_active: now });
20882
- }
20883
21093
  }, _SessionRecorder_initListeners = function _SessionRecorder_initListeners() {
20884
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);
20885
21100
  }, _SessionRecorder_trackEvents = async function _SessionRecorder_trackEvents() {
20886
21101
  if (__classPrivateFieldGet(this, _SessionRecorder_uploading, "f"))
20887
21102
  return;
@@ -20894,30 +21109,38 @@ _SessionRecorder_uploading = new WeakMap(), _SessionRecorder_uploadingMaxTs = ne
20894
21109
  __classPrivateFieldSet(this, _SessionRecorder_uploadingMaxTs, events[events.length - 1].timestamp, "f");
20895
21110
  const start_ts_ms = events[0].timestamp;
20896
21111
  const end_ts_ms = events[events.length - 1].timestamp;
20897
- const state = readSessionState();
20898
- if (!state) {
20899
- __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");
20900
21115
  return;
20901
21116
  }
20902
- const chunk_seq = state.chunk_seq;
20903
- await uploadSessionEvents({
20904
- user_id: this.userId,
20905
- session_uuid: this.sessionUuid,
20906
- chunk_seq,
20907
- chunk_start_ts: new Date(start_ts_ms).toISOString(),
20908
- chunk_end_ts: new Date(end_ts_ms).toISOString(),
20909
- initial_url: chunk_seq === 0 ? state.initial_url : undefined,
20910
- events,
20911
- });
21117
+ const chunk_start_ts = new Date(start_ts_ms).toISOString();
21118
+ const chunk_end_ts = new Date(end_ts_ms).toISOString();
21119
+ if (this.mode === "manual") {
21120
+ await this.onEvents({
21121
+ session_uuid: this.sessionManager.sessionUuid,
21122
+ window_id: this.sessionManager.windowId,
21123
+ chunk_seq,
21124
+ chunk_start_ts,
21125
+ chunk_end_ts,
21126
+ events,
21127
+ });
21128
+ }
21129
+ else {
21130
+ await uploadSessionEvents({
21131
+ user_id: this.userId,
21132
+ session_uuid: this.sessionManager.sessionUuid,
21133
+ window_id: this.sessionManager.windowId,
21134
+ chunk_seq,
21135
+ chunk_start_ts,
21136
+ chunk_end_ts,
21137
+ events,
21138
+ });
21139
+ }
20912
21140
  const removedBytes = events.reduce((sum, e) => sum + estimateEventSize(e), 0);
20913
21141
  this.sessionEvents = this.sessionEvents.slice(snapshot_count);
20914
21142
  this.bufferBytes = Math.max(0, this.bufferBytes - removedBytes);
20915
- const after = readSessionState();
20916
- if (after &&
20917
- after.session_uuid === state.session_uuid &&
20918
- after.chunk_seq === chunk_seq) {
20919
- writeSessionState({ ...after, chunk_seq: chunk_seq + 1 });
20920
- }
21143
+ this.sessionManager.commitChunkSeq(chunk_seq);
20921
21144
  }
20922
21145
  catch (err) {
20923
21146
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "Event tracking failed", err);