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.
@@ -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,65 +20682,61 @@ 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
20741
  const { TIMEOUT = 30 * 60 * 1000, BUFFER_SIZE = 10, maskingOptions = ["passwords"], recordCrossOriginIframes = false, } = recordingOptions;
20723
20742
  this.TIMEOUT = TIMEOUT;
@@ -20746,15 +20765,15 @@ class SessionRecorder {
20746
20765
  this.rrwebStop();
20747
20766
  this.rrwebStop = null;
20748
20767
  __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"));
20768
+ clearSessionState();
20769
+ window.removeEventListener("pagehide", __classPrivateFieldGet(this, _SessionRecorder_handlePageHide, "f"));
20751
20770
  }
20752
20771
  catch (err) {
20753
20772
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "Stop failed", err);
20754
20773
  }
20755
20774
  }
20756
20775
  }
20757
- _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) {
20758
20777
  if (!this.debug)
20759
20778
  return;
20760
20779
  if (error) {
@@ -20779,7 +20798,7 @@ _SessionRecorder_trackEventsThrottled = new WeakMap(), _SessionRecorder_handleVi
20779
20798
  plugins: [getRecordConsolePlugin()],
20780
20799
  checkoutEveryNth: 100,
20781
20800
  });
20782
- __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_initFocusListener).call(this);
20801
+ __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_initListeners).call(this);
20783
20802
  }, _SessionRecorder_isUserInteraction = function _SessionRecorder_isUserInteraction(event) {
20784
20803
  var _a;
20785
20804
  if (event.type === 3) {
@@ -20791,15 +20810,16 @@ _SessionRecorder_trackEventsThrottled = new WeakMap(), _SessionRecorder_handleVi
20791
20810
  var _a;
20792
20811
  try {
20793
20812
  const now = Date.now();
20794
- const lastActive = Number(localStorage.getItem("userlensSessionLastActive"));
20795
- // check inactivity timeout
20796
- if (lastActive && now - lastActive > this.TIMEOUT) {
20813
+ const state = readSessionState();
20814
+ if (state && now - state.last_active > this.TIMEOUT) {
20797
20815
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_resetSession).call(this);
20798
20816
  takeFullSnapshot(true);
20799
20817
  }
20800
- // only update lastActive on actual user interactions, not DOM mutations
20801
20818
  if (__classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_isUserInteraction).call(this, event)) {
20802
- localStorage.setItem("userlensSessionLastActive", now.toString());
20819
+ const latest = readSessionState();
20820
+ if (latest) {
20821
+ writeSessionState({ ...latest, last_active: now });
20822
+ }
20803
20823
  }
20804
20824
  this.sessionEvents.push(event);
20805
20825
  if (this.sessionEvents.length >= this.BUFFER_SIZE) {
@@ -20810,24 +20830,29 @@ _SessionRecorder_trackEventsThrottled = new WeakMap(), _SessionRecorder_handleVi
20810
20830
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "Event handling failed", err);
20811
20831
  }
20812
20832
  }, _SessionRecorder_resetSession = function _SessionRecorder_resetSession() {
20813
- __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_removeLocalSessionData).call(this);
20833
+ clearSessionState();
20814
20834
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_clearEvents).call(this);
20815
20835
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_createSession).call(this);
20816
20836
  }, _SessionRecorder_createSession = function _SessionRecorder_createSession() {
20817
20837
  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) {
20838
+ const state = readSessionState();
20839
+ const isExpired = !state || now - state.last_active > this.TIMEOUT;
20840
+ if (!state || isExpired) {
20822
20841
  this.sessionUuid = generateUuid();
20823
- 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
+ });
20824
20849
  }
20825
20850
  else {
20826
- this.sessionUuid = storedUuid;
20851
+ this.sessionUuid = state.session_uuid;
20852
+ writeSessionState({ ...state, last_active: now });
20827
20853
  }
20828
- localStorage.setItem("userlensSessionLastActive", now.toString());
20829
- }, _SessionRecorder_initFocusListener = function _SessionRecorder_initFocusListener() {
20830
- window.addEventListener("visibilitychange", __classPrivateFieldGet(this, _SessionRecorder_handleVisibilityChange, "f"));
20854
+ }, _SessionRecorder_initListeners = function _SessionRecorder_initListeners() {
20855
+ window.addEventListener("pagehide", __classPrivateFieldGet(this, _SessionRecorder_handlePageHide, "f"));
20831
20856
  }, _SessionRecorder_throttle = function _SessionRecorder_throttle(func, delay) {
20832
20857
  let lastCall = 0;
20833
20858
  return (...args) => {
@@ -20838,34 +20863,45 @@ _SessionRecorder_trackEventsThrottled = new WeakMap(), _SessionRecorder_handleVi
20838
20863
  }
20839
20864
  };
20840
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");
20841
20871
  try {
20842
- if (this.sessionEvents.length === 0) {
20843
- return;
20844
- }
20845
- const chunkTimestamp = this.sessionEvents[this.sessionEvents.length - 1].timestamp;
20846
20872
  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
- }
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;
20856
20880
  }
20857
- else {
20858
- 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 });
20859
20895
  }
20860
20896
  }
20861
20897
  catch (err) {
20862
20898
  __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "Event tracking failed", err);
20863
20899
  }
20900
+ finally {
20901
+ __classPrivateFieldSet(this, _SessionRecorder_uploading, false, "f");
20902
+ }
20864
20903
  }, _SessionRecorder_clearEvents = function _SessionRecorder_clearEvents() {
20865
20904
  this.sessionEvents = [];
20866
- }, _SessionRecorder_removeLocalSessionData = function _SessionRecorder_removeLocalSessionData() {
20867
- localStorage.removeItem("userlensSessionUuid");
20868
- localStorage.removeItem("userlensSessionLastActive");
20869
20905
  };
20870
20906
 
20871
20907
  exports.default = SessionRecorder;