@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
JavaScript
;
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