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