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 // The Shopify cart token is part of the hash so a NEW cart (e.g. after an order 66 // clears the old one) is re-stamped with the same attribution. 67 function payloadHash(p) { 68 return [p.pid, p.sid, p.fid, p.uid, p.vid, p.eid, getCookie('cart') || ''].join('|'); 69 } 70 71 // External-URL experiment arms: the edge 302s the visitor here with 72 // ?gf_eid&gf_vid&gf_sid&gf_pid (EXPERIMENT_ATTRIBUTION_PARAMS in 73 // @getfunneled/classes/experiment-attribution). That landing attribution is persisted in 74 // localStorage (same _gf_attr shape, fid empty) so later pages in the visit 75 // still stamp the cart, and it competes with the parent-domain _gf_attr cookie 76 // on freshness (ts) â an old 30-day funnel cookie must not override it. 77 var LANDING_KEY = '_gf_landing_attr'; 78 79 function landingFromUrl() { 80 try { 81 var params = new URLSearchParams(location.search); 82 var p = { 83 pid: params.get('gf_pid') || '', 84 sid: params.get('gf_sid') || '', 85 fid: '', 86 uid: '', 87 vid: params.get('gf_vid') || '', 88 eid: params.get('gf_eid') || '', 89 ts: Date.now() 90 }; 91 if (!p.pid || !p.sid || !p.eid || !p.vid) return null; 92 return p; 93 } catch (_e) { 94 return null; 95 } 96 } 97 98 function parseAttr(raw) { 99 if (!raw) return null; 100 try { 101 var p = JSON.parse(decodeURIComponent(raw)); 102 return p && typeof p === 'object' && p.pid ? p : null; 103 } catch (_e) { 104 return null; 105 } 106 } 107 108 function freshest(a, b) { 109 if (!a) return b; 110 if (!b) return a; 111 return (b.ts || 0) > (a.ts || 0) ? b : a; 112 } 113 114 function alreadyApplied(p) { 115 try { 116 var applied = JSON.parse(localStorage.getItem('_gf_applied') || '{}'); 117 return applied.hash === payloadHash(p); 118 } catch (_e) { 119 return false; 120 } 121 } 122 123 function markApplied(p) { 124 try { 125 localStorage.setItem('_gf_applied', JSON.stringify({ hash: payloadHash(p) })); 126 } catch (_e) {} 127 } 128 129 function updateCart(p) { 130 var body = { attributes: {} }; 131 if (p.pid) body.attributes._gf_pid = p.pid; 132 if (p.sid) body.attributes._gf_sid = p.sid; 133 if (p.fid) body.attributes._gf_fid = p.fid; 134 if (p.uid) body.attributes._gf_uid = p.uid; 135 if (p.vid) body.attributes._gf_vid = p.vid; 136 if (p.eid) body.attributes._gf_eid = p.eid; 137 body.attributes._gf_src = 'indirect'; 138 139 wideEvent.cart_attributes = body.attributes; 140 141 fetch('/cart/update.js', { 142 method: 'POST', 143 headers: { 'Content-Type': 'application/json' }, 144 body: JSON.stringify(body) 145 }) 146 .then((res) => { 147 wideEvent.cart_update_status = res.status; 148 markApplied(p); 149 emit('cart_updated'); 150 }) 151 .catch((err) => { 152 wideEvent.cart_update_error = String(err); 153 emit('cart_update_failed'); 154 }); 155 } 156 157 function apply(p) { 158 if (window.requestIdleCallback) { 159 requestIdleCallback(() => { 160 updateCart(p); 161 }); 162 } else { 163 setTimeout(() => { 164 updateCart(p); 165 }, 0); 166 } 167 } 168 169 // Read attribution: freshest of the _gf_attr cookie and the persisted landing attribution 170 try { 171 var allCookies = document.cookie; 172 wideEvent.cookie_count = (allCookies.match(/=/g) || []).length; 173 wideEvent.cookie_length = allCookies.length; 174 175 var raw = getCookie('_gf_attr'); 176 wideEvent.has_gf_attr = !!raw; 177 wideEvent.gf_attr_raw = raw; 178 179 var landing = landingFromUrl(); 180 wideEvent.has_landing_params = !!landing; 181 if (landing) {
182 try { 183 localStorage.setItem(LANDING_KEY, encodeURIComponent(JSON.stringify(landing))); 184 } catch (_e) {} 185 } 186 var stored = landing; 187 if (!stored) { 188 try { 189 stored = parseAttr(localStorage.getItem(LANDING_KEY)); 190 } catch (_e) { 191 stored = null; 192 } 193 } 194 var cookieAttr = parseAttr(raw); 195 var p = freshest(cookieAttr, stored); 196 wideEvent.attribution_source = !p ? 'none' : p === cookieAttr ? 'cookie' : 'landing'; 197 198 if (!raw && !stored) { 199 emit('no_cookie'); 200 return; 201 } 202 203 wideEvent.payload = p; 204 wideEvent.has_pid = !!p?.pid; 205 206 if (!p || !p.pid) { 207 emit('no_pid'); 208 return; 209 } 210 211 if (p.ts) { 212 var age_ms = Date.now() - p.ts; 213 wideEvent.cookie_age_ms = age_ms; 214 wideEvent.cookie_age_hours = Math.round(age_ms / 3600000); 215 if (age_ms > TTL_MS) { 216 emit('cookie_expired'); 217 return; 218 } 219 } 220 221 var wasApplied = alreadyApplied(p); 222 wideEvent.already_applied = wasApplied; 223 if (wasApplied) { 224 emit('already_applied'); 225 return; 226 } 227 228 emit('applying'); 229 apply(p); 230 } catch (e) { 231 wideEvent.error = String(e); 232 emit('error'); 233 } 234})();
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.