UNPKG

@carpetai/rrweb-recorder

Version:

A React component for recording user sessions using rrweb. Meant to be used with CarpetAI's Analytics Agent.

211 lines (205 loc) 7.07 kB
'use strict'; var jsxRuntime = require('react/jsx-runtime'); var rrweb = require('rrweb'); var react = require('react'); const defaultSaveSessionData = async (sessionData, apiKey, apiUrl) => { const url = apiUrl || '/api/session-replay'; try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ ...sessionData, apiKey }) }); if (!response.ok) { throw new Error(`Failed to save session data: ${response.statusText}`); } } catch (error) { throw error; } }; const getOrCreateSessionId = (providedSessionId) => { if (typeof window === 'undefined') return ''; if (providedSessionId) { return providedSessionId; } let sessionId = sessionStorage.getItem('rrweb_session_id'); if (!sessionId) { sessionId = crypto.randomUUID(); sessionStorage.setItem('rrweb_session_id', sessionId); } return sessionId; }; const shouldExcludePath = (excludePaths) => { if (!excludePaths || typeof window === 'undefined') return false; const currentUrl = window.location.href; return excludePaths.some(path => currentUrl.startsWith(path)); }; const createMetaEvent = () => { if (typeof window === 'undefined') return null; return { type: 4, timestamp: Date.now(), data: { href: window.location.href, url: window.location.href, width: window.innerWidth, height: window.innerHeight, title: document.title, userAgent: navigator.userAgent, viewport: { width: window.innerWidth, height: window.innerHeight } } }; }; function useSessionRecorder({ autoStart = true, apiKey, apiUrl = "https://carpet-engine-437efa6609f6.herokuapp.com/api/session-events", maxSessionDuration = 30 * 60 * 1000, saveInterval = 5000, excludePaths = ['http://localhost', 'https://localhost', 'http://127.0.0.1', 'https://127.0.0.1'], recordCanvas = false, recordCrossOriginIframes = false, onSessionStart, onSessionStop, onError, maskAllInputs = false, }) { const [isRecording, setIsRecording] = react.useState(false); const sessionIdRef = react.useRef(null); const eventsRef = react.useRef([]); const recorderRef = react.useRef(undefined); const saveIntervalRef = react.useRef(null); const sessionStartTimeRef = react.useRef(0); const lastSavedIndexRef = react.useRef(0); const emit = (event) => { eventsRef.current.push(event); }; const saveEvents = async () => { const currentSessionId = sessionIdRef.current; if (eventsRef.current.length === 0 || !currentSessionId) return; try { const newEvents = eventsRef.current.slice(lastSavedIndexRef.current); if (newEvents.length === 0) return; const sessionData = { sessionId: currentSessionId, events: newEvents, timestamp: Date.now(), }; if (apiKey) { await defaultSaveSessionData(sessionData, apiKey, apiUrl); } lastSavedIndexRef.current = eventsRef.current.length; } catch (error) { onError === null || onError === void 0 ? void 0 : onError(error); } }; const addMetaEvent = () => { if (!isRecording || typeof window === 'undefined') return; const metaEvent = createMetaEvent(); if (metaEvent) { eventsRef.current.push(metaEvent); } }; const startRecording = () => { if (isRecording || typeof window === 'undefined') return; const currentSessionId = getOrCreateSessionId(); sessionIdRef.current = currentSessionId; sessionStartTimeRef.current = Date.now(); lastSavedIndexRef.current = 0; eventsRef.current = []; try { recorderRef.current = rrweb.record({ emit, recordCanvas, recordCrossOriginIframes, maskAllInputs, }); setIsRecording(true); onSessionStart === null || onSessionStart === void 0 ? void 0 : onSessionStart(currentSessionId); addMetaEvent(); saveIntervalRef.current = setInterval(saveEvents, saveInterval); setTimeout(() => { if (isRecording) { stopRecording(); } }, maxSessionDuration); } catch (error) { onError === null || onError === void 0 ? void 0 : onError(error); } }; const stopRecording = () => { if (!isRecording) return; if (recorderRef.current) { recorderRef.current(); recorderRef.current = undefined; } if (saveIntervalRef.current) { clearInterval(saveIntervalRef.current); saveIntervalRef.current = null; } setIsRecording(false); onSessionStop === null || onSessionStop === void 0 ? void 0 : onSessionStop(sessionIdRef.current); saveEvents(); }; react.useEffect(() => { if (autoStart && !shouldExcludePath(excludePaths)) { const timer = setTimeout(() => { startRecording(); }, 1000); return () => clearTimeout(timer); } }, [autoStart, excludePaths]); react.useEffect(() => { if (typeof window === 'undefined') return; const handlePopState = () => { if (isRecording) { setTimeout(() => { addMetaEvent(); }, 100); } }; const handleHashChange = () => { if (isRecording) { setTimeout(() => { addMetaEvent(); }, 100); } }; window.addEventListener('popstate', handlePopState); window.addEventListener('hashchange', handleHashChange); return () => { window.removeEventListener('popstate', handlePopState); window.removeEventListener('hashchange', handleHashChange); }; }, [isRecording]); react.useEffect(() => { return () => { if (isRecording) { stopRecording(); } }; }, []); return { isRecording, sessionId: sessionIdRef.current, startRecording, stopRecording }; } function SessionRecorderInner(props) { useSessionRecorder(props); return null; } function SessionRecorder(props) { return (jsxRuntime.jsx(SessionRecorderInner, { ...props })); } exports.SessionRecorder = SessionRecorder; exports.useSessionRecorder = useSessionRecorder; //# sourceMappingURL=index.js.map