@v-tilt/browser 1.1.5 → 1.2.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/array.js +1 -1
- package/dist/array.js.map +1 -1
- package/dist/array.no-external.js +1 -1
- package/dist/array.no-external.js.map +1 -1
- package/dist/entrypoints/array.d.ts +1 -0
- package/dist/entrypoints/external-scripts-loader.d.ts +24 -0
- package/dist/entrypoints/module.es.d.ts +1 -0
- package/dist/entrypoints/recorder.d.ts +23 -0
- package/dist/extensions/replay/index.d.ts +13 -0
- package/dist/extensions/replay/session-recording-utils.d.ts +92 -0
- package/dist/extensions/replay/session-recording-wrapper.d.ts +61 -0
- package/dist/extensions/replay/session-recording.d.ts +95 -0
- package/dist/extensions/replay/types.d.ts +211 -0
- package/dist/external-scripts-loader.js +2 -0
- package/dist/external-scripts-loader.js.map +1 -0
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/module.d.ts +264 -5
- package/dist/module.js +1 -1
- package/dist/module.js.map +1 -1
- package/dist/module.no-external.d.ts +264 -5
- package/dist/module.no-external.js +1 -1
- package/dist/module.no-external.js.map +1 -1
- package/dist/recorder.js +2 -0
- package/dist/recorder.js.map +1 -0
- package/dist/types.d.ts +84 -4
- package/dist/utils/globals.d.ts +42 -0
- package/dist/vtilt.d.ts +36 -0
- package/lib/config.js +2 -0
- package/lib/entrypoints/array.d.ts +1 -0
- package/lib/entrypoints/array.js +1 -0
- package/lib/entrypoints/external-scripts-loader.d.ts +24 -0
- package/lib/entrypoints/external-scripts-loader.js +107 -0
- package/lib/entrypoints/module.es.d.ts +1 -0
- package/lib/entrypoints/module.es.js +1 -0
- package/lib/entrypoints/recorder.d.ts +23 -0
- package/lib/entrypoints/recorder.js +42 -0
- package/lib/extensions/replay/index.d.ts +13 -0
- package/lib/extensions/replay/index.js +31 -0
- package/lib/extensions/replay/session-recording-utils.d.ts +92 -0
- package/lib/extensions/replay/session-recording-utils.js +212 -0
- package/lib/extensions/replay/session-recording-wrapper.d.ts +61 -0
- package/lib/extensions/replay/session-recording-wrapper.js +149 -0
- package/lib/extensions/replay/session-recording.d.ts +95 -0
- package/lib/extensions/replay/session-recording.js +700 -0
- package/lib/extensions/replay/types.d.ts +211 -0
- package/lib/extensions/replay/types.js +8 -0
- package/lib/types.d.ts +84 -4
- package/lib/utils/globals.d.ts +42 -0
- package/lib/utils/globals.js +2 -0
- package/lib/vtilt.d.ts +36 -0
- package/lib/vtilt.js +106 -0
- package/package.json +4 -1
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Lazy Loaded Session Recording
|
|
4
|
+
*
|
|
5
|
+
* The actual rrweb session recording implementation.
|
|
6
|
+
* This is loaded on demand when recording is enabled.
|
|
7
|
+
*
|
|
8
|
+
* Based on PostHog's lazy-loaded-session-recorder.ts
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.LazyLoadedSessionRecording = exports.SESSION_RECORDING_BATCH_KEY = void 0;
|
|
12
|
+
const types_1 = require("@rrweb/types");
|
|
13
|
+
const globals_1 = require("../../utils/globals");
|
|
14
|
+
const utils_1 = require("../../utils");
|
|
15
|
+
const session_recording_utils_1 = require("./session-recording-utils");
|
|
16
|
+
const fflate_1 = require("fflate");
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Constants
|
|
19
|
+
// ============================================================================
|
|
20
|
+
const LOGGER_PREFIX = "[SessionRecording]";
|
|
21
|
+
const BASE_ENDPOINT = "/api/s";
|
|
22
|
+
const TWO_SECONDS = 2000;
|
|
23
|
+
const ONE_MINUTE = 60 * 1000;
|
|
24
|
+
const FIVE_MINUTES = 5 * ONE_MINUTE;
|
|
25
|
+
const DEFAULT_CANVAS_QUALITY = 0.4;
|
|
26
|
+
const DEFAULT_CANVAS_FPS = 4;
|
|
27
|
+
const MAX_CANVAS_FPS = 12;
|
|
28
|
+
const MAX_CANVAS_QUALITY = 1;
|
|
29
|
+
exports.SESSION_RECORDING_BATCH_KEY = "recordings";
|
|
30
|
+
// Retry configuration (PostHog-style exponential backoff)
|
|
31
|
+
const MAX_SNAPSHOT_RETRIES = 5;
|
|
32
|
+
const INITIAL_RETRY_DELAY_MS = 3000; // 3 seconds
|
|
33
|
+
const MAX_RETRY_DELAY_MS = 30000; // 30 seconds
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Helpers
|
|
36
|
+
// ============================================================================
|
|
37
|
+
function getRRWebRecord() {
|
|
38
|
+
var _a, _b;
|
|
39
|
+
return (_b = (_a = globals_1.assignableWindow === null || globals_1.assignableWindow === void 0 ? void 0 : globals_1.assignableWindow.__VTiltExtensions__) === null || _a === void 0 ? void 0 : _a.rrweb) === null || _b === void 0 ? void 0 : _b.record;
|
|
40
|
+
}
|
|
41
|
+
function newQueuedEvent(rrwebMethod) {
|
|
42
|
+
return {
|
|
43
|
+
rrwebMethod,
|
|
44
|
+
enqueuedAt: Date.now(),
|
|
45
|
+
attempt: 1,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
// ============================================================================
|
|
49
|
+
// Lazy Loaded Session Recording Class
|
|
50
|
+
// ============================================================================
|
|
51
|
+
class LazyLoadedSessionRecording {
|
|
52
|
+
constructor(instance, config = {}) {
|
|
53
|
+
this._endpoint = BASE_ENDPOINT;
|
|
54
|
+
// Recording state
|
|
55
|
+
this._captureStarted = false;
|
|
56
|
+
this._isIdle = "unknown";
|
|
57
|
+
this._lastActivityTimestamp = Date.now();
|
|
58
|
+
// IDs
|
|
59
|
+
this._sessionId = "";
|
|
60
|
+
this._windowId = "";
|
|
61
|
+
this._queuedRRWebEvents = [];
|
|
62
|
+
// ============================================================================
|
|
63
|
+
// Event Handlers
|
|
64
|
+
// ============================================================================
|
|
65
|
+
this._onBeforeUnload = () => {
|
|
66
|
+
this._flushBuffer();
|
|
67
|
+
};
|
|
68
|
+
this._onOffline = () => {
|
|
69
|
+
this._tryAddCustomEvent("browser offline", {});
|
|
70
|
+
};
|
|
71
|
+
this._onOnline = () => {
|
|
72
|
+
this._tryAddCustomEvent("browser online", {});
|
|
73
|
+
};
|
|
74
|
+
this._onVisibilityChange = () => {
|
|
75
|
+
if (globals_1.document === null || globals_1.document === void 0 ? void 0 : globals_1.document.visibilityState) {
|
|
76
|
+
const label = "window " + globals_1.document.visibilityState;
|
|
77
|
+
this._tryAddCustomEvent(label, {});
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
this._instance = instance;
|
|
81
|
+
this._config = config;
|
|
82
|
+
this._buffer = this._clearBuffer();
|
|
83
|
+
}
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// Public API (implements LazyLoadedSessionRecordingInterface)
|
|
86
|
+
// ============================================================================
|
|
87
|
+
get isStarted() {
|
|
88
|
+
return this._captureStarted;
|
|
89
|
+
}
|
|
90
|
+
/** @deprecated Use isStarted instead */
|
|
91
|
+
get started() {
|
|
92
|
+
return this._captureStarted;
|
|
93
|
+
}
|
|
94
|
+
get sessionId() {
|
|
95
|
+
return this._sessionId;
|
|
96
|
+
}
|
|
97
|
+
get status() {
|
|
98
|
+
if (!this._captureStarted) {
|
|
99
|
+
return "disabled";
|
|
100
|
+
}
|
|
101
|
+
if (this._isIdle === true) {
|
|
102
|
+
return "paused";
|
|
103
|
+
}
|
|
104
|
+
return "active";
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Start session recording (interface method)
|
|
108
|
+
*/
|
|
109
|
+
start(startReason) {
|
|
110
|
+
if (!this._isRecordingEnabled()) {
|
|
111
|
+
this.stop();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
this._startCapture(startReason);
|
|
115
|
+
// Add event listeners
|
|
116
|
+
if (globals_1.window) {
|
|
117
|
+
(0, utils_1.addEventListener)(globals_1.window, "beforeunload", this._onBeforeUnload);
|
|
118
|
+
(0, utils_1.addEventListener)(globals_1.window, "offline", this._onOffline);
|
|
119
|
+
(0, utils_1.addEventListener)(globals_1.window, "online", this._onOnline);
|
|
120
|
+
(0, utils_1.addEventListener)(globals_1.window, "visibilitychange", this._onVisibilityChange);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Stop session recording (interface method)
|
|
125
|
+
*/
|
|
126
|
+
stop() {
|
|
127
|
+
if (this._captureStarted && this._stopRrweb) {
|
|
128
|
+
this._stopRrweb();
|
|
129
|
+
this._stopRrweb = undefined;
|
|
130
|
+
this._captureStarted = false;
|
|
131
|
+
if (globals_1.window) {
|
|
132
|
+
globals_1.window.removeEventListener("beforeunload", this._onBeforeUnload);
|
|
133
|
+
globals_1.window.removeEventListener("offline", this._onOffline);
|
|
134
|
+
globals_1.window.removeEventListener("online", this._onOnline);
|
|
135
|
+
globals_1.window.removeEventListener("visibilitychange", this._onVisibilityChange);
|
|
136
|
+
}
|
|
137
|
+
this._clearBuffer();
|
|
138
|
+
if (this._fullSnapshotTimer) {
|
|
139
|
+
clearInterval(this._fullSnapshotTimer);
|
|
140
|
+
}
|
|
141
|
+
console.info(`${LOGGER_PREFIX} stopped`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** @deprecated Use stop() instead */
|
|
145
|
+
stopRecording() {
|
|
146
|
+
this.stop();
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Add a custom event to the recording
|
|
150
|
+
*/
|
|
151
|
+
addCustomEvent(tag, payload) {
|
|
152
|
+
return this._tryAddCustomEvent(tag, payload);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Take a full snapshot
|
|
156
|
+
*/
|
|
157
|
+
takeFullSnapshot() {
|
|
158
|
+
return this._tryTakeFullSnapshot();
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Log a message to the recording
|
|
162
|
+
*/
|
|
163
|
+
log(message, level = "log") {
|
|
164
|
+
this.onRRwebEmit({
|
|
165
|
+
type: 6, // Plugin event type
|
|
166
|
+
data: {
|
|
167
|
+
plugin: "rrweb/console@1",
|
|
168
|
+
payload: {
|
|
169
|
+
level,
|
|
170
|
+
trace: [],
|
|
171
|
+
payload: [JSON.stringify(message)],
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
timestamp: Date.now(),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Update configuration
|
|
179
|
+
*/
|
|
180
|
+
updateConfig(config) {
|
|
181
|
+
this._config = { ...this._config, ...config };
|
|
182
|
+
}
|
|
183
|
+
// ============================================================================
|
|
184
|
+
// Recording Control
|
|
185
|
+
// ============================================================================
|
|
186
|
+
_isRecordingEnabled() {
|
|
187
|
+
return !!this._config.enabled && !!globals_1.window;
|
|
188
|
+
}
|
|
189
|
+
_startCapture(startReason) {
|
|
190
|
+
// Check for required features
|
|
191
|
+
if (typeof Object.assign === "undefined" ||
|
|
192
|
+
typeof Array.from === "undefined") {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (this._captureStarted) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
this._captureStarted = true;
|
|
199
|
+
// Update session/window IDs
|
|
200
|
+
const sessionId = this._instance.getSessionId();
|
|
201
|
+
this._sessionId = sessionId || this._generateId();
|
|
202
|
+
this._windowId = this._generateId();
|
|
203
|
+
// Load rrweb if not already loaded
|
|
204
|
+
if (!getRRWebRecord()) {
|
|
205
|
+
this._loadRecorder(() => {
|
|
206
|
+
this._onScriptLoaded();
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
this._onScriptLoaded();
|
|
211
|
+
}
|
|
212
|
+
console.info(`${LOGGER_PREFIX} starting`);
|
|
213
|
+
if (startReason) {
|
|
214
|
+
this._reportStarted(startReason);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
_loadRecorder(callback) {
|
|
218
|
+
var _a;
|
|
219
|
+
// Use the external script loader to dynamically load the recorder
|
|
220
|
+
const loadExternalDependency = (_a = globals_1.assignableWindow.__VTiltExtensions__) === null || _a === void 0 ? void 0 : _a.loadExternalDependency;
|
|
221
|
+
if (!loadExternalDependency) {
|
|
222
|
+
console.error(`${LOGGER_PREFIX} loadExternalDependency not available. Make sure external-scripts-loader is initialized.`);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
loadExternalDependency(this._instance, "recorder", (err) => {
|
|
226
|
+
if (err) {
|
|
227
|
+
console.error(`${LOGGER_PREFIX} Could not load recorder:`, err);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
callback();
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
_onScriptLoaded() {
|
|
234
|
+
var _a, _b;
|
|
235
|
+
const rrwebRecord = getRRWebRecord();
|
|
236
|
+
if (!rrwebRecord) {
|
|
237
|
+
console.error(`${LOGGER_PREFIX} onScriptLoaded was called but rrwebRecord is not available`);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
// Build rrweb options
|
|
241
|
+
const sessionRecordingOptions = {
|
|
242
|
+
blockClass: this._config.blockClass || "vt-no-capture",
|
|
243
|
+
blockSelector: this._config.blockSelector,
|
|
244
|
+
ignoreClass: this._config.ignoreClass || "vt-ignore-input",
|
|
245
|
+
maskTextClass: this._config.maskTextClass || "vt-mask",
|
|
246
|
+
maskTextSelector: this._config.maskTextSelector,
|
|
247
|
+
maskTextFn: undefined,
|
|
248
|
+
maskAllInputs: (_a = this._config.maskAllInputs) !== null && _a !== void 0 ? _a : true,
|
|
249
|
+
maskInputOptions: { password: true, ...this._config.maskInputOptions },
|
|
250
|
+
maskInputFn: undefined,
|
|
251
|
+
slimDOMOptions: {},
|
|
252
|
+
collectFonts: false,
|
|
253
|
+
inlineStylesheet: true,
|
|
254
|
+
recordCrossOriginIframes: false,
|
|
255
|
+
};
|
|
256
|
+
// Canvas recording
|
|
257
|
+
const canvasConfig = this._getCanvasConfig();
|
|
258
|
+
if (canvasConfig.enabled) {
|
|
259
|
+
sessionRecordingOptions.recordCanvas = true;
|
|
260
|
+
sessionRecordingOptions.sampling = { canvas: canvasConfig.fps };
|
|
261
|
+
sessionRecordingOptions.dataURLOptions = {
|
|
262
|
+
type: "image/webp",
|
|
263
|
+
quality: canvasConfig.quality,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
// Masking
|
|
267
|
+
const maskingConfig = this._getMaskingConfig();
|
|
268
|
+
if (maskingConfig) {
|
|
269
|
+
sessionRecordingOptions.maskAllInputs =
|
|
270
|
+
(_b = maskingConfig.maskAllInputs) !== null && _b !== void 0 ? _b : true;
|
|
271
|
+
sessionRecordingOptions.maskTextSelector = maskingConfig.maskTextSelector;
|
|
272
|
+
sessionRecordingOptions.blockSelector = maskingConfig.blockSelector;
|
|
273
|
+
}
|
|
274
|
+
// Gather plugins
|
|
275
|
+
const plugins = this._gatherRRWebPlugins();
|
|
276
|
+
// Start recording
|
|
277
|
+
this._stopRrweb = rrwebRecord({
|
|
278
|
+
emit: (event) => {
|
|
279
|
+
this.onRRwebEmit(event);
|
|
280
|
+
},
|
|
281
|
+
plugins,
|
|
282
|
+
...sessionRecordingOptions,
|
|
283
|
+
});
|
|
284
|
+
// Reset activity tracking
|
|
285
|
+
this._lastActivityTimestamp = Date.now();
|
|
286
|
+
this._isIdle = typeof this._isIdle === "boolean" ? this._isIdle : "unknown";
|
|
287
|
+
// Add custom events for debugging
|
|
288
|
+
this._tryAddCustomEvent("$session_options", {
|
|
289
|
+
sessionRecordingOptions,
|
|
290
|
+
activePlugins: plugins.map((p) => p === null || p === void 0 ? void 0 : p.name),
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
_gatherRRWebPlugins() {
|
|
294
|
+
var _a, _b;
|
|
295
|
+
const plugins = [];
|
|
296
|
+
// Console recording plugin
|
|
297
|
+
if (this._config.captureConsole) {
|
|
298
|
+
try {
|
|
299
|
+
const getRecordConsolePlugin = (_b = (_a = globals_1.assignableWindow.__VTiltExtensions__) === null || _a === void 0 ? void 0 : _a.rrwebPlugins) === null || _b === void 0 ? void 0 : _b.getRecordConsolePlugin;
|
|
300
|
+
if (getRecordConsolePlugin) {
|
|
301
|
+
plugins.push(getRecordConsolePlugin());
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
catch (e) {
|
|
305
|
+
console.warn(`${LOGGER_PREFIX} Failed to load console plugin`, e);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return plugins;
|
|
309
|
+
}
|
|
310
|
+
// ============================================================================
|
|
311
|
+
// Event Processing
|
|
312
|
+
// ============================================================================
|
|
313
|
+
onRRwebEmit(rawEvent) {
|
|
314
|
+
this._processQueuedEvents();
|
|
315
|
+
if (!rawEvent || typeof rawEvent !== "object") {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
// Handle meta events (page URL)
|
|
319
|
+
if (rawEvent.type === types_1.EventType.Meta) {
|
|
320
|
+
const metaData = rawEvent.data;
|
|
321
|
+
this._lastHref = metaData.href;
|
|
322
|
+
}
|
|
323
|
+
// Skip if recording is paused
|
|
324
|
+
if ((0, session_recording_utils_1.isRecordingPausedEvent)(rawEvent)) {
|
|
325
|
+
// Allow recording paused events through
|
|
326
|
+
}
|
|
327
|
+
else if (this._isIdle === true) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
// Schedule full snapshot on full snapshot events
|
|
331
|
+
if (rawEvent.type === types_1.EventType.FullSnapshot) {
|
|
332
|
+
this._scheduleFullSnapshot();
|
|
333
|
+
}
|
|
334
|
+
// Truncate large console logs
|
|
335
|
+
const event = (0, session_recording_utils_1.truncateLargeConsoleLogs)(rawEvent);
|
|
336
|
+
// Update session/window IDs based on activity
|
|
337
|
+
this._updateWindowAndSessionIds(event);
|
|
338
|
+
// Skip idle events unless it's the idle marker
|
|
339
|
+
if (this._isIdle === true && !(0, session_recording_utils_1.isSessionIdleEvent)(event)) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
// Compress event if enabled
|
|
343
|
+
const eventToSend = this._config.compressEvents !== false ? (0, session_recording_utils_1.compressEvent)(event) : event;
|
|
344
|
+
const size = (0, session_recording_utils_1.estimateSize)(eventToSend);
|
|
345
|
+
const properties = {
|
|
346
|
+
$snapshot_bytes: size,
|
|
347
|
+
$snapshot_data: eventToSend,
|
|
348
|
+
$session_id: this._sessionId,
|
|
349
|
+
$window_id: this._windowId,
|
|
350
|
+
};
|
|
351
|
+
if (this.status === "disabled") {
|
|
352
|
+
this._clearBuffer();
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
this._captureSnapshotBuffered(properties);
|
|
356
|
+
}
|
|
357
|
+
_processQueuedEvents() {
|
|
358
|
+
if (this._queuedRRWebEvents.length === 0) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const itemsToProcess = [...this._queuedRRWebEvents];
|
|
362
|
+
this._queuedRRWebEvents = [];
|
|
363
|
+
itemsToProcess.forEach((queuedEvent) => {
|
|
364
|
+
if (Date.now() - queuedEvent.enqueuedAt <= TWO_SECONDS) {
|
|
365
|
+
this._tryRRWebMethod(queuedEvent);
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
_updateWindowAndSessionIds(event) {
|
|
370
|
+
const isUserInteraction = (0, session_recording_utils_1.isInteractiveEvent)(event);
|
|
371
|
+
// Check for idle state
|
|
372
|
+
if (!isUserInteraction && !this._isIdle) {
|
|
373
|
+
const timeSinceLastActivity = event.timestamp - this._lastActivityTimestamp;
|
|
374
|
+
const idleThreshold = this._config.sessionIdleThresholdMs || session_recording_utils_1.RECORDING_IDLE_THRESHOLD_MS;
|
|
375
|
+
if (timeSinceLastActivity > idleThreshold) {
|
|
376
|
+
this._isIdle = true;
|
|
377
|
+
// Clear full snapshot timer while idle
|
|
378
|
+
if (this._fullSnapshotTimer) {
|
|
379
|
+
clearInterval(this._fullSnapshotTimer);
|
|
380
|
+
}
|
|
381
|
+
this._tryAddCustomEvent("sessionIdle", {
|
|
382
|
+
eventTimestamp: event.timestamp,
|
|
383
|
+
lastActivityTimestamp: this._lastActivityTimestamp,
|
|
384
|
+
threshold: idleThreshold,
|
|
385
|
+
bufferLength: this._buffer.data.length,
|
|
386
|
+
bufferSize: this._buffer.size,
|
|
387
|
+
});
|
|
388
|
+
this._flushBuffer();
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
// Handle returning from idle
|
|
392
|
+
let returningFromIdle = false;
|
|
393
|
+
if (isUserInteraction) {
|
|
394
|
+
this._lastActivityTimestamp = event.timestamp;
|
|
395
|
+
if (this._isIdle) {
|
|
396
|
+
const idleWasUnknown = this._isIdle === "unknown";
|
|
397
|
+
this._isIdle = false;
|
|
398
|
+
if (!idleWasUnknown) {
|
|
399
|
+
this._tryAddCustomEvent("sessionNoLongerIdle", {
|
|
400
|
+
reason: "user activity",
|
|
401
|
+
type: event.type,
|
|
402
|
+
});
|
|
403
|
+
returningFromIdle = true;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (this._isIdle) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
// Check for session/window ID changes
|
|
411
|
+
const newSessionId = this._instance.getSessionId();
|
|
412
|
+
if (newSessionId && newSessionId !== this._sessionId) {
|
|
413
|
+
this._sessionId = newSessionId;
|
|
414
|
+
this._windowId = this._generateId();
|
|
415
|
+
this.stop();
|
|
416
|
+
this.start("session_id_changed");
|
|
417
|
+
}
|
|
418
|
+
else if (returningFromIdle) {
|
|
419
|
+
this._scheduleFullSnapshot();
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
// ============================================================================
|
|
423
|
+
// Buffer Management
|
|
424
|
+
// ============================================================================
|
|
425
|
+
_clearBuffer() {
|
|
426
|
+
this._buffer = {
|
|
427
|
+
size: 0,
|
|
428
|
+
data: [],
|
|
429
|
+
sessionId: this._sessionId,
|
|
430
|
+
windowId: this._windowId,
|
|
431
|
+
};
|
|
432
|
+
return this._buffer;
|
|
433
|
+
}
|
|
434
|
+
_flushBuffer() {
|
|
435
|
+
if (this._flushBufferTimer) {
|
|
436
|
+
clearTimeout(this._flushBufferTimer);
|
|
437
|
+
this._flushBufferTimer = undefined;
|
|
438
|
+
}
|
|
439
|
+
if (this.status === "buffering" ||
|
|
440
|
+
this.status === "paused" ||
|
|
441
|
+
this.status === "disabled") {
|
|
442
|
+
this._flushBufferTimer = setTimeout(() => {
|
|
443
|
+
this._flushBuffer();
|
|
444
|
+
}, session_recording_utils_1.RECORDING_BUFFER_TIMEOUT);
|
|
445
|
+
return this._buffer;
|
|
446
|
+
}
|
|
447
|
+
if (this._buffer.data.length > 0) {
|
|
448
|
+
const snapshotBuffers = (0, session_recording_utils_1.splitBuffer)(this._buffer);
|
|
449
|
+
snapshotBuffers.forEach((snapshotBuffer) => {
|
|
450
|
+
this._captureSnapshot({
|
|
451
|
+
$snapshot_bytes: snapshotBuffer.size,
|
|
452
|
+
$snapshot_data: snapshotBuffer.data,
|
|
453
|
+
$session_id: snapshotBuffer.sessionId,
|
|
454
|
+
$window_id: snapshotBuffer.windowId,
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
return this._clearBuffer();
|
|
459
|
+
}
|
|
460
|
+
_captureSnapshotBuffered(properties) {
|
|
461
|
+
var _a;
|
|
462
|
+
const additionalBytes = 2 + (((_a = this._buffer) === null || _a === void 0 ? void 0 : _a.data.length) || 0);
|
|
463
|
+
const snapshotBytes = properties.$snapshot_bytes;
|
|
464
|
+
if (!this._isIdle &&
|
|
465
|
+
(this._buffer.size + snapshotBytes + additionalBytes >
|
|
466
|
+
session_recording_utils_1.RECORDING_MAX_EVENT_SIZE ||
|
|
467
|
+
this._buffer.sessionId !== this._sessionId)) {
|
|
468
|
+
this._buffer = this._flushBuffer();
|
|
469
|
+
}
|
|
470
|
+
this._buffer.size += snapshotBytes;
|
|
471
|
+
this._buffer.data.push(properties.$snapshot_data);
|
|
472
|
+
if (!this._flushBufferTimer && !this._isIdle) {
|
|
473
|
+
this._flushBufferTimer = setTimeout(() => {
|
|
474
|
+
this._flushBuffer();
|
|
475
|
+
}, session_recording_utils_1.RECORDING_BUFFER_TIMEOUT);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
_captureSnapshot(properties) {
|
|
479
|
+
// Send directly to /s/ endpoint (dedicated session recording endpoint)
|
|
480
|
+
const config = this._instance.getConfig();
|
|
481
|
+
const apiHost = config.api_host || "";
|
|
482
|
+
const token = config.token || "";
|
|
483
|
+
if (!apiHost || !token) {
|
|
484
|
+
console.warn(`${LOGGER_PREFIX} Missing api_host or token, cannot send snapshot`);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
const useCompression = this._config.compressEvents !== false;
|
|
488
|
+
const url = `${apiHost.replace(/\/$/, "")}${this._endpoint}?token=${encodeURIComponent(token)}${useCompression ? "&compression=gzip-js" : ""}`;
|
|
489
|
+
// PostHog-compatible payload format
|
|
490
|
+
const payload = {
|
|
491
|
+
token,
|
|
492
|
+
distinct_id: this._instance.getDistinctId() || "",
|
|
493
|
+
$session_id: this._sessionId,
|
|
494
|
+
$window_id: this._windowId,
|
|
495
|
+
$snapshot_data: properties.$snapshot_data,
|
|
496
|
+
$snapshot_bytes: properties.$snapshot_bytes,
|
|
497
|
+
$lib: "web",
|
|
498
|
+
$lib_version: this._instance.version || "unknown",
|
|
499
|
+
timestamp: Date.now(),
|
|
500
|
+
};
|
|
501
|
+
// Send using beacon or fetch
|
|
502
|
+
this._sendSnapshot(url, payload, useCompression);
|
|
503
|
+
}
|
|
504
|
+
_sendSnapshot(url, payload, useCompression) {
|
|
505
|
+
const payloadString = JSON.stringify(payload);
|
|
506
|
+
// Check if payload contains FullSnapshot (type 2)
|
|
507
|
+
// FullSnapshots are critical and should use fetch (not sendBeacon) to ensure delivery
|
|
508
|
+
const snapshotData = payload.$snapshot_data;
|
|
509
|
+
const hasFullSnapshot = Array.isArray(snapshotData) && snapshotData.some((e) => (e === null || e === void 0 ? void 0 : e.type) === 2);
|
|
510
|
+
// sendBeacon has ~64KB limit - use fetch for larger payloads or payloads with FullSnapshot
|
|
511
|
+
const BEACON_SIZE_LIMIT = 60000; // 60KB to be safe
|
|
512
|
+
const shouldUseFetch = payloadString.length > BEACON_SIZE_LIMIT || hasFullSnapshot;
|
|
513
|
+
// Try to compress if enabled
|
|
514
|
+
if (useCompression && typeof globals_1.window !== "undefined") {
|
|
515
|
+
try {
|
|
516
|
+
const compressed = (0, fflate_1.gzipSync)((0, fflate_1.strToU8)(payloadString));
|
|
517
|
+
// Create blob from Uint8Array - cast to any to avoid TypeScript issues with ArrayBufferLike
|
|
518
|
+
const blob = new Blob([compressed], {
|
|
519
|
+
type: "application/octet-stream",
|
|
520
|
+
});
|
|
521
|
+
// Use fetch for large payloads or those with FullSnapshot
|
|
522
|
+
if (shouldUseFetch) {
|
|
523
|
+
this._fetchWithRetry(url, blob, "application/octet-stream");
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
// Try sendBeacon for smaller payloads without FullSnapshot
|
|
527
|
+
if (typeof navigator !== "undefined" && navigator.sendBeacon) {
|
|
528
|
+
const sent = navigator.sendBeacon(url, blob);
|
|
529
|
+
if (sent) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
// Fall back to fetch with retry
|
|
534
|
+
this._fetchWithRetry(url, blob, "application/octet-stream");
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
catch (_a) {
|
|
538
|
+
// Fall through to uncompressed
|
|
539
|
+
console.warn(`${LOGGER_PREFIX} Compression failed, sending uncompressed`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
// Uncompressed fallback
|
|
543
|
+
const uncompressedUrl = url.replace("&compression=gzip-js", "");
|
|
544
|
+
// Use fetch for large payloads or those with FullSnapshot
|
|
545
|
+
if (shouldUseFetch) {
|
|
546
|
+
const blob = new Blob([payloadString], { type: "application/json" });
|
|
547
|
+
this._fetchWithRetry(uncompressedUrl, blob, "application/json");
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (typeof navigator !== "undefined" && navigator.sendBeacon) {
|
|
551
|
+
const blob = new Blob([payloadString], { type: "application/json" });
|
|
552
|
+
const sent = navigator.sendBeacon(uncompressedUrl, blob);
|
|
553
|
+
if (sent) {
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
// Fall back to fetch with retry
|
|
558
|
+
const blob = new Blob([payloadString], { type: "application/json" });
|
|
559
|
+
this._fetchWithRetry(uncompressedUrl, blob, "application/json");
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Fetch with exponential backoff retry (PostHog-style)
|
|
563
|
+
* Retries on network errors and 5xx server errors
|
|
564
|
+
*/
|
|
565
|
+
_fetchWithRetry(url, body, contentType, attempt = 0) {
|
|
566
|
+
// keepalive has a 64KB limit - disable for larger payloads (like FullSnapshots)
|
|
567
|
+
const KEEPALIVE_THRESHOLD = 60000; // 60KB to be safe
|
|
568
|
+
const useKeepalive = body.size < KEEPALIVE_THRESHOLD;
|
|
569
|
+
fetch(url, {
|
|
570
|
+
method: "POST",
|
|
571
|
+
body,
|
|
572
|
+
headers: {
|
|
573
|
+
"Content-Type": contentType,
|
|
574
|
+
},
|
|
575
|
+
keepalive: useKeepalive,
|
|
576
|
+
})
|
|
577
|
+
.then((response) => {
|
|
578
|
+
// Success - no action needed
|
|
579
|
+
if (response.ok) {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
// Client error (4xx) - don't retry
|
|
583
|
+
if (response.status >= 400 && response.status < 500) {
|
|
584
|
+
console.warn(`${LOGGER_PREFIX} Snapshot rejected by server (${response.status}), not retrying`);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
// Server error (5xx) - retry
|
|
588
|
+
this._scheduleRetry(url, body, contentType, attempt);
|
|
589
|
+
})
|
|
590
|
+
.catch(() => {
|
|
591
|
+
// Network error - retry
|
|
592
|
+
this._scheduleRetry(url, body, contentType, attempt);
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Schedule a retry with exponential backoff and jitter
|
|
597
|
+
*/
|
|
598
|
+
_scheduleRetry(url, body, contentType, attempt) {
|
|
599
|
+
if (attempt >= MAX_SNAPSHOT_RETRIES) {
|
|
600
|
+
console.warn(`${LOGGER_PREFIX} Failed to send snapshot after ${MAX_SNAPSHOT_RETRIES} retries, giving up`);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
// Exponential backoff with jitter: 3s, 6s, 12s, 24s... capped at 30s
|
|
604
|
+
const baseDelay = INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt);
|
|
605
|
+
const cappedDelay = Math.min(baseDelay, MAX_RETRY_DELAY_MS);
|
|
606
|
+
const jitter = (Math.random() - 0.5) * cappedDelay; // +/- 50%
|
|
607
|
+
const delay = Math.ceil(cappedDelay + jitter);
|
|
608
|
+
console.warn(`${LOGGER_PREFIX} Snapshot send failed, retrying in ${Math.round(delay / 1000)}s (attempt ${attempt + 1}/${MAX_SNAPSHOT_RETRIES})`);
|
|
609
|
+
setTimeout(() => {
|
|
610
|
+
this._fetchWithRetry(url, body, contentType, attempt + 1);
|
|
611
|
+
}, delay);
|
|
612
|
+
}
|
|
613
|
+
// ============================================================================
|
|
614
|
+
// Snapshot Scheduling
|
|
615
|
+
// ============================================================================
|
|
616
|
+
_scheduleFullSnapshot() {
|
|
617
|
+
if (this._fullSnapshotTimer) {
|
|
618
|
+
clearInterval(this._fullSnapshotTimer);
|
|
619
|
+
}
|
|
620
|
+
if (this._isIdle === true) {
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
const interval = this._config.fullSnapshotIntervalMs || FIVE_MINUTES;
|
|
624
|
+
if (!interval) {
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
this._fullSnapshotTimer = setInterval(() => {
|
|
628
|
+
this._tryTakeFullSnapshot();
|
|
629
|
+
}, interval);
|
|
630
|
+
}
|
|
631
|
+
// ============================================================================
|
|
632
|
+
// RRWeb Method Helpers
|
|
633
|
+
// ============================================================================
|
|
634
|
+
_tryRRWebMethod(queuedEvent) {
|
|
635
|
+
try {
|
|
636
|
+
queuedEvent.rrwebMethod();
|
|
637
|
+
return true;
|
|
638
|
+
}
|
|
639
|
+
catch (e) {
|
|
640
|
+
if (this._queuedRRWebEvents.length < 10) {
|
|
641
|
+
this._queuedRRWebEvents.push({
|
|
642
|
+
enqueuedAt: queuedEvent.enqueuedAt || Date.now(),
|
|
643
|
+
attempt: queuedEvent.attempt + 1,
|
|
644
|
+
rrwebMethod: queuedEvent.rrwebMethod,
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
else {
|
|
648
|
+
console.warn(`${LOGGER_PREFIX} Could not emit queued rrweb event`, e, queuedEvent);
|
|
649
|
+
}
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
_tryAddCustomEvent(tag, payload) {
|
|
654
|
+
return this._tryRRWebMethod(newQueuedEvent(() => getRRWebRecord().addCustomEvent(tag, payload)));
|
|
655
|
+
}
|
|
656
|
+
_tryTakeFullSnapshot() {
|
|
657
|
+
return this._tryRRWebMethod(newQueuedEvent(() => getRRWebRecord().takeFullSnapshot()));
|
|
658
|
+
}
|
|
659
|
+
// ============================================================================
|
|
660
|
+
// Configuration Helpers
|
|
661
|
+
// ============================================================================
|
|
662
|
+
_getCanvasConfig() {
|
|
663
|
+
var _a, _b, _c;
|
|
664
|
+
const captureCanvas = this._config.captureCanvas;
|
|
665
|
+
const enabled = (_a = captureCanvas === null || captureCanvas === void 0 ? void 0 : captureCanvas.recordCanvas) !== null && _a !== void 0 ? _a : false;
|
|
666
|
+
const fps = (_b = captureCanvas === null || captureCanvas === void 0 ? void 0 : captureCanvas.canvasFps) !== null && _b !== void 0 ? _b : DEFAULT_CANVAS_FPS;
|
|
667
|
+
const quality = (_c = captureCanvas === null || captureCanvas === void 0 ? void 0 : captureCanvas.canvasQuality) !== null && _c !== void 0 ? _c : DEFAULT_CANVAS_QUALITY;
|
|
668
|
+
return {
|
|
669
|
+
enabled,
|
|
670
|
+
fps: (0, session_recording_utils_1.clampToRange)(fps, 0, MAX_CANVAS_FPS, "canvas recording fps", DEFAULT_CANVAS_FPS),
|
|
671
|
+
quality: (0, session_recording_utils_1.clampToRange)(quality, 0, MAX_CANVAS_QUALITY, "canvas recording quality", DEFAULT_CANVAS_QUALITY),
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
_getMaskingConfig() {
|
|
675
|
+
var _a;
|
|
676
|
+
const masking = this._config.masking;
|
|
677
|
+
if (!masking) {
|
|
678
|
+
return undefined;
|
|
679
|
+
}
|
|
680
|
+
return {
|
|
681
|
+
maskAllInputs: (_a = masking.maskAllInputs) !== null && _a !== void 0 ? _a : true,
|
|
682
|
+
maskTextSelector: masking.maskTextSelector,
|
|
683
|
+
blockSelector: masking.blockSelector,
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
// ============================================================================
|
|
687
|
+
// Utility Methods
|
|
688
|
+
// ============================================================================
|
|
689
|
+
_generateId() {
|
|
690
|
+
return Math.random().toString(36).substring(2, 15);
|
|
691
|
+
}
|
|
692
|
+
_reportStarted(startReason, tagPayload) {
|
|
693
|
+
console.info(`${LOGGER_PREFIX} ${startReason.replace(/_/g, " ")}`, tagPayload);
|
|
694
|
+
if (startReason !== "recording_initialized" &&
|
|
695
|
+
startReason !== "session_id_changed") {
|
|
696
|
+
this._tryAddCustomEvent(startReason, tagPayload);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
exports.LazyLoadedSessionRecording = LazyLoadedSessionRecording;
|