visionify-analytics-tracker
Version:
Track user session and page usage in apps
191 lines (190 loc) • 7.65 kB
JavaScript
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) {
return isVisibleRef.current
? Math.floor((now - pageEnterTimeRef.current) / 1000)
: Math.floor((lastVisibleTimeRef.current - pageEnterTimeRef.current) / 1000);
}
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 };
}