@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.
Files changed (53) hide show
  1. package/dist/array.js +1 -1
  2. package/dist/array.js.map +1 -1
  3. package/dist/array.no-external.js +1 -1
  4. package/dist/array.no-external.js.map +1 -1
  5. package/dist/entrypoints/array.d.ts +1 -0
  6. package/dist/entrypoints/external-scripts-loader.d.ts +24 -0
  7. package/dist/entrypoints/module.es.d.ts +1 -0
  8. package/dist/entrypoints/recorder.d.ts +23 -0
  9. package/dist/extensions/replay/index.d.ts +13 -0
  10. package/dist/extensions/replay/session-recording-utils.d.ts +92 -0
  11. package/dist/extensions/replay/session-recording-wrapper.d.ts +61 -0
  12. package/dist/extensions/replay/session-recording.d.ts +95 -0
  13. package/dist/extensions/replay/types.d.ts +211 -0
  14. package/dist/external-scripts-loader.js +2 -0
  15. package/dist/external-scripts-loader.js.map +1 -0
  16. package/dist/main.js +1 -1
  17. package/dist/main.js.map +1 -1
  18. package/dist/module.d.ts +264 -5
  19. package/dist/module.js +1 -1
  20. package/dist/module.js.map +1 -1
  21. package/dist/module.no-external.d.ts +264 -5
  22. package/dist/module.no-external.js +1 -1
  23. package/dist/module.no-external.js.map +1 -1
  24. package/dist/recorder.js +2 -0
  25. package/dist/recorder.js.map +1 -0
  26. package/dist/types.d.ts +84 -4
  27. package/dist/utils/globals.d.ts +42 -0
  28. package/dist/vtilt.d.ts +36 -0
  29. package/lib/config.js +2 -0
  30. package/lib/entrypoints/array.d.ts +1 -0
  31. package/lib/entrypoints/array.js +1 -0
  32. package/lib/entrypoints/external-scripts-loader.d.ts +24 -0
  33. package/lib/entrypoints/external-scripts-loader.js +107 -0
  34. package/lib/entrypoints/module.es.d.ts +1 -0
  35. package/lib/entrypoints/module.es.js +1 -0
  36. package/lib/entrypoints/recorder.d.ts +23 -0
  37. package/lib/entrypoints/recorder.js +42 -0
  38. package/lib/extensions/replay/index.d.ts +13 -0
  39. package/lib/extensions/replay/index.js +31 -0
  40. package/lib/extensions/replay/session-recording-utils.d.ts +92 -0
  41. package/lib/extensions/replay/session-recording-utils.js +212 -0
  42. package/lib/extensions/replay/session-recording-wrapper.d.ts +61 -0
  43. package/lib/extensions/replay/session-recording-wrapper.js +149 -0
  44. package/lib/extensions/replay/session-recording.d.ts +95 -0
  45. package/lib/extensions/replay/session-recording.js +700 -0
  46. package/lib/extensions/replay/types.d.ts +211 -0
  47. package/lib/extensions/replay/types.js +8 -0
  48. package/lib/types.d.ts +84 -4
  49. package/lib/utils/globals.d.ts +42 -0
  50. package/lib/utils/globals.js +2 -0
  51. package/lib/vtilt.d.ts +36 -0
  52. package/lib/vtilt.js +106 -0
  53. 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;