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.
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_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,61 @@ 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_handlePageHide.set(this, () => {
20659
20683
  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);
20667
- }
20668
- takeFullSnapshot(true);
20669
- }
20684
+ if (this.sessionEvents.length === 0)
20685
+ return;
20686
+ const events = [...this.sessionEvents];
20687
+ const state = readSessionState();
20688
+ if (!state)
20689
+ return;
20690
+ const chunk_seq = __classPrivateFieldGet(this, _SessionRecorder_uploading, "f") ? state.chunk_seq + 1 : state.chunk_seq;
20691
+ writeSessionState({ ...state, chunk_seq: chunk_seq + 1 });
20692
+ const start_ts_ms = events[0].timestamp;
20693
+ const end_ts_ms = events[events.length - 1].timestamp;
20694
+ uploadSessionEvents({
20695
+ user_id: this.userId,
20696
+ session_uuid: this.sessionUuid,
20697
+ chunk_seq,
20698
+ chunk_start_ts: new Date(start_ts_ms).toISOString(),
20699
+ chunk_end_ts: new Date(end_ts_ms).toISOString(),
20700
+ initial_url: chunk_seq === 0 ? state.initial_url : undefined,
20701
+ events,
20702
+ keepalive: true,
20703
+ }).catch(() => { });
20704
+ this.sessionEvents = [];
20670
20705
  }
20671
20706
  catch (err) {
20672
- __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "Visibility change handling failed", err);
20707
+ __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "Page hide handling failed", err);
20673
20708
  }
20674
20709
  });
20675
20710
  try {
20676
- // Check for browser environment
20677
20711
  if (typeof window === "undefined")
20678
20712
  return;
20679
20713
  if (typeof document === "undefined")
20680
20714
  return;
20681
20715
  if (typeof localStorage === "undefined")
20682
20716
  return;
20683
- // Check for required APIs
20684
- if (typeof CompressionStream === "undefined")
20717
+ if (typeof sessionStorage === "undefined")
20685
20718
  return;
20686
20719
  if (typeof MutationObserver === "undefined")
20687
20720
  return;
20688
- if (typeof TextEncoder === "undefined")
20689
- return;
20690
20721
  if (typeof fetch === "undefined")
20691
20722
  return;
20692
- if (typeof Blob === "undefined")
20693
- return;
20694
20723
  if (typeof crypto === "undefined" || !crypto.getRandomValues)
20695
20724
  return;
20696
- // Check localStorage actually works (can be blocked even if defined)
20697
20725
  const testKey = "__userlens_test__";
20698
20726
  localStorage.setItem(testKey, "1");
20699
20727
  localStorage.removeItem(testKey);
20700
- // Set debug mode early so it's available for error logging
20728
+ sessionStorage.setItem(testKey, "1");
20729
+ sessionStorage.removeItem(testKey);
20701
20730
  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;
20731
+ if (!((_b = config.WRITE_CODE) === null || _b === void 0 ? void 0 : _b.trim()) || !((_c = config.userId) === null || _c === void 0 ? void 0 : _c.trim())) {
20732
+ return;
20716
20733
  }
20734
+ saveWriteCode(config.WRITE_CODE);
20735
+ this.userId = config.userId;
20717
20736
  const { recordingOptions = {} } = config;
20718
20737
  const { TIMEOUT = 30 * 60 * 1000, BUFFER_SIZE = 10, maskingOptions = ["passwords"], recordCrossOriginIframes = false, } = recordingOptions;
20719
20738
  this.TIMEOUT = TIMEOUT;
@@ -20742,15 +20761,15 @@ class SessionRecorder {
20742
20761
  this.rrwebStop();
20743
20762
  this.rrwebStop = null;
20744
20763
  __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"));
20764
+ clearSessionState();
20765
+ window.removeEventListener("pagehide", __classPrivateFieldGet(this, _SessionRecorder_handlePageHide, "f"));
20747
20766
  }
20748
20767
  catch (err) {
20749
20768
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "Stop failed", err);
20750
20769
  }
20751
20770
  }
20752
20771
  }
20753
- _SessionRecorder_trackEventsThrottled = new WeakMap(), _SessionRecorder_handleVisibilityChange = new WeakMap(), _SessionRecorder_instances = new WeakSet(), _SessionRecorder_log = function _SessionRecorder_log(message, error) {
20772
+ _SessionRecorder_trackEventsThrottled = new WeakMap(), _SessionRecorder_uploading = new WeakMap(), _SessionRecorder_handlePageHide = new WeakMap(), _SessionRecorder_instances = new WeakSet(), _SessionRecorder_log = function _SessionRecorder_log(message, error) {
20754
20773
  if (!this.debug)
20755
20774
  return;
20756
20775
  if (error) {
@@ -20775,7 +20794,7 @@ _SessionRecorder_trackEventsThrottled = new WeakMap(), _SessionRecorder_handleVi
20775
20794
  plugins: [getRecordConsolePlugin()],
20776
20795
  checkoutEveryNth: 100,
20777
20796
  });
20778
- __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_initFocusListener).call(this);
20797
+ __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_initListeners).call(this);
20779
20798
  }, _SessionRecorder_isUserInteraction = function _SessionRecorder_isUserInteraction(event) {
20780
20799
  var _a;
20781
20800
  if (event.type === 3) {
@@ -20787,15 +20806,16 @@ _SessionRecorder_trackEventsThrottled = new WeakMap(), _SessionRecorder_handleVi
20787
20806
  var _a;
20788
20807
  try {
20789
20808
  const now = Date.now();
20790
- const lastActive = Number(localStorage.getItem("userlensSessionLastActive"));
20791
- // check inactivity timeout
20792
- if (lastActive && now - lastActive > this.TIMEOUT) {
20809
+ const state = readSessionState();
20810
+ if (state && now - state.last_active > this.TIMEOUT) {
20793
20811
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_resetSession).call(this);
20794
20812
  takeFullSnapshot(true);
20795
20813
  }
20796
- // only update lastActive on actual user interactions, not DOM mutations
20797
20814
  if (__classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_isUserInteraction).call(this, event)) {
20798
- localStorage.setItem("userlensSessionLastActive", now.toString());
20815
+ const latest = readSessionState();
20816
+ if (latest) {
20817
+ writeSessionState({ ...latest, last_active: now });
20818
+ }
20799
20819
  }
20800
20820
  this.sessionEvents.push(event);
20801
20821
  if (this.sessionEvents.length >= this.BUFFER_SIZE) {
@@ -20806,24 +20826,29 @@ _SessionRecorder_trackEventsThrottled = new WeakMap(), _SessionRecorder_handleVi
20806
20826
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "Event handling failed", err);
20807
20827
  }
20808
20828
  }, _SessionRecorder_resetSession = function _SessionRecorder_resetSession() {
20809
- __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_removeLocalSessionData).call(this);
20829
+ clearSessionState();
20810
20830
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_clearEvents).call(this);
20811
20831
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_createSession).call(this);
20812
20832
  }, _SessionRecorder_createSession = function _SessionRecorder_createSession() {
20813
20833
  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) {
20834
+ const state = readSessionState();
20835
+ const isExpired = !state || now - state.last_active > this.TIMEOUT;
20836
+ if (!state || isExpired) {
20818
20837
  this.sessionUuid = generateUuid();
20819
- localStorage.setItem("userlensSessionUuid", this.sessionUuid);
20838
+ const initial_url = typeof window !== "undefined" ? window.location.href : "";
20839
+ writeSessionState({
20840
+ session_uuid: this.sessionUuid,
20841
+ last_active: now,
20842
+ chunk_seq: 0,
20843
+ initial_url,
20844
+ });
20820
20845
  }
20821
20846
  else {
20822
- this.sessionUuid = storedUuid;
20847
+ this.sessionUuid = state.session_uuid;
20848
+ writeSessionState({ ...state, last_active: now });
20823
20849
  }
20824
- localStorage.setItem("userlensSessionLastActive", now.toString());
20825
- }, _SessionRecorder_initFocusListener = function _SessionRecorder_initFocusListener() {
20826
- window.addEventListener("visibilitychange", __classPrivateFieldGet(this, _SessionRecorder_handleVisibilityChange, "f"));
20850
+ }, _SessionRecorder_initListeners = function _SessionRecorder_initListeners() {
20851
+ window.addEventListener("pagehide", __classPrivateFieldGet(this, _SessionRecorder_handlePageHide, "f"));
20827
20852
  }, _SessionRecorder_throttle = function _SessionRecorder_throttle(func, delay) {
20828
20853
  let lastCall = 0;
20829
20854
  return (...args) => {
@@ -20834,34 +20859,45 @@ _SessionRecorder_trackEventsThrottled = new WeakMap(), _SessionRecorder_handleVi
20834
20859
  }
20835
20860
  };
20836
20861
  }, _SessionRecorder_trackEvents = async function _SessionRecorder_trackEvents() {
20862
+ if (__classPrivateFieldGet(this, _SessionRecorder_uploading, "f"))
20863
+ return;
20864
+ if (this.sessionEvents.length === 0)
20865
+ return;
20866
+ __classPrivateFieldSet(this, _SessionRecorder_uploading, true, "f");
20837
20867
  try {
20838
- if (this.sessionEvents.length === 0) {
20839
- return;
20840
- }
20841
- const chunkTimestamp = this.sessionEvents[this.sessionEvents.length - 1].timestamp;
20842
20868
  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
- }
20869
+ const snapshot_count = events.length;
20870
+ const start_ts_ms = events[0].timestamp;
20871
+ const end_ts_ms = events[events.length - 1].timestamp;
20872
+ const state = readSessionState();
20873
+ if (!state) {
20874
+ __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "No session state during upload — skipping chunk");
20875
+ return;
20852
20876
  }
20853
- else {
20854
- await uploadSessionEvents(this.userId, this.sessionUuid, events, chunkTimestamp);
20877
+ const chunk_seq = state.chunk_seq;
20878
+ await uploadSessionEvents({
20879
+ user_id: this.userId,
20880
+ session_uuid: this.sessionUuid,
20881
+ chunk_seq,
20882
+ chunk_start_ts: new Date(start_ts_ms).toISOString(),
20883
+ chunk_end_ts: new Date(end_ts_ms).toISOString(),
20884
+ initial_url: chunk_seq === 0 ? state.initial_url : undefined,
20885
+ events,
20886
+ });
20887
+ this.sessionEvents = this.sessionEvents.slice(snapshot_count);
20888
+ const after = readSessionState();
20889
+ if (after && after.session_uuid === state.session_uuid) {
20890
+ writeSessionState({ ...after, chunk_seq: after.chunk_seq + 1 });
20855
20891
  }
20856
20892
  }
20857
20893
  catch (err) {
20858
20894
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "Event tracking failed", err);
20859
20895
  }
20896
+ finally {
20897
+ __classPrivateFieldSet(this, _SessionRecorder_uploading, false, "f");
20898
+ }
20860
20899
  }, _SessionRecorder_clearEvents = function _SessionRecorder_clearEvents() {
20861
20900
  this.sessionEvents = [];
20862
- }, _SessionRecorder_removeLocalSessionData = function _SessionRecorder_removeLocalSessionData() {
20863
- localStorage.removeItem("userlensSessionUuid");
20864
- localStorage.removeItem("userlensSessionLastActive");
20865
20901
  };
20866
20902
 
20867
20903
  export { SessionRecorder as default };