diff --git a/static/js/analytics-tracker.min.js b/static/js/analytics-tracker.min.js new file mode 100644 index 0000000..922ee6d --- /dev/null +++ b/static/js/analytics-tracker.min.js @@ -0,0 +1,243 @@ +(function() { +'use strict'; +const HEARTBEAT_INTERVAL = 60000; +const SCROLL_DEBOUNCE_MS = 500; +const TRACK_ENDPOINT = '/api/analytics/track'; +const HEARTBEAT_ENDPOINT = '/api/analytics/heartbeat'; +const SCROLL_ENDPOINT = '/api/analytics/scroll'; +const ERROR_ENDPOINT = '/api/analytics/error'; +const PERFORMANCE_ENDPOINT = '/api/analytics/performance'; +let pageStartTime = Date.now(); +let currentPageViewId = null; +let maxScrollDepth = 0; +let scrollTimeout = null; +function init() { +const pageViewMeta = document.querySelector('meta[name="page-view-id"]'); +if (pageViewMeta && pageViewMeta.content) { +currentPageViewId = parseInt(pageViewMeta.content, 10); +} +setInterval(sendHeartbeat, HEARTBEAT_INTERVAL); +document.addEventListener('click', handleClick, true); +window.addEventListener('beforeunload', handleUnload); +document.addEventListener('visibilitychange', handleVisibilityChange); +window.addEventListener('scroll', handleScroll, { passive: true }); +window.onerror = handleError; +window.addEventListener('unhandledrejection', handlePromiseRejection); +if (document.readyState === 'complete') { +trackPerformance(); +} else { +window.addEventListener('load', function() { +setTimeout(trackPerformance, 100); +}); +} +trackContactClicks(); +} +function handleClick(e) { +const target = e.target.closest('a, button, [data-track], input[type="submit"], .clickable, .company-card, nav a'); +if (!target) return; +const data = { +type: 'click', +page_view_id: currentPageViewId, +element_type: getElementType(target, e), +element_id: target.id || null, +element_text: (target.textContent || '').trim().substring(0, 100) || null, +element_class: target.className || null, +target_url: target.href || target.closest('a')?.href || null, +x: e.clientX, +y: e.clientY +}; +sendTracking(data); +} +function getElementType(target, e) { +if (target.closest('nav')) return 'nav'; +if (target.closest('.company-card')) return 'company_card'; +if (target.closest('form')) return 'form'; +if (target.closest('.sidebar')) return 'sidebar'; +if (target.closest('.dropdown')) return 'dropdown'; +if (target.closest('.modal')) return 'modal'; +if (target.tagName === 'A') return 'link'; +if (target.tagName === 'BUTTON') return 'button'; +if (target.tagName === 'INPUT') return 'input'; +if (target.hasAttribute('data-track')) return target.getAttribute('data-track'); +return target.tagName.toLowerCase(); +} +function handleUnload() { +if (!currentPageViewId) return; +const timeOnPage = Math.round((Date.now() - pageStartTime) / 1000); +const data = JSON.stringify({ +type: 'page_time', +page_view_id: currentPageViewId, +time_seconds: timeOnPage +}); +navigator.sendBeacon(TRACK_ENDPOINT, data); +if (maxScrollDepth > 0) { +navigator.sendBeacon(SCROLL_ENDPOINT, JSON.stringify({ +page_view_id: currentPageViewId, +scroll_depth: maxScrollDepth +})); +} +} +function handleVisibilityChange() { +if (document.visibilityState === 'hidden' && currentPageViewId) { +const timeOnPage = Math.round((Date.now() - pageStartTime) / 1000); +sendTracking({ +type: 'page_time', +page_view_id: currentPageViewId, +time_seconds: timeOnPage +}); +} +} +function handleScroll() { +if (scrollTimeout) { +clearTimeout(scrollTimeout); +} +scrollTimeout = setTimeout(function() { +calculateScrollDepth(); +}, SCROLL_DEBOUNCE_MS); +} +function calculateScrollDepth() { +const scrollTop = window.pageYOffset || document.documentElement.scrollTop; +const scrollHeight = document.documentElement.scrollHeight; +const clientHeight = document.documentElement.clientHeight; +const scrollPercent = Math.round((scrollTop + clientHeight) / scrollHeight * 100); +if (scrollPercent > maxScrollDepth) { +maxScrollDepth = Math.min(scrollPercent, 100); +if (currentPageViewId && (maxScrollDepth === 25 || maxScrollDepth === 50 || +maxScrollDepth === 75 || maxScrollDepth >= 95)) { +fetch(SCROLL_ENDPOINT, { +method: 'POST', +headers: { 'Content-Type': 'application/json' }, +body: JSON.stringify({ +page_view_id: currentPageViewId, +scroll_depth: maxScrollDepth +}), +credentials: 'same-origin' +}).catch(function() { +}); +} +} +} +function handleError(message, source, lineno, colno, error) { +const errorData = { +message: message ? message.toString() : 'Unknown error', +source: source, +lineno: lineno, +colno: colno, +stack: error && error.stack ? error.stack : null, +url: window.location.href +}; +fetch(ERROR_ENDPOINT, { +method: 'POST', +headers: { 'Content-Type': 'application/json' }, +body: JSON.stringify(errorData), +credentials: 'same-origin' +}).catch(function() { +}); +return false; +} +function handlePromiseRejection(event) { +const reason = event.reason; +handleError( +'Unhandled Promise Rejection: ' + (reason && reason.message ? reason.message : String(reason)), +null, +null, +null, +reason instanceof Error ? reason : null +); +} +function trackPerformance() { +if (!currentPageViewId) return; +if (!window.performance || !performance.timing) return; +const timing = performance.timing; +const navStart = timing.navigationStart; +const metrics = { +page_view_id: currentPageViewId, +dom_content_loaded_ms: timing.domContentLoadedEventEnd - navStart, +load_time_ms: timing.loadEventEnd - navStart +}; +if (performance.getEntriesByType) { +const paintEntries = performance.getEntriesByType('paint'); +paintEntries.forEach(function(entry) { +if (entry.name === 'first-paint') { +metrics.first_paint_ms = Math.round(entry.startTime); +} +if (entry.name === 'first-contentful-paint') { +metrics.first_contentful_paint_ms = Math.round(entry.startTime); +} +}); +} +if (metrics.load_time_ms > 0 && metrics.load_time_ms < 300000) { +fetch(PERFORMANCE_ENDPOINT, { +method: 'POST', +headers: { 'Content-Type': 'application/json' }, +body: JSON.stringify(metrics), +credentials: 'same-origin' +}).catch(function() { +}); +} +} +function trackContactClicks() { +document.querySelectorAll('a[href^="mailto:"]').forEach(function(link) { +link.addEventListener('click', function(e) { +trackConversion('contact_click', 'email', link.href.replace('mailto:', '')); +}); +}); +document.querySelectorAll('a[href^="tel:"]').forEach(function(link) { +link.addEventListener('click', function(e) { +trackConversion('contact_click', 'phone', link.href.replace('tel:', '')); +}); +}); +if (window.location.pathname.startsWith('/company/')) { +document.querySelectorAll('a[target="_blank"][href^="http"]').forEach(function(link) { +const href = link.href.toLowerCase(); +if (!href.includes('facebook.com') && !href.includes('linkedin.com') && +!href.includes('instagram.com') && !href.includes('twitter.com')) { +link.addEventListener('click', function(e) { +trackConversion('contact_click', 'website', link.href); +}); +} +}); +} +} +function trackConversion(eventType, targetType, targetValue) { +let companyId = null; +const companyMeta = document.querySelector('meta[name="company-id"]'); +if (companyMeta && companyMeta.content) { +companyId = parseInt(companyMeta.content, 10); +} +fetch('/api/analytics/conversion', { +method: 'POST', +headers: { 'Content-Type': 'application/json' }, +body: JSON.stringify({ +event_type: eventType, +target_type: targetType, +target_value: targetValue, +company_id: companyId +}), +credentials: 'same-origin' +}).catch(function() { +}); +} +function sendHeartbeat() { +fetch(HEARTBEAT_ENDPOINT, { +method: 'POST', +headers: { 'Content-Type': 'application/json' }, +credentials: 'same-origin' +}).catch(function() { +}); +} +function sendTracking(data) { +fetch(TRACK_ENDPOINT, { +method: 'POST', +headers: { 'Content-Type': 'application/json' }, +body: JSON.stringify(data), +credentials: 'same-origin' +}).catch(function() { +}); +} +if (document.readyState === 'loading') { +document.addEventListener('DOMContentLoaded', init); +} else { +init(); +} +})(); \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 529c382..a47d80d 100755 --- a/templates/base.html +++ b/templates/base.html @@ -37,1474 +37,10 @@ - + - + @@ -1548,7 +84,7 @@ {% block head_extra %}{% endblock %} - + {% if is_staging %}