1/** 2 * AutoSEO Conversion Tracker 3 * 4 * Intercepts Google Ads (gtag / dataLayer) and Facebook Pixel (fbq) 5 * conversion events and beacons them to the local WP REST proxy, 6 * which forwards to the AutoSEO API server-side. 7 * 8 * SAFETY INVARIANT: The original gtag/fbq/dataLayer.push calls are 9 * NEVER wrapped in try/catch -- they execute exactly as they would 10 * without this script. Only our own tracking logic is guarded. 11 */ 12(function () { 13 'use strict'; 14 15 var currentScript = document.currentScript || {}; 16 var config = window.autoseoConversionTracker || {}; 17 if (!config.endpoint && currentScript.getAttribute) { 18 config.endpoint = currentScript.getAttribute('data-endpoint') || ''; 19 config.token = currentScript.getAttribute('data-token') || ''; 20 config.pageviewEndpoint = currentScript.getAttribute('data-pageview-endpoint') || ''; 21 config.articleId = currentScript.getAttribute('data-article-id') || ''; 22 } 23 if (!config.endpoint) return; 24 25 var CONVERSION_EVENTS_GTAG = ['conversion', 'purchase']; 26 var CONVERSION_EVENTS_FBQ = ['Purchase', 'Subscribe', 'CompleteRegistration', 'Lead', 'StartTrial']; 27 var DATALAYER_EVENTS = ['purchase', 'conversion']; 28 var ATTRIBUTION_WINDOW_MS = 30 * 24 * 60 * 60 * 1000; 29 var VISITOR_STORAGE_KEY = 'autoseo_visitor_id'; 30 var ATTRIBUTION_STORAGE_KEY = 'autoseo_last_article_attribution'; 31 32 var queue = []; 33 var flushTimer = null; 34 var seen = {}; 35 var visitorId = getVisitorId(); 36 var currentArticleView = null; 37 38 captureInboundAttribution(); 39 recordCurrentArticleView(); 40 41 function dedupeKey(source, type, value) { 42 return source + '|' + type + '|' + (value || '') + '|' + Math.floor(Date.now() / 2000); 43 } 44 45 function enqueue(source, type, params) { 46 var key = dedupeKey(source, type, params && params.value); 47 if (seen[key]) return; 48 seen[key] = true; 49 50 var val = params && params.value ? parseFloat(params.value) : null; 51 if (val !== null && (isNaN(val) || !isFinite(val))) val = null; 52 var attribution = getStoredAttribution(); 53 54 var event = { 55 event_source: source, 56 event_type: String(type).substring(0, 100), 57 event_value: val, 58 event_currency: params && (params.currency || params.Currency) || null, 59 page_url: (location.pathname + location.search).substring(0, 500), 60 referrer_url: document.referrer ? document.referrer.substring(0, 500) : null, 61 visitor_id: visitorId, 62 timestamp: new Date().toISOString() 63 }; 64 65 if (attribution) { 66 event.attributed_article_id = attribution.article_id; 67 event.article_viewed_at = attribution.viewed_at; 68 event.article_attribution_source = attribution.source || null; 69 } 70 71 queue.push(event); 72 73 if (!flushTimer) { 74 flushTimer = setTimeout(flush, 1500); 75 } 76 } 77 78 function storageGet(key) { 79 try { 80 return localStorage.getItem(key); 81 } catch (e) { 82 return null; 83 } 84 } 85 86 function storageSet(key, value) { 87 try { 88 localStorage.setItem(key, value); 89 } catch (e) {} 90 } 91 92 function getVisitorId() { 93 var inbound = getQueryParam('autoseo_vid'); 94 if (isValidVisitorId(inbound)) { 95 storageSet(VISITOR_STORAGE_KEY, inbound); 96 return inbound; 97 } 98 99 var existing = storageGet(VISITOR_STORAGE_KEY); 100 if (isValidVisitorId(existing)) return existing; 101 102 var generated = 'as_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 12); 103 storageSet(VISITOR_STORAGE_KEY, generated); 104 return generated; 105 } 106 107 function isValidVisitorId(value) { 108 return typeof value === 'string' && value.length > 0 && value.length <= 128 && /^[A-Za-z0-9._:-]+$/.test(value); 109 } 110 111 function getQueryParam(name) { 112 try { 113 return new URLSearchParams(window.location.search).get(name); 114 } catch (e) { 115 return null; 116 } 117 } 118 119 function captureInboundAttribution() { 120 var articleId = parseInt(getQueryParam('autoseo_aid'), 10); 121 if (!articleId || articleId < 1) return; 122 123 var viewedAt = getQueryParam('autoseo_avt') || new Date().toISOString(); 124 var viewedTime = Date.parse(viewedAt);
125 if (!viewedTime || Date.now() - viewedTime > ATTRIBUTION_WINDOW_MS) return; 126 127 storeAttribution({ 128 article_id: articleId, 129 viewed_at: new Date(viewedTime).toISOString(), 130 source: (getQueryParam('autoseo_src') || 'hosted_blog_link').substring(0, 50) 131 }); 132 } 133 134 function recordCurrentArticleView() { 135 var articleId = parseInt(config.articleId, 10); 136 if (!articleId || articleId < 1) return; 137 138 var viewedAt = new Date().toISOString(); 139 currentArticleView = { 140 articleId: articleId, 141 viewedAt: viewedAt, 142 startedAt: Date.now(), 143 pageviewSent: false, 144 timeOnPageSent: false 145 }; 146 storeAttribution({ 147 article_id: articleId, 148 viewed_at: viewedAt, 149 source: 'wordpress' 150 }); 151 sendArticlePageview(articleId, viewedAt); 152 } 153 154 function storeAttribution(attribution) { 155 storageSet(ATTRIBUTION_STORAGE_KEY, JSON.stringify(attribution)); 156 } 157 158 function getStoredAttribution() { 159 try { 160 var raw = storageGet(ATTRIBUTION_STORAGE_KEY); 161 if (!raw) return null; 162 var attribution = JSON.parse(raw); 163 var articleId = parseInt(attribution.article_id, 10); 164 var viewedTime = Date.parse(attribution.viewed_at); 165 if (!articleId || !viewedTime || Date.now() - viewedTime > ATTRIBUTION_WINDOW_MS) return null; 166 return { 167 article_id: articleId, 168 viewed_at: new Date(viewedTime).toISOString(), 169 source: attribution.source ? String(attribution.source).substring(0, 50) : null 170 }; 171 } catch (e) { 172 return null; 173 } 174 } 175 176 function sendArticlePageview(articleId, viewedAt) { 177 if (!config.pageviewEndpoint) return; 178 179 var sessionKey = 'autoseo_pv_sent_' + articleId + '_' + location.pathname; 180 try { 181 if (sessionStorage.getItem(sessionKey)) return; 182 sessionStorage.setItem(sessionKey, '1'); 183 } catch (e) {} 184 185 var payload = JSON.stringify({ 186 article_id: articleId, 187 visitor_id: visitorId, 188 event_type: 'pageview', 189 page_url: (location.pathname + location.search).substring(0, 500), 190 referrer_url: document.referrer ? document.referrer.substring(0, 500) : null, 191 timestamp: viewedAt 192 }); 193 194 try { 195 if (navigator.sendBeacon) { 196 var blob = new Blob([payload], { type: 'application/json' }); 197 if (navigator.sendBeacon(config.pageviewEndpoint, blob)) { 198 markArticlePageviewSent(articleId); 199 return; 200 } 201 } 202 if (window.fetch) { 203 fetch(config.pageviewEndpoint, { 204 method: 'POST', 205 body: payload, 206 headers: { 'Content-Type': 'application/json' }, 207 keepalive: true, 208 credentials: 'same-origin' 209 }).then(function (response) { 210 if (response.ok) markArticlePageviewSent(articleId); 211 }).catch(function () {}); 212 } 213 } catch (e) {} 214 } 215 216 function markArticlePageviewSent(articleId) { 217 if (!currentArticleView || currentArticleView.articleId !== articleId) return; 218 currentArticleView.pageviewSent = true; 219 } 220 221 function sendArticleTimeOnPage() { 222 if (!currentArticleView || !currentArticleView.pageviewSent || currentArticleView.timeOnPageSent) return; 223 if (!config.pageviewEndpoint) return; 224 225 var durationSeconds = Math.round((Date.now() - currentArticleView.startedAt) / 1000); 226 if (durationSeconds < 3) return; 227 if (durationSeconds > 3600) durationSeconds = 3600; 228 229 currentArticleView.timeOnPageSent = true; 230 var payload = JSON.stringify({ 231 article_id: currentArticleView.articleId, 232 visitor_id: visitorId, 233 event_type: 'time_on_page', 234 duration_seconds: durationSeconds, 235 page_url: (location.pathname + location.search).substring(0, 500), 236 referrer_url: document.referrer ? document.referrer.substring(0, 500) : null, 237 timestamp: currentArticleView.viewedAt 238 }); 239 240 try { 241 if (navigator.sendBeacon) { 242 var blob = new Blob([payload], { type: 'application/json' }); 243 if (navigator.sendBeacon(config.pageviewEndpoint, blob)) return; 244 } 245 if (window.fetch) { 246 fetch(config.pageviewEndpoint, { 247 method: 'POST', 248 body: payload, 249 headers: { 'Content-Type': 'application/json' }, 250 keepalive: true, 251 credentials: 'same-origin' 252 }).catch(function () {}); 253 } 254 } catch (e) {} 255 } 256 257 function flush() { 258 flushTimer = null; 259 if (!queue.length) return; 260 261 var events = queue.splice(0, 20); 262 var payload = JSON.stringify({ 263 token: config.token || '', 264 events: events 265 }); 266 267 try { 268 if (navigator.sendBeacon) { 269 var blob = new Blob([payload], { type: 'application/json' }); 270 if (navigator.sendBeacon(config.endpoint, blob)) { 271 return; 272 } 273 } 274 if (window.fetch) { 275 fetch(config.endpoint, { 276 method: 'POST', 277 body: payload, 278 headers: { 'Content-Type': 'application/json' }, 279 keepalive: true, 280 credentials: 'same-origin' 281 }).catch(function () {}); 282 return; 283 } 284 var xhr = new XMLHttpRequest(); 285 xhr.open('POST', config.endpoint, true); 286 xhr.setRequestHeader('Content-Type', 'application/json'); 287 xhr.send(payload); 288 } catch (e) {} 289 } 290 291 // --- gtag() interception --- 292 function wrapGtag() { 293 if (!window.gtag || window.__autoseo_gtag_wrapped) return; 294 if (typeof window.gtag !== 'function') return; 295 var original = window.gtag; 296 window.__autoseo_gtag_wrapped = true; 297 window.gtag = function () { 298 // Original runs UNGUARDED -- behaves exactly as if we weren't here
299 var result = original.apply(this, arguments); 300 // Our tracking is guarded -- failures are silent and harmless 301 try { 302 if (arguments[0] === 'event' && CONVERSION_EVENTS_GTAG.indexOf(arguments[1]) !== -1) { 303 enqueue('google_ads', arguments[1], arguments[2] || {}); 304 } 305 } catch (e) {} 306 return result; 307 }; 308 } 309 310 // --- dataLayer.push() interception --- 311 var wrappedDlInstance = null; 312 function wrapDataLayer() { 313 var dl = window.dataLayer; 314 if (!dl || !Array.isArray(dl)) return; 315 if (dl === wrappedDlInstance) return; 316 wrappedDlInstance = dl; 317 var originalPush = dl.push; 318 dl.push = function () { 319 // Original runs UNGUARDED 320 var result = originalPush.apply(dl, arguments); 321 // Our tracking is guarded 322 try { 323 for (var i = 0; i < arguments.length; i++) { 324 var obj = arguments[i]; 325 if (obj && typeof obj === 'object') { 326 var evt = obj.event || (obj[0] === 'event' ? obj[1] : null); 327 if (evt && DATALAYER_EVENTS.indexOf(evt.toLowerCase ? evt.toLowerCase() : evt) !== -1) { 328 var ecom = obj.ecommerce || obj; 329 enqueue('dataLayer', evt, { 330 value: ecom.value || null, 331 currency: ecom.currency || null 332 }); 333 } 334 } 335 } 336 } catch (e) {} 337 return result; 338 }; 339 } 340 341 // --- fbq() interception --- 342 // Keep window.fbq itself intact. The standard Meta bootstrap stub closes over 343 // its original function object; replacing window.fbq before fbevents.js adds 344 // callMethod can leave real pixel calls stuck in the old queue. 345 function wrapFbq() { 346 if (!window.fbq || window.__autoseo_fbq_wrapped) return; 347 if (typeof window.fbq !== 'function') return; 348 if (typeof window.fbq.callMethod !== 'function') return; 349 350 var originalCallMethod = window.fbq.callMethod; 351 window.__autoseo_fbq_wrapped = true; 352 window.fbq.callMethod = function () { 353 // Original runs UNGUARDED 354 var result = originalCallMethod.apply(this, arguments); 355 // Our tracking is guarded 356 try { 357 if (arguments[0] === 'track' && CONVERSION_EVENTS_FBQ.indexOf(arguments[1]) !== -1) { 358 enqueue('facebook', arguments[1], arguments[2] || {}); 359 } 360 } catch (e) {} 361 return result; 362 }; 363 } 364 365 // --- Safe wrapper: if wrapping itself fails, the original global is untouched --- 366 function safeWrap(fn) { 367 try { fn(); } catch (e) {} 368 } 369 370 function tryWrapAll() { 371 safeWrap(wrapGtag); 372 safeWrap(wrapDataLayer); 373 safeWrap(wrapFbq); 374 } 375 376 // Wrap immediately if already loaded 377 tryWrapAll(); 378 379 // Use defineProperty to catch late-loading scripts that create gtag/fbq/dataLayer 380 function watchProperty(name, wrapFn) { 381 if (window[name]) return; 382 var value; 383 var insideSetter = false; 384 try { 385 Object.defineProperty(window, name, { 386 configurable: true, 387 get: function () { return value; }, 388 set: function (v) { 389 if (insideSetter) { value = v; return; } 390 insideSetter = true; 391 value = v; 392 // Restore normal property FIRST, before calling wrapFn. 393 // Even if wrapFn throws, the property is restored and usable. 394 try { 395 Object.defineProperty(window, name, { 396 configurable: true, 397 writable: true, 398 value: v 399 }); 400 } catch (e) { 401 // defineProperty restore failed; value is still readable 402 // via the getter. Don't try window[name]=v (re-enters setter). 403 } 404 insideSetter = false;
405 safeWrap(wrapFn); 406 } 407 }); 408 } catch (e) { 409 // defineProperty on window not supported -- poll fallback handles it 410 } 411 } 412 413 watchProperty('gtag', wrapGtag); 414 watchProperty('fbq', wrapFbq); 415 watchProperty('dataLayer', wrapDataLayer); 416 417 // Fallback poll for environments where defineProperty on window fails 418 var pollCount = 0; 419 var pollInterval = setInterval(function () { 420 tryWrapAll(); 421 pollCount++; 422 if (pollCount > 20) clearInterval(pollInterval); 423 }, 500); 424 425 // Flush on page unload 426 try { 427 window.addEventListener('pagehide', sendArticleTimeOnPage); 428 window.addEventListener('pagehide', flush); 429 window.addEventListener('beforeunload', flush); 430 } catch (e) {} 431})();
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.