1/** 2 * Facebook Pixel Events - External JavaScript Handler 3 * 4 * This script fires pixel events in an isolated execution context, 5 * ensuring events are sent even if other plugins cause JavaScript errors. 6 * 7 * Supports WooCommerce Blocks via Store API fetch interception. 8 * The pixel event data is read from the Store API response extensions. 9 * 10 * @package FacebookCommerce 11 */ 12 13(function() { 14 'use strict'; 15 16 // Data from PHP (may be missing if no events were queued for initial render). 17 // Keep Store API interception active even when eventQueue is empty/undefined. 18 var data = (typeof wc_facebook_pixel_data !== 'undefined' && wc_facebook_pixel_data) ? 19 wc_facebook_pixel_data : 20 { eventQueue: [] }; 21 window.wcFacebookPixelFiredEvents = window.wcFacebookPixelFiredEvents || {}; 22 var firedEvents = window.wcFacebookPixelFiredEvents; 23 24 /** 25 * Build event data object for fbq() 26 * 27 * @param {Object} event Event object from PHP 28 * @return {Object} Prepared event data 29 */ 30 function isCachedPage() { 31 return window.__wc_fb_page_generated && 32 (Math.floor(Date.now() / 1000) - window.__wc_fb_page_generated) > 1800; 33 } 34 35 function buildEventData(event) { 36 return { 37 method: event.method || 'track', 38 name: event.name, 39 params: event.params || {}, 40 eventId: isCachedPage() ? null : (event.eventId || null) 41 }; 42 } 43 44 /** 45 * Check if event should be skipped (already fired) 46 * 47 * @param {string|null} eventId Event ID for deduplication 48 * @return {boolean} True if should skip 49 */ 50 function shouldSkipEvent(eventId) { 51 return eventId && firedEvents[eventId]; 52 } 53 54 /** 55 * Mark event as fired for deduplication 56 * 57 * @param {string|null} eventId Event ID 58 */ 59 function markEventFired(eventId) { 60 if (eventId) { 61 firedEvents[eventId] = true; 62 } 63 } 64 65 /** 66 * Log warning to console (with safety check) 67 * 68 * @param {string} message Warning message 69 * @param {*} data Additional data to log 70 */ 71 function logWarning(message, data) { 72 if (typeof console !== 'undefined' && console.warn) { 73 console.warn('[FB Pixel]', message, data); 74 } 75 } 76 77 /** 78 * Fire a single event using fbq() 79 * 80 * @param {Object} event Event object with name, params, method, eventId 81 */ 82 function fireEvent(event) { 83 var eventData = buildEventData(event); 84 85 // Skip if already fired (deduplication) 86 if (shouldSkipEvent(eventData.eventId)) { 87 return; 88 } 89 90 try { 91 var params = eventData.params; 92 93 if (window.FacebookSignals && typeof window.FacebookSignals.trackEvent === 'function') { 94 window.FacebookSignals.trackEvent(eventData.name, params, null, eventData.method, eventData.eventId); 95 } else { 96 if (typeof fbq !== 'function') { 97 logWarning('fbq not available, skipping event:', eventData.name); 98 return; 99 } 100 101 if (eventData.eventId) { 102 fbq(eventData.method, eventData.name, params, {eventID: eventData.eventId}); 103 } else { 104 fbq(eventData.method, eventData.name, params); 105 } 106 } 107 108 markEventFired(eventData.eventId); 109 110 } catch (e) { 111 logWarning('Event error: ' + eventData.name, e); 112 } 113 } 114 115 /** 116 * Fire all queued events from PHP 117 */ 118 function fireQueuedEvents() { 119 var events = data.eventQueue; 120 121 if (!events || !Array.isArray(events)) { 122 return; 123 } 124 125 for (var i = 0; i < events.length; i++) { 126 try { 127 fireEvent(events[i]); 128 } catch (e) { 129 logWarning('fireQueuedEvents loop error:', e); 130 } 131 } 132 133 // Clear events after firing to prevent duplicate firing 134 data.eventQueue = []; 135 } 136 137 // ========================================================================= 138 // WooCommerce Blocks: Store API approach 139 // ========================================================================= 140 141 /** 142 * Process pixel event data from Store API response. 143 * 144 * @param {Object} eventData Event data from Store API extensions 145 */ 146 function processStoreApiEvent(eventData) { 147 if (!eventData || !eventData.event) { 148 return; 149 } 150 151 var params = eventData.params || {}; 152 153 var event = { 154 method: 'track', 155 name: eventData.event, 156 params: params, 157 eventId: params.event_id || null 158 }; 159 160 fireEvent(event); 161 } 162 163 /** 164 * Returns the request URL from a fetch() input. 165 * 166 * @param {*} input fetch() input argument 167 * @return {string} 168 */ 169 function getRequestUrl(input) { 170 if (typeof input === 'string') { 171 return input; 172 } 173 174 if (input && typeof input.url === 'string') { 175 return input.url; 176 } 177 178 return ''; 179 } 180 181 /** 182 * Checks whether a URL targets the Store API add-item endpoint. 183 * 184 * @param {string} url Request URL 185 * @return {boolean} 186 */ 187 function isStoreApiAddItemRequest(url) { 188 return typeof url === 'string' && 189 (url.indexOf('/wc/store/v1/cart/add-item') !== -1 || 190 url.indexOf('/wc/store/cart/add-item') !== -1); 191 } 192 193 /** 194 * Checks whether a URL targets the Store API batch endpoint. 195 * 196 * @param {string} url Request URL 197 * @return {boolean} 198 */ 199 function isStoreApiBatchRequest(url) { 200 return typeof url === 'string' && 201 (url.indexOf('/wc/store/v1/batch') !== -1 || 202 url.indexOf('/wc/store/batch') !== -1); 203 } 204 205 /** 206 * Process Store API response payload and extract plugin extension data. 207 * Handles both direct cart responses and batch responses. 208 * 209 * @param {Object} responseData Parsed Store API response payload 210 */ 211 function processStoreApiResponseData(responseData) { 212 if (!responseData || typeof responseData !== 'object') { 213 return; 214 } 215 216 // Direct response shape: { extensions: { 'facebook-for-woocommerce': ... } } 217 if (responseData.extensions && responseData.extensions['facebook-for-woocommerce']) { 218 processStoreApiEvent(responseData.extensions['facebook-for-woocommerce']); 219 } else if (Array.isArray(responseData.responses)) { 220 // Batch response shape: { responses: [ { body: { extensions: ... } } ] } 221 for (var i = 0; i < responseData.responses.length; i++) { 222 var item = responseData.responses[i]; 223 if (item && item.body && item.body.extensions && item.body.extensions['facebook-for-woocommerce'] && 224 item.status >= 200 && item.status < 300) { 225 processStoreApiEvent(item.body.extensions['facebook-for-woocommerce']); 226 } 227 } 228 } 229 } 230 231 /** 232 * Set up fetch interceptor to capture Store API responses. 233 * Intercepts cart/add-item and batch requests to fire AddToCart pixel events. 234 */ 235 function setupFetchInterceptor() { 236 var originalFetch = window.fetch; 237 if (!originalFetch) { 238 return; 239 } 240 241 window.fetch = function() { 242 var args = arguments; 243 var url = getRequestUrl(args[0]); 244 245 var isAddToCartRequest = isStoreApiAddItemRequest(url); 246 var isBatchRequest = isStoreApiBatchRequest(url); 247 248 return originalFetch.apply(this, args).then(function(response) { 249 if ((isAddToCartRequest || isBatchRequest) && response.ok) { 250 // Clone response so we can read it without consuming 251 response.clone().json().then(function(responseData) { 252 processStoreApiResponseData(responseData); 253 }).catch(function(e) { 254 logWarning('Store API JSON parse error:', e); 255 }); 256 } 257 return response; 258 }); 259 }; 260 } 261 262 /** 263 * Initialize pixel event handling. 264 * 265 * If fbq() is already available, fires queued events immediately. 266 * If not (e.g. consent manager blocking the SDK), uses Object.defineProperty 267 * to set a trap on window.fbq â our handler fires automatically the moment 268 * fbq is assigned, with zero overhead in between. No polling, no timers. 269 * 270 * Also sets up Store API interceptor for WooCommerce Blocks AJAX AddToCart. 271 */
272 function init() { 273 // Set up fetch interceptor for WooCommerce Blocks Store API 274 setupFetchInterceptor(); 275 276 if (typeof fbq === 'function') { 277 fireQueuedEvents(); 278 return; 279 } 280 281 // fbq doesn't exist yet â watch for it (zero overhead, no timers). 282 // Consent managers block fbq until the 283 // user accepts. This fires the moment they assign window.fbq. 284 var _fbq = window.fbq; 285 Object.defineProperty(window, 'fbq', { 286 configurable: true, 287 enumerable: true, 288 get: function() { return _fbq; }, 289 set: function(value) { 290 _fbq = value; 291 if (typeof value === 'function') { 292 // Restore normal property so FB SDK works normally 293 Object.defineProperty(window, 'fbq', { 294 configurable: true, 295 enumerable: true, 296 writable: true, 297 value: value 298 }); 299 setTimeout(fireQueuedEvents, 0); 300 } 301 } 302 }); 303 } 304 305 // Start 306 if (document.readyState === 'complete') { 307 init(); 308 } else { 309 window.addEventListener('load', init); 310 } 311 312})();
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.