UNPKG

insitella

Version:

User activity tracking package

454 lines (415 loc) 16.1 kB
function insitella() { const apiBaseUrl = "http://192.168.10.152/insitella/analytics"; // "http://localhost:5000/insitella/analytics";; const socket = io("http://192.168.10.152"); //http://localhost:5000/ const apiUrlPattern = "api.dev"; // URLs containing this pattern will be considered API calls const isApiRequest = (url) => { try { return url.includes(apiUrlPattern); } catch { return url.includes(apiUrlPattern); } }; const setCookie = (name, value, hoursToExpire) => { const expiryDate = new Date(Date.now() + hoursToExpire * 60 * 60 * 1000).toUTCString(); document.cookie = `${name}=${value};expires=${expiryDate};path=/`; }; const getCookie = (cookieName) => document.cookie .split(";") .map((c) => c.trim()) .find((c) => c.startsWith(`${cookieName}=`)) ?.split("=")[1] || null; const saveSessionData = (key, value) => sessionStorage.setItem(key, JSON.stringify(value)); const getSessionData = (key, defaultValue) => { const storedValue = sessionStorage.getItem(key); return storedValue ? JSON.parse(storedValue) : defaultValue; }; const detectDeviceType = () => { const ua = navigator.userAgent; if (/mobile/i.test(ua)) return "Mobile"; if (/tablet|ipad|playbook|silk/i.test(ua)) return "Tablet"; return "Desktop"; }; const detectBrowser = () => { const ua = navigator.userAgent; if (ua.includes("Firefox")) return "Firefox"; if (ua.includes("Edg")) return "Edge"; if (ua.includes("Chrome")) return "Chrome"; if (ua.includes("Safari")) return "Safari"; if (ua.includes("Trident")) return "Internet Explorer"; return "Unknown"; }; const initializeUserSession = () => { const existingSession = getSessionData("insitella_userevents", null); if (existingSession) { existingSession.userEvents ??= []; return existingSession; } return { sessionId: getCookie("userId") || `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`, startTime: new Date().toISOString(), clientName: document.querySelector("title")?.innerText || "", device: detectDeviceType(), browser: detectBrowser(), userEvents: [], endTime: null, }; }; const persistUserSession = (userSession) => saveSessionData("insitella_userevents", userSession); const renderCookieConsentBanner = (onConsentAccept) => { if (getCookie("cookieAccepted")) return; const consentBannerHTML = ` <div id="cookiePopup" style="position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; justify-content: center; align-items: center; z-index: 9999;"> <div style="background: #fff; position: fixed; bottom: 20px; left: 50px; max-width: 500px; border-radius: 15px; text-align: center; border: 1px solid #493179; padding: 25px; box-shadow: 0 0 18px rgba(0,0,0,0.13);"> <div style="margin-top: 10px;"> <h1 style="font-size: 25px; font-weight: 600;">GDPR Compliance Notice</h1> <h5>What data do we collect?</h5> <ul style="list-style-type: disc; text-align: left;"> <li>Personal info (name, email, location) when you sign up or interact.</li> <li>Usage patterns, preferences, and interactions to enhance your experience.</li> </ul> <div style="display: flex; justify-content: center;"> <button id="cookieCancelBtn" style="padding: 10px 20px; margin: 0 5px; border: none; font-size: 16px; font-weight: 500; border-radius: 5px; cursor: pointer; background: #eee; color: #333;"> Cancel </button> <button id="cookieAcceptBtn" style="padding: 10px 20px; margin: 0 5px; border: none; font-size: 16px; font-weight: 500; border-radius: 5px; cursor: pointer; background: #493179; color: #fff;"> Accept </button> </div> </div> </div> </div> `; document.body.insertAdjacentHTML("beforeend", consentBannerHTML); document.getElementById("cookieAcceptBtn")?.addEventListener("click", onConsentAccept); document.getElementById("cookieCancelBtn")?.addEventListener("click", () => document.getElementById("cookiePopup")?.remove() ); }; const handleConsentAcceptance = async (userSession, userInfo) => { document.getElementById("cookiePopup")?.remove(); if (!getCookie("userId")) { setCookie("userId", userSession.sessionId, 30); } if (navigator.geolocation) { try { const getUserPosition = () => new Promise((resolve, reject) => navigator.geolocation.getCurrentPosition(resolve, reject) ); const { coords } = await getUserPosition(); const res = await fetch( `https://nominatim.openstreetmap.org/reverse?lat=${coords.latitude}&lon=${coords.longitude}&format=json` ); const locationData = await res.json(); userInfo.userLocation = { latitude: coords.latitude.toString(), longitude: coords.longitude.toString(), cityName: locationData.address?.city || locationData.address?.town || locationData.address?.village || "", country: locationData.address?.country || "", }; } catch (err) { console.warn("Location fetch failed", err); } } setCookie("serverUpdateIntervalMs", 5000, 30); await fetch(`${apiBaseUrl}/userInfo`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(userInfo), }); setCookie("cookieAccepted", "true", 24); }; const trackPageVisit = (displayConsentBanner) => { console.log("Tracking page visit:", window.location.href); displayConsentBanner(); }; const recordClickEvent = (event, userSession, persistSessionFn) => { const currentPageUrl = lastPageUrl; let pageSession = userSession.userEvents.find((ev) => ev.pagesVisited.url === currentPageUrl); if (!pageSession) { pageSession = { pagesVisited: { url: currentPageUrl, timestamp: new Date().toISOString() }, clicks: [], brokenLinks: [], }; userSession.userEvents.push(pageSession); } const clickedElement = event.target.closest( 'button, a, input, [role="button"], .p-button, .p-menuitem-link, .p-radiobutton, .p-checkbox-box, .p-select, .p-select-label'); if (!clickedElement) return; const clickData = { tag: clickedElement.tagName, id: clickedElement.id, class: clickedElement.className, label: clickedElement.getAttribute("label") || clickedElement.getAttribute("aria-label") || clickedElement.innerText.trim().slice(0, 100), href: clickedElement.href || clickedElement.getAttribute("href") || null, timestamp: new Date().toISOString(), }; pageSession.clicks.push(clickData); persistSessionFn(userSession); if (clickData.href?.startsWith("http")) { fetch(clickData.href, { method: "HEAD" }) .then((res) => { if (!res.ok) { pageSession.brokenLinks.push({ href: clickData.href, status: res.status, statusText: res.statusText, method: 'HEAD', type: 'click', isApi: false }); persistSessionFn(userSession); } }) .catch(() => { pageSession.brokenLinks.push({ href: clickData.href, status: 0, statusText: "Fetch Failed", method: 'HEAD', type: 'click', isApi: false }); persistSessionFn(userSession); }); } }; const normalizeUrl = (url) => { try { return new URL(url, location.href).href; } catch { return url; } }; const recordFailedApiCall = (userSession, apiData) => { const currentPageUrl = lastPageUrl; let pageSession = userSession.userEvents.find((ev) => ev.pagesVisited.url === currentPageUrl); if (!pageSession) { pageSession = { pagesVisited: { url: currentPageUrl, timestamp: new Date().toISOString() }, clicks: [], brokenLinks: [] }; userSession.userEvents.push(pageSession); } const href = normalizeUrl(apiData.url); pageSession.brokenLinks.push({ href, status: apiData.status, statusText: apiData.statusText, method: apiData.method, type: apiData.type, isApi: apiData.isApi, timestamp: new Date().toISOString() }); persistUserSession(userSession); }; const transmitUserEvents = async (userSession) => { if (isTransmitting) return; const sessionData = getSessionData("insitella_userevents", null); if (!sessionData) return; const hasPendingEvents = sessionData.userEvents.some( (ev) => (ev.clicks?.length || 0) > 0 || (ev.brokenLinks?.length || 0) > 0 ); if (!hasPendingEvents) return; isTransmitting = true; const snapshot = JSON.parse(JSON.stringify(sessionData.userEvents)); try { const res = await fetch(`${apiBaseUrl}/userClickEvents`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ...sessionData, userEvents: snapshot }), }); if (!res.ok) throw new Error(`Failed: ${res.status}`); await res.json(); userSession.userEvents = userSession.userEvents.map(ev => { const sentPage = snapshot.find(s => s.pagesVisited.url === ev.pagesVisited.url); if (!sentPage) return ev; const newClicks = ev.clicks.filter(click => !sentPage.clicks.some(sentClick => sentClick.timestamp === click.timestamp)); const newBrokenLinks = ev.brokenLinks.filter(link => !sentPage.brokenLinks.some(sentLink => sentLink.timestamp === link.timestamp)); if (newClicks.length > 0 || newBrokenLinks.length > 0) { return { ...ev, clicks: newClicks, brokenLinks: newBrokenLinks }; } return null; }).filter(ev => ev !== null); persistUserSession(userSession); } catch (err) { console.error("Transmission error:", err); } finally { isTransmitting = false; } }; const scheduleEventTransmission = (userSession) => { const intervalMs = parseInt(getCookie("serverUpdateIntervalMs"), 10); if (!intervalMs) return; clearInterval(eventDataTimer); eventDataTimer = setInterval( () => transmitUserEvents(userSession), intervalMs ); clearInterval(bootstrapTimer); bootstrapTimer = null; }; const interceptFetch = (userSession) => { const originalFetch = window.fetch; window.fetch = async (...args) => { const request = args[0]; const url = typeof request === 'string' ? request : request.url; const method = typeof request === 'string' ? (args[1]?.method || 'GET') : request.method || 'GET'; try { const response = await originalFetch.apply(window, args); if (!response.ok && isApiRequest(url)) { recordFailedApiCall(userSession, { url, method, status: response.status, statusText: response.statusText, type: 'fetch', isApi: true, error: 'HTTP Error' }); } return response; } catch (error) { if (isApiRequest(url)) { recordFailedApiCall(userSession, { url, method, status: 0, statusText: error.message, type: 'fetch', isApi: true, error: 'Network Error' }); } throw error; } }; }; const interceptXHR = (userSession) => { const XHR = XMLHttpRequest.prototype; const originalOpen = XHR.open; const originalSend = XHR.send; XHR.open = function(method, url) { this.insitellaMethod = method; this.insitellaUrl = url; return originalOpen.apply(this, arguments); }; XHR.send = function() { if (!isApiRequest(this.insitellaUrl)) return originalSend.apply(this, arguments); const recordXHRError = (status, statusText, error) => { recordFailedApiCall(userSession, { url: this.insitellaUrl, method: this.insitellaMethod, status, statusText, type: 'xhr', isApi: true, error }); }; this.addEventListener('load', () => { if (this.status >= 400) recordXHRError(this.status, this.statusText, 'HTTP Error'); }); this.addEventListener('error', () => { recordXHRError(0, 'Network Error', 'Network Error'); }); this.addEventListener('timeout', () => { recordXHRError(0, 'Timeout', 'Timeout'); }); return originalSend.apply(this, arguments); }; }; const displayProductNotification = (data) => { const existingNotification = document.getElementById('notification'); if(existingNotification) { existingNotification.remove() } const notificationDiv = document.createElement('div'); notificationDiv.id = 'notification'; notificationDiv.innerHTML = data; document.body.appendChild(notificationDiv); }; const closeNotification = () => { const notificationDiv = document.getElementById('notification'); if (notificationDiv) { notificationDiv.remove(); } }; window.closeNotification = closeNotification; let lastPageUrl = window.location.href; let isTransmitting = false; let eventDataTimer = null; let bootstrapTimer = null; socket.on('connect', () => { console.log('Connected to the server'); }); socket.on('welcome', (data) => { console.log('Received welcome message:', data); }); socket.on('receive_message', (data) => displayProductNotification(data)); (() => { const userSession = initializeUserSession(); const storedUserId = getCookie("userId"); const userInfo = { sessionId: storedUserId || userSession.sessionId, startTime: new Date().toISOString(), clientName: document.querySelector("title")?.innerText || "", device: detectDeviceType(), browser: detectBrowser(), platform: navigator.platform, userAgent: navigator.userAgent, userLocation: {}, }; interceptFetch(userSession); interceptXHR(userSession); const { pushState } = history; history.pushState = (...args) => { pushState.apply(history, args); lastPageUrl = window.location.href; trackPageVisit(() => renderCookieConsentBanner(() => handleConsentAcceptance(userSession, userInfo) ) ); }; window.addEventListener("popstate", () => { lastPageUrl = window.location.href; trackPageVisit(() => renderCookieConsentBanner(() => handleConsentAcceptance(userSession, userInfo) )) } ); window.addEventListener("DOMContentLoaded", () => { lastPageUrl = window.location.href; trackPageVisit(() => renderCookieConsentBanner(() => handleConsentAcceptance(userSession, userInfo) )) } ); document.addEventListener("click", (e) => recordClickEvent(e, userSession, persistUserSession), true); transmitUserEvents(userSession); bootstrapTimer = setInterval( () => scheduleEventTransmission(userSession), 1000 ); })(); } module.exports = { insitella };