1// Public measurement IDs belong in Analytics.astro. Never put API keys here. 2const campaigns = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term']; 3export function cleanUrl(value, keepCampaigns = false) { 4 try { 5 const url = new URL(value, 'https://ar.bd'); 6 if (!['http:', 'https:'].includes(url.protocol)) return ''; 7 const query = new URLSearchParams(); 8 if (keepCampaigns) for (const key of campaigns) { 9 const v = url.searchParams.get(key); 10 if (v && /^[\w .~-]{1,100}$/.test(v)) query.set(key, v); 11 } 12 return url.origin + url.pathname + (query.size ? '?' + query : ''); 13 } catch { return ''; } 14} 15 16export function actionFor(el) { 17 if (!el || el.disabled || el.classList.contains('is-filtered')) return null; 18 const d = el.dataset; 19 if ('askStart' in d) return ['ai_start', {}]; 20 if ('group' in d) return ['project_filter', {filter_type:'kind', filter_value:d.group}]; 21 if ('topic' in d) return [el.closest('#project-map') ? 'project_filter' : 'note_filter', {filter_type:'topic', filter_value:d.topic}]; 22 if ('focusSelect' in d || 'focusNode' in d) return ['project_select', {project_id:d.focusSelect || d.focusNode}]; 23 if ('toggleGraph' in d) return ['project_map_toggle', {expanded:el.getAttribute('aria-expanded') === 'true'}]; 24 if (el.matches('.story-stop,[data-story-prev],[data-story-next]')) return ['timeline_chapter', {chapter:el.closest('[data-story]')?.querySelector('[role="tab"][aria-selected="true"]')?.id || ''}]; 25 if (el.matches('a[href]')) { 26 const url = cleanUrl(el.href); 27 if (!url) return null; 28 const host = new URL(url).hostname; 29 if (host !== location.hostname) return ['outbound_click', {link_url:url, link_domain:host}]; 30 if (el.closest('[data-writing-preview],.archive-picks,.archive-list')) return ['article_open', {link_url:url}]; 31 } 32 return null; 33} 34 35function start() { 36 const config = document.querySelector('script[data-site-analytics]'); 37 if (!config || location.hostname !== 'ar.bd' || navigator.doNotTrack === '1') return; 38 const pageUrl = cleanUrl(location.href, true); 39 const referrer = document.referrer ? cleanUrl(document.referrer) : ''; 40 const gaId = config.dataset.gaId; 41 window.dataLayer = window.dataLayer || []; 42 window.gtag = function () { window.dataLayer.push(arguments); }; 43 window.gtag('js', new Date()); 44 window.gtag('config', gaId, {page_location:pageUrl, page_referrer:referrer, 45 allow_google_signals:false, allow_ad_personalization_signals:false}); 46 const ga = document.createElement('script'); 47 ga.async = true; 48 ga.src = 'https://www.googletagmanager.com/gtag/js?id=' + encodeURIComponent(gaId); 49 document.head.append(ga); 50 51 const queued = []; 52 let ready = false; 53 // Strip search text, arbitrary URL parameters and fragments while retaining 54 // the campaign fields used for Facebook/LinkedIn attribution. 55 window.arAnalyticsBeforeSend = (_type, payload) => ({...payload, 56 url:cleanUrl(payload.url, true).replace(location.origin, ''), 57 referrer:payload.referrer ? cleanUrl(payload.referrer) : ''}); 58 const umami = document.createElement('script'); 59 umami.src = 'https://agent-analytics.asif.dev/script.js'; 60 umami.defer = true; 61 umami.dataset.websiteId = config.dataset.umamiId; 62 umami.dataset.domains = 'ar.bd'; 63 umami.dataset.autoTrack = 'false'; 64 umami.dataset.beforeSend = 'arAnalyticsBeforeSend'; 65 umami.addEventListener('load', () => { 66 if (!window.umami) return; 67 ready = true; 68 Promise.resolve(window.umami.track()).catch(() => {}); 69 for (const event of queued.splice(0)) Promise.resolve(window.umami.track(...event)).catch(() => {}); 70 }); 71 document.head.append(umami); 72 73 function track(name, data) { 74 window.gtag('event', name, data); 75 if (ready) Promise.resolve(window.umami.track(name, data)).catch(() => {}); 76 else if (queued.length < 20) queued.push([name, data]); 77 } 78 // Observe existing controls without altering their behavior or historic pages. 79 // Bubbling runs after the control's own handler updates its state. The event 80 // retains its original target even when the AI launcher replaces its button. 81 document.addEventListener('click', event => { 82 const el = event.target.closest?.('a,button,[role="button"]'); 83 if (!el) return; 84 const action = actionFor(el); if (action) track(...action); 85 }); 86 document.addEventListener('keydown', event => { 87 const el = event.target.closest?.('[data-focus-node]'); 88 if (el && ['Enter',' '].includes(event.key)) { 89 const action = actionFor(el); if (action) track(...action); 90 } 91 }); 92 document.addEventListener('toggle', event => { 93 const el = event.target; 94 if (el.matches('details') && el.open) track('content_expand', {section:el.closest('section')?.id || 'content'}); 95 }, true); 96 document.addEventListener('change', event => { 97 const el = event.target; 98 if (el.matches('input[type="search"]')) track('site_search', { 99 section:el.closest('section')?.id || 'writing', query_length:el.value.trim().length, 100 }); 101 }); 102} 103if (typeof document !== 'undefined') start();
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.