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