1(function() { 2 'use strict'; 3 4 // Ensure dataLayer exists 5 window.dataLayer = window.dataLayer || []; 6 7 // Get configuration from WordPress 8 const config = window.gtmTrackerData || { 9 context: {}, 10 trackLinks: [], 11 trackForms: [], 12 debugMode: false 13 }; 14 15 // Debug logging helper 16 function debugLog(message, data) { 17 if (config.debugMode) { 18 if (data) { 19 console.log('MobileAction GTM Tracker: ' + message, data); 20 } else { 21 console.log('MobileAction GTM Tracker: ' + message); 22 } 23 } 24 } 25 26 // Generate unique form submission ID 27 function generateFormSubmissionId() { 28 const date = new Date(); 29 const dateStr = date.getFullYear().toString() + 30 (date.getMonth() + 1).toString().padStart(2, '0') + 31 date.getDate().toString().padStart(2, '0'); 32 const randomStr = Math.random().toString(36).substring(2, 8).toUpperCase(); 33 return 'FS-' + dateStr + '-' + randomStr; 34 } 35 36 // Get or create persistent user ID 37 function getPersistentUserId() { 38 // Check if user already has a persistent ID in sessionStorage 39 let persistentId = sessionStorage.getItem('ma_persistent_user_id'); 40 41 if (!persistentId) { 42 // Check if logged in user has ID from JWT 43 if (config.context && config.context.userId) { 44 persistentId = config.context.userId; 45 } else { 46 // Generate new ID for form submission 47 persistentId = generateFormSubmissionId(); 48 } 49 sessionStorage.setItem('ma_persistent_user_id', persistentId); 50 } 51 52 return persistentId; 53 } 54 55 // Helper functions 56 function buildBase() { 57 // Validate gtmContext exists 58 if (!config.context) { 59 if (config.debugMode) { 60 console.warn('MobileAction GTM Tracker: gtmContext not found'); 61 } 62 return { 63 user: { 64 status: 'guest', 65 userId: null, 66 organizationId: null 67 }, 68 page: { 69 type: 'other', 70 url: window.location.href, 71 path: window.location.pathname, 72 title: document.title 73 } 74 }; 75 } 76 77 return { 78 user: { 79 status: config.context.userStatus || 'guest', 80 userId: config.context.userId || null, 81 organizationId: config.context.organizationId || null 82 }, 83 page: { 84 type: config.context.pageType || 'other', 85 url: window.location.href, 86 path: window.location.pathname, 87 title: document.title 88 } 89 }; 90 } 91 92 function pushEvent(payload) { 93 try { 94 if (window.dataLayer && typeof window.dataLayer.push === 'function') { 95 window.dataLayer.push(payload); 96 debugLog('Event pushed', payload); 97 } else { 98 console.error('MobileAction GTM Tracker: dataLayer not available'); 99 } 100 } catch (error) { 101 console.error('MobileAction GTM Tracker: Error pushing event', error); 102 } 103 } 104 105 // Initialize tracking - check if DOM is already loaded 106 function initializeTracking() { 107 debugLog('Initializing with config', config); 108 109 // Check if cookie exists and warn if not 110 if (!document.cookie.includes('MA_userAuth')) { 111 if (config.debugMode) { 112 console.warn('MobileAction GTM Tracker: MA_userAuth cookie not found - user will be tracked as guest'); 113 } 114 } 115 116 // Validate and log all configured selectors on initialization 117 if (config.trackLinks) { 118 debugLog('Configured click tracking selectors:'); 119 config.trackLinks.forEach(function(cfg, index) { 120 try { 121 // Test if selector is valid 122 const elements = document.querySelectorAll(cfg.selector); 123 debugLog(`[${index}] Selector: "${cfg.selector}" - Found ${elements.length} elements`, { 124 selector: cfg.selector, 125 interactionName: cfg.interactionName, 126 elementsFound: elements.length 127 }); 128 129 // In debug mode, log the first few matching elements 130 if (config.debugMode && elements.length > 0) {
131 elements.forEach(function(el, i) { 132 if (i < 3) { // Log first 3 matches 133 console.log(` Match ${i + 1}:`, { 134 tag: el.tagName, 135 id: el.id || 'no-id', 136 classes: el.className || 'no-classes', 137 attributes: Array.from(el.attributes).map(attr => `${attr.name}="${attr.value}"`) 138 }); 139 } 140 }); 141 } 142 } catch (error) { 143 console.error(`MobileAction GTM Tracker: Invalid selector at index ${index}`, { 144 selector: cfg.selector, 145 error: error.message, 146 hint: 'Check for escaped quotes or invalid syntax', 147 rawSelector: cfg.selector, 148 charCodes: cfg.selector.split('').map(c => c.charCodeAt(0)) 149 }); 150 } 151 }); 152 } 153 154 // 1. Initial page load event (not page_view) 155 const baseData = buildBase(); 156 157 // Check if user has been identified from a previous form submission 158 const persistentUserId = sessionStorage.getItem('ma_persistent_user_id'); 159 let userStatus = baseData.user.status; 160 let userId = baseData.user.userId; 161 162 if (persistentUserId && !userId) { 163 // User was identified in a previous session 164 userStatus = 'identified'; 165 userId = persistentUserId; 166 } 167 168 pushEvent({ 169 event: 'page_load', 170 page: { 171 url: baseData.page.url, 172 path: baseData.page.path, 173 title: baseData.page.title, 174 type: baseData.page.type 175 }, 176 user: { 177 status: userStatus, 178 userId: userId, 179 organizationId: baseData.user.organizationId 180 } 181 }); 182 183 // 2. Delegated click tracking for all elements 184 if (config.trackLinks && config.trackLinks.length > 0) { 185 document.addEventListener('click', function(e) { 186 const target = e.target; 187 188 // Check all configured selectors 189 config.trackLinks.forEach(function(cfg) { 190 try { 191 // Check if clicked element or any of its parents match the selector 192 let matchedElement = null; 193 let currentElement = target; 194 195 // Walk up the DOM tree to find a matching element 196 while (currentElement && currentElement !== document.body) { 197 if (currentElement.matches && currentElement.matches(cfg.selector)) { 198 matchedElement = currentElement; 199 break; 200 } 201 currentElement = currentElement.parentElement; 202 } 203 204 if (matchedElement) { 205 // Prevent tracking the same click multiple times 206 if (e.gtmTracked) return; 207 e.gtmTracked = true; 208 209 // Extract element identifier 210 let elementId = ''; 211 212 // Try to get ID from the element 213 if (matchedElement.id) { 214 elementId = matchedElement.id; 215 } else if (cfg.selector.includes('#')) { 216 // Extract ID from selector if present 217 const match = cfg.selector.match(/#([^\s\[\.]+)/); 218 elementId = match ? match[1] : ''; 219 } else { 220 // Use a sanitized version of selector as identifier 221 elementId = cfg.selector.replace(/[^\w-]/g, '_'); 222 } 223 224 // Get element text content 225 let elementText = matchedElement.textContent.trim(); 226 if (!elementText && matchedElement.getAttribute('aria-label')) { 227 elementText = matchedElement.getAttribute('aria-label'); 228 } 229 if (!elementText && matchedElement.getAttribute('title')) { 230 elementText = matchedElement.getAttribute('title'); 231 } 232 233 // Get data attributes for debugging 234 const dataAttrs = {}; 235 Array.from(matchedElement.attributes).forEac
235h(attr => { 236 if (attr.name.startsWith('data-')) { 237 dataAttrs[attr.name] = attr.value; 238 } 239 }); 240 241 const baseData = buildBase(); 242 const persistentUserId = sessionStorage.getItem('ma_persistent_user_id'); 243 let userStatus = baseData.user.status; 244 let userId = baseData.user.userId; 245 246 if (persistentUserId && !userId) { 247 userStatus = 'identified'; 248 userId = persistentUserId; 249 } 250 251 pushEvent({ 252 event: 'interaction', 253 interaction: { 254 type: 'click', 255 name: cfg.interactionName, 256 element: elementId, 257 elementText: elementText, 258 location: cfg.location, 259 }, 260 user: { 261 status: userStatus, 262 userId: userId, 263 organizationId: baseData.user.organizationId 264 }, 265 page: { 266 type: baseData.page.type, 267 url: baseData.page.url, 268 path: baseData.page.path 269 } 270 }); 271 272 debugLog('Element clicked:', { 273 selector: cfg.selector, 274 matchedElement: { 275 tag: matchedElement.tagName, 276 id: matchedElement.id, 277 classes: matchedElement.className, 278 dataAttributes: dataAttrs 279 }, 280 clickTarget: { 281 tag: target.tagName, 282 id: target.id, 283 classes: target.className 284 } 285 }); 286 } 287 } catch (error) { 288 if (config.debugMode) { 289 console.error('MobileAction GTM Tracker: Invalid selector', { 290 selector: cfg.selector, 291 error: error.message 292 }); 293 } 294 } 295 }); 296 }); 297 } 298 299 // 3. HubSpot form submission tracking via postMessage 300 if (config.trackForms && config.trackForms.length > 0) { 301 window.addEventListener('message', function(event) { 302 // Validate message origin if needed 303 // if (event.origin !== 'https://forms.hubspot.com') return; 304 305 if (event.data && event.data.type === 'hsFormCallback' && event.data.eventName === 'onFormSubmit') { 306 debugLog('HubSpot form submitted', event.data); 307 308 const hubspotFormId = event.data.id || ''; 309 310 // Find matching form configuration by HubSpot form ID 311 let matchedForm = null; 312 config.trackForms.forEach(function(cfg) { 313 if (cfg.hubspotFormId && cfg.hubspotFormId === hubspotFormId) { 314 matchedForm = cfg; 315 } 316 }); 317 318 // If no match by ID, use the first form config as fallback 319 if (!matchedForm && config.trackForms.length > 0) { 320 matchedForm = config.trackForms[0]; 321 debugLog('No form ID match found, using default form config'); 322 } 323 324 if (matchedForm) { 325 const baseData = buildBase(); 326 const persistentUserId = getPersistentUserId(); 327 const interactionName = matchedForm.formName + '_submitted'; 328 329 pushEvent({ 330 event: 'interaction', 331 interaction: { 332 type: 'form_submission', 333 name: interactionName 334 }, 335 user: { 336 status: 'identified', 337 userId: persistentUserId 338 }, 339 page: { 340 type: baseData.page.type, 341 url: baseData.page.url, 342 path: baseData.page.path 343 }, 344 form: { 345 formName: matchedForm.formName + '_form', 346 intent: matchedForm.intent 347 } 348 }); 349 350 debugLog('Form submission tracked with config:', { 351 formId: hubspotFormId, 352 formName: matchedForm.formName, 353 userId: persistentUserId 354 }); 355 } else { 356 debugLog('No form configuration found for submission'); 357 } 358 } 359 }); 360 } 361 362 debugLog('Initialization complete'); 363 } 364 365 // Check if DOM is already loaded 366 if (document.readyState === 'loading') { 367 // DOM not yet loaded, wait for it 368 document.addEventListener('DOMContentLoaded', initializeTracking); 369 } else { 370 // DOM is already loaded, initialize immediately 371 initializeTracking(); 372 } 373 374 // Expose test function for debugging 375 window.testGTMSelector = function(selector) { 376 try { 377 const elements = document.querySelectorAll(selector); 378 console.log(`Testing selector: "${selector}"`); 379 console.log(`Found ${elements.length} elements`); 380 if (elements.length > 0) {
381 elements.forEach((el, i) => { 382 console.log(`Element ${i + 1}:`, { 383 tag: el.tagName, 384 id: el.id, 385 classes: el.className, 386 innerHTML: el.innerHTML.substring(0, 100) + '...', 387 attributes: Array.from(el.attributes).map(attr => `${attr.name}="${attr.value}"`) 388 }); 389 }); 390 } 391 return elements; 392 } catch (error) { 393 console.error('Invalid selector:', error.message); 394 return null; 395 } 396 }; 397 398})();
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.