userlens-session-recorder 2.1.0 → 2.2.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.
- package/dist/index.cjs.js +285 -93
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.esm.js +285 -93
- package/dist/index.esm.js.map +1 -1
- package/dist/sessionManager.d.ts +61 -0
- package/dist/storage.d.ts +10 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/userlens-session-recorder.umd.js +285 -93
- package/dist/userlens-session-recorder.umd.js.map +1 -1
- package/dist/utils.d.ts +1 -5
- package/package.json +1 -1
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export type SessionChange = "none" | "expired" | "rotated";
|
|
2
|
+
/**
|
|
3
|
+
* Owns the (session_uuid, window_id, chunk_seq, last_active) lifecycle.
|
|
4
|
+
*
|
|
5
|
+
* Two storage scopes:
|
|
6
|
+
* - shared (localStorage): session_uuid + last_active — visible to every tab
|
|
7
|
+
* of the same origin so tabs can adopt one logical session and a single
|
|
8
|
+
* idle timeout governs the whole user.
|
|
9
|
+
* - per-tab (sessionStorage): window_id + chunk_seq — independent per tab so
|
|
10
|
+
* concurrent tabs can upload chunks without colliding on chunk_seq.
|
|
11
|
+
*
|
|
12
|
+
* The recorder talks to this class instead of touching storage directly.
|
|
13
|
+
*/
|
|
14
|
+
export declare class SessionManager {
|
|
15
|
+
#private;
|
|
16
|
+
private readonly timeoutMs;
|
|
17
|
+
private _sessionUuid;
|
|
18
|
+
private _windowId;
|
|
19
|
+
constructor(timeoutMs: number);
|
|
20
|
+
initialize(): void;
|
|
21
|
+
get sessionUuid(): string;
|
|
22
|
+
get windowId(): string;
|
|
23
|
+
/**
|
|
24
|
+
* Reconciles in-memory session_uuid with whatever shared storage says.
|
|
25
|
+
* Called on every event so cross-tab session changes are picked up quickly.
|
|
26
|
+
*
|
|
27
|
+
* Returns:
|
|
28
|
+
* "expired" — shared session has been idle past timeout; we rotated.
|
|
29
|
+
* "rotated" — another tab rotated the session_uuid; we adopted it.
|
|
30
|
+
* "none" — no change, business as usual.
|
|
31
|
+
*
|
|
32
|
+
* Caller takes a fresh full snapshot on "expired" or "rotated" so this
|
|
33
|
+
* window's first chunk in the new session is replayable on its own.
|
|
34
|
+
*/
|
|
35
|
+
syncWithSharedState(now: number): SessionChange;
|
|
36
|
+
/** Bump shared last_active. Call only on real user interactions. */
|
|
37
|
+
bumpActivity(now: number): void;
|
|
38
|
+
/** Read this window's current chunk_seq for a pending upload. */
|
|
39
|
+
currentChunkSeq(): number | null;
|
|
40
|
+
/**
|
|
41
|
+
* Mark a chunk_seq as committed after a successful upload. No-ops if the
|
|
42
|
+
* stored seq has moved on (e.g. a session rotation reset it to 0 mid-flight).
|
|
43
|
+
*/
|
|
44
|
+
commitChunkSeq(uploadedSeq: number): void;
|
|
45
|
+
/**
|
|
46
|
+
* Reserve and return the current chunk_seq for a pagehide flush, advancing
|
|
47
|
+
* storage so the next normal upload uses seq+1. Returns null if window state
|
|
48
|
+
* is missing.
|
|
49
|
+
*/
|
|
50
|
+
reserveChunkSeq(): number | null;
|
|
51
|
+
/** Hard reset: wipe shared session and start a fresh one. */
|
|
52
|
+
reset(): void;
|
|
53
|
+
/**
|
|
54
|
+
* Called from pagehide. Lets a future reload of the same tab reuse the
|
|
55
|
+
* window_id (because the flag is gone), while a cloned tab still sees the
|
|
56
|
+
* flag set in its inherited sessionStorage and treats itself as a duplicate.
|
|
57
|
+
*/
|
|
58
|
+
notifyPageHide(): void;
|
|
59
|
+
/** Full teardown — call on stop(). */
|
|
60
|
+
dispose(): void;
|
|
61
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SharedSessionState, WindowState } from "./types";
|
|
2
|
+
export declare function readSharedSessionState(): SharedSessionState | null;
|
|
3
|
+
export declare function writeSharedSessionState(state: SharedSessionState): void;
|
|
4
|
+
export declare function clearSharedSessionState(): void;
|
|
5
|
+
export declare function readWindowState(): WindowState | null;
|
|
6
|
+
export declare function writeWindowState(state: WindowState): void;
|
|
7
|
+
export declare function clearWindowState(): void;
|
|
8
|
+
export declare function wasPrimaryWindowFlagAlreadySet(): boolean;
|
|
9
|
+
export declare function markPrimaryWindow(): void;
|
|
10
|
+
export declare function clearPrimaryWindowFlag(): void;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -10,10 +10,10 @@ interface SessionRecordingOptions {
|
|
|
10
10
|
}
|
|
11
11
|
interface EventBatch {
|
|
12
12
|
session_uuid: string;
|
|
13
|
+
window_id: string;
|
|
13
14
|
chunk_seq: number;
|
|
14
15
|
chunk_start_ts: string;
|
|
15
16
|
chunk_end_ts: string;
|
|
16
|
-
initial_url?: string;
|
|
17
17
|
events: eventWithTime[];
|
|
18
18
|
keepalive?: boolean;
|
|
19
19
|
}
|
|
@@ -41,7 +41,7 @@ declare class SessionRecorder {
|
|
|
41
41
|
private TIMEOUT;
|
|
42
42
|
private BUFFER_SIZE;
|
|
43
43
|
private maskingOptions;
|
|
44
|
-
private
|
|
44
|
+
private sessionManager;
|
|
45
45
|
private sessionEvents;
|
|
46
46
|
private bufferBytes;
|
|
47
47
|
private rrwebStop;
|
|
@@ -12190,6 +12190,8 @@
|
|
|
12190
12190
|
!function(t2) {
|
|
12191
12191
|
t2[t2.NotStarted = 0] = "NotStarted", t2[t2.Running = 1] = "Running", t2[t2.Stopped = 2] = "Stopped";
|
|
12192
12192
|
}(n$1 || (n$1 = {}));
|
|
12193
|
+
const { addCustomEvent } = record;
|
|
12194
|
+
const { freezePage } = record;
|
|
12193
12195
|
const { takeFullSnapshot } = record;
|
|
12194
12196
|
|
|
12195
12197
|
var __defProp = Object.defineProperty;
|
|
@@ -20594,7 +20596,7 @@
|
|
|
20594
20596
|
function saveWriteCode(writeCode) {
|
|
20595
20597
|
window.localStorage.setItem("$ul_WRITE_CODE", btoa(`${writeCode}:`));
|
|
20596
20598
|
}
|
|
20597
|
-
|
|
20599
|
+
function getWriteCode() {
|
|
20598
20600
|
try {
|
|
20599
20601
|
const raw = window.localStorage.getItem("$ul_WRITE_CODE");
|
|
20600
20602
|
if (raw == null)
|
|
@@ -20610,37 +20612,6 @@
|
|
|
20610
20612
|
catch {
|
|
20611
20613
|
return null;
|
|
20612
20614
|
}
|
|
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
20615
|
}
|
|
20645
20616
|
|
|
20646
20617
|
const INGEST_BASE_URL = "https://ul-ingest.userlens.io";
|
|
@@ -20652,15 +20623,13 @@
|
|
|
20652
20623
|
}
|
|
20653
20624
|
const body = {
|
|
20654
20625
|
session_uuid: args.session_uuid,
|
|
20626
|
+
window_id: args.window_id,
|
|
20655
20627
|
chunk_seq: args.chunk_seq,
|
|
20656
20628
|
chunk_start_ts: args.chunk_start_ts,
|
|
20657
20629
|
chunk_end_ts: args.chunk_end_ts,
|
|
20658
20630
|
user_id: args.user_id,
|
|
20659
20631
|
events: args.events,
|
|
20660
20632
|
};
|
|
20661
|
-
if (args.initial_url !== undefined) {
|
|
20662
|
-
body.initial_url = args.initial_url;
|
|
20663
|
-
}
|
|
20664
20633
|
const res = await fetch(`${INGEST_BASE_URL}/session-recording`, {
|
|
20665
20634
|
method: "POST",
|
|
20666
20635
|
headers: {
|
|
@@ -20675,7 +20644,224 @@
|
|
|
20675
20644
|
return "ok";
|
|
20676
20645
|
};
|
|
20677
20646
|
|
|
20678
|
-
|
|
20647
|
+
const SHARED_KEY = "userlensSession";
|
|
20648
|
+
const WINDOW_KEY = "userlensWindow";
|
|
20649
|
+
// Set in sessionStorage on construct, cleared on pagehide. If we read it on
|
|
20650
|
+
// init and it's already set, sessionStorage was cloned (Cmd+click, duplicate,
|
|
20651
|
+
// window.open) — discard inherited window_id and mint a fresh one.
|
|
20652
|
+
const PRIMARY_WINDOW_KEY = "userlensPrimaryWindow";
|
|
20653
|
+
function readSharedSessionState() {
|
|
20654
|
+
try {
|
|
20655
|
+
const raw = window.localStorage.getItem(SHARED_KEY);
|
|
20656
|
+
if (!raw)
|
|
20657
|
+
return null;
|
|
20658
|
+
const parsed = JSON.parse(raw);
|
|
20659
|
+
if (typeof parsed !== "object" || parsed === null ||
|
|
20660
|
+
typeof parsed.session_uuid !== "string" ||
|
|
20661
|
+
typeof parsed.last_active !== "number")
|
|
20662
|
+
return null;
|
|
20663
|
+
return parsed;
|
|
20664
|
+
}
|
|
20665
|
+
catch {
|
|
20666
|
+
return null;
|
|
20667
|
+
}
|
|
20668
|
+
}
|
|
20669
|
+
function writeSharedSessionState(state) {
|
|
20670
|
+
try {
|
|
20671
|
+
window.localStorage.setItem(SHARED_KEY, JSON.stringify(state));
|
|
20672
|
+
}
|
|
20673
|
+
catch { }
|
|
20674
|
+
}
|
|
20675
|
+
function clearSharedSessionState() {
|
|
20676
|
+
try {
|
|
20677
|
+
window.localStorage.removeItem(SHARED_KEY);
|
|
20678
|
+
}
|
|
20679
|
+
catch { }
|
|
20680
|
+
}
|
|
20681
|
+
function readWindowState() {
|
|
20682
|
+
try {
|
|
20683
|
+
const raw = window.sessionStorage.getItem(WINDOW_KEY);
|
|
20684
|
+
if (!raw)
|
|
20685
|
+
return null;
|
|
20686
|
+
const parsed = JSON.parse(raw);
|
|
20687
|
+
if (typeof parsed !== "object" || parsed === null ||
|
|
20688
|
+
typeof parsed.window_id !== "string" ||
|
|
20689
|
+
typeof parsed.chunk_seq !== "number")
|
|
20690
|
+
return null;
|
|
20691
|
+
return parsed;
|
|
20692
|
+
}
|
|
20693
|
+
catch {
|
|
20694
|
+
return null;
|
|
20695
|
+
}
|
|
20696
|
+
}
|
|
20697
|
+
function writeWindowState(state) {
|
|
20698
|
+
try {
|
|
20699
|
+
window.sessionStorage.setItem(WINDOW_KEY, JSON.stringify(state));
|
|
20700
|
+
}
|
|
20701
|
+
catch { }
|
|
20702
|
+
}
|
|
20703
|
+
function clearWindowState() {
|
|
20704
|
+
try {
|
|
20705
|
+
window.sessionStorage.removeItem(WINDOW_KEY);
|
|
20706
|
+
}
|
|
20707
|
+
catch { }
|
|
20708
|
+
}
|
|
20709
|
+
function wasPrimaryWindowFlagAlreadySet() {
|
|
20710
|
+
try {
|
|
20711
|
+
return window.sessionStorage.getItem(PRIMARY_WINDOW_KEY) === "1";
|
|
20712
|
+
}
|
|
20713
|
+
catch {
|
|
20714
|
+
return false;
|
|
20715
|
+
}
|
|
20716
|
+
}
|
|
20717
|
+
function markPrimaryWindow() {
|
|
20718
|
+
try {
|
|
20719
|
+
window.sessionStorage.setItem(PRIMARY_WINDOW_KEY, "1");
|
|
20720
|
+
}
|
|
20721
|
+
catch { }
|
|
20722
|
+
}
|
|
20723
|
+
function clearPrimaryWindowFlag() {
|
|
20724
|
+
try {
|
|
20725
|
+
window.sessionStorage.removeItem(PRIMARY_WINDOW_KEY);
|
|
20726
|
+
}
|
|
20727
|
+
catch { }
|
|
20728
|
+
}
|
|
20729
|
+
|
|
20730
|
+
var _SessionManager_instances, _SessionManager_initWindow, _SessionManager_joinOrStartSession, _SessionManager_startFresh;
|
|
20731
|
+
/**
|
|
20732
|
+
* Owns the (session_uuid, window_id, chunk_seq, last_active) lifecycle.
|
|
20733
|
+
*
|
|
20734
|
+
* Two storage scopes:
|
|
20735
|
+
* - shared (localStorage): session_uuid + last_active — visible to every tab
|
|
20736
|
+
* of the same origin so tabs can adopt one logical session and a single
|
|
20737
|
+
* idle timeout governs the whole user.
|
|
20738
|
+
* - per-tab (sessionStorage): window_id + chunk_seq — independent per tab so
|
|
20739
|
+
* concurrent tabs can upload chunks without colliding on chunk_seq.
|
|
20740
|
+
*
|
|
20741
|
+
* The recorder talks to this class instead of touching storage directly.
|
|
20742
|
+
*/
|
|
20743
|
+
class SessionManager {
|
|
20744
|
+
constructor(timeoutMs) {
|
|
20745
|
+
_SessionManager_instances.add(this);
|
|
20746
|
+
this.timeoutMs = timeoutMs;
|
|
20747
|
+
}
|
|
20748
|
+
initialize() {
|
|
20749
|
+
__classPrivateFieldGet(this, _SessionManager_instances, "m", _SessionManager_initWindow).call(this);
|
|
20750
|
+
__classPrivateFieldGet(this, _SessionManager_instances, "m", _SessionManager_joinOrStartSession).call(this);
|
|
20751
|
+
}
|
|
20752
|
+
get sessionUuid() {
|
|
20753
|
+
return this._sessionUuid;
|
|
20754
|
+
}
|
|
20755
|
+
get windowId() {
|
|
20756
|
+
return this._windowId;
|
|
20757
|
+
}
|
|
20758
|
+
/**
|
|
20759
|
+
* Reconciles in-memory session_uuid with whatever shared storage says.
|
|
20760
|
+
* Called on every event so cross-tab session changes are picked up quickly.
|
|
20761
|
+
*
|
|
20762
|
+
* Returns:
|
|
20763
|
+
* "expired" — shared session has been idle past timeout; we rotated.
|
|
20764
|
+
* "rotated" — another tab rotated the session_uuid; we adopted it.
|
|
20765
|
+
* "none" — no change, business as usual.
|
|
20766
|
+
*
|
|
20767
|
+
* Caller takes a fresh full snapshot on "expired" or "rotated" so this
|
|
20768
|
+
* window's first chunk in the new session is replayable on its own.
|
|
20769
|
+
*/
|
|
20770
|
+
syncWithSharedState(now) {
|
|
20771
|
+
const shared = readSharedSessionState();
|
|
20772
|
+
if (shared && now - shared.last_active > this.timeoutMs) {
|
|
20773
|
+
__classPrivateFieldGet(this, _SessionManager_instances, "m", _SessionManager_startFresh).call(this, now);
|
|
20774
|
+
return "expired";
|
|
20775
|
+
}
|
|
20776
|
+
if (shared && shared.session_uuid !== this._sessionUuid) {
|
|
20777
|
+
this._sessionUuid = shared.session_uuid;
|
|
20778
|
+
writeWindowState({ window_id: this._windowId, chunk_seq: 0 });
|
|
20779
|
+
return "rotated";
|
|
20780
|
+
}
|
|
20781
|
+
return "none";
|
|
20782
|
+
}
|
|
20783
|
+
/** Bump shared last_active. Call only on real user interactions. */
|
|
20784
|
+
bumpActivity(now) {
|
|
20785
|
+
const latest = readSharedSessionState();
|
|
20786
|
+
if (latest) {
|
|
20787
|
+
writeSharedSessionState({ ...latest, last_active: now });
|
|
20788
|
+
}
|
|
20789
|
+
}
|
|
20790
|
+
/** Read this window's current chunk_seq for a pending upload. */
|
|
20791
|
+
currentChunkSeq() {
|
|
20792
|
+
var _a, _b;
|
|
20793
|
+
return (_b = (_a = readWindowState()) === null || _a === void 0 ? void 0 : _a.chunk_seq) !== null && _b !== void 0 ? _b : null;
|
|
20794
|
+
}
|
|
20795
|
+
/**
|
|
20796
|
+
* Mark a chunk_seq as committed after a successful upload. No-ops if the
|
|
20797
|
+
* stored seq has moved on (e.g. a session rotation reset it to 0 mid-flight).
|
|
20798
|
+
*/
|
|
20799
|
+
commitChunkSeq(uploadedSeq) {
|
|
20800
|
+
const after = readWindowState();
|
|
20801
|
+
if (after && after.window_id === this._windowId && after.chunk_seq === uploadedSeq) {
|
|
20802
|
+
writeWindowState({ window_id: this._windowId, chunk_seq: uploadedSeq + 1 });
|
|
20803
|
+
}
|
|
20804
|
+
}
|
|
20805
|
+
/**
|
|
20806
|
+
* Reserve and return the current chunk_seq for a pagehide flush, advancing
|
|
20807
|
+
* storage so the next normal upload uses seq+1. Returns null if window state
|
|
20808
|
+
* is missing.
|
|
20809
|
+
*/
|
|
20810
|
+
reserveChunkSeq() {
|
|
20811
|
+
const win = readWindowState();
|
|
20812
|
+
if (!win)
|
|
20813
|
+
return null;
|
|
20814
|
+
writeWindowState({ window_id: this._windowId, chunk_seq: win.chunk_seq + 1 });
|
|
20815
|
+
return win.chunk_seq;
|
|
20816
|
+
}
|
|
20817
|
+
/** Hard reset: wipe shared session and start a fresh one. */
|
|
20818
|
+
reset() {
|
|
20819
|
+
clearSharedSessionState();
|
|
20820
|
+
__classPrivateFieldGet(this, _SessionManager_instances, "m", _SessionManager_joinOrStartSession).call(this);
|
|
20821
|
+
}
|
|
20822
|
+
/**
|
|
20823
|
+
* Called from pagehide. Lets a future reload of the same tab reuse the
|
|
20824
|
+
* window_id (because the flag is gone), while a cloned tab still sees the
|
|
20825
|
+
* flag set in its inherited sessionStorage and treats itself as a duplicate.
|
|
20826
|
+
*/
|
|
20827
|
+
notifyPageHide() {
|
|
20828
|
+
clearPrimaryWindowFlag();
|
|
20829
|
+
}
|
|
20830
|
+
/** Full teardown — call on stop(). */
|
|
20831
|
+
dispose() {
|
|
20832
|
+
clearSharedSessionState();
|
|
20833
|
+
clearWindowState();
|
|
20834
|
+
clearPrimaryWindowFlag();
|
|
20835
|
+
}
|
|
20836
|
+
}
|
|
20837
|
+
_SessionManager_instances = new WeakSet(), _SessionManager_initWindow = function _SessionManager_initWindow() {
|
|
20838
|
+
const cloned = wasPrimaryWindowFlagAlreadySet();
|
|
20839
|
+
const existing = readWindowState();
|
|
20840
|
+
if (existing && !cloned) {
|
|
20841
|
+
this._windowId = existing.window_id;
|
|
20842
|
+
}
|
|
20843
|
+
else {
|
|
20844
|
+
this._windowId = generateUuid();
|
|
20845
|
+
writeWindowState({ window_id: this._windowId, chunk_seq: 0 });
|
|
20846
|
+
}
|
|
20847
|
+
markPrimaryWindow();
|
|
20848
|
+
}, _SessionManager_joinOrStartSession = function _SessionManager_joinOrStartSession() {
|
|
20849
|
+
const now = Date.now();
|
|
20850
|
+
const shared = readSharedSessionState();
|
|
20851
|
+
if (!shared || now - shared.last_active > this.timeoutMs) {
|
|
20852
|
+
__classPrivateFieldGet(this, _SessionManager_instances, "m", _SessionManager_startFresh).call(this, now);
|
|
20853
|
+
}
|
|
20854
|
+
else {
|
|
20855
|
+
this._sessionUuid = shared.session_uuid;
|
|
20856
|
+
writeSharedSessionState({ ...shared, last_active: now });
|
|
20857
|
+
}
|
|
20858
|
+
}, _SessionManager_startFresh = function _SessionManager_startFresh(now) {
|
|
20859
|
+
this._sessionUuid = generateUuid();
|
|
20860
|
+
writeSharedSessionState({ session_uuid: this._sessionUuid, last_active: now });
|
|
20861
|
+
writeWindowState({ window_id: this._windowId, chunk_seq: 0 });
|
|
20862
|
+
};
|
|
20863
|
+
|
|
20864
|
+
var _SessionRecorder_instances, _SessionRecorder_uploading, _SessionRecorder_uploadingMaxTs, _SessionRecorder_bufferTimer, _SessionRecorder_log, _SessionRecorder_initRecorder, _SessionRecorder_isUserInteraction, _SessionRecorder_isVisibilityMarker, _SessionRecorder_handleEvent, _SessionRecorder_armBufferTimer, _SessionRecorder_flushBuffer, _SessionRecorder_handlePageHide, _SessionRecorder_handleVisibilityChange, _SessionRecorder_initListeners, _SessionRecorder_trackEvents, _SessionRecorder_clearEvents;
|
|
20679
20865
|
const BUFFER_TIMEOUT_MS = 2000;
|
|
20680
20866
|
const MAX_BUFFER_BYTES = 900 * 1024;
|
|
20681
20867
|
function estimateEventSize(event) {
|
|
@@ -20699,6 +20885,7 @@
|
|
|
20699
20885
|
_SessionRecorder_bufferTimer.set(this, null);
|
|
20700
20886
|
_SessionRecorder_handlePageHide.set(this, () => {
|
|
20701
20887
|
try {
|
|
20888
|
+
this.sessionManager.notifyPageHide();
|
|
20702
20889
|
if (this.sessionEvents.length === 0)
|
|
20703
20890
|
return;
|
|
20704
20891
|
let events;
|
|
@@ -20710,24 +20897,21 @@
|
|
|
20710
20897
|
}
|
|
20711
20898
|
if (events.length === 0)
|
|
20712
20899
|
return;
|
|
20713
|
-
const
|
|
20714
|
-
if (
|
|
20900
|
+
const chunk_seq = this.sessionManager.reserveChunkSeq();
|
|
20901
|
+
if (chunk_seq === null)
|
|
20715
20902
|
return;
|
|
20716
|
-
const chunk_seq = __classPrivateFieldGet(this, _SessionRecorder_uploading, "f") ? state.chunk_seq + 1 : state.chunk_seq;
|
|
20717
|
-
writeSessionState({ ...state, chunk_seq: chunk_seq + 1 });
|
|
20718
20903
|
const start_ts_ms = events[0].timestamp;
|
|
20719
20904
|
const end_ts_ms = events[events.length - 1].timestamp;
|
|
20720
|
-
const initial_url = chunk_seq === 0 ? state.initial_url : undefined;
|
|
20721
20905
|
const chunk_start_ts = new Date(start_ts_ms).toISOString();
|
|
20722
20906
|
const chunk_end_ts = new Date(end_ts_ms).toISOString();
|
|
20723
20907
|
if (this.mode === "manual") {
|
|
20724
20908
|
try {
|
|
20725
20909
|
void this.onEvents({
|
|
20726
|
-
session_uuid: this.sessionUuid,
|
|
20910
|
+
session_uuid: this.sessionManager.sessionUuid,
|
|
20911
|
+
window_id: this.sessionManager.windowId,
|
|
20727
20912
|
chunk_seq,
|
|
20728
20913
|
chunk_start_ts,
|
|
20729
20914
|
chunk_end_ts,
|
|
20730
|
-
initial_url,
|
|
20731
20915
|
events,
|
|
20732
20916
|
keepalive: true,
|
|
20733
20917
|
});
|
|
@@ -20739,11 +20923,11 @@
|
|
|
20739
20923
|
else {
|
|
20740
20924
|
uploadSessionEvents({
|
|
20741
20925
|
user_id: this.userId,
|
|
20742
|
-
session_uuid: this.sessionUuid,
|
|
20926
|
+
session_uuid: this.sessionManager.sessionUuid,
|
|
20927
|
+
window_id: this.sessionManager.windowId,
|
|
20743
20928
|
chunk_seq,
|
|
20744
20929
|
chunk_start_ts,
|
|
20745
20930
|
chunk_end_ts,
|
|
20746
|
-
initial_url,
|
|
20747
20931
|
events,
|
|
20748
20932
|
keepalive: true,
|
|
20749
20933
|
}).catch(() => { });
|
|
@@ -20755,6 +20939,19 @@
|
|
|
20755
20939
|
__classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "Page hide handling failed", err);
|
|
20756
20940
|
}
|
|
20757
20941
|
});
|
|
20942
|
+
// Player groups multi-tab recordings into segments by which tab was visible
|
|
20943
|
+
// when. Without these markers it has to guess from chunk timestamps and
|
|
20944
|
+
// active-source heuristics, which is unreliable when two tabs overlap.
|
|
20945
|
+
_SessionRecorder_handleVisibilityChange.set(this, () => {
|
|
20946
|
+
try {
|
|
20947
|
+
if (typeof document === "undefined" || !document.visibilityState)
|
|
20948
|
+
return;
|
|
20949
|
+
record.addCustomEvent("window " + document.visibilityState, {});
|
|
20950
|
+
}
|
|
20951
|
+
catch (err) {
|
|
20952
|
+
__classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "Visibility change handling failed", err);
|
|
20953
|
+
}
|
|
20954
|
+
});
|
|
20758
20955
|
try {
|
|
20759
20956
|
if (typeof window === "undefined")
|
|
20760
20957
|
return;
|
|
@@ -20799,6 +20996,7 @@
|
|
|
20799
20996
|
this.maskingOptions = maskingOptions;
|
|
20800
20997
|
this.recordCrossOriginIframes = recordCrossOriginIframes;
|
|
20801
20998
|
this.sessionEvents = [];
|
|
20999
|
+
this.sessionManager = new SessionManager(this.TIMEOUT);
|
|
20802
21000
|
__classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_initRecorder).call(this);
|
|
20803
21001
|
}
|
|
20804
21002
|
catch (err) {
|
|
@@ -20807,7 +21005,8 @@
|
|
|
20807
21005
|
}
|
|
20808
21006
|
}
|
|
20809
21007
|
getSessionId() {
|
|
20810
|
-
|
|
21008
|
+
var _a;
|
|
21009
|
+
return (_a = this.sessionManager) === null || _a === void 0 ? void 0 : _a.sessionUuid;
|
|
20811
21010
|
}
|
|
20812
21011
|
stop() {
|
|
20813
21012
|
try {
|
|
@@ -20817,15 +21016,16 @@
|
|
|
20817
21016
|
this.rrwebStop();
|
|
20818
21017
|
this.rrwebStop = null;
|
|
20819
21018
|
__classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_clearEvents).call(this);
|
|
20820
|
-
|
|
21019
|
+
this.sessionManager.dispose();
|
|
20821
21020
|
window.removeEventListener("pagehide", __classPrivateFieldGet(this, _SessionRecorder_handlePageHide, "f"));
|
|
21021
|
+
document.removeEventListener("visibilitychange", __classPrivateFieldGet(this, _SessionRecorder_handleVisibilityChange, "f"));
|
|
20822
21022
|
}
|
|
20823
21023
|
catch (err) {
|
|
20824
21024
|
__classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "Stop failed", err);
|
|
20825
21025
|
}
|
|
20826
21026
|
}
|
|
20827
21027
|
}
|
|
20828
|
-
_SessionRecorder_uploading = new WeakMap(), _SessionRecorder_uploadingMaxTs = new WeakMap(), _SessionRecorder_bufferTimer = new WeakMap(), _SessionRecorder_handlePageHide = new WeakMap(), _SessionRecorder_instances = new WeakSet(), _SessionRecorder_log = function _SessionRecorder_log(message, error) {
|
|
21028
|
+
_SessionRecorder_uploading = new WeakMap(), _SessionRecorder_uploadingMaxTs = new WeakMap(), _SessionRecorder_bufferTimer = new WeakMap(), _SessionRecorder_handlePageHide = new WeakMap(), _SessionRecorder_handleVisibilityChange = new WeakMap(), _SessionRecorder_instances = new WeakSet(), _SessionRecorder_log = function _SessionRecorder_log(message, error) {
|
|
20829
21029
|
if (!this.debug)
|
|
20830
21030
|
return;
|
|
20831
21031
|
if (error) {
|
|
@@ -20837,7 +21037,7 @@
|
|
|
20837
21037
|
}, _SessionRecorder_initRecorder = function _SessionRecorder_initRecorder() {
|
|
20838
21038
|
if (this.rrwebStop)
|
|
20839
21039
|
return;
|
|
20840
|
-
|
|
21040
|
+
this.sessionManager.initialize();
|
|
20841
21041
|
this.rrwebStop = record({
|
|
20842
21042
|
emit: (event, isCheckout) => {
|
|
20843
21043
|
__classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_handleEvent).call(this, event, isCheckout);
|
|
@@ -20858,19 +21058,35 @@
|
|
|
20858
21058
|
return source === 2 || source === 5 || source === 6;
|
|
20859
21059
|
}
|
|
20860
21060
|
return false;
|
|
21061
|
+
}, _SessionRecorder_isVisibilityMarker = function _SessionRecorder_isVisibilityMarker(event) {
|
|
21062
|
+
var _a;
|
|
21063
|
+
if (event.type !== 5)
|
|
21064
|
+
return false;
|
|
21065
|
+
const tag = (_a = event.data) === null || _a === void 0 ? void 0 : _a.tag;
|
|
21066
|
+
return tag === "window visible" || tag === "window hidden";
|
|
20861
21067
|
}, _SessionRecorder_handleEvent = function _SessionRecorder_handleEvent(event, _isCheckout) {
|
|
20862
21068
|
try {
|
|
20863
21069
|
const now = Date.now();
|
|
20864
|
-
const
|
|
20865
|
-
|
|
20866
|
-
|
|
21070
|
+
const change = this.sessionManager.syncWithSharedState(now);
|
|
21071
|
+
// A rotated session starts mid-page: no recorder construction and no
|
|
21072
|
+
// visibilitychange fires, so without the marker emit below the new
|
|
21073
|
+
// session has no visibility marker and the player can't attribute
|
|
21074
|
+
// windows. Skip it when the triggering event IS a visibility marker
|
|
21075
|
+
// (refocus after idle) — that event lands right after the snapshot
|
|
21076
|
+
// anyway, and emitting another would double it.
|
|
21077
|
+
if (change === "expired") {
|
|
21078
|
+
__classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_clearEvents).call(this);
|
|
21079
|
+
takeFullSnapshot(true);
|
|
21080
|
+
if (!__classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_isVisibilityMarker).call(this, event))
|
|
21081
|
+
__classPrivateFieldGet(this, _SessionRecorder_handleVisibilityChange, "f").call(this);
|
|
21082
|
+
}
|
|
21083
|
+
else if (change === "rotated") {
|
|
20867
21084
|
takeFullSnapshot(true);
|
|
21085
|
+
if (!__classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_isVisibilityMarker).call(this, event))
|
|
21086
|
+
__classPrivateFieldGet(this, _SessionRecorder_handleVisibilityChange, "f").call(this);
|
|
20868
21087
|
}
|
|
20869
21088
|
if (__classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_isUserInteraction).call(this, event)) {
|
|
20870
|
-
|
|
20871
|
-
if (latest) {
|
|
20872
|
-
writeSessionState({ ...latest, last_active: now });
|
|
20873
|
-
}
|
|
21089
|
+
this.sessionManager.bumpActivity(now);
|
|
20874
21090
|
}
|
|
20875
21091
|
this.sessionEvents.push(event);
|
|
20876
21092
|
this.bufferBytes += estimateEventSize(event);
|
|
@@ -20896,30 +21112,13 @@
|
|
|
20896
21112
|
__classPrivateFieldSet(this, _SessionRecorder_bufferTimer, null, "f");
|
|
20897
21113
|
}
|
|
20898
21114
|
void __classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_trackEvents).call(this);
|
|
20899
|
-
}, _SessionRecorder_resetSession = function _SessionRecorder_resetSession() {
|
|
20900
|
-
clearSessionState();
|
|
20901
|
-
__classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_clearEvents).call(this);
|
|
20902
|
-
__classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_createSession).call(this);
|
|
20903
|
-
}, _SessionRecorder_createSession = function _SessionRecorder_createSession() {
|
|
20904
|
-
const now = Date.now();
|
|
20905
|
-
const state = readSessionState();
|
|
20906
|
-
const isExpired = !state || now - state.last_active > this.TIMEOUT;
|
|
20907
|
-
if (!state || isExpired) {
|
|
20908
|
-
this.sessionUuid = generateUuid();
|
|
20909
|
-
const initial_url = typeof window !== "undefined" ? window.location.href : "";
|
|
20910
|
-
writeSessionState({
|
|
20911
|
-
session_uuid: this.sessionUuid,
|
|
20912
|
-
last_active: now,
|
|
20913
|
-
chunk_seq: 0,
|
|
20914
|
-
initial_url,
|
|
20915
|
-
});
|
|
20916
|
-
}
|
|
20917
|
-
else {
|
|
20918
|
-
this.sessionUuid = state.session_uuid;
|
|
20919
|
-
writeSessionState({ ...state, last_active: now });
|
|
20920
|
-
}
|
|
20921
21115
|
}, _SessionRecorder_initListeners = function _SessionRecorder_initListeners() {
|
|
20922
21116
|
window.addEventListener("pagehide", __classPrivateFieldGet(this, _SessionRecorder_handlePageHide, "f"));
|
|
21117
|
+
document.addEventListener("visibilitychange", __classPrivateFieldGet(this, _SessionRecorder_handleVisibilityChange, "f"));
|
|
21118
|
+
// Emit the initial state so the player knows whether this tab started
|
|
21119
|
+
// visible or hidden (a tab opened in the background never fires
|
|
21120
|
+
// visibilitychange until the user focuses it).
|
|
21121
|
+
__classPrivateFieldGet(this, _SessionRecorder_handleVisibilityChange, "f").call(this);
|
|
20923
21122
|
}, _SessionRecorder_trackEvents = async function _SessionRecorder_trackEvents() {
|
|
20924
21123
|
if (__classPrivateFieldGet(this, _SessionRecorder_uploading, "f"))
|
|
20925
21124
|
return;
|
|
@@ -20932,45 +21131,38 @@
|
|
|
20932
21131
|
__classPrivateFieldSet(this, _SessionRecorder_uploadingMaxTs, events[events.length - 1].timestamp, "f");
|
|
20933
21132
|
const start_ts_ms = events[0].timestamp;
|
|
20934
21133
|
const end_ts_ms = events[events.length - 1].timestamp;
|
|
20935
|
-
const
|
|
20936
|
-
if (
|
|
20937
|
-
__classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "No
|
|
21134
|
+
const chunk_seq = this.sessionManager.currentChunkSeq();
|
|
21135
|
+
if (chunk_seq === null) {
|
|
21136
|
+
__classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "No window state during upload — skipping chunk");
|
|
20938
21137
|
return;
|
|
20939
21138
|
}
|
|
20940
|
-
const chunk_seq = state.chunk_seq;
|
|
20941
|
-
const initial_url = chunk_seq === 0 ? state.initial_url : undefined;
|
|
20942
21139
|
const chunk_start_ts = new Date(start_ts_ms).toISOString();
|
|
20943
21140
|
const chunk_end_ts = new Date(end_ts_ms).toISOString();
|
|
20944
21141
|
if (this.mode === "manual") {
|
|
20945
21142
|
await this.onEvents({
|
|
20946
|
-
session_uuid: this.sessionUuid,
|
|
21143
|
+
session_uuid: this.sessionManager.sessionUuid,
|
|
21144
|
+
window_id: this.sessionManager.windowId,
|
|
20947
21145
|
chunk_seq,
|
|
20948
21146
|
chunk_start_ts,
|
|
20949
21147
|
chunk_end_ts,
|
|
20950
|
-
initial_url,
|
|
20951
21148
|
events,
|
|
20952
21149
|
});
|
|
20953
21150
|
}
|
|
20954
21151
|
else {
|
|
20955
21152
|
await uploadSessionEvents({
|
|
20956
21153
|
user_id: this.userId,
|
|
20957
|
-
session_uuid: this.sessionUuid,
|
|
21154
|
+
session_uuid: this.sessionManager.sessionUuid,
|
|
21155
|
+
window_id: this.sessionManager.windowId,
|
|
20958
21156
|
chunk_seq,
|
|
20959
21157
|
chunk_start_ts,
|
|
20960
21158
|
chunk_end_ts,
|
|
20961
|
-
initial_url,
|
|
20962
21159
|
events,
|
|
20963
21160
|
});
|
|
20964
21161
|
}
|
|
20965
21162
|
const removedBytes = events.reduce((sum, e) => sum + estimateEventSize(e), 0);
|
|
20966
21163
|
this.sessionEvents = this.sessionEvents.slice(snapshot_count);
|
|
20967
21164
|
this.bufferBytes = Math.max(0, this.bufferBytes - removedBytes);
|
|
20968
|
-
|
|
20969
|
-
if (after &&
|
|
20970
|
-
after.session_uuid === state.session_uuid &&
|
|
20971
|
-
after.chunk_seq === chunk_seq) {
|
|
20972
|
-
writeSessionState({ ...after, chunk_seq: chunk_seq + 1 });
|
|
20973
|
-
}
|
|
21165
|
+
this.sessionManager.commitChunkSeq(chunk_seq);
|
|
20974
21166
|
}
|
|
20975
21167
|
catch (err) {
|
|
20976
21168
|
__classPrivateFieldGet(this, _SessionRecorder_instances, "m", _SessionRecorder_log).call(this, "Event tracking failed", err);
|