UNPKG

overcentric

Version:

Overcentric watches your website, product, and users - and tells you what matters and what to do about it.

226 lines (225 loc) 8.24 kB
import { record } from "@rrweb/record"; import { CONFIG, log } from "./config"; import { getDeviceId } from "./identity"; const DEFAULT_MAX_RECORDING_DURATION = 90 * 60 * 1000; const MAX_EVENTS_IN_MEMORY = 1000; const MIN_RECORDING_DURATION = 1 * 1000; let activeRecording = null; function stopActiveRecording() { if (activeRecording) { if (activeRecording.stopFn) { activeRecording.stopFn(); } if (activeRecording.flushInterval) { clearInterval(activeRecording.flushInterval); } if (activeRecording.maxDurationTimeout) { clearTimeout(activeRecording.maxDurationTimeout); } if (activeRecording.beforeUnloadHandler) { try { window.removeEventListener("beforeunload", activeRecording.beforeUnloadHandler); } catch (e) { } } if (activeRecording.visibilityHandler) { try { document.removeEventListener("visibilitychange", activeRecording.visibilityHandler); } catch (e) { } } activeRecording = null; delete window.overcentricStopRecording; log.debug("Active recording stopped and cleaned up"); } } async function initRecording(sessionId, maxDuration = DEFAULT_MAX_RECORDING_DURATION) { var _a; if (activeRecording) { if (activeRecording.sessionId === sessionId) { log.debug("Recording already active for this session, skipping"); return; } log.debug("New session detected, stopping previous recording"); stopActiveRecording(); } log.debug("Starting recording"); const deviceId = getDeviceId(); const identityId = (_a = window.overcentricUserIdentity) === null || _a === void 0 ? void 0 : _a.uniqueIdentifier; const events = []; const BATCH_SIZE = 50; const recordingStartTime = Date.now(); let isRecordingStopped = false; try { const response = await fetch(`${CONFIG.basePath}/recordings`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ session_id: sessionId, device_id: deviceId, identity_id: identityId, project_id: window.overcentricProjectId, context: window.overcentricContext, hostname: window.location.hostname, pathname: window.location.pathname, }), }); if (!response.ok) { throw new Error(`Failed to register recording: ${response.status}`); } log.debug("Recording session registered"); } catch (error) { log.error("Failed to register recording session", error); return; } const sendRecordingEvents = async (eventsToSend) => { if (!eventsToSend.length) return; try { const response = await fetch(`${CONFIG.basePath}/recordings/${sessionId}/chunks`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ project_id: window.overcentricProjectId, events: eventsToSend, timestamp: Date.now(), }), }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } log.debug(`Sent ${eventsToSend.length} recording events`); } catch (error) { log.error("Failed to send recording events", error); events.length = 0; } }; const stopRecording = () => { if (!isRecordingStopped) { isRecordingStopped = true; if (events.length > 0) { sendRecordingEvents(events.splice(0)); } stopActiveRecording(); log.debug("Recording stopped"); } }; const visibilityHandler = () => { if (document.visibilityState === "hidden") { log.debug("Tab hidden, stopping recording"); stopRecording(); } }; document.addEventListener("visibilitychange", visibilityHandler); const flushInterval = setInterval(() => { if (isRecordingStopped) return; if (Date.now() - recordingStartTime >= maxDuration) { log.debug(`Maximum recording duration of ${maxDuration / 1000}s reached, stopping recording`); stopRecording(); return; } if (events.length >= MAX_EVENTS_IN_MEMORY) { log.warn(`Too many events in memory (${events.length}), forcing flush`); sendRecordingEvents(events.splice(0)); } else if (events.length > 0) { sendRecordingEvents(events.splice(0)); } }, 2000); const beforeUnloadHandler = () => { const recordingDuration = Date.now() - recordingStartTime; if (events.length > 0 && !isRecordingStopped && recordingDuration >= MIN_RECORDING_DURATION) { try { const blob = new Blob([ JSON.stringify({ project_id: window.overcentricProjectId, events: events.splice(0), timestamp: Date.now(), }), ], { type: "application/json" }); const success = navigator.sendBeacon(`${CONFIG.basePath}/recordings/${sessionId}/chunks`, blob); if (!success) { log.warn("sendBeacon failed, data may be lost on page unload"); } } catch (error) { log.warn("Failed to send events on page unload"); } } stopRecording(); }; window.addEventListener("beforeunload", beforeUnloadHandler); let maxDurationTimeout = null; if (maxDuration > 0) { maxDurationTimeout = setTimeout(stopRecording, maxDuration); } window.overcentricStopRecording = stopRecording; const stopFn = record({ emit(event) { if (isRecordingStopped) return; events.push(event); if (events.length >= MAX_EVENTS_IN_MEMORY) { log.warn(`Event buffer full (${events.length}), forcing immediate flush`); sendRecordingEvents(events.splice(0)); } else if (events.length >= BATCH_SIZE) { sendRecordingEvents(events.splice(0)); } }, maskInputOptions: { password: true }, recordCanvas: true, sampling: { mousemove: true, scroll: 150, input: "last", canvas: 2, }, recordCrossOriginIframes: false, slimDOMOptions: { script: true, comment: true, headFavicon: true, headWhitespace: true, headMetaSocial: true, headMetaRobots: true, headMetaHttpEquiv: true, headMetaVerification: true, headMetaAuthorship: true, }, }); activeRecording = { sessionId, stopFn: stopFn || null, flushInterval, maxDurationTimeout, beforeUnloadHandler, visibilityHandler, }; } async function updateRecordingIdentity(sessionId, userIdentity) { try { const response = await fetch(`${CONFIG.basePath}/recordings/${sessionId}/identity`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ session_id: sessionId, identity_id: userIdentity.uniqueIdentifier, project_id: window.overcentricProjectId, }), }); if (!response.ok) { throw new Error(`Failed to update recording identity: ${response.status}`); } log.debug("Recording identity updated"); } catch (error) { log.error("Failed to update recording identity", error); } } export { initRecording, updateRecordingIdentity, stopActiveRecording };