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.
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;
@@ -12,6 +10,7 @@ export default class SessionRecorder {
12
10
  private sessionEvents;
13
11
  private rrwebStop;
14
12
  private debug;
13
+ private recordCrossOriginIframes;
15
14
  constructor(config: SessionRecorderConfig);
16
15
  getSessionId(): string | undefined;
17
16
  stop(): void;
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,70 +20678,67 @@ 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
- const { TIMEOUT = 30 * 60 * 1000, BUFFER_SIZE = 10, maskingOptions = ["passwords"], } = recordingOptions;
20737
+ const { TIMEOUT = 30 * 60 * 1000, BUFFER_SIZE = 10, maskingOptions = ["passwords"], recordCrossOriginIframes = false, } = recordingOptions;
20719
20738
  this.TIMEOUT = TIMEOUT;
20720
20739
  this.BUFFER_SIZE = BUFFER_SIZE;
20721
20740
  this.maskingOptions = maskingOptions;
20741
+ this.recordCrossOriginIframes = recordCrossOriginIframes;
20722
20742
  this.sessionEvents = [];
20723
20743
  __classPrivateFieldSet(this, _SessionRecorder_trackEventsThrottled, __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_throttle).call(this, () => {
20724
20744
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_trackEvents).call(this);
@@ -20741,15 +20761,15 @@ class SessionRecorder {
20741
20761
  this.rrwebStop();
20742
20762
  this.rrwebStop = null;
20743
20763
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_clearEvents).call(this);
20744
- __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_removeLocalSessionData).call(this);
20745
- window.removeEventListener("visibilitychange", __classPrivateFieldGet(this, _SessionRecorder_handleVisibilityChange, "f"));
20764
+ clearSessionState();
20765
+ window.removeEventListener("pagehide", __classPrivateFieldGet(this, _SessionRecorder_handlePageHide, "f"));
20746
20766
  }
20747
20767
  catch (err) {
20748
20768
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "Stop failed", err);
20749
20769
  }
20750
20770
  }
20751
20771
  }
20752
- _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) {
20753
20773
  if (!this.debug)
20754
20774
  return;
20755
20775
  if (error) {
@@ -20770,10 +20790,11 @@ _SessionRecorder_trackEventsThrottled = new WeakMap(), _SessionRecorder_handleVi
20770
20790
  maskInputOptions: {
20771
20791
  password: this.maskingOptions.includes("passwords"),
20772
20792
  },
20793
+ recordCrossOriginIframes: this.recordCrossOriginIframes,
20773
20794
  plugins: [getRecordConsolePlugin()],
20774
20795
  checkoutEveryNth: 100,
20775
20796
  });
20776
- __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_initFocusListener).call(this);
20797
+ __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_initListeners).call(this);
20777
20798
  }, _SessionRecorder_isUserInteraction = function _SessionRecorder_isUserInteraction(event) {
20778
20799
  var _a;
20779
20800
  if (event.type === 3) {
@@ -20785,15 +20806,16 @@ _SessionRecorder_trackEventsThrottled = new WeakMap(), _SessionRecorder_handleVi
20785
20806
  var _a;
20786
20807
  try {
20787
20808
  const now = Date.now();
20788
- const lastActive = Number(localStorage.getItem("userlensSessionLastActive"));
20789
- // check inactivity timeout
20790
- if (lastActive && now - lastActive > this.TIMEOUT) {
20809
+ const state = readSessionState();
20810
+ if (state && now - state.last_active > this.TIMEOUT) {
20791
20811
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_resetSession).call(this);
20792
20812
  takeFullSnapshot(true);
20793
20813
  }
20794
- // only update lastActive on actual user interactions, not DOM mutations
20795
20814
  if (__classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_isUserInteraction).call(this, event)) {
20796
- localStorage.setItem("userlensSessionLastActive", now.toString());
20815
+ const latest = readSessionState();
20816
+ if (latest) {
20817
+ writeSessionState({ ...latest, last_active: now });
20818
+ }
20797
20819
  }
20798
20820
  this.sessionEvents.push(event);
20799
20821
  if (this.sessionEvents.length >= this.BUFFER_SIZE) {
@@ -20804,24 +20826,29 @@ _SessionRecorder_trackEventsThrottled = new WeakMap(), _SessionRecorder_handleVi
20804
20826
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "Event handling failed", err);
20805
20827
  }
20806
20828
  }, _SessionRecorder_resetSession = function _SessionRecorder_resetSession() {
20807
- __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_removeLocalSessionData).call(this);
20829
+ clearSessionState();
20808
20830
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_clearEvents).call(this);
20809
20831
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_createSession).call(this);
20810
20832
  }, _SessionRecorder_createSession = function _SessionRecorder_createSession() {
20811
20833
  const now = Date.now();
20812
- const lastActive = Number(localStorage.getItem("userlensSessionLastActive"));
20813
- const storedUuid = localStorage.getItem("userlensSessionUuid");
20814
- const isExpired = !lastActive || now - lastActive > this.TIMEOUT;
20815
- if (!storedUuid || isExpired) {
20834
+ const state = readSessionState();
20835
+ const isExpired = !state || now - state.last_active > this.TIMEOUT;
20836
+ if (!state || isExpired) {
20816
20837
  this.sessionUuid = generateUuid();
20817
- 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
+ });
20818
20845
  }
20819
20846
  else {
20820
- this.sessionUuid = storedUuid;
20847
+ this.sessionUuid = state.session_uuid;
20848
+ writeSessionState({ ...state, last_active: now });
20821
20849
  }
20822
- localStorage.setItem("userlensSessionLastActive", now.toString());
20823
- }, _SessionRecorder_initFocusListener = function _SessionRecorder_initFocusListener() {
20824
- window.addEventListener("visibilitychange", __classPrivateFieldGet(this, _SessionRecorder_handleVisibilityChange, "f"));
20850
+ }, _SessionRecorder_initListeners = function _SessionRecorder_initListeners() {
20851
+ window.addEventListener("pagehide", __classPrivateFieldGet(this, _SessionRecorder_handlePageHide, "f"));
20825
20852
  }, _SessionRecorder_throttle = function _SessionRecorder_throttle(func, delay) {
20826
20853
  let lastCall = 0;
20827
20854
  return (...args) => {
@@ -20832,34 +20859,45 @@ _SessionRecorder_trackEventsThrottled = new WeakMap(), _SessionRecorder_handleVi
20832
20859
  }
20833
20860
  };
20834
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");
20835
20867
  try {
20836
- if (this.sessionEvents.length === 0) {
20837
- return;
20838
- }
20839
- const chunkTimestamp = this.sessionEvents[this.sessionEvents.length - 1].timestamp;
20840
20868
  const events = [...this.sessionEvents];
20841
- __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_clearEvents).call(this);
20842
- if (this.mode === "manual") {
20843
- if (this.onEvents) {
20844
- this.onEvents({
20845
- sessionId: this.sessionUuid,
20846
- events,
20847
- chunkTimestamp,
20848
- });
20849
- }
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;
20850
20876
  }
20851
- else {
20852
- 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 });
20853
20891
  }
20854
20892
  }
20855
20893
  catch (err) {
20856
20894
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "Event tracking failed", err);
20857
20895
  }
20896
+ finally {
20897
+ __classPrivateFieldSet(this, _SessionRecorder_uploading, false, "f");
20898
+ }
20858
20899
  }, _SessionRecorder_clearEvents = function _SessionRecorder_clearEvents() {
20859
20900
  this.sessionEvents = [];
20860
- }, _SessionRecorder_removeLocalSessionData = function _SessionRecorder_removeLocalSessionData() {
20861
- localStorage.removeItem("userlensSessionUuid");
20862
- localStorage.removeItem("userlensSessionLastActive");
20863
20901
  };
20864
20902
 
20865
20903
  export { SessionRecorder as default };