userlens-session-recorder 1.2.2 → 2.0.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.
@@ -1,39 +1,20 @@
1
- import { eventWithTime } from 'rrweb';
2
-
3
1
  type MaskingOption = "passwords" | "all";
4
- type RecorderMode = "auto" | "manual";
5
2
  interface SessionRecordingOptions {
6
3
  TIMEOUT?: number;
7
4
  BUFFER_SIZE?: number;
8
5
  maskingOptions?: MaskingOption[];
9
6
  recordCrossOriginIframes?: boolean;
10
7
  }
11
- interface EventBatch {
12
- sessionId: string;
13
- events: eventWithTime[];
14
- chunkTimestamp: number;
15
- }
16
- type OnEventsCallback = (batch: EventBatch) => void;
17
- interface AutoModeConfig {
18
- mode?: "auto";
8
+ interface SessionRecorderConfig {
19
9
  WRITE_CODE: string;
20
10
  userId: string;
21
11
  recordingOptions?: SessionRecordingOptions;
22
12
  debug?: boolean;
23
13
  }
24
- interface ManualModeConfig {
25
- mode: "manual";
26
- onEvents: OnEventsCallback;
27
- recordingOptions?: SessionRecordingOptions;
28
- debug?: boolean;
29
- }
30
- type SessionRecorderConfig = AutoModeConfig | ManualModeConfig;
31
14
 
32
15
  declare class SessionRecorder {
33
16
  #private;
34
- private mode;
35
17
  private userId?;
36
- private onEvents?;
37
18
  private TIMEOUT;
38
19
  private BUFFER_SIZE;
39
20
  private maskingOptions;
@@ -48,4 +29,4 @@ declare class SessionRecorder {
48
29
  }
49
30
 
50
31
  export { SessionRecorder as default };
51
- export type { AutoModeConfig, EventBatch, ManualModeConfig, MaskingOption, OnEventsCallback, RecorderMode, SessionRecorderConfig };
32
+ export type { MaskingOption, SessionRecorderConfig };
@@ -20611,48 +20611,71 @@
20611
20611
  return null;
20612
20612
  }
20613
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
+ }
20614
20645
 
20615
- const SESSIONS_BASE_URL = "https://sessions.userlens.io";
20616
- // Gzip + base64 encode payload
20617
- async function compressAndEncode(data) {
20618
- const jsonStr = JSON.stringify(data);
20619
- const stream = new Blob([new TextEncoder().encode(jsonStr)])
20620
- .stream()
20621
- .pipeThrough(new CompressionStream("gzip"));
20622
- const buffer = await new Response(stream).arrayBuffer();
20623
- const bytes = new Uint8Array(buffer);
20624
- let binary = "";
20625
- for (let i = 0; i < bytes.length; i++) {
20626
- binary += String.fromCharCode(bytes[i]);
20627
- }
20628
- return btoa(binary);
20629
- }
20630
- const uploadSessionEvents = async (userId, sessionUuid, events, chunkTimestamp) => {
20646
+ const INGEST_BASE_URL = "https://ul-ingest.userlens.io";
20647
+ const uploadSessionEvents = async (args) => {
20648
+ var _a;
20631
20649
  const writeCode = getWriteCode();
20632
20650
  if (!writeCode) {
20633
20651
  return;
20634
20652
  }
20635
- // Encode the entire payload - no readable data in request
20636
- const data = {
20637
- userId: userId,
20638
- chunk_timestamp: chunkTimestamp,
20639
- events: events,
20653
+ const body = {
20654
+ session_uuid: args.session_uuid,
20655
+ chunk_seq: args.chunk_seq,
20656
+ chunk_start_ts: args.chunk_start_ts,
20657
+ chunk_end_ts: args.chunk_end_ts,
20658
+ user_id: args.user_id,
20659
+ events: args.events,
20640
20660
  };
20641
- const encodedPayload = await compressAndEncode(data);
20642
- const res = await fetch(`${SESSIONS_BASE_URL}/session/${sessionUuid}`, {
20661
+ if (args.initial_url !== undefined) {
20662
+ body.initial_url = args.initial_url;
20663
+ }
20664
+ const res = await fetch(`${INGEST_BASE_URL}/session-recording`, {
20643
20665
  method: "POST",
20644
20666
  headers: {
20645
- "Content-Type": "text/plain",
20667
+ "Content-Type": "application/json",
20646
20668
  Authorization: `Basic ${writeCode}`,
20647
20669
  },
20648
- body: encodedPayload,
20670
+ body: JSON.stringify(body),
20671
+ keepalive: (_a = args.keepalive) !== null && _a !== void 0 ? _a : false,
20649
20672
  });
20650
20673
  if (!res.ok)
20651
20674
  throw new Error("Userlens HTTP error: failed to track");
20652
20675
  return "ok";
20653
20676
  };
20654
20677
 
20655
- var _SessionRecorder_instances, _SessionRecorder_trackEventsThrottled, _SessionRecorder_log, _SessionRecorder_initRecorder, _SessionRecorder_isUserInteraction, _SessionRecorder_handleEvent, _SessionRecorder_resetSession, _SessionRecorder_createSession, _SessionRecorder_handleVisibilityChange, _SessionRecorder_initFocusListener, _SessionRecorder_throttle, _SessionRecorder_trackEvents, _SessionRecorder_clearEvents, _SessionRecorder_removeLocalSessionData;
20678
+ var _SessionRecorder_instances, _SessionRecorder_trackEventsThrottled, _SessionRecorder_uploading, _SessionRecorder_log, _SessionRecorder_initRecorder, _SessionRecorder_isUserInteraction, _SessionRecorder_handleEvent, _SessionRecorder_resetSession, _SessionRecorder_createSession, _SessionRecorder_handlePageHide, _SessionRecorder_initListeners, _SessionRecorder_throttle, _SessionRecorder_trackEvents, _SessionRecorder_clearEvents;
20656
20679
  class SessionRecorder {
20657
20680
  constructor(config) {
20658
20681
  var _a, _b, _c;
@@ -20661,65 +20684,61 @@
20661
20684
  this.rrwebStop = null;
20662
20685
  this.debug = false;
20663
20686
  _SessionRecorder_trackEventsThrottled.set(this, void 0);
20664
- _SessionRecorder_handleVisibilityChange.set(this, () => {
20687
+ _SessionRecorder_uploading.set(this, false);
20688
+ _SessionRecorder_handlePageHide.set(this, () => {
20665
20689
  try {
20666
- if (document.visibilityState === "visible") {
20667
- if (!this.rrwebStop)
20668
- return;
20669
- const now = Date.now();
20670
- const lastActive = Number(localStorage.getItem("userlensSessionLastActive"));
20671
- if (lastActive && now - lastActive > this.TIMEOUT) {
20672
- __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_resetSession).call(this);
20673
- }
20674
- takeFullSnapshot(true);
20675
- }
20690
+ if (this.sessionEvents.length === 0)
20691
+ return;
20692
+ const events = [...this.sessionEvents];
20693
+ const state = readSessionState();
20694
+ if (!state)
20695
+ return;
20696
+ const chunk_seq = __classPrivateFieldGet(this, _SessionRecorder_uploading, "f") ? state.chunk_seq + 1 : state.chunk_seq;
20697
+ writeSessionState({ ...state, chunk_seq: chunk_seq + 1 });
20698
+ const start_ts_ms = events[0].timestamp;
20699
+ const end_ts_ms = events[events.length - 1].timestamp;
20700
+ uploadSessionEvents({
20701
+ user_id: this.userId,
20702
+ session_uuid: this.sessionUuid,
20703
+ chunk_seq,
20704
+ chunk_start_ts: new Date(start_ts_ms).toISOString(),
20705
+ chunk_end_ts: new Date(end_ts_ms).toISOString(),
20706
+ initial_url: chunk_seq === 0 ? state.initial_url : undefined,
20707
+ events,
20708
+ keepalive: true,
20709
+ }).catch(() => { });
20710
+ this.sessionEvents = [];
20676
20711
  }
20677
20712
  catch (err) {
20678
- __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "Visibility change handling failed", err);
20713
+ __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "Page hide handling failed", err);
20679
20714
  }
20680
20715
  });
20681
20716
  try {
20682
- // Check for browser environment
20683
20717
  if (typeof window === "undefined")
20684
20718
  return;
20685
20719
  if (typeof document === "undefined")
20686
20720
  return;
20687
20721
  if (typeof localStorage === "undefined")
20688
20722
  return;
20689
- // Check for required APIs
20690
- if (typeof CompressionStream === "undefined")
20723
+ if (typeof sessionStorage === "undefined")
20691
20724
  return;
20692
20725
  if (typeof MutationObserver === "undefined")
20693
20726
  return;
20694
- if (typeof TextEncoder === "undefined")
20695
- return;
20696
20727
  if (typeof fetch === "undefined")
20697
20728
  return;
20698
- if (typeof Blob === "undefined")
20699
- return;
20700
20729
  if (typeof crypto === "undefined" || !crypto.getRandomValues)
20701
20730
  return;
20702
- // Check localStorage actually works (can be blocked even if defined)
20703
20731
  const testKey = "__userlens_test__";
20704
20732
  localStorage.setItem(testKey, "1");
20705
20733
  localStorage.removeItem(testKey);
20706
- // Set debug mode early so it's available for error logging
20734
+ sessionStorage.setItem(testKey, "1");
20735
+ sessionStorage.removeItem(testKey);
20707
20736
  this.debug = (_a = config.debug) !== null && _a !== void 0 ? _a : false;
20708
- if (config.mode === "manual") {
20709
- this.mode = "manual";
20710
- if (!config.onEvents || typeof config.onEvents !== "function") {
20711
- return;
20712
- }
20713
- this.onEvents = config.onEvents;
20714
- }
20715
- else {
20716
- this.mode = "auto";
20717
- if (!((_b = config.WRITE_CODE) === null || _b === void 0 ? void 0 : _b.trim()) || !((_c = config.userId) === null || _c === void 0 ? void 0 : _c.trim())) {
20718
- return;
20719
- }
20720
- saveWriteCode(config.WRITE_CODE);
20721
- this.userId = config.userId;
20737
+ if (!((_b = config.WRITE_CODE) === null || _b === void 0 ? void 0 : _b.trim()) || !((_c = config.userId) === null || _c === void 0 ? void 0 : _c.trim())) {
20738
+ return;
20722
20739
  }
20740
+ saveWriteCode(config.WRITE_CODE);
20741
+ this.userId = config.userId;
20723
20742
  const { recordingOptions = {} } = config;
20724
20743
  const { TIMEOUT = 30 * 60 * 1000, BUFFER_SIZE = 10, maskingOptions = ["passwords"], recordCrossOriginIframes = false, } = recordingOptions;
20725
20744
  this.TIMEOUT = TIMEOUT;
@@ -20748,15 +20767,15 @@
20748
20767
  this.rrwebStop();
20749
20768
  this.rrwebStop = null;
20750
20769
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_clearEvents).call(this);
20751
- __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_removeLocalSessionData).call(this);
20752
- window.removeEventListener("visibilitychange", __classPrivateFieldGet(this, _SessionRecorder_handleVisibilityChange, "f"));
20770
+ clearSessionState();
20771
+ window.removeEventListener("pagehide", __classPrivateFieldGet(this, _SessionRecorder_handlePageHide, "f"));
20753
20772
  }
20754
20773
  catch (err) {
20755
20774
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "Stop failed", err);
20756
20775
  }
20757
20776
  }
20758
20777
  }
20759
- _SessionRecorder_trackEventsThrottled = new WeakMap(), _SessionRecorder_handleVisibilityChange = new WeakMap(), _SessionRecorder_instances = new WeakSet(), _SessionRecorder_log = function _SessionRecorder_log(message, error) {
20778
+ _SessionRecorder_trackEventsThrottled = new WeakMap(), _SessionRecorder_uploading = new WeakMap(), _SessionRecorder_handlePageHide = new WeakMap(), _SessionRecorder_instances = new WeakSet(), _SessionRecorder_log = function _SessionRecorder_log(message, error) {
20760
20779
  if (!this.debug)
20761
20780
  return;
20762
20781
  if (error) {
@@ -20781,7 +20800,7 @@
20781
20800
  plugins: [getRecordConsolePlugin()],
20782
20801
  checkoutEveryNth: 100,
20783
20802
  });
20784
- __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_initFocusListener).call(this);
20803
+ __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_initListeners).call(this);
20785
20804
  }, _SessionRecorder_isUserInteraction = function _SessionRecorder_isUserInteraction(event) {
20786
20805
  var _a;
20787
20806
  if (event.type === 3) {
@@ -20793,15 +20812,16 @@
20793
20812
  var _a;
20794
20813
  try {
20795
20814
  const now = Date.now();
20796
- const lastActive = Number(localStorage.getItem("userlensSessionLastActive"));
20797
- // check inactivity timeout
20798
- if (lastActive && now - lastActive > this.TIMEOUT) {
20815
+ const state = readSessionState();
20816
+ if (state && now - state.last_active > this.TIMEOUT) {
20799
20817
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_resetSession).call(this);
20800
20818
  takeFullSnapshot(true);
20801
20819
  }
20802
- // only update lastActive on actual user interactions, not DOM mutations
20803
20820
  if (__classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_isUserInteraction).call(this, event)) {
20804
- localStorage.setItem("userlensSessionLastActive", now.toString());
20821
+ const latest = readSessionState();
20822
+ if (latest) {
20823
+ writeSessionState({ ...latest, last_active: now });
20824
+ }
20805
20825
  }
20806
20826
  this.sessionEvents.push(event);
20807
20827
  if (this.sessionEvents.length >= this.BUFFER_SIZE) {
@@ -20812,24 +20832,29 @@
20812
20832
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "Event handling failed", err);
20813
20833
  }
20814
20834
  }, _SessionRecorder_resetSession = function _SessionRecorder_resetSession() {
20815
- __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_removeLocalSessionData).call(this);
20835
+ clearSessionState();
20816
20836
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_clearEvents).call(this);
20817
20837
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_createSession).call(this);
20818
20838
  }, _SessionRecorder_createSession = function _SessionRecorder_createSession() {
20819
20839
  const now = Date.now();
20820
- const lastActive = Number(localStorage.getItem("userlensSessionLastActive"));
20821
- const storedUuid = localStorage.getItem("userlensSessionUuid");
20822
- const isExpired = !lastActive || now - lastActive > this.TIMEOUT;
20823
- if (!storedUuid || isExpired) {
20840
+ const state = readSessionState();
20841
+ const isExpired = !state || now - state.last_active > this.TIMEOUT;
20842
+ if (!state || isExpired) {
20824
20843
  this.sessionUuid = generateUuid();
20825
- localStorage.setItem("userlensSessionUuid", this.sessionUuid);
20844
+ const initial_url = typeof window !== "undefined" ? window.location.href : "";
20845
+ writeSessionState({
20846
+ session_uuid: this.sessionUuid,
20847
+ last_active: now,
20848
+ chunk_seq: 0,
20849
+ initial_url,
20850
+ });
20826
20851
  }
20827
20852
  else {
20828
- this.sessionUuid = storedUuid;
20853
+ this.sessionUuid = state.session_uuid;
20854
+ writeSessionState({ ...state, last_active: now });
20829
20855
  }
20830
- localStorage.setItem("userlensSessionLastActive", now.toString());
20831
- }, _SessionRecorder_initFocusListener = function _SessionRecorder_initFocusListener() {
20832
- window.addEventListener("visibilitychange", __classPrivateFieldGet(this, _SessionRecorder_handleVisibilityChange, "f"));
20856
+ }, _SessionRecorder_initListeners = function _SessionRecorder_initListeners() {
20857
+ window.addEventListener("pagehide", __classPrivateFieldGet(this, _SessionRecorder_handlePageHide, "f"));
20833
20858
  }, _SessionRecorder_throttle = function _SessionRecorder_throttle(func, delay) {
20834
20859
  let lastCall = 0;
20835
20860
  return (...args) => {
@@ -20840,34 +20865,45 @@
20840
20865
  }
20841
20866
  };
20842
20867
  }, _SessionRecorder_trackEvents = async function _SessionRecorder_trackEvents() {
20868
+ if (__classPrivateFieldGet(this, _SessionRecorder_uploading, "f"))
20869
+ return;
20870
+ if (this.sessionEvents.length === 0)
20871
+ return;
20872
+ __classPrivateFieldSet(this, _SessionRecorder_uploading, true, "f");
20843
20873
  try {
20844
- if (this.sessionEvents.length === 0) {
20845
- return;
20846
- }
20847
- const chunkTimestamp = this.sessionEvents[this.sessionEvents.length - 1].timestamp;
20848
20874
  const events = [...this.sessionEvents];
20849
- __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_clearEvents).call(this);
20850
- if (this.mode === "manual") {
20851
- if (this.onEvents) {
20852
- this.onEvents({
20853
- sessionId: this.sessionUuid,
20854
- events,
20855
- chunkTimestamp,
20856
- });
20857
- }
20875
+ const snapshot_count = events.length;
20876
+ const start_ts_ms = events[0].timestamp;
20877
+ const end_ts_ms = events[events.length - 1].timestamp;
20878
+ const state = readSessionState();
20879
+ if (!state) {
20880
+ __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "No session state during upload — skipping chunk");
20881
+ return;
20858
20882
  }
20859
- else {
20860
- await uploadSessionEvents(this.userId, this.sessionUuid, events, chunkTimestamp);
20883
+ const chunk_seq = state.chunk_seq;
20884
+ await uploadSessionEvents({
20885
+ user_id: this.userId,
20886
+ session_uuid: this.sessionUuid,
20887
+ chunk_seq,
20888
+ chunk_start_ts: new Date(start_ts_ms).toISOString(),
20889
+ chunk_end_ts: new Date(end_ts_ms).toISOString(),
20890
+ initial_url: chunk_seq === 0 ? state.initial_url : undefined,
20891
+ events,
20892
+ });
20893
+ this.sessionEvents = this.sessionEvents.slice(snapshot_count);
20894
+ const after = readSessionState();
20895
+ if (after && after.session_uuid === state.session_uuid) {
20896
+ writeSessionState({ ...after, chunk_seq: after.chunk_seq + 1 });
20861
20897
  }
20862
20898
  }
20863
20899
  catch (err) {
20864
20900
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "Event tracking failed", err);
20865
20901
  }
20902
+ finally {
20903
+ __classPrivateFieldSet(this, _SessionRecorder_uploading, false, "f");
20904
+ }
20866
20905
  }, _SessionRecorder_clearEvents = function _SessionRecorder_clearEvents() {
20867
20906
  this.sessionEvents = [];
20868
- }, _SessionRecorder_removeLocalSessionData = function _SessionRecorder_removeLocalSessionData() {
20869
- localStorage.removeItem("userlensSessionUuid");
20870
- localStorage.removeItem("userlensSessionLastActive");
20871
20907
  };
20872
20908
 
20873
20909
  exports.default = SessionRecorder;