1(() => { 2 // Wide debug logging for staging/dev domains 3 var DEBUG = true; 4 var wideEvent = { 5 script: 'attribution.js', 6 hostname: location.hostname, 7 url: location.href, 8 debug: DEBUG 9 }; 10 11 function emit(outcome) { 12 if (!DEBUG) return; 13 wideEvent.outcome = outcome; 14 wideEvent.duration_ms = Date.now() - wideEvent.start_time; 15 console.log('[GF Attribution]', JSON.stringify(wideEvent, null, 2)); 16 } 17 18 wideEvent.start_time = Date.now(); 19 20 // Bail out if running in a sandboxed iframe without cookie access 21 try { 22 var _ = document.cookie; 23 } catch (_e) { 24 wideEvent.sandboxed = true; 25 emit('sandboxed_exit'); 26 return; 27 } 28 29 var el = document.currentScript || document.querySelector('[data-gf-subdomain], [data-gf-source="theme-app-embed"]'); 30 // Extract context from data attributes or from the hosted script URL. Theme app extensions 31 // serve assets from Shopify CDN, so absence of a funnel subdomain should not block cart attribution. 32 var configuredSubdomain = el?.getAttribute('data-gf-subdomain'); 33 var shopDomain = el?.getAttribute('data-gf-shop'); 34 var isThemeAppEmbed = el?.getAttribute('data-gf-source') === 'theme-app-embed'; 35 var scriptHost = null; 36 if (el?.src) { 37 try { 38 var srcHost = new URL(el.src).hostname; 39 if (srcHost && srcHost !== location.hostname) scriptHost = srcHost; 40 } catch (_e) {} 41 } 42 var subdomain = configuredSubdomain || scriptHost || null; 43 wideEvent.script_src = el ? el.src : null; 44 wideEvent.asset_host = scriptHost; 45 wideEvent.source = el?.getAttribute('data-gf-source') || null; 46 wideEvent.shop_domain = shopDomain; 47 wideEvent.subdomain = subdomain; 48 wideEvent.subdomain_source = configuredSubdomain ? 'data_attr' : scriptHost ? 'script_src' : 'none'; 49 wideEvent.theme_app_embed = isThemeAppEmbed; 50 wideEvent.no_subdomain = !subdomain; 51 52 if (!subdomain && !isThemeAppEmbed) { 53 wideEvent.exit_reason = 'no_subdomain'; 54 emit('no_subdomain'); 55 return; 56 } 57 58 var TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days 59 60 function getCookie(name) { 61 var match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`)); 62 return match ? match[1] : null; 63 } 64 65 function payloadHash(p) { 66 return [p.pid, p.sid, p.fid, p.uid, p.vid, p.eid].join('|'); 67 } 68 69 function alreadyApplied(p) { 70 try { 71 var applied = JSON.parse(localStorage.getItem('_gf_applied') || '{}'); 72 return applied.hash === payloadHash(p); 73 } catch (_e) { 74 return false; 75 } 76 } 77 78 function markApplied(p) { 79 try { 80 localStorage.setItem('_gf_applied', JSON.stringify({ hash: payloadHash(p) })); 81 } catch (_e) {} 82 } 83 84 function updateCart(p) { 85 var body = { attributes: {} }; 86 if (p.pid) body.attributes._gf_pid = p.pid; 87 if (p.sid) body.attributes._gf_sid = p.sid; 88 if (p.fid) body.attributes._gf_fid = p.fid; 89 if (p.uid) body.attributes._gf_uid = p.uid; 90 if (p.vid) body.attributes._gf_vid = p.vid; 91 if (p.eid) body.attributes._gf_eid = p.eid; 92 body.attributes._gf_src = 'indirect'; 93 94 wideEvent.cart_attributes = body.attributes; 95 96 fetch('/cart/update.js', { 97 method: 'POST', 98 headers: { 'Content-Type': 'application/json' }, 99 body: JSON.stringify(body) 100 }) 101 .then((res) => { 102 wideEvent.cart_update_status = res.status; 103 markApplied(p); 104 emit('cart_updated'); 105 }) 106 .catch((err) => { 107 wideEvent.cart_update_error = String(err); 108 emit('cart_update_failed'); 109 }); 110 } 111 112 function apply(p) { 113 if (window.requestIdleCallback) { 114 requestIdleCallback(() => { 115 updateCart(p); 116 }); 117 } else { 118 setTimeout(() => { 119 updateCart(p); 120 }, 0); 121 } 122 } 123 124 // Read attribution from cookie 125 try { 126 var allCookies = document.cookie; 127 wideEvent.cookie_count = (allCookies.match(/=/g) || []).length; 128 wideEvent.cookie_length = allCookies.length; 129 130 var raw = getCookie('_gf_attr'); 131 wideEvent.has_gf_attr = !!raw; 132 wideEvent.gf_attr_raw = raw; 133 134 if (!raw) { 135 emit('no_cookie'); 136 return; 137 } 138 139 var p = JSON.parse(decodeURIComponent(raw)); 140 wideEvent.payload = p; 141 wideEvent.has_pid = !!p.pid; 142 143 if (!p || !p.pid) { 144 emit('no_pid'); 145 return; 146 } 147 148 if (p.ts) { 149 var age_ms = Date.now() - p.ts; 150 wideEvent.cookie_age_ms = age_ms; 151 wideEvent.cookie_age_hours = Math.round(age_ms / 3600000); 152 if (age_ms > TTL_MS) { 153 emit('cookie_expired'); 154 return; 155 } 156 } 157 158 var wasApplied = alreadyApplied(p); 159 wideEvent.already_applied = wasApplied; 160 if (wasApplied) { 161 emit('already_applied'); 162 return; 163 } 164 165 emit('applying');
166 apply(p); 167 } catch (e) { 168 wideEvent.error = String(e); 169 emit('error'); 170 } 171})();
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.