1/* First-party aggregate website events. No person/device identifiers or app telemetry. */ 2(function () { 3 'use strict'; 4 var REF = /^[a-z0-9][a-z0-9_-]{0,47}$/; 5 var PAGES = ['/', '/download', '/pricing', '/about', '/roadmap', '/account', '/welcome', '/signin', '/trial', '/privacy']; 6 var DETAILS = { 7 download_click: ['nav', 'hero', 'footer', 'pricing', 'download', 'account', 'welcome'], 8 discord_click: ['nav', 'hero', 'footer', 'community', 'about', 'roadmap'], 9 faq_open: ['screen_privacy', 'end_session', 'offline', 'supported_browsers', 'restarting', 'trial_account', 'trial_end', 'cancel', 'multiple_macs'], 10 calculator_use: [''] 11 }; 12 var path = location.pathname.replace(/\.html$/, '').replace(/\/$/, '') || '/'; 13 if (path === '/index') path = '/'; 14 var page = PAGES.indexOf(path) !== -1 ? path : ''; 15 var referral = ''; 16 var seen = Object.create(null); 17 18 function optedOut() { 19 return navigator.globalPrivacyControl === true || navigator.doNotTrack === '1' || 20 navigator.doNotTrack === 'yes' || window.doNotTrack === '1'; 21 } 22 function clearReferral() { referral = ''; } 23 function getReferral() { 24 if (optedOut() || !page) clearReferral(); 25 return referral; 26 } 27 28 // The shared campaign ID stays in the URL so reloads, copied links and new 29 // tabs work without cookies, browser storage or a visitor/session identifier. 30 try { 31 var url = new URL(location.href); 32 var candidate = url.searchParams.get('ref') || ''; 33 if (!optedOut() && page && REF.test(candidate)) referral = candidate; 34 } catch (_) { /* Bad URLs must not affect the page. */ } 35 36 function track(event, detail) { 37 detail = detail || ''; 38 if (!page || optedOut() || !Object.prototype.hasOwnProperty.call(DETAILS, event) || DETAILS[event].indexOf(detail) === -1) return; 39 var once = event === 'faq_open' || event === 'calculator_use'; 40 var key = event + ':' + detail; 41 if (once && seen[key]) return; 42 if (once) seen[key] = true; 43 try { 44 // Never read page contents, input values, account state, full URLs, or referrers. 45 var request = fetch('/api/analytics.php', { 46 method: 'POST', credentials: 'omit', keepalive: true, referrerPolicy: 'no-referrer', 47 headers: { 'Content-Type': 'application/json' }, 48 body: JSON.stringify({ event: event, page: page, detail: detail, referral_id: getReferral() }) 49 }); 50 if (request && typeof request.catch === 'function') request.catch(function () {}); 51 } catch (_) { /* Analytics never blocks navigation or other interactions. */ } 52 } 53 54 function installerURL(channel) { 55 channel = channel === 'beta' ? 'beta' : 'stable'; 56 var query = new URLSearchParams({ channel: channel, page: page || '/download' }); 57 var ref = getReferral(); 58 if (ref) query.set('ref', ref); 59 if (optedOut()) query.set('tracking', 'off'); 60 return '/api/download.php?' + query.toString(); 61 } 62 function checkoutContext() { 63 if (optedOut()) { clearReferral(); return false; } 64 if (['/pricing', '/account', '/welcome'].indexOf(page) === -1) return undefined; 65 return { page: page, referral_id: getReferral() }; 66 } 67 window.AttentusAnalytics = { track: track, getReferral: getReferral, installerURL: installerURL, checkoutContext: checkoutContext }; 68 69 function updateInstaller(link) { 70 try { 71 var url = new URL(link.href, location.href); 72 if (url.origin !== location.origin) return; 73 if (url.pathname === '/api/download.php' || 74 (url.pathname === '/update/serve.php' && url.searchParams.get('asset') === 'installer')) { 75 link.href = installerURL(url.searchParams.get('channel')); 76 } 77 } catch (_) {} 78 } 79 80 function updateReferral(link) { 81 try { 82 var url = new URL(link.href, location.href); 83 var destination = url.pathname.replace(/\.html$/, '').replace(/\/$/, '') || '/'; 84 if (destination === '/index') destination = '/'; 85 // Only public website pages; never append campaign state to external, 86 // authentication API, or arbitrary download URLs. 87 if (url.origin !== location.origin || PAGES.indexOf(destination) === -1) return; 88 var ref = getReferral(); 89 if (optedOut()) url.searchParams.delete('ref'); 90 else if (ref && !url.searchParams.has('ref')) url.searchParams.set('ref', ref); 91 link.href = url.pathname + url.search + url.hash; 92 } catch (_) {} 93 } 94 95 function ready() { 96 if (!page) return; 97 document.querySelectorAll('a[href]').forEach(updateReferral); 98 document.querySelectorAll('a[data-analytics-event="download_click"]').forEach(updateInstaller); 99 document.addEventListener('click', function (event) { 100 var link = event.target.closest && event.target.closest('a[href]'); 101 if (!link) return; 102 updateReferral(link); 103 var name = link.dataset.analyticsEvent;
104 try { 105 var url = new URL(link.href, location.href); 106 if (name === 'download_click') { 107 // Signed-in UI may turn a download CTA into an account link. 108 if (url.origin !== location.origin || ['/download', '/api/download.php', '/update/serve.php'].indexOf(url.pathname) === -1) return; 109 updateInstaller(link); 110 } else if (name === 'discord_click') { 111 if (url.protocol !== 'https:' || url.hostname !== 'discord.gg') return; 112 } else { return; } 113 track(name, link.dataset.analyticsDetail); 114 } catch (_) {} 115 }, true); 116 document.querySelectorAll('details[data-analytics-faq]').forEach(function (item) { 117 item.addEventListener('toggle', function () { 118 if (item.open) track('faq_open', item.dataset.analyticsFaq); 119 }); 120 }); 121 var calculator = document.getElementById('cHrs'); 122 if (page === '/' && calculator) calculator.addEventListener('input', function () { track('calculator_use', ''); }); 123 } 124 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', ready, { once: true }); 125 else ready(); 126})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.