1espial = (function () { 2 "use strict"; 3 4 var location = window.location; 5 var document = window.document; 6 var scriptEl = document.currentScript; 7 var endpoint = scriptEl.getAttribute("data-api"); // Construct analytics endpoint 8 var lastPage; // Keeps track of the location.pathname 9 var scrollDepthSteps = [0, 25, 50, 75, 100]; 10 var currentScrollDepth = 0; // Keeps track of the scroll depts 11 12 // Check for proper setup 13 if (!scriptEl.hasAttribute("data-domain")) 14 console.error("Analytics configuration: domain not set."); 15 if (!scriptEl.hasAttribute("data-api")) 16 console.error("Analytics configuration: endpoint not set."); 17 18 /** 19 * Helper for logging when events are ignored 20 * @param {string} reason Text stating reason for ignoring event 21 * @param {object} options 22 * @param {function} [options.callback] Function to call after logging 23 */ 24 function onIgnoredEvent(reason, options) { 25 if (reason) console.warn("Ignoring Event: " + reason); 26 options && options.callback && options.callback(); 27 } 28 29 /** 30 * Returns matches based on wildcardPath 31 * @param {string} wildcardPath 32 * @returns {array} 33 */ 34 function pathMatches(wildcardPath) { 35 var actualPath = location.pathname; 36 return actualPath.match( 37 new RegExp( 38 "^" + 39 wildcardPath 40 .trim() 41 .replace(/\*\*/g, ".*") 42 .replace(/([^\.])\*/g, "$1[^\\s/]*") + 43 "/?$", 44 ), 45 ); 46 } 47 48 /** 49 * Throttles a mainFunction 50 * @returns {function} A throttled version of the function 51 */ 52 function throttle(mainFunction, delay) { 53 let timerFlag = null; // Variable to keep track of the timer 54 55 // Returning a throttled version 56 return (...args) => { 57 if (timerFlag === null) { 58 // If there is no timer currently running 59 mainFunction(...args); // Execute the main function 60 timerFlag = setTimeout(() => { 61 // Set a timer to clear the timerFlag after the specified delay 62 timerFlag = null; // Clear the timerFlag to allow the main function to be executed again 63 }, delay); 64 } 65 }; 66 } 67 68 /** 69 * Check if the href of a link is outbound 70 * @param {HTMLElement} link 71 * @returns {boolean} 72 */ 73 function isOutboundLink(link) { 74 return link && link.href && link.host && link.host !== location.host; 75 } 76 77 /** 78 * Returns the current scroll depth 79 * @returns {int} 80 */ 81 function getScrollPercent() { 82 var h = document.documentElement, 83 b = document.body, 84 st = "scrollTop", 85 sh = "scrollHeight"; 86 let scrollDepth = Math.ceil( 87 ((h[st] || b[st]) / ((h[sh] || b[sh]) - h.clientHeight)) * 100, 88 ); 89 if (scrollDepth > 95) scrollDepth = 100; 90 return scrollDepth; 91 } 92 93 /** 94 * Returns the first link-element in the DOM that is a parent of the cliked element 95 * @param {HTMLElement} link 96 * @returns {HTMLElement|null} 97 */ 98 function getLinkEl(link) { 99 while ( 100 link && 101 (typeof link.tagName === "undefined" || !isLink(link) || !link.href) 102 ) { 103 link = link.parentNode; 104 } 105 return link; 106 } 107 108 /** 109 * Returns the first button-element in the DOM that is a parent of the cliked element 110 * @param {HTMLElement} link 111 * @returns {HTMLElement|null} 112 */ 113 function getButtonEl(el) { 114 while (el && (typeof el.tagName === "undefined" || !isButton(el))) { 115 el = el.parentNode; 116 } 117 return el; 118 } 119 120 /** 121 * Check if a element is a link 122 * @param {HTMLElement} element 123 * @returns {boolean} 124 */ 125 function isLink(element) { 126 return element && element.tagName && element.tagName.toLowerCase() === "a"; 127 } 128 129 /** 130 * Check if a element is a button 131 * @param {HTMLElement} element 132 * @returns {boolean} 133 */ 134 function isButton(element) { 135 return ( 136 element && element.tagName && element.tagName.toLowerCase() === "button" 137 ); 138 } 139 140 /** 141 * Handle clicks 142 * @param {event} event 143 * @returns {function|null} 144 */ 145 function handleLinkClickEvent(event) { 146 if (event.type === "auxclick" && event.button !== "MIDDLE_MOUSE_BUTTON") { 147 return; 148 } 149 150 var link = getLinkEl(event.target); 151 var button = getButtonEl(event.target); 152 153 if (isOutboundLink(link)) { 154 return sendLinkClickEvent(event, link, { 155 name: "outbound link: click", 156 props: { url: link.href }, 157 }); 158 } 159 160 const buttonClassRegex = new RegExp("button__StyledButton-sc"); 161 if (button && buttonClassRegex.test(button.getAttribute("class"))) { 162 if (isButton(button)) 163 trigger("cta: click", { props: { str_value: button.textContent } }); 164 if (isLink(button)) 165 trigger("cta: click", { 166 props: { url: link.href, str_value: button.textContent }, 167 }); 168 } 169 170 const linkClassRegex = new RegExp("button__StyledAnchor-sc"); 171 if (link && linkClassRegex.test(link.getAttribute("class"))) { 172 return sendLinkClickEvent(event, link, { 173 name: "cta: click", 174 props: { url: link.href, str_value: link.textContent }, 175 }); 176 } 177 178 // TODO: Implement sendLinkClickEvent for download links 179 // TODO: Implement sendLinkClickEvent for mailto: and tel: 180 } 181 182 /** 183 * Check if we should follow the link 184 * @param {event} event 185 * @param {HTMLElement} link 186 * @returns {boolean} 187 */ 188 function shouldFollowLink(event, link) { 189 // If default has been prevented by an external script, we should not intercept navigation. 190 if (event.defaultPrevented) { 191 return false;
192 } 193 194 var targetsCurrentWindow = 195 !link.target || link.target.match(/^_(self|parent|top)$/i); 196 var isRegularClick = 197 !(event.ctrlKey || event.metaKey || event.shiftKey) && 198 event.type === "click"; 199 return targetsCurrentWindow && isRegularClick; 200 } 201 202 /** 203 * Track click event 204 * @param {Event} event 205 * @param {HTMLElement} link 206 * @param {Object} eventAttrs 207 */ 208 function sendLinkClickEvent(event, link, eventAttrs) { 209 var followedLink = false; 210 211 function followLink() { 212 if (!followedLink) { 213 followedLink = true; 214 window.location = link.href; 215 } 216 } 217 218 if (shouldFollowLink(event, link)) { 219 var attrs = { props: eventAttrs.props, callback: followLink }; 220 attrs.revenue = eventAttrs.revenue; 221 trigger(eventAttrs.name, attrs); 222 setTimeout(followLink, 5000); 223 event.preventDefault(); 224 } else { 225 var attrs = { props: eventAttrs.props }; 226 attrs.revenue = eventAttrs.revenue; 227 trigger(eventAttrs.name, attrs); 228 } 229 } 230 231 /** 232 * Handles tracking scroll depth 233 */ 234 function handleScrollEvent() { 235 // Hold the scrollDepth as we enter the function 236 const tempScrollDepth = currentScrollDepth; 237 // Find new the closest scroll step 238 let newScrollDepth = Math.max( 239 ...scrollDepthSteps.filter((num) => num <= getScrollPercent()), 240 ); 241 242 // If the scrollDepth has increased 243 if (newScrollDepth > tempScrollDepth) { 244 // Uppdate current scroll depth 245 currentScrollDepth = newScrollDepth; 246 // Since the function is throttled, the user might scroll so fast some steps are skipped. 247 let newScrollDepthIndex = scrollDepthSteps.indexOf(newScrollDepth); 248 let tempScrollDepthIndex = scrollDepthSteps.indexOf(tempScrollDepth); 249 250 // If the scrolldepth when entering the function is not the same as
251 // the one before the current we know we have skipped one or more steps 252 if (newScrollDepthIndex - tempScrollDepthIndex > 1) { 253 for (let i = tempScrollDepthIndex + 1; i < newScrollDepthIndex; i++) { 254 trigger("scroll", { props: { int_value: scrollDepthSteps[i] } }); 255 } 256 } 257 trigger("scroll", { props: { int_value: currentScrollDepth } }); 258 } 259 } 260 261 /** 262 * Log event 263 * @param {string} eventName Name of the event 264 * @param {object} options 265 * @param {object} [options.meta] 266 * @param {object} [options.props] Attach custom properties (also known as custom dimensions in Google Analytics) when sending pageviews or custom events in order to create custom metrics. 267 * @param {object} [options.revenue] Used for ecommerce and assigning dynamic monetary values to goals and custom events to track revenue attribution. 268 */ 269 function trigger(eventName, options) { 270 // Ignore localhost and headless browsers 271 if ( 272 /^localhost$|^127(\.[0-9]+){0,2}\.[0-9]+$|^\[::1?\]$/.test( 273 location.hostname, 274 ) || 275 location.protocol === "file:" 276 ) { 277 return onIgnoredEvent("localhost", options); 278 } 279 if ( 280 window._phantom || 281 window.__nightmare || 282 window.navigator.webdriver || 283 window.Cypress 284 ) { 285 return onIgnoredEvent(null, options); 286 } 287 // Ignore if flagged in localStorage 288 // Wrapped in try to prevent throwing error if localStorage is not accessible 289 try { 290 if (window.localStorage.analytics_ignore === "true") { 291 return onIgnoredEvent("localStorage flag", options); 292 } 293 } catch (e) { 294 // Do nothing 295 } 296 297 // Check if url is in include or exclude list 298 var dataIncludeAttr = scriptEl && scriptEl.getAttribute("data-include"); 299 var dataExcludeAttr = scriptEl && scriptEl.getAttribute("data-exclude"); 300 301 if (eventName === "pageview") { 302 var isIncluded = 303 !dataIncludeAttr || 304 (dataIncludeAttr && dataIncludeAttr.split(",").some(pathMatches)); 305 var isExcluded = 306 dataExcludeAttr && dataExcludeAttr.split(",").some(pathMatches); 307 if (!isIncluded || isExcluded) 308 return onIgnoredEvent("exclusion rule", options); 309 } 310 311 // Build the payload 312 var payload = {}; 313 payload.n = eventName; 314 payload.u = location.href; 315 payload.d = scriptEl.getAttribute("data-domain"); 316 payload.r = options && options.r ? options.r : document.referrer || null; 317 318 if (options && options.meta) { 319 payload.m = options.meta; 320 } 321 if (options && options.props) { 322 payload.p = options.props; 323 } 324 if (options && options.revenue) { 325 payload.$ = options.revenue; 326 } 327 328 // Attach attributes from script-tag to payload properties 329 var propAttributes = scriptEl.getAttributeNames().filter(function (name) { 330 return name.substring(0, 6) === "event-"; 331 }); 332 333 var props = payload.p || {}; 334 335 propAttributes.forEach(function (attribute) { 336 var propKey = attribute.replace("event-", ""); 337 var propValue = scriptEl.getAttribute(attribute); 338 props[propKey] = props[propKey] || propValue; 339 }); 340 341 payload.p = props; 342 343 // Send request 344 var request = new XMLHttpRequest(); 345 request.open("POST", endpoint, true); 346 request.setRequestHeader("Content-Type", "application/json"); 347 348 request.send(JSON.stringify(payload)); 349 350 request.onreadystatechange = function () { 351 if (request.readyState === 4) { 352 options && options.callback && options.callback(); 353 } 354 }; 355 } 356 357 /** 358 * Trigger a new pageview if the location has changed 359 */ 360 function pageview() { 361 if (lastPage === location.pathname) return; 362 lastPage = location.pathname; 363 trigger("pageview"); 364 } 365 366 /** 367 * Initializes the component 368 * @public 369 */ 370 function init() { 371 // If pushState is used we inject a function track changes to pushState 372 var his = window.history; 373 if (his.pushState) { 374 var originalPushState = his["pushState"]; 375 his.pushState = function () { 376 originalPushState.apply(this, arguments); 377 pageview(); 378 }; 379 window.addEventListener("popstate", pageview); 380 } 381 382 // Trigger a pageview when the page once the document is ready 383 if (document.visibilityState === "prerender") { 384 document.addEventListener("visibilitychange", function () { 385 if (!lastPage && document.visibilityState === "visible") { 386 pageview(); 387 } 388 }); 389 } else { 390 pageview(); 391 } 392 393 document.addEventListener("click", handleLinkClickEvent); 394 document.addEventListener("auxclick", handleLinkClickEvent); 395 document.addEventListener("scroll", throttle(handleScrollEvent, 500)); 396 } 397 398 // Initialize the component 399 init(); 400 401 return { 402 trigger: trigger, 403 }; 404})();
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.