/** * Lazy Loaded Session Recording * * The actual rrweb session recording implementation. * This is loaded on demand when recording is enabled. * * Based on PostHog's lazy-loaded-session-recorder.ts */ import { type eventWithTime } from "@rrweb/types"; import type { VTilt } from "../../vtilt"; import { LazyLoadedSessionRecordingInterface } from "../../utils/globals"; import type { SessionRecordingConfig, SessionRecordingStatus } from "./types"; export declare const SESSION_RECORDING_BATCH_KEY = "recordings"; export declare class LazyLoadedSessionRecording implements LazyLoadedSessionRecordingInterface { private _instance; private _endpoint; private _flushBufferTimer?; private _fullSnapshotTimer?; private _captureStarted; private _stopRrweb?; private _isIdle; private _lastActivityTimestamp; private _lastHref?; private _sessionFirstEventTs; private _sessionLastEventTs; /** Prevents duplicate hard flushes when multiple unload signals fire. */ private _lifecycleFlushDone; private _sessionId; private _windowId; private _distinctId; private _sampledOut; private _unsubscribeIdentified?; private _unsubscribeReset?; private _buffer; private _queuedRRWebEvents; private _config; constructor(instance: VTilt, config?: SessionRecordingConfig); get isStarted(): boolean; /** @deprecated Use isStarted instead */ get started(): boolean; get sessionId(): string; get status(): SessionRecordingStatus; /** * Keep batch `$current_url` aligned with analytics SPA `$pageview`. * On a pathname-significant change, take a FullSnapshot so rrweb emits * Meta(href)+FullSnapshot — the native page boundary (MPA already does this * on full loads; History navigations previously only updated `_lastHref`). */ setLastHref(href: string): void; /** * Start session recording (interface method) */ start(startReason?: string): void; /** * Stop session recording (interface method) */ stop(): void; /** @deprecated Use stop() instead */ stopRecording(): void; /** * Add a custom event to the recording */ addCustomEvent(tag: string, payload: unknown): boolean; /** * Take a full snapshot */ takeFullSnapshot(): boolean; /** * Log a message to the recording */ log(message: string, level?: "log" | "warn" | "error"): void; /** * Update configuration */ updateConfig(config: Partial): void; private _onBeforeUnload; private _onPageHide; private _onOffline; private _onOnline; private _onVisibilityChange; private _softFlush; private _hardFlushForLifecycle; private _isRecordingEnabled; /** Sticky per-session sample decision (sessionStorage). */ private _isSampledIn; private _currentDistinctId; private _subscribeIdentityLifecycle; private _unsubscribeIdentityLifecycle; /** * Flush under buffer stamps (pre-change identity), then adopt live * session/window/distinct and take a FullSnapshot. */ private _onIdentityBoundary; private _adoptLiveIds; private _startCapture; private _loadRecorder; private _onScriptLoaded; private _gatherRRWebPlugins; onRRwebEmit(rawEvent: eventWithTime): void; private _processQueuedEvents; private _updateWindowAndSessionIds; private _clearBuffer; private _trackEventTimestamp; private _getRecordingSpanMs; private _meetsMinimumDuration; private _flushBuffer; /** * Flush the buffer. Soft flushes never clear on failure. Hard flushes only * clear when send succeeds (bfcache may revive the page after pagehide). * Identity boundaries pass bypassMinimum so pre-identify pixels are not * held under a post-identify stamp. */ private _forceFlushBuffer; private _sendBufferContents; private _captureSnapshotBuffered; private _captureSnapshot; /** * Send a snapshot batch to the ingestion endpoint. * * Transport selection: * * - FullSnapshots, or batches over `BEACON_SIZE_LIMIT`, always use * fetch (with retry). They're too important / too big for sendBeacon. * - Smaller batches use `sendBeacon` first. * * Encoding selection (this is the part that fixes the server-side * `Z_BUF_ERROR`): * * - **fetch path** sends the gzip bytes as a binary `Blob` and uses * `?compression=gzip-js`. Reliable because the request lifetime is * bounded by the JS context. * - **sendBeacon path** sends the gzip bytes as a base64 *string* and * uses `?compression=base64`. `sendBeacon(url, Blob([binary]))` * races with tab discard / `pagehide`: the browser queues the * beacon synchronously and serializes the body after JS is torn * down, by which time the Uint8Array's backing `ArrayBuffer` may * have been detached. A string-backed Blob owns its own UTF-8 copy * and survives unload. The server already understands both flags. * * The query-param flag is *only* added once we know the encoded body * actually carries that compression — never up front from configuration — * so the URL can never claim compression while the body is empty/JSON. */ private _sendSnapshot; /** * fetch path — binary gzip body, `compression=gzip-js`. Auth travels * in the `x-api-key` header (see `_sendSnapshot` for the rationale). */ private _sendViaFetch; /** * sendBeacon path — base64 text body, `compression=base64`. The token * must be on the URL (caller pre-built `baseUrl` with * `includeTokenQuery: true`) because beacons cannot carry headers. * Falls back to fetch+keepalive — same URL, no header — if the beacon * is refused, so the two transports look identical on the wire. */ private _sendViaBeacon; /** * Fetch with exponential backoff retry (PostHog-style) * Retries on network errors and 5xx server errors. * * When `token` is non-empty the request is authenticated via the * `x-api-key` header (clean URL, harder to block). When empty the * caller has already authenticated via `?token=` in the URL (used by * the beacon-fallback path where the URL must match the beacon's). */ private _fetchWithRetry; /** * Schedule a retry with exponential backoff and jitter */ private _scheduleRetry; private _scheduleFullSnapshot; private _tryRRWebMethod; private _tryAddCustomEvent; private _tryTakeFullSnapshot; private _getCanvasConfig; private _getMaskingConfig; private _generateId; private _reportStarted; }