UNPKG

visitory

Version:

Visitory Session Tracking SDK

350 lines (349 loc) 12.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SessionTracker = void 0; const DEFAULT_API_ENDPOINT = 'https://qualified-hound-398.convex.site'; class SessionTracker { constructor(apiKey, apiEndpoint = DEFAULT_API_ENDPOINT) { this.events = []; this.isRecording = false; this.consoleHandlers = new Map(); this.retryCount = 0; this.maxRetries = 3; this.retryDelay = 1000; // 1 second this.lastUrl = window.location.href; this.stopTimeout = null; this.minSessionDuration = 1000; // Minimum 1 second duration this.unloadHandler = null; this.STORAGE_KEY = 'visitory_session'; this.SESSION_TIMEOUT = 60 * 1000; // 1 minute this.lastEventType = null; this.lastEventTimestamp = null; this.EVENT_DEBOUNCE = 100; // 100ms debounce for similar events if (SessionTracker.instance) { console.log('[SessionTracker] Using existing session:', SessionTracker.instance.sessionId); return SessionTracker.instance; } this.apiKey = apiKey; this.apiEndpoint = apiEndpoint; // Try to restore existing session const storedSession = this.getStoredSession(); if (storedSession && !this.isSessionExpired(storedSession.startTime)) { this.sessionId = storedSession.sessionId; this.startTime = storedSession.startTime; this.events = storedSession.events; console.log('[SessionTracker] Restored existing session:', { sessionId: this.sessionId, startTime: new Date(this.startTime).toISOString(), eventCount: this.events.length, }); } else { // Create new session this.sessionId = Math.random().toString(36).substring(7); this.startTime = Date.now(); this.events = []; console.log('[SessionTracker] Created new session:', { sessionId: this.sessionId, startTime: new Date(this.startTime).toISOString(), apiEndpoint: this.apiEndpoint, }); } SessionTracker.instance = this; // Set up unload handler this.unloadHandler = () => { if (this.isRecording) { console.log('[SessionTracker] Page unloading, saving session:', this.sessionId); this.saveSession(); } }; window.addEventListener('beforeunload', this.unloadHandler); // Start recording immediately this.start(); } getStoredSession() { try { const stored = localStorage.getItem(this.STORAGE_KEY); if (!stored) return null; return JSON.parse(stored); } catch (error) { console.error('[SessionTracker] Error reading stored session:', error); return null; } } saveSessionState() { try { const sessionState = { sessionId: this.sessionId, startTime: this.startTime, events: this.events, }; localStorage.setItem(this.STORAGE_KEY, JSON.stringify(sessionState)); console.log('[SessionTracker] Session state saved:', { sessionId: this.sessionId, eventCount: this.events.length, }); } catch (error) { console.error('[SessionTracker] Error saving session state:', error); } } isSessionExpired(startTime) { return Date.now() - startTime > this.SESSION_TIMEOUT; } addEvent(event) { this.events.push(event); console.log('[SessionTracker] Event recorded:', { sessionId: this.sessionId, type: event.type, timestamp: new Date(event.timestamp).toISOString(), }); this.saveSessionState(); } start() { if (this.isRecording) { console.log('[SessionTracker] Session already recording:', this.sessionId); return; } this.isRecording = true; console.log('[SessionTracker] Starting session recording:', { sessionId: this.sessionId, startTime: new Date(this.startTime).toISOString(), }); // Add initial page load event this.addEvent({ type: 'PAGE_LOAD', timestamp: Date.now(), data: { url: window.location.href, pageTitle: document.title, }, }); // Track button clicks document.addEventListener('click', (event) => { const target = event.target; // Track button clicks if (target.tagName === 'BUTTON' || target.closest('button')) { const button = target.tagName === 'BUTTON' ? target : target.closest('button'); this.addEvent({ type: 'BUTTON_CLICK', timestamp: Date.now(), data: { url: window.location.href, pageTitle: document.title, elementId: button.id, elementClass: button.className, elementText: button.textContent || undefined, }, }); } // Track link clicks if (target.tagName === 'A' || target.closest('a')) { const link = (target.tagName === 'A' ? target : target.closest('a')); this.addEvent({ type: 'LINK_CLICK', timestamp: Date.now(), data: { url: window.location.href, pageTitle: document.title, elementId: link.id, elementClass: link.className, elementText: link.textContent || undefined, linkHref: link.href, }, }); } }); // Track form submissions document.addEventListener('submit', (event) => { const form = event.target; this.addEvent({ type: 'FORM_SUBMIT', timestamp: Date.now(), data: { url: window.location.href, pageTitle: document.title, formId: form.id, formAction: form.action, }, }); }); // Track input changes with debounce let inputTimeout = null; document.addEventListener('input', (event) => { const target = event.target; if (inputTimeout) { clearTimeout(inputTimeout); } inputTimeout = window.setTimeout(() => { this.addEvent({ type: 'INPUT_CHANGE', timestamp: Date.now(), data: { url: window.location.href, pageTitle: document.title, elementId: target.id, elementClass: target.className, value: target.value, }, }); }, 500); // 500ms debounce }); // Track scroll events with throttling let lastScrollTime = 0; window.addEventListener('scroll', () => { const now = Date.now(); if (now - lastScrollTime > 100) { // Throttle to 100ms lastScrollTime = now; this.addEvent({ type: 'SCROLL', timestamp: now, data: { url: window.location.href, pageTitle: document.title, scrollPosition: window.scrollY, }, }); } }); // Track mouse movements with throttling let lastMouseMoveTime = 0; document.addEventListener('mousemove', (event) => { const now = Date.now(); if (now - lastMouseMoveTime > 100) { // Throttle to 100ms lastMouseMoveTime = now; this.addEvent({ type: 'MOUSE_MOVE', timestamp: now, data: { url: window.location.href, pageTitle: document.title, mouseX: event.clientX, mouseY: event.clientY, }, }); } }); // Track key presses document.addEventListener('keydown', (event) => { this.addEvent({ type: 'KEY_PRESS', timestamp: Date.now(), data: { url: window.location.href, pageTitle: document.title, keyCode: event.keyCode, key: event.key, }, }); }); // Track page loads window.addEventListener('load', () => { this.addEvent({ type: 'PAGE_LOAD', timestamp: Date.now(), data: { url: window.location.href, pageTitle: document.title, }, }); }); } stop() { if (!this.isRecording) { console.log('[SessionTracker] Session not recording:', this.sessionId); return; } this.isRecording = false; this.saveSession(); } async saveSession() { const endTime = Date.now(); const duration = endTime - this.startTime; const sessionData = { sessionId: this.sessionId, startTime: this.startTime, endTime, duration, events: this.events, metadata: { url: window.location.href, userAgent: navigator.userAgent, deviceType: this.getDeviceType(), browser: this.getBrowser(), os: this.getOS(), screenSize: { width: window.innerWidth, height: window.innerHeight, }, }, }; try { const response = await fetch(`${this.apiEndpoint}/session-replays`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.apiKey}`, }, body: JSON.stringify(sessionData), }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } console.log('[SessionTracker] Session saved successfully:', { sessionId: this.sessionId, eventCount: this.events.length, }); // Clear stored session after successful save localStorage.removeItem(this.STORAGE_KEY); } catch (error) { console.error('[SessionTracker] Error saving session:', error); } } getDeviceType() { const ua = navigator.userAgent; if (/(tablet|ipad|playbook|silk)|(android(?!.*mobi))/i.test(ua)) { return 'tablet'; } if (/Mobile|Android|iP(hone|od)|IEMobile|BlackBerry|Kindle|Silk-Accelerated|(hpw|web)OS|Opera M(obi|ini)/.test(ua)) { return 'mobile'; } return 'desktop'; } getBrowser() { const ua = navigator.userAgent; let browser = 'unknown'; if (ua.includes('Firefox')) browser = 'firefox'; else if (ua.includes('Chrome')) browser = 'chrome'; else if (ua.includes('Safari')) browser = 'safari'; else if (ua.includes('Edge')) browser = 'edge'; else if (ua.includes('MSIE') || ua.includes('Trident/')) browser = 'ie'; return browser; } getOS() { const ua = navigator.userAgent; let os = 'unknown'; if (ua.includes('Windows')) os = 'windows'; else if (ua.includes('Mac')) os = 'macos'; else if (ua.includes('Linux')) os = 'linux'; else if (ua.includes('Android')) os = 'android'; else if (ua.includes('iOS')) os = 'ios'; return os; } } exports.SessionTracker = SessionTracker; SessionTracker.instance = null;