insitella
Version:
User activity tracking package
454 lines (415 loc) • 16.1 kB
JavaScript
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 };