1(function () { 2 'use strict'; 3 4 var loader = document.currentScript; 5 var endpoint = loader && loader.dataset ? loader.dataset.endpoint : ''; 6 var site = loader && loader.dataset ? loader.dataset.site || '' : ''; 7 if (!endpoint || typeof window.fetch !== 'function') return; 8 9 var queue = []; 10 var flushTimer = null; 11 var maxEvents = 20; 12 var sessionTimeout = 30 * 60 * 1000; 13 var maxEngagement = 30 * 60 * 1000; 14 var sessionStorageKey = 'blogmaker_analytics_session_v1'; 15 var fallbackSession = null; 16 var visibleSince = null; 17 var blockedProperties = { 18 blog_id: true, 19 blog_url: true, 20 client_id: true, 21 clientid: true, 22 domain: true, 23 email: true, 24 hostname: true, 25 namespace: true, 26 referrer: true, 27 site: true, 28 url: true, 29 user_id: true, 30 userid: true, 31 website_hash: true, 32 website_id: true 33 }; 34 35 function uuid() { 36 if (window.crypto && typeof window.crypto.randomUUID === 'function') { 37 return window.crypto.randomUUID(); 38 } 39 40 var bytes = new Uint8Array(16); 41 if (window.crypto && typeof window.crypto.getRandomValues === 'function') { 42 window.crypto.getRandomValues(bytes); 43 } else { 44 for (var index = 0; index < bytes.length; index++) bytes[index] = Math.floor(Math.random() * 256); 45 } 46 bytes[6] = (bytes[6] & 15) | 64; 47 bytes[8] = (bytes[8] & 63) | 128; 48 var hex = Array.prototype.map.call(bytes, function (value) { 49 return value.toString(16).padStart(2, '0'); 50 }).join(''); 51 return hex.slice(0, 8) + '-' + hex.slice(8, 12) + '-' + hex.slice(12, 16) + '-' + 52 hex.slice(16, 20) + '-' + hex.slice(20); 53 } 54 55 function properties(value) { 56 var normalized = {}; 57 var accepted = 0; 58 Object.keys(value || {}).some(function (key) { 59 var property = value[key]; 60 if (blockedProperties[key.toLowerCase()]) return false; 61 if (['string', 'number', 'boolean'].indexOf(typeof property) === -1) return false; 62 if (typeof property === 'number' && !Number.isFinite(property)) return false; 63 normalized[key] = property; 64 accepted++; 65 return accepted >= 20; 66 }); 67 return normalized; 68 } 69 70 function sessionId(now) { 71 var stored = null; 72 try { 73 var value = window.sessionStorage.getItem(sessionStorageKey); 74 stored = value ? JSON.parse(value) : fallbackSession; 75 } catch (error) { 76 stored = fallbackSession; 77 } 78 var reusable = stored && typeof stored.id === 'string' && 79 /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(stored.id) && 80 typeof stored.lastActivityAt === 'number' && Number.isFinite(stored.lastActivityAt) && 81 now >= stored.lastActivityAt && now - stored.lastActivityAt <= sessionTimeout; 82 var current = { id: reusable ? stored.id : uuid(), lastActivityAt: now }; 83 fallbackSession = current; 84 try { 85 window.sessionStorage.setItem(sessionStorageKey, JSON.stringify(current)); 86 } catch (error) { 87 // The in-memory fallback still scopes the token to this page. The server stores only a 88 // site/day HMAC of this short-lived token. 89 } 90 return current.id; 91 } 92 93 function event(eventName, eventProperties, engagementMs) { 94 var now = Date.now(); 95 var search = new URLSearchParams(window.location.search); 96 return { 97 engagementMs: Math.max(0, Math.min(Math.floor(engagementMs || 0), maxEngagement)), 98 eventId: uuid(), 99 eventName: eventName, 100 occurredAt: new Date(now).toISOString(), 101 path: window.location.pathname, 102 referrer: document.referrer, 103 sessionId: sessionId(now), 104 utmSource: search.get('utm_source') || '', 105 utmMedium: search.get('utm_medium') || '', 106 utmCampaign: search.get('utm_campaign') || '', 107 utmContent: search.get('utm_content') || '', 108 utmTerm: search.get('utm_term') || '', 109 properties: properties(eventProperties) 110 }; 111 } 112 113 function flush(keepalive) { 114 if (flushTimer !== null) { 115 window.clearTimeout(flushTimer); 116 flushTimer = null; 117 } 118 var events = queue.splice(0, maxEvents); 119 if (!events.length) return; 120 121 try { 122 var payload = { events: events }; 123 if (site) payload.site = site; 124 window.fetch(endpoint, { 125 method: 'POST', 126 headers: { 'Content-Type': 'application/json' }, 127 body: JSON.stringify(payload), 128 credentials: 'omit', 129 keepalive: !!keepalive 130 }).catch(function () {}); 131 } catch (error) { 132 // Analytics is best-effort and must never interrupt the reader experience. 133 } 134 } 135 136 function enqueue(eventName, eventProperties, engagementMs) { 137 queue.push(event(eventName, eventProperties || {}, engagementMs)); 138 if (queue.length > maxEvents) queue = queue.slice(-maxEvents); 139 if (flushTimer === null) flushTimer = window.setTimeout(function () { flush(false); }, 250); 140 } 141 142 function trackCustomerInteractions() { 143 // This shared client also runs on Blogmaker's own product surfaces. Interaction reporting is 144 // intentionally limited to customer blogs, whose script tag has no fixed `data-site` value. 145 if (site) return; 146 147 var playedVideos = []; 148 var downloadPattern = /\.(?:7z|csv|docx?|epub|gz|mp3|mp4|pdf|pptx?|rar|txt|xlsx?|zip)(?:$|[?#])/i; 149 150 document.addEventListener('click', function (clickEvent) { 151 var target = clickEvent.target; 152 if (!target || typeof target.closest !== 'function') return; 153 var link = target.closest('a'); 154 if (!link) return; 155 156 var eventName = ''; 157 if (link.matches('.single-image-gallery')) { 158 eventName = 'gallery_image_click'; 159 } else if (link.closest('.pagination')) { 160 eventName = 'pagination_click'; 161 } else if (link.closest('.post-tags, .tags-top')) { 162 eventName = 'tag_click'; 163 } else if (link.matches('.cta-button, .content-button')) { 164 eventName = 'cta_click'; 165 } else { 166 var href = link.getAttribute('href') || ''; 167 if (link.hasAttribute('download') || downloadPattern.test(href)) { 168 eventName = 'download_click'; 169 } else { 170 try { 171 var destination = new URL(href, window.location.href); 172 if (/^https?:$/.test(destination.protocol) && destination.hostname !== window.location.hostname) { 173 eventName = 'outbound_link_click'; 174 } 175 } catch (error) {} 176 } 177 } 178 179 if (eventName) enqueue(eventName, {}); 180 }); 181 182 // Native media `play` does not bubble, so listen during capture. Repeated pause/play actions 183 // on the same element count as one play for this page view. 184 document.addEventListener('play', function (playEvent) { 185 var video = playEvent.target; 186 if (!video || video.tagName !== 'VIDEO' || playedVideos.indexOf(video) !== -1) return; 187 playedVideos.push(video); 188 enqueue('video_play', {}); 189 }, true); 190 191 // Search terms are deliberately excluded; only a valid search submission is counted. 192 document.addEventListener('keypress', function (keyEvent) { 193 var input = keyEvent.target; 194 if (keyEvent.key !== 'Enter' || !input || input.id !== 'search-js') return; 195 if (String(input.value || '').trim().length >= 2) enqueue('search', {}); 196 }); 197 198 // The newsletter UI submits through the shared fetch endpoint. Observe only its success code 199 // from a cloned response; the email itself is never read or copied into analytics. 200 var originalFetch = window.fetch; 201 window.fetch = function (resource, options) { 202 var responsePromise = originalFetch.apply(this, arguments); 203 try { 204 var body = options && options.body; 205 var resourceUrl = typeof resource === 'string' ? resource : (resource && resource.url) || ''; 206 var isNewsletterRequest = /\/internal\/function(?:[?#]|$)/.test(resourceUrl) && 207 body && typeof body.has === 'function' && body.has('token') && body.has('email'); 208 if (isNewsletterRequest) { 209 responsePromise.then(function (response) { 210 if (!response || typeof response.clone !== 'function') return; 211 response.clone().json().then(function (data) { 212 if (data && Number(data.code) === 3) enqueue('newsletter_subscription', {}); 213 }).catch(function () {}); 214 }).catch(function () {}); 215 } 216 } catch (error) {} 217 return responsePromise; 218 }; 219 } 220 221 function takeVisibleEngagement() { 222 if (visibleSince === null) return 0; 223 var now = Date.now(); 224 var engagementMs = Math.max(0, Math.min(now - visibleSince, maxEngagement)); 225 visibleSince = now; 226 return Math.floor(engagementMs); 227 } 228 229 function heartbeat() { 230 if (document.visibilityState === 'visible') enqueue('analytics_heartbeat', {}, takeVisibleEngagement()); 231 } 232
233 function finishVisiblePeriod() { 234 var engagementMs = takeVisibleEngagement(); 235 visibleSince = null; 236 if (engagementMs > 0) enqueue('analytics_engagement', {}, engagementMs); 237 } 238 239 trackCustomerInteractions(); 240 enqueue('page_view', { page_title: document.title }); 241 if (document.visibilityState === 'visible') visibleSince = Date.now(); 242 document.addEventListener('visibilitychange', function () { 243 if (document.visibilityState === 'hidden') { 244 finishVisiblePeriod(); 245 flush(true); 246 } else { 247 visibleSince = Date.now(); 248 heartbeat(); 249 } 250 }); 251 window.addEventListener('pagehide', function () { 252 finishVisiblePeriod(); 253 flush(true); 254 }); 255 window.setInterval(heartbeat, 30000); 256})();
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.