Last Updated: 2025-08-22
const nvStorage = (() => {
let m={}, ok=1;
const warn=()=>{if(ok){console.warn('Local storage disabled');ok=0;}};
return {
getItem:k=>{try{return ok?localStorage.getItem(k):m[k]||null;}catch(e){warn();return m[k]||null;}},
setItem:(k,v)=>{try{if(ok)return localStorage.setItem(k,v);}catch(e){warn();}m[k]=v;},
removeItem:k=>{try{if(ok)return localStorage.removeItem(k);}catch(e){warn();}delete m[k];}
};
})();
window.stopNimvueTracking = function() {
nvStorage.setItem('nv_tracking_halted', '1');
};
window.startNimvueTracking = function() {
nvStorage.removeItem('nv_tracking_halted');
};
if (window.nvConfig && window.nvConfig.disableTracking) {
window.stopNimvueTracking();
}
(() => {
if (window.nvScriptLoaded) return;
window.nvScriptLoaded = true;
function isTrackingHalted() {
return nvStorage.getItem('nv_tracking_halted') === '1';
}
const ingestUrl = 'http://127.0.0.1:8000/ingest';
const infoUrl = 'http://127.0.0.1:8000/info';
const clientId = document.querySelector('script[data-ba-measurement-id]')?.getAttribute('data-ba-measurement-id');
if (!clientId) throw new Error('Missing measurement ID');
searchParams = '', scrollBreakpoints = '', engagedTime = 0, isTracking = false, isUserActive = false, idleTimeout = null, currentPageEventId = null, stopTracking = 0;
const eventQueue = [], FLUSH_INTERVAL = 700, MAX_BATCH_SIZE = 10;
let flushTimer = null;
function queueEvent(ev) {
if (isTrackingHalted()) return;
eventQueue.push(...ev);
if (eventQueue.length >= MAX_BATCH_SIZE) return flushEventQueue();
if (!flushTimer) flushTimer = setTimeout(flushEventQueue, FLUSH_INTERVAL);
}
function flushEventQueue() {
if (isTrackingHalted()) return;
if (!eventQueue.length) return;
const toSend = eventQueue.splice(0), resetFlush = () => flushTimer = null;
fetch(ingestUrl, {
method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(toSend)
}).then(res => res.ok ? res.json() : Promise.reject('Bad network response')).then(data => {
if (data?.mh) createMeasurementDataHash().then(hash => {
if (hash !== data.mh) {
getMeasurementData().then(newData => newData && nvStorage.setItem('nv_measurementData', JSON.stringify(newData)));
}
});
}).catch(e => console.error('Batch send error:', e)).finally(resetFlush);
}
const UUID = () => 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => (c == 'x' ? Math.random() * 16 | 0 : (Math.random() * 16 | 0 & 0x3 | 0x8)).toString(16));
function buildEvent(type, time, p1Key, p1Val, p2Key, p2Val) {
const eventId = UUID();
if (type === 'page_view') currentPageEventId = eventId;
const info = {};
if (p1Key && p1Val !== undefined) info[p1Key] = p1Val;
if (p2Key && p2Val !== undefined) info[p2Key] = p2Val;
return [{
ei: eventId, ci: clientId, et: type,
pp: location.href,
sr: `${screen.width}x${screen.height}`, pt: document.title, pr: document.referrer,
...(Object.keys(info).length ? { einfo: info } : {})
}];
}
async function getMeasurementData() {
try {
const cachedData = nvStorage.getItem('nv_measurementData');
if (cachedData) {
return JSON.parse(cachedData);
}
const res = await fetch(infoUrl, {
method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify([{ client_id: clientId }])
});
if (!res.ok) throw new Error('Failed to fetch measurement data');
const data = await res.json();
nvStorage.setItem('nv_measurementData', JSON.stringify(data));
return data;
} catch (e) { console.error('Measurement fetch error:', e); throw e; }
}
function flushEngagement() {
if (isTrackingHalted()) return;
if (engagedTime > 0) {
const now = Date.now();
const events = [
...buildEvent("session_engagement_time", now, "engaged_time", engagedTime, "current_page_event_id", currentPageEventId)
];
queueEvent(events);
engagedTime = 0;
}
}
function startEngagementTracking() {
if (isTracking) return; isTracking = true;
['mousemove', 'keydown', 'click', 'scroll'].forEach(evt => window.addEventListener(evt, markUserActive));
document.addEventListener('visibilitychange', () => { isUserActive = !document.hidden; });
startIdleWatcher(); startTicking();
window.addEventListener('beforeunload', () => {
if (isTrackingHalted()) return;
if (engagedTime > 0) {
const now = Date.now(), events = [
...buildEvent("session_engagement_time", now, "engaged_time", engagedTime, "current_page_event_id", currentPageEventId)
];
try {
navigator.sendBeacon(ingestUrl, new Blob([JSON.stringify(events)], { type: 'text/plain' }));
} catch (e) {
queueEvent(events);
}
}
});
}
function markUserActive() { isUserActive = true; clearTimeout(idleTimeout); startIdleWatcher(); }
function startIdleWatcher() { idleTimeout = setTimeout(() => isUserActive = false, 10000); }
function startTicking() { setInterval(() => { if (!document.hidden && isUserActive && isTracking) engagedTime += 0.5; }, 500); }
function getScrollPercentage() {
const t = document.documentElement, total = t.scrollHeight - t.clientHeight, pos = window.scrollY || t.scrollTop;
return (pos / total) * 100;
}
function processFurtherEvents() { startScrollTracking(); startSearchTracking(); startEngagementTracking(); }
function startScrollTracking() {
const sent = new Set();
if (scrollBreakpoints && scrollBreakpoints.trim()) {
const breakpoints = scrollBreakpoints.split(',').map(bp => parseInt(bp.trim(), 10)).filter(bp => !isNaN(bp) && bp >= 0 && bp <= 100);
if (breakpoints.length) window.addEventListener('scroll', () => {
if (isTrackingHalted()) return;
const percent = Math.round(getScrollPercentage());
if (breakpoints.includes(percent) && !sent.has(percent)) {
sent.add(percent);
queueEvent(buildEvent('scroll', Date.now(), 'percent_scrolled', percent, 'current_page_event_id', currentPageEventId));
}
});
}
}
function startSearchTracking() {
if (searchParams && searchParams.trim()) {
const params = searchParams.split(',').map(p => p.trim()), urlParams = new URLSearchParams(location.search);
params.filter(param => urlParams.has(param)).forEach(term => {
if (isTrackingHalted()) return;
const val = urlParams.get(term)?.trim().toLowerCase();
if (val) queueEvent(buildEvent('search', Date.now(), 'search_term', val));
});
}
}
function initSession() {
if (isTrackingHalted()) return;
const timestamp = Date.now();
queueEvent(buildEvent('page_view', timestamp));
processFurtherEvents();
}
getMeasurementData().then(data => {
searchParams = data?.sp || '';
scrollBreakpoints = data?.sb || '';
stopTracking = data?.st || 0;
if (stopTracking === 1) return console.warn('Tracking stopped for this measurement');
initSession();
});
window.nv_create_event = function({ event_name, parameters = {} }) {
if (isTrackingHalted()) return;
if (!event_name || typeof parameters !== 'object') return console.error("Invalid custom event");
const cleanParams = {};
for (const [k, v] of Object.entries(parameters)) cleanParams[k.slice(0, 36)] = v;
const payload = [{
ei: UUID(), ci: clientId, et: event_name,
pp: location.href,
sr: `${screen.width}x${screen.height}`, pt: document.title, pr: document.referrer,
...(Object.keys(cleanParams).length ? { einfo: cleanParams } : {})
}];
window.nv_events = window.nv_events || [];
window.nv_events.push({ event_name, parameters: cleanParams });
queueEvent(payload);
};
function createMeasurementDataHash() {
const data = nvStorage.getItem('nv_measurementData');
if (!data) return null;
const encoder = new TextEncoder();
return crypto.subtle.digest('SHA-256', encoder.encode(data))
.then(hash => Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('').slice(0, 16))
.catch(e => (console.error('Hashing error:', e), null));
}
var enableSpaSupport = window.nvConfig && window.nvConfig.enableSpaSupport;
if (enableSpaSupport) {
let lastPath = location.pathname + location.search;
let spaDebounceTimer;
function spaPageView() {
if (isTrackingHalted()) return;
flushEngagement();
queueEvent(buildEvent('page_view', Date.now()));
}
function debouncedSpaPageView() {
clearTimeout(spaDebounceTimer);
spaDebounceTimer = setTimeout(spaPageView, 500);
}
setInterval(() => {
const currentPath = location.pathname + location.search;
if (currentPath !== lastPath) {
lastPath = currentPath;
debouncedSpaPageView();
}
}, 250);
}
})();