1/** 2 * Facebook Tracking Functionality 3 * 4 * Supports both common events (handled in mu-plugins) and site-specific custom events. 5 * Implements deduplication with Facebook Pixel via event_id parameter. 6 */ 7 8(function(window) { 9 'use strict'; 10 11 const IHFacebookTracking = { 12 config: {}, 13 events: { 14 PageView: false, 15 ViewContent: false 16 }, 17 18 init: function() { 19 this.config = window.ihFacebookConfig || {}; 20 this.handlePageEvents(); 21 22 // Handle newsletter signup event. 23 this.handleNewsletterSignup(); 24 25 // Listen for pixel events with event IDs for deduplication 26 this.handlePixelEventDeduplication(); 27 28 // Handle outbound link tracking 29 this.handleOutboundLinkTracking(); 30 }, 31 32 handlePageEvents: function() { 33 // Always track page view 34 this.trackPageView(); 35 36 // Handle ViewContent based on page type 37 this.handleViewContent(); 38 }, 39 40 /** 41 * Generic event tracking method 42 * @param {string} eventName - Name of the event to track 43 * @param {Object} eventData - Additional data for the event 44 * @returns {Promise} - Promise that resolves when tracking is complete 45 */ 46 trackEvent: async function(eventName, eventData = {}) { 47 if ( ! eventName ) { 48 return; 49 } 50 51 const requestData = { 52 event_name: eventName, 53 event_url: window.location.href, 54 ...eventData 55 }; 56 57 return this.sendToServer('/events', requestData); 58 }, 59 60 /** 61 * Track page view event. 62 * @returns {void} 63 */ 64 trackPageView: function() { 65 if ( this.events.PageView ) { 66 return; // Prevent duplicate 67 } 68 69 const eventData = { 70 title: document.title, 71 referrer: document.referrer 72 }; 73 74 this.trackEvent( 'PageView', eventData ); 75 this.events.PageView = true; 76 }, 77 78 /** 79 * Handle ViewContent event. 80 * @returns {void} 81 */ 82 handleViewContent: function() { 83 if ( ! this.config.isSingle || ! this.config.articleData ) { 84 return; 85 } 86 87 if ( this.events.ViewContent ) { 88 return; // Prevent duplicate 89 } 90 91 this.trackEvent('ViewContent', this.config.articleData); 92 this.events.ViewContent = true; 93 }, 94 95 /** 96 * Handle pixel events with event IDs for deduplication 97 * Listens for custom events dispatched from analytics.js 98 * @returns {void} 99 */ 100 handlePixelEventDeduplication: function() { 101 document.addEventListener('fb-conversion-api-event', (event) => { 102 const eventDetail = event.detail; 103 104 // Extract event_id for deduplication 105 const eventId = eventDetail.event_id; 106 const eventName = eventDetail.event_name; 107 108 // Remove event_id from data to avoid sending it as custom property 109 const { event_id, event_name: _, ...eventData } = eventDetail; 110 111 // Send to Conversion API with event_id for deduplication 112 const requestData = { 113 event_name: eventName, 114 event_id: eventId, // Critical for deduplication 115 event_url: window.location.href, 116 ...eventData 117 }; 118 119 this.sendToServer('/events', requestData); 120 }); 121 }, 122 123 /** 124 * Send data to server 125 * @param {string} endpoint - API endpoint 126 * @param {Object} data - Data to send 127 * @returns {Promise} - Promise that resolves with the response 128 */ 129 sendToServer: async function(endpoint, data) { 130 const url = this.config.apiEndpoint + endpoint; 131 132 const headers = { 133 'Accept': 'application/json', 134 'Content-Type': 'application/json', 135 }; 136 137 // Present for logged-in users only; without it the endpoint rejects 138 // their requests as a failed cookie check. 139 if ( this.config.nonce ) { 140 headers['X-WP-Nonce'] = this.config.nonce; 141 } 142 143 try { 144 const response = await fetch( url, { 145 method: 'POST', 146 headers, 147 body: JSON.stringify(data) 148 }); 149 150 if ( ! response.ok ) { 151 throw new Error(`Failed to track event: ${response.statusText}`); 152 } 153 154 return await response.json(); 155 } catch ( error ) { 156 throw error; 157 } 158 }, 159 160 /** 161 * Helper method to attach event listeners for tracking 162 * @param {string} selector - CSS selector for elements to track 163 * @param {string} eventType - Type of event to listen for (e.g., 'click') 164 * @param {string} trackingEventName - Name of the event to track 165 * @param {Function} dataCallback - Optional callback to get additional event data 166 */ 167 attachTrackingListener: function(selector, eventType, trackingEventName, dataCallback) { 168 document.querySelectorAll(selector).forEach(element => { 169 element.addEventListener(eventType, (event) => { 170 const baseData = {
171 element_id: element.id, 172 element_class: element.className, 173 element_type: element.tagName.toLowerCase() 174 }; 175 176 const additionalData = dataCallback ? dataCallback(element, event) : {}; 177 this.trackEvent(trackingEventName, { ...baseData, ...additionalData }); 178 }); 179 }); 180 }, 181 182 /** 183 * Handle outbound link tracking 184 * Tracks clicks on external links and sends events to both Facebook Pixel and CAPI 185 * @returns {void} 186 */ 187 handleOutboundLinkTracking: function() { 188 // Use event delegation for better performance 189 document.addEventListener('click', (event) => { 190 const link = event.target.closest('a'); 191 192 // Only process actual link clicks 193 if (!link || !link.href) { 194 return; 195 } 196 197 // Check if link is external 198 if (!this.isExternalLink(link.href)) { 199 return; 200 } 201 202 // Generate unique event ID for deduplication 203 const eventId = this.generateEventId(); 204 205 // Prepare event data 206 const eventData = this.prepareOutboundLinkEventData(link, eventId); 207 208 // Fire Facebook Pixel event 209 if (window.fbq) { 210 window.fbq('trackCustom', 'OutboundLinkClick', { 211 destination_domain: eventData.destination_domain, 212 destination_url: eventData.destination_url, 213 source_page: eventData.source_page, 214 utm_source: eventData.utm_source, 215 utm_medium: eventData.utm_medium, 216 utm_campaign: eventData.utm_campaign, 217 utm_content: eventData.utm_content, 218 referrer: eventData.referrer 219 }, { 220 eventID: eventId 221 }); 222 } 223 224 // Send to Conversion API 225 this.sendToServer('/events', eventData).catch(error => { 226 // Silently handle API errors to avoid blocking user experience 227 }); 228 }); 229 }, 230 231 /** 232 * Check if a URL is external to our current domain 233 * @param {string} url - URL to check 234 * @returns {boolean} - True if external, false if internal 235 */ 236 isExternalLink: function(url) { 237 try { 238 const linkHost = new URL(url, window.location.origin).hostname; 239 const currentHost = window.location.hostname; 240 241 return linkHost !== currentHost; 242 } catch (error) { 243 // If URL parsing fails, assume it's internal to be safe 244 return false; 245 } 246 }, 247 248 /** 249 * Prepare event data for outbound link tracking 250 * @param {HTMLAnchorElement} link - The clicked link element 251 * @param {string} eventId - Unique event ID for deduplication 252 * @returns {Object} - Event data object 253 */ 254 prepareOutboundLinkEventData: function(link, eventId) { 255 const destinationUrl = link.href; 256 const destinationDomain = new URL(destinationUrl).hostname; 257 const currentPageUrl = window.location.href; 258 const utmParams = this.extractUtmParameters(currentPageUrl); 259 260 return { 261 event_name: 'OutboundLinkClick', 262 event_id: eventId, 263 event_url: currentPageUrl, 264 destination_domain: destinationDomain, 265 destination_url: destinationUrl, 266 source_page: currentPageUrl, 267 referrer: document.referrer || '', 268 ...utmParams 269 }; 270 }, 271 272 /** 273 * Extract UTM parameters from current page URL 274 * @param {string} url - URL to extract UTM parameters from 275 * @returns {Object} - Object containing UTM parameters 276 */ 277 extractUtmParameters: function(url) { 278 const utmParams = { 279 utm_source: '', 280 utm_medium: '', 281 utm_campaign: '', 282 utm_content: '' 283 }; 284 285 try { 286 const urlParams = new URLSearchParams(new URL(url).search); 287 288 Object.keys(utmParams).forEach(param => { 289 const value = urlParams.get(param); 290 if (value) { 291 utmParams[param] = value; 292 } 293 }); 294 } catch (error) { 295 // Silently handle UTM parameter extraction errors 296 } 297 298 return utmParams; 299 }, 300 301 /** 302 * Generate a unique event ID for deduplication 303 * @returns {string} - Unique event ID 304 */ 305 generateEventId: function() { 306 return 'fb_' + Date.now() + '_' + Math.random().toString(36).substring(2, 9); 307 }, 308 309 /** 310 * Handle newsletter signup event. 311 * @returns {void} 312 */ 313 handleNewsletterSignup: function() { 314 document.addEventListener('ih-newsletter-signup', (event) => { 315 // Get Braze external ID (email hash) from the event 316 const brazeExternalId = event.detail.hash || null; 317 318 const eventData = { 319 source: 'newsletter', 320 }; 321 322 // Add Braze external ID if available (this is the key integration) 323 if (brazeExternalId) { 324 eventData.external_id = brazeExternalId; 325 } 326 327 this.trackEvent('CompleteRegistration', eventData); 328 }); 329 }, 330 }; 331 332 // Initialize when DOM is ready. 333 if (document.readyState === 'loading') { 334 document.addEventListener('DOMContentLoaded', () => {
335 IHFacebookTracking.init(); 336 }); 337 } else { 338 IHFacebookTracking.init(); 339 } 340 341 // Expose to global scope. 342 window.IHFacebookTracking = IHFacebookTracking; 343 344})(window);
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.