document.addEventListener("DOMContentLoaded", () => { // --- CONFIG --- const SERVER_URL = "https://strangely-saved-parrot.ngrok.app/account/de7695d7-4ef6-4a5c-b97b-6687b74f62dd/tracker/events"; // --- UNIFIED TRACKING FUNCTION --- function getSessionToken() { try { const storedPhoneNumber = sessionStorage.getItem('phoneNumber'); if (storedPhoneNumber) return storedPhoneNumber; } catch (error) { console.error('Error accessing session storage:', error); } if (window.session_token) return window.session_token; const meta = document.querySelector("meta[name='session-token']"); if (meta) return meta.content; const params = new URLSearchParams(window.location.search); return params.get("session_token") || "unknown"; } function voxgaugeTrack(eventType, eventData = {}) { // Get session token // Send data function function sendData(data) { data.session_token = getSessionToken(); data.url = window.location.href; data.timestamp = new Date().toISOString(); fetch(SERVER_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data) }).catch(() => {}); } // Validate event type const validEvents = [ 'input', 'click', 'validation_error', 'custom_event', 'page_view', 'form_submit', 'error', // Add custom event types 'button_click', 'navigation', 'form_validation_start', 'form_validation_complete', 'field_focus', 'field_blur', 'section_view', 'step_change', 'input_change' ]; if (!validEvents.includes(eventType)) { console.error('Invalid event type. Use: ' + validEvents.join(', ')); return; } // Prepare event data with common structure const finalEventData = { event: eventType, tag: eventData.tag || null, id: eventData.id || null, name: eventData.name || null, class: eventData.class || null, value: eventData.value || null, message: eventData.message || null, category: eventData.category || null, action: eventData.action || null, label: eventData.label || null, metadata: eventData.metadata || {} }; // Send the event sendData(finalEventData); } // Make voxgaugeTrack globally available window.voxgaugeTrack = voxgaugeTrack; // --- AUTO-TRACKING (Optional) --- function initAutoTracking() { // Debounce helper function debounce(fn, delay = 500) { let timer; return function (...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; } // Auto track input events const trackInput = debounce((e) => { const el = e.target; if (el.type === "password" || el.hasAttribute("data-no-track")) return; let value; if (el.tagName === "SELECT") { value = el.options[el.selectedIndex]?.text; } else if (el.type === "checkbox" || el.type === "radio") { value = el.checked; } else { value = el.value?.slice(0, 300); } voxgaugeTrack('input', { tag: el.tagName.toLowerCase(), id: el.id || null, name: el.name || null, class: el.className || null, value: value, category: 'form_interaction', action: 'input_change', label: el.name || el.id || 'Form input', metadata: { inputType: el.type || null, field_type: el.type || 'text' } }); }); // Auto track click events const trackClick = (e) => { const el = e.target.closest("button, a, input[type='submit']"); if (!el || el.hasAttribute("data-no-track")) return; console.log(el.tagName) const isNavigationLink = el.tagName === 'A' && el.href && (el.href.includes('.html') || el.href.startsWith('http') || el.href.startsWith('/')); const eventData = { tag: el.tagName.toLowerCase(), id: el.id || null, name: el.name || null, class: el.className || null, value: el.name || el.innerText?.slice(0, 100) || el.value || null, category: 'user_action', action: 'button_click', label: el.innerText?.slice(0, 50) || el.name || 'Button clicked', metadata: { href: el.href || null, button_type: el.type || 'button' } }; if (isNavigationLink) { // Use beacon for navigation const data = { event: "click", ...eventData, session_token: getSessionToken(), url: window.location.href, timestamp: new Date().toISOString() }; navigator.sendBeacon(SERVER_URL, JSON.stringify(data)); } else { console.log(eventData) voxgaugeTrack('click', eventData); } }; // Auto track validation errors function addValidationTracking(el) { if (el.hasAttribute("data-no-track") || el.type === "password" || el.dataset.trackingInitialized === "true") return; el.addEventListener("invalid", (e) => { e.preventDefault(); voxgaugeTrack('validation_error', { tag: el.tagName.toLowerCase(), id: el.id || null, name: el.name || null, class: el.className || null, value: el.value, message: el.validationMessage, category: 'form_validation', action: 'validation_failed', label: el.name || el.id || 'Validation error', metadata: { field_type: el.type || 'text', validation_type: 'native' } }); }); el.dataset.trackingInitialized = "true"; } // Initialize listeners document.querySelectorAll("input, textarea, select").forEach((el) => { if (el.hasAttribute("data-no-track") || el.type === "password" || el.dataset.trackingInitialized === "true") return; const type = el.type; const tag = el.tagName.toLowerCase(); if (["text", "email", "number", "url", "tel", "search"].includes(type) || tag === "textarea") { el.addEventListener("blur", trackInput); } else { el.addEventListener("change", trackInput); } addValidationTracking(el); }); if (!window.__clickTrackingInitialized) { document.addEventListener("click", trackClick); window.__clickTrackingInitialized = true; } } // --- CUSTOM ERROR TRACKING --- const errorClasses = ["error", "invalid-feedback", "form-error", "ng-invalid", "mat-error", "Mui-error"]; function isErrorNode(node) { if (!node || !node.textContent) return false; if (node.nodeType !== 1 && node.nodeType !== 3) return false; if (node.classList) { for (const cls of errorClasses) { if (node.classList.contains(cls)) return true; } } if (node.getAttribute && node.getAttribute("role") === "alert") return true; const txt = node.textContent.toLowerCase(); if (txt.includes("only") && txt.includes("letters")) return true; if (txt.includes("required")) return true; if (txt.includes("invalid")) return true; return false; } function findRelatedInput(node) { if (node.previousElementSibling && (node.previousElementSibling.tagName === "INPUT" || node.previousElementSibling.tagName === "TEXTAREA")) { return node.previousElementSibling; } if (node.parentElement) { const input = node.parentElement.querySelector("input, textarea"); if (input) return input; } if (node.id) { const label = document.querySelector('label[for="' + node.id + '"]'); if (label) { const input = document.getElementById(label.getAttribute("for")); if (input) return input; } } return null; } // Observe for custom error messages const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (isErrorNode(node)) { const relatedInput = findRelatedInput(node); if (relatedInput && !relatedInput.hasAttribute("data-no-track") && relatedInput.type !== "password") { voxgaugeTrack('validation_error', { tag: relatedInput.tagName.toLowerCase(), id: relatedInput.id || null, name: relatedInput.name || null, class: relatedInput.className || null, value: relatedInput.value || null, message: node.textContent.trim(), category: 'form_validation', action: 'validation_failed', label: relatedInput.name || relatedInput.id || 'Custom validation error', metadata: { field_type: relatedInput.type || 'text', validation_type: 'custom', error_element: node.tagName || 'span' } }); } } }); }); }); // Move observer initialization to DOMContentLoaded window.addEventListener("DOMContentLoaded", () => { // Initialize the observer once body is available if (document.body) { observer.observe(document.body, { childList: true, subtree: true }); } initAutoTracking(); // Track page view voxgaugeTrack('page_view', { category: 'navigation', action: 'page_loaded', label: 'Page viewed', metadata: { page_title: document.title, referrer: document.referrer, page_url: window.location.href } }); }); // --- SPA SUPPORT --- const spaObserver = new MutationObserver(() => { // Re-initialize listeners for new elements document.querySelectorAll("input, textarea, select").forEach((el) => { if (el.hasAttribute("data-no-track") || el.type === "password" || el.dataset.trackingInitialized === "true") return; const type = el.type; const tag = el.tagName.toLowerCase(); if (["text", "email", "number", "url", "tel", "search"].includes(type) || tag === "textarea") { el.addEventListener("blur", trackInput); } else { el.addEventListener("change", trackInput); } addValidationTracking(el); }); }); spaObserver.observe(document, { childList: true, subtree: true }); // --- USAGE EXAMPLES --- console.log('VoxGauge Tracking initialized with common data structure!'); console.log('Usage examples:'); console.log('voxgaugeTrack("custom_event", { category: "ecommerce", action: "purchase", value: 99.99, metadata: { product_id: "PROD123" } })'); console.log('voxgaugeTrack("form_submit", { tag: "form", id: "contact-form", category: "form_submission", action: "form_submitted", metadata: { success: true } })'); console.log('voxgaugeTrack("error", { category: "system_error", action: "api_failed", message: "API request failed", metadata: { error_code: 500 } })'); });