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.
@@ -0,0 +1,61 @@
1
+ export type SessionChange = "none" | "expired" | "rotated";
2
+ /**
3
+ * Owns the (session_uuid, window_id, chunk_seq, last_active) lifecycle.
4
+ *
5
+ * Two storage scopes:
6
+ * - shared (localStorage): session_uuid + last_active — visible to every tab
7
+ * of the same origin so tabs can adopt one logical session and a single
8
+ * idle timeout governs the whole user.
9
+ * - per-tab (sessionStorage): window_id + chunk_seq — independent per tab so
10
+ * concurrent tabs can upload chunks without colliding on chunk_seq.
11
+ *
12
+ * The recorder talks to this class instead of touching storage directly.
13
+ */
14
+ export declare class SessionManager {
15
+ #private;
16
+ private readonly timeoutMs;
17
+ private _sessionUuid;
18
+ private _windowId;
19
+ constructor(timeoutMs: number);
20
+ initialize(): void;
21
+ get sessionUuid(): string;
22
+ get windowId(): string;
23
+ /**
24
+ * Reconciles in-memory session_uuid with whatever shared storage says.
25
+ * Called on every event so cross-tab session changes are picked up quickly.
26
+ *
27
+ * Returns:
28
+ * "expired" — shared session has been idle past timeout; we rotated.
29
+ * "rotated" — another tab rotated the session_uuid; we adopted it.
30
+ * "none" — no change, business as usual.
31
+ *
32
+ * Caller takes a fresh full snapshot on "expired" or "rotated" so this
33
+ * window's first chunk in the new session is replayable on its own.
34
+ */
35
+ syncWithSharedState(now: number): SessionChange;
36
+ /** Bump shared last_active. Call only on real user interactions. */
37
+ bumpActivity(now: number): void;
38
+ /** Read this window's current chunk_seq for a pending upload. */
39
+ currentChunkSeq(): number | null;
40
+ /**
41
+ * Mark a chunk_seq as committed after a successful upload. No-ops if the
42
+ * stored seq has moved on (e.g. a session rotation reset it to 0 mid-flight).
43
+ */
44
+ commitChunkSeq(uploadedSeq: number): void;
45
+ /**
46
+ * Reserve and return the current chunk_seq for a pagehide flush, advancing
47
+ * storage so the next normal upload uses seq+1. Returns null if window state
48
+ * is missing.
49
+ */
50
+ reserveChunkSeq(): number | null;
51
+ /** Hard reset: wipe shared session and start a fresh one. */
52
+ reset(): void;
53
+ /**
54
+ * Called from pagehide. Lets a future reload of the same tab reuse the
55
+ * window_id (because the flag is gone), while a cloned tab still sees the
56
+ * flag set in its inherited sessionStorage and treats itself as a duplicate.
57
+ */
58
+ notifyPageHide(): void;
59
+ /** Full teardown — call on stop(). */
60
+ dispose(): void;
61
+ }
@@ -0,0 +1,10 @@
1
+ import type { SharedSessionState, WindowState } from "./types";
2
+ export declare function readSharedSessionState(): SharedSessionState | null;
3
+ export declare function writeSharedSessionState(state: SharedSessionState): void;
4
+ export declare function clearSharedSessionState(): void;
5
+ export declare function readWindowState(): WindowState | null;
6
+ export declare function writeWindowState(state: WindowState): void;
7
+ export declare function clearWindowState(): void;
8
+ export declare function wasPrimaryWindowFlagAlreadySet(): boolean;
9
+ export declare function markPrimaryWindow(): void;
10
+ export declare function clearPrimaryWindowFlag(): void;
@@ -10,10 +10,10 @@ interface SessionRecordingOptions {
10
10
  }
11
11
  interface EventBatch {
12
12
  session_uuid: string;
13
+ window_id: string;
13
14
  chunk_seq: number;
14
15
  chunk_start_ts: string;
15
16
  chunk_end_ts: string;
16
- initial_url?: string;
17
17
  events: eventWithTime[];
18
18
  keepalive?: boolean;
19
19
  }
@@ -41,7 +41,7 @@ declare class SessionRecorder {
41
41
  private TIMEOUT;
42
42
  private BUFFER_SIZE;
43
43
  private maskingOptions;
44
- private sessionUuid;
44
+ private sessionManager;
45
45
  private sessionEvents;
46
46
  private bufferBytes;
47
47
  private rrwebStop;
@@ -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,24 +20897,21 @@
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
- const initial_url = chunk_seq === 0 ? state.initial_url : undefined;
20721
20905
  const chunk_start_ts = new Date(start_ts_ms).toISOString();
20722
20906
  const chunk_end_ts = new Date(end_ts_ms).toISOString();
20723
20907
  if (this.mode === "manual") {
20724
20908
  try {
20725
20909
  void this.onEvents({
20726
- session_uuid: this.sessionUuid,
20910
+ session_uuid: this.sessionManager.sessionUuid,
20911
+ window_id: this.sessionManager.windowId,
20727
20912
  chunk_seq,
20728
20913
  chunk_start_ts,
20729
20914
  chunk_end_ts,
20730
- initial_url,
20731
20915
  events,
20732
20916
  keepalive: true,
20733
20917
  });
@@ -20739,11 +20923,11 @@
20739
20923
  else {
20740
20924
  uploadSessionEvents({
20741
20925
  user_id: this.userId,
20742
- session_uuid: this.sessionUuid,
20926
+ session_uuid: this.sessionManager.sessionUuid,
20927
+ window_id: this.sessionManager.windowId,
20743
20928
  chunk_seq,
20744
20929
  chunk_start_ts,
20745
20930
  chunk_end_ts,
20746
- initial_url,
20747
20931
  events,
20748
20932
  keepalive: true,
20749
20933
  }).catch(() => { });
@@ -20755,6 +20939,19 @@
20755
20939
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "Page hide handling failed", err);
20756
20940
  }
20757
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
+ });
20758
20955
  try {
20759
20956
  if (typeof window === "undefined")
20760
20957
  return;
@@ -20799,6 +20996,7 @@
20799
20996
  this.maskingOptions = maskingOptions;
20800
20997
  this.recordCrossOriginIframes = recordCrossOriginIframes;
20801
20998
  this.sessionEvents = [];
20999
+ this.sessionManager = new SessionManager(this.TIMEOUT);
20802
21000
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_initRecorder).call(this);
20803
21001
  }
20804
21002
  catch (err) {
@@ -20807,7 +21005,8 @@
20807
21005
  }
20808
21006
  }
20809
21007
  getSessionId() {
20810
- return this.sessionUuid;
21008
+ var _a;
21009
+ return (_a = this.sessionManager) === null || _a === void 0 ? void 0 : _a.sessionUuid;
20811
21010
  }
20812
21011
  stop() {
20813
21012
  try {
@@ -20817,15 +21016,16 @@
20817
21016
  this.rrwebStop();
20818
21017
  this.rrwebStop = null;
20819
21018
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_clearEvents).call(this);
20820
- clearSessionState();
21019
+ this.sessionManager.dispose();
20821
21020
  window.removeEventListener("pagehide", __classPrivateFieldGet(this, _SessionRecorder_handlePageHide, "f"));
21021
+ document.removeEventListener("visibilitychange", __classPrivateFieldGet(this, _SessionRecorder_handleVisibilityChange, "f"));
20822
21022
  }
20823
21023
  catch (err) {
20824
21024
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "Stop failed", err);
20825
21025
  }
20826
21026
  }
20827
21027
  }
20828
- _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) {
20829
21029
  if (!this.debug)
20830
21030
  return;
20831
21031
  if (error) {
@@ -20837,7 +21037,7 @@
20837
21037
  }, _SessionRecorder_initRecorder = function _SessionRecorder_initRecorder() {
20838
21038
  if (this.rrwebStop)
20839
21039
  return;
20840
- __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_createSession).call(this);
21040
+ this.sessionManager.initialize();
20841
21041
  this.rrwebStop = record({
20842
21042
  emit: (event, isCheckout) => {
20843
21043
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_handleEvent).call(this, event, isCheckout);
@@ -20861,16 +21061,16 @@
20861
21061
  }, _SessionRecorder_handleEvent = function _SessionRecorder_handleEvent(event, _isCheckout) {
20862
21062
  try {
20863
21063
  const now = Date.now();
20864
- const state = readSessionState();
20865
- if (state && now - state.last_active > this.TIMEOUT) {
20866
- __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") {
20867
21070
  takeFullSnapshot(true);
20868
21071
  }
20869
21072
  if (__classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_isUserInteraction).call(this, event)) {
20870
- const latest = readSessionState();
20871
- if (latest) {
20872
- writeSessionState({ ...latest, last_active: now });
20873
- }
21073
+ this.sessionManager.bumpActivity(now);
20874
21074
  }
20875
21075
  this.sessionEvents.push(event);
20876
21076
  this.bufferBytes += estimateEventSize(event);
@@ -20896,30 +21096,13 @@
20896
21096
  __classPrivateFieldSet(this, _SessionRecorder_bufferTimer, null, "f");
20897
21097
  }
20898
21098
  void __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_trackEvents).call(this);
20899
- }, _SessionRecorder_resetSession = function _SessionRecorder_resetSession() {
20900
- clearSessionState();
20901
- __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_clearEvents).call(this);
20902
- __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_createSession).call(this);
20903
- }, _SessionRecorder_createSession = function _SessionRecorder_createSession() {
20904
- const now = Date.now();
20905
- const state = readSessionState();
20906
- const isExpired = !state || now - state.last_active > this.TIMEOUT;
20907
- if (!state || isExpired) {
20908
- this.sessionUuid = generateUuid();
20909
- const initial_url = typeof window !== "undefined" ? window.location.href : "";
20910
- writeSessionState({
20911
- session_uuid: this.sessionUuid,
20912
- last_active: now,
20913
- chunk_seq: 0,
20914
- initial_url,
20915
- });
20916
- }
20917
- else {
20918
- this.sessionUuid = state.session_uuid;
20919
- writeSessionState({ ...state, last_active: now });
20920
- }
20921
21099
  }, _SessionRecorder_initListeners = function _SessionRecorder_initListeners() {
20922
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);
20923
21106
  }, _SessionRecorder_trackEvents = async function _SessionRecorder_trackEvents() {
20924
21107
  if (__classPrivateFieldGet(this, _SessionRecorder_uploading, "f"))
20925
21108
  return;
@@ -20932,45 +21115,38 @@
20932
21115
  __classPrivateFieldSet(this, _SessionRecorder_uploadingMaxTs, events[events.length - 1].timestamp, "f");
20933
21116
  const start_ts_ms = events[0].timestamp;
20934
21117
  const end_ts_ms = events[events.length - 1].timestamp;
20935
- const state = readSessionState();
20936
- if (!state) {
20937
- __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");
20938
21121
  return;
20939
21122
  }
20940
- const chunk_seq = state.chunk_seq;
20941
- const initial_url = chunk_seq === 0 ? state.initial_url : undefined;
20942
21123
  const chunk_start_ts = new Date(start_ts_ms).toISOString();
20943
21124
  const chunk_end_ts = new Date(end_ts_ms).toISOString();
20944
21125
  if (this.mode === "manual") {
20945
21126
  await this.onEvents({
20946
- session_uuid: this.sessionUuid,
21127
+ session_uuid: this.sessionManager.sessionUuid,
21128
+ window_id: this.sessionManager.windowId,
20947
21129
  chunk_seq,
20948
21130
  chunk_start_ts,
20949
21131
  chunk_end_ts,
20950
- initial_url,
20951
21132
  events,
20952
21133
  });
20953
21134
  }
20954
21135
  else {
20955
21136
  await uploadSessionEvents({
20956
21137
  user_id: this.userId,
20957
- session_uuid: this.sessionUuid,
21138
+ session_uuid: this.sessionManager.sessionUuid,
21139
+ window_id: this.sessionManager.windowId,
20958
21140
  chunk_seq,
20959
21141
  chunk_start_ts,
20960
21142
  chunk_end_ts,
20961
- initial_url,
20962
21143
  events,
20963
21144
  });
20964
21145
  }
20965
21146
  const removedBytes = events.reduce((sum, e) => sum + estimateEventSize(e), 0);
20966
21147
  this.sessionEvents = this.sessionEvents.slice(snapshot_count);
20967
21148
  this.bufferBytes = Math.max(0, this.bufferBytes - removedBytes);
20968
- const after = readSessionState();
20969
- if (after &&
20970
- after.session_uuid === state.session_uuid &&
20971
- after.chunk_seq === chunk_seq) {
20972
- writeSessionState({ ...after, chunk_seq: chunk_seq + 1 });
20973
- }
21149
+ this.sessionManager.commitChunkSeq(chunk_seq);
20974
21150
  }
20975
21151
  catch (err) {
20976
21152
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "Event tracking failed", err);