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