UNPKG

visionify-analytics-tracker

Version:

Track user session and page usage in apps

196 lines (195 loc) 7.87 kB
import { useEffect, useRef } from 'react'; import { v4 as uuidv4 } from 'uuid'; import { sendEventBatch } from './sendEvent'; let usageTrackerMounted = false; function getIdFromStorage() { try { const stored = localStorage.getItem('visionify_id'); if (stored) return stored; const newId = uuidv4(); localStorage.setItem('visionify_id', newId); return newId; } catch { return uuidv4(); } } export function useUsageTracker({ user_name, company_name, site_name, trackablePaths, endpoint, batchIntervalMs = 5 * 60 * 1000, inactivityTimeoutMs = 5 * 60 * 1000, currentPath, devMode = false, }) { const idRef = useRef(getIdFromStorage()); const pageEnterTimeRef = useRef(Date.now()); const lastActivityRef = useRef(Date.now()); const previousPathRef = useRef(currentPath); const isVisibleRef = useRef(document.visibilityState === 'visible'); const lastVisibleTimeRef = useRef(Date.now()); const pageMapRef = useRef(new Map()); const lastFlushRef = useRef(0); const pausedRef = useRef(false); useEffect(() => { if (devMode) { if (usageTrackerMounted) { console.warn('[visionify-analytics-tracker] useUsageTracker mounted more than once! This can cause API spam.'); } usageTrackerMounted = true; } return () => { if (devMode) usageTrackerMounted = false; }; }, [devMode]); function calculateDuration(now) { const maxDurationSeconds = 1.5 * Math.floor(batchIntervalMs / 1000); const currDuration = isVisibleRef.current ? Math.floor((now - pageEnterTimeRef.current) / 1000) : Math.floor((lastVisibleTimeRef.current - pageEnterTimeRef.current) / 1000); if (currDuration > maxDurationSeconds) { return maxDurationSeconds; } return currDuration; } function handleVisibilityChange() { const now = Date.now(); const wasVisible = isVisibleRef.current; const isVisible = document.visibilityState === 'visible'; if (wasVisible && !isVisible) lastVisibleTimeRef.current = now; else if (!wasVisible && isVisible) pageEnterTimeRef.current = now; isVisibleRef.current = isVisible; if (devMode) console.log('[visionify-analytics-tracker] Tab visibility:', isVisible); } function debouncedFlush() { if (pausedRef.current) return; const now = Date.now(); if (now - lastFlushRef.current < 1000) return; lastFlushRef.current = now; flushPageLocalStorage(); } useEffect(() => { if (pausedRef.current) return; const now = Date.now(); const prevPath = previousPathRef.current; if (prevPath !== currentPath) { const duration = calculateDuration(now); if (trackablePaths.includes(prevPath) && duration > 0) { const map = pageMapRef.current; if (map.has(prevPath)) map.get(prevPath).duration_seconds += duration; else map.set(prevPath, { id: idRef.current, user_name, company_name, site_name, path: prevPath, entered_at: new Date(pageEnterTimeRef.current).toISOString(), duration_seconds: duration, }); if (devMode) console.log('[visionify-analytics-tracker] Page event:', map.get(prevPath)); } const arr = Array.from(pageMapRef.current.values()); try { localStorage.setItem('visionify_pageviews', JSON.stringify(arr)); } catch { } pageEnterTimeRef.current = now; previousPathRef.current = currentPath; } }, [currentPath, trackablePaths, devMode, user_name, company_name, site_name]); function flushPageLocalStorage() { if (pausedRef.current) return; const now = Date.now(); const current = previousPathRef.current; const duration = calculateDuration(now); const map = pageMapRef.current; if (trackablePaths.includes(current) && duration > 0) { if (map.has(current)) map.get(current).duration_seconds += duration; else map.set(current, { id: idRef.current, user_name, company_name, site_name, path: current, entered_at: new Date(pageEnterTimeRef.current).toISOString(), duration_seconds: duration, }); } const arr = Array.from(map.values()); if (devMode) { console.log('[visionify-analytics-tracker] [Data]:', JSON.stringify(arr, null, 2)); } if (arr.length > 0) { sendEventBatch(arr, endpoint, devMode); } map.clear(); pageEnterTimeRef.current = now; try { localStorage.setItem('visionify_pageviews', JSON.stringify([])); } catch { } } useEffect(() => { const handleActivity = () => { if (pausedRef.current) { pausedRef.current = false; idRef.current = uuidv4(); try { localStorage.setItem('visionify_id', idRef.current); } catch { } if (devMode) console.log('[visionify-analytics-tracker] Tracking resumed.'); lastActivityRef.current = Date.now(); pageEnterTimeRef.current = Date.now(); previousPathRef.current = currentPath; } else { lastActivityRef.current = Date.now(); } }; const checkInactivity = () => { if (!pausedRef.current && Date.now() - lastActivityRef.current > inactivityTimeoutMs) { pausedRef.current = true; if (devMode) console.log('[visionify-analytics-tracker] Inactivity detected. Tracking paused.'); } }; const inactivityInterval = setInterval(checkInactivity, 30_000); window.addEventListener('mousemove', handleActivity); window.addEventListener('keydown', handleActivity); document.addEventListener('visibilitychange', handleVisibilityChange); return () => { clearInterval(inactivityInterval); window.removeEventListener('mousemove', handleActivity); window.removeEventListener('keydown', handleActivity); document.removeEventListener('visibilitychange', handleVisibilityChange); }; }, [inactivityTimeoutMs, devMode, currentPath]); useEffect(() => { const flushInterval = setInterval(debouncedFlush, batchIntervalMs); return () => clearInterval(flushInterval); }, [batchIntervalMs]); useEffect(() => { window.addEventListener('beforeunload', debouncedFlush); document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') debouncedFlush(); }); return () => { window.removeEventListener('beforeunload', debouncedFlush); document.removeEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') debouncedFlush(); }); }; }, [endpoint, trackablePaths, devMode]); return { flushPageLocalStorage }; }