userlens-session-recorder 1.2.2 → 2.0.1

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