1/** 2 * Generates a clean, isolated interaction_data object where all unused keys 3 * explicitly evaluate to `undefined` to force GTM memory resets and auto-pruning. 4 */ 5function createInteractionPayload(eventSpecificData) { 6 const baseSchema = { 7 // Universal Core 8 type: undefined, 9 page_location: document.location.href, 10 page_title: document.title || undefined, 11 interaction_timestamp: Date.now(), 12 interaction_id: (window.gUMG && window.gUMG.interactionToken) || undefined, 13 hashed_ip: (window.gUMG && window.gUMG.clientData?.hashedIP) || undefined, 14 15 // Shared / Overlapping Fields 16 scroll_position: undefined, 17 18 // Click-Specific 19 link_url: undefined, 20 link_text: undefined, 21 outbound: undefined, 22 click_location: undefined, 23 click_type: undefined, 24 target_element: undefined, 25 target_element_meta: undefined, 26 landmark_info: undefined, 27 descendant_path: undefined, 28 click_number: undefined, 29 click_id: undefined, 30 click_on_touch: undefined, 31 pointer_type: undefined, 32 modifier_key: undefined, 33 34 // Scroll-Specific 35 scroll_trigger: undefined, 36 scroll_depth: undefined, 37 scroll_location: undefined, 38 total_scroll_px: undefined, 39 reversal_count: undefined, 40 pause_count: undefined, 41 42 // Type/Input-Specific 43 char_count: undefined, 44 backspace_count: undefined, 45 input_duration_ms: undefined, 46 47 // Timing-Specific 48 engaged_time_sec: undefined, 49 engaged_time_ms: undefined, 50 timing_trigger: undefined 51 }; 52 53 // Merging specific event data overwrites defaults while keeping unused keys 'undefined' 54 return Object.assign({}, baseSchema, eventSpecificData); 55} 56 57(function () { 58 'use strict'; 59 60 window.dataLayer = window.dataLayer || []; 61 window.clickStack = window.clickStack || []; 62 63 const clickStack = window.clickStack; 64 const bufferQueue = []; 65 const BUFFER_DELAY_MS = 2200; 66 67 /** 68 * Flushes any buffered clicks immediately on page unload. 69 */ 70 function flushBuffer() { 71 while (bufferQueue.length > 0) { 72 dispatchBufferedClick(bufferQueue.shift()); 73 } 74 } 75 76 window.addEventListener('visibilitychange', function () { 77 if (document.visibilityState === 'hidden') { 78 flushBuffer(); 79 } 80 }); 81 window.addEventListener('pagehide', flushBuffer); 82 83 /** 84 * Formats and pushes the click payload to window.dataLayer & clickStack. 85 */ 86 function dispatchBufferedClick(bufferedEvent) { 87 if (!bufferedEvent) return; 88 89const payload = { 90 event: "user_interaction", 91 "interaction_data": createInteractionPayload({ 92 "type": "click", 93 94 // Click-Specific Attributes 95 "link_url": bufferedEvent.targetURL || undefined, 96 "link_text": bufferedEvent.clickText || undefined, 97 "outbound": bufferedEvent.outbound, 98 99 // Page Context (Overrides factory default with buffered location/title) 100 "page_location": bufferedEvent.pageURL, 101 "page_title": bufferedEvent.pageTitle, 102 103 // Location & Coordinates 104 "click_location": bufferedEvent.clickLocation, 105 "scroll_position": bufferedEvent.scrollPosition, 106 107 // Single Categorical Behavioral State 108 "click_type": bufferedEvent.clickType, 109 110 // Target Element Identifiers & Accessibility Meta 111 "target_element": bufferedEvent.targetElement, 112 "target_element_meta": bufferedEvent.targetElementMeta, 113 "landmark_info": bufferedEvent.landmarkInfo, 114 "descendant_path": bufferedEvent.descendantPath || undefined, 115 116 // Interaction Metadata 117 "click_number": bufferedEvent.click, 118 "click_id": bufferedEvent.clickId, 119 "click_on_touch": bufferedEvent.clickOnTouch, 120 "pointer_type": bufferedEvent.pointerType || undefined, 121 "modifier_key": bufferedEvent.modifierKey, 122 123 // Session & Timing (Overrides factory default Date.now() with precise click event time) 124 "interaction_timestamp": bufferedEvent.clickTime, 125 "interaction_id": bufferedEvent.interactionToken || undefined, 126 "hashed_ip": bufferedEvent.hashedIP || undefined 127 }) 128}; 129 130 131 clickStack.push(payload); 132 window.dataLayer.push(payload); 133 } 134 135 // Main Click Listener 136 window.addEventListener('click', function (event) { 137 let a = new clickEvent(event, event.target); 138 bufferQueue.push(a); 139 140 if (bufferQueue.length >= 5) { 141 const latestClickTime = a.clickTime; 142 const fifthPriorClickTime = bufferQueue[bufferQueue.length - 5].clickTime; 143 144 if (latestClickTime - fifthPriorClickTime < 2000) { 145 bufferQueue.forEach(item => item.rageClick = true); 146 } 147 } 148 149 setTimeout(() => { 150 if (bufferQueue.length > 0) { 151 dispatchBufferedClick(bufferQueue.shift()); 152 } 153 }, BUFFER_DELAY_MS); 154 }); 155 156 class targetProperties { 157 #landmarks = [ 158 { name: "aside", role: "complementary", excludedAncestors: [], requiredAttributes: [], descriptor: "Aside" }, 159 { name: "footer", role: "contentinfo", excludedAncestors: ['article', 'aside', 'main', 'nav', 'section'], requiredAttributes: [], descriptor: "Content Info (Footer)" }, 160 { name: "form", role: "form", excludedAncestors: [], requiredAttributes: ['aria-label', 'title', 'aria-labelledby'], descriptor: "Form" }, 161 { name: "header", role: "banner", excludedAncestors: ['article', 'aside', 'main', 'nav', 'section'], requiredAttributes: [], descriptor: "Banner (Header)" },
162 { name: "section", role: "region", excludedAncestors: [], requiredAttributes: ['aria-label', 'title', 'aria-labelledby'], descriptor: "Region" }, 163 { name: "main", role: "main", excludedAncestors: [], requiredAttributes: [], descriptor: "Main" }, 164 { name: "nav", role: "navigation", excludedAncestors: [], requiredAttributes: [], descriptor: "Navigation" } 165 ]; 166 167 #ancestors = []; 168 169 constructor(target) { 170 this.theTarget = []; 171 this.#getAncestors(target); 172 this.#describeTarget(target); 173 this.landmark = this.#determineLandmark(); 174 this.descendantPath = this.#ancestors.map(u => u.name).reverse().join(' > ').substring(0, 500) || undefined; 175 this.isInteractive = this.#isInteractive(target); 176 } 177 178 #describeTarget(target) { 179 this.theTarget.push(this.#describeNode(target)); 180 } 181 182 #describeNode(node) { 183 if (!node || !node.tagName) return { name: undefined, role: undefined, title: undefined, "aria-label": undefined, id: undefined, classes: undefined }; 184 185 const classList = node.classList && node.classList.length > 0 186 ? Array.from(node.classList).slice(0, 2).join('.') 187 : undefined; 188 189 return { 190 name: node.tagName ? node.tagName.toLowerCase() : undefined, 191 role: node.getAttribute && node.getAttribute('role') ? node.getAttribute('role').toLowerCase() : undefined, 192 title: node.title ? node.title.substring(0, 30) : undefined, 193 "aria-label": node.getAttribute && node.getAttribute('aria-label') ? node.getAttribute('aria-label').substring(0, 30) : undefined, 194 id: node.id ? "#" + node.id : undefined, 195 classes: classList 196 }; 197 } 198 199 #getAncestors(target) { 200 let current = target; 201 while (current !== null && typeof current.tagName !== 'undefined' && current.tagName !== "BODY" && current.tagName !== "HTML") { 202 this.#ancestors.push(this.#describeNode(current)); 203 current = current.parentElement; 204 } 205 } 206 207 #determineLandmark() { 208 let determinedLandmark = { name: "", role: "", descriptor: "" }; 209 210 for (let z = 0; z < this.#ancestors.length; z++) { 211 const currentDescendant = this.#ancestors[z]; 212 213 for (let x = 0; x < this.#landmarks.length; x++) { 214 const landmark = this.#landmarks[x]; 215 const isTagMatch = currentDescendant.name && landmark.name.toLowerCase() === currentDescendant.name.toLowerCase(); 216 const isRoleMatch = currentDescendant.role && landmark.role.toLowerCase() === currentDescendant.role.toLowerCase(); 217 218 if (isTagMatch || isRoleMatch) { 219 if (!this.#hasExcludedAncestors(x, z) && this.#hasRequiredAttributes(x, z)) { 220 determinedLandmark.name = landmark.name; 221 determinedLandmark.role = landmark.role; 222 determinedLandmark.descriptor = landmark.descriptor; 223 return determinedLandmark; 224 } 225 } 226 } 227 } 228 return determinedLandmark; 229 } 230 231 #hasExcludedAncestors(landmarkIndex, ancestorIndex) { 232 const checkAncestors = this.#ancestors.slice(ancestorIndex + 1).map(u => u.name); 233 const exclusions = this.#landmarks[landmarkIndex].excludedAncestors; 234 return exclusions.some(excluded => checkAncestors.includes(excluded)); 235 } 236 237 #hasRequiredAttributes(landmarkIndex, ancestorIndex) { 238 const requiredAttributes = this.#landmarks[landmarkIndex].requiredAttributes; 239 if (requiredAttributes.length === 0) return true; 240 241 const currentDescendant = this.#ancestors[ancestorIndex]; 242 return requiredAttributes.some(attr => Boolean(currentDescendant[attr])); 243 } 244 245 #isInteractive(target) { 246 const interactiveTags = ["button", "a", "input", "select", "textarea"]; 247 const interactiveRoles = ["button", "link", "checkbox", "menuitem", "tab", "option"]; 248 249 return this.#ancestors.some(node => 250 interactiveTags.includes(node.name) || interactiveRoles.includes(node.role) 251 ); 252 } 253 } 254 255 class clickEvent { 256 #tempObj = {}; 257 258 #easyRand(len = 16) { 259 const chars = '0123456789abcdef'; 260 let res = ''; 261 for (let i = 0; i < len; i++) { 262 if (i > 0 && i % 4 === 0 && len >= 12) res += '-'; 263 res += chars[Math.floor(Math.random() * 16)]; 264 } 265 return res; 266 } 267 268 isTouchDevice() { 269 return ('ontouchstart' in window) || (navigator.maxTouchPoints > 0); 270 } 271 272 isOutboundLink(targetURL) { 273 if (!targetURL || typeof targetURL !== 'string') return false;
274 try { 275 const url = new URL(targetURL, window.location.href); 276 if (!url.protocol.startsWith('http')) return false; 277 278 const currentHost = window.location.hostname.replace(/^www\./i, '').toLowerCase(); 279 const targetHost = url.hostname.replace(/^www\./i, '').toLowerCase(); 280 281 const isSameDomain = targetHost === currentHost; 282 const isSubDomain = targetHost.endsWith('.' + currentHost); 283 284 return !(isSameDomain || isSubDomain); 285 } catch (e) { 286 return false; 287 } 288 } 289 290 getModifierKey(event) { 291 if (event.ctrlKey) return 'ctrl'; 292 if (event.metaKey) return 'cmd'; 293 if (event.shiftKey) return 'shift'; 294 if (event.altKey) return 'alt'; 295 return 'none'; 296 } 297 298 get clickType() { 299 if (this.rageClick && this.deadClick) return "dead_rage"; 300 if (this.rageClick) return "rage"; 301 if (this.deadClick) return "dead"; 302 return "standard"; 303 } 304 305 constructor(event, target) { 306 this.target = target; 307 this.#tempObj = new targetProperties(target); 308 309 // Coordinates & Canvas 310 const clickXY = `${event.clientX}/${event.clientY}`; 311 const pageXY = `${event.pageX}/${event.pageY}`; 312 const canvasWH = `${window.innerWidth}/${window.innerHeight}`; 313 const locParts = [clickXY, pageXY, canvasWH].filter(p => p && p.trim() !== ''); 314 this.clickLocation = locParts.length > 0 ? locParts.join(' | ') : undefined; 315 316 this.scrollPosition = `${Math.round(window.scrollX || 0)}/${Math.round(window.scrollY || 0)}`; 317 318 // URLs & Context 319 this.pageURL = document.location.href; 320 this.pageTitle = document.title || undefined; 321 this.clickText = (target.innerText || "").trim().substring(0, 100) || undefined; 322 323 const linkElement = target.closest ? target.closest('a') : null; 324 let rawTargetURL = ""; 325 326 if (target.href) { 327 rawTargetURL = typeof target.href === 'string' ? target.href : (target.href.baseVal || ""); 328 } else if (linkElement && linkElement.href) { 329 rawTargetURL = typeof linkElement.href === 'string' ? linkElement.href : (linkElement.href.baseVal || ""); 330 } 331 332 this.targetURL = rawTargetURL.substring(0, 500) || undefined; 333 this.outbound = this.isOutboundLink(this.targetURL); 334 335 // Behavioral Base States 336 this.interactiveClick = this.#tempObj.isInteractive; 337 this.deadClick = !this.interactiveClick; 338 this.rageClick = false; 339 340 // Target Structural Element 341 const targetObj = this.#tempObj.theTarget[0] || {}; 342 const tag = targetObj.name || ''; 343 const id = targetObj.id || ''; 344 const classes = targetObj.classes ? '.' + targetObj.classes : ''; 345 const selector = `${tag}${id}${classes}`.trim(); 346 this.targetElement = selector ? selector.substring(0, 100) : undefined; 347 348 // Target Semantic Meta 349 const role = targetObj.role ? `role:${targetObj.role}` : ''; 350 const aria = targetObj["aria-label"] ? `aria:${targetObj["aria-label"]}` : ''; 351 const title = targetObj.title ? `title:${targetObj.title}` : ''; 352 const metaParts = [role, aria, title].filter(p => p && p.trim() !== ''); 353 this.targetElementMeta = metaParts.length > 0 ? metaParts.join(' | ').substring(0, 100) : undefined; 354 355 // Landmark Info 356 const lm = this.#tempObj.landmark || {}; 357 const lmParts = [lm.name, lm.role, lm.descriptor].filter(p => p && String(p).trim() !== ''); 358 this.landmarkInfo = lmParts.length > 0 ? lmParts.join(' | ') : undefined; 359 360 this.descendantPath = this.#tempObj.descendantPath; 361 362 // Metadata 363 this.click = clickStack.length + bufferQueue.length + 1; 364 this.clickTime = Date.now(); 365 this.clickId = this.#easyRand(16); 366 this.modifierKey = this.getModifierKey(event); 367 368 const gUMG = window.gUMG || {}; 369 this.interactionToken = gUMG.interactionToken || undefined; 370 this.hashedIP = gUMG.clientData?.hashedIP || undefined; 371 372 this.clickOnTouch = this.isTouchDevice(); 373 this.pointerType = event.pointerType || undefined; 374 } 375 } 376})(); 377 378
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.