1/*! 2 * Parker University â CampusReel UTM bridge (v1.1.0) 3 * 4 * Injects Parker's existing UTM attribution into CampusReel's lead payload so 5 * it survives through to Salesforce. 6 * 7 * WHY THIS SHAPE 8 * -------------- 9 * CampusReel's embed bundle renders its exit-intent popup directly in this 10 * page (not a cross-origin iframe) and submits leads with: 11 * 12 * fetch(baseUrl + "/campusreel-embed/save_lead_data", 13 * { method: "POST", body: JSON.stringify(payload) }) 14 * 15 * The payload includes `source_url` and a free-form `custom_answers` hash 16 * (already used for "What Is Your Program of Interest?"). Those two fields are 17 * our injection points. 18 * 19 * We patch window.fetch rather than CampusReelEmbed._vrPopupsHelpers because 20 * their popup module captures the helper into a local at load time 21 * (`var k = y.saveLeadDataToServer`), so replacing the property later has no 22 * effect. The fetch call, by contrast, is resolved at submit time. 23 * 24 * LOAD ORDER 25 * ---------- 26 * This MUST execute before https://www.campusreel.org/campusreel-embed/ 27 * parker-university.js. It reads attribution lazily at submit time, so 28 * parker-utm-tracking.js may load in any order relative to this file. 29 * 30 * SAFETY 31 * ------ 32 * Fails open in every branch. Any unexpected shape, parse failure, or missing 33 * attribution passes the original request through untouched. A broken bridge 34 * must never cost a lead. 35 */ 36(function (window) { 37 'use strict'; 38 39 if (!window.fetch || window.__ParkerCampusReelBridge) return; 40 window.__ParkerCampusReelBridge = true; 41 42 var CONFIG = window.ParkerCampusReelBridgeConfig || {}; 43 44 var ENDPOINT = CONFIG.endpoint || '/campusreel-embed/save_lead_data'; 45 var PREFIX = CONFIG.prefix || 'pkr_trkr_'; // custom_answers key prefix 46 var MAX_LEN = CONFIG.maxLen || 255; // Salesforce Text cap 47 var ENRICH_URL = CONFIG.enrichSourceUrl !== false; // default on 48 var ENRICH_ANS = CONFIG.enrichAnswers !== false; // default on 49 var DEBUG = !!CONFIG.debug; 50 51 // Mirrors the touch keys written by parker-utm-tracking.js. 52 var TOUCH_KEYS = CONFIG.touchKeys || [ 53 'utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term', 54 'utm_id', 'click_id', 'click_id_param', 'gad_source', 'srsltid', 55 'referrer_host' 56 ]; 57 58 function log() { 59 if (DEBUG && window.console) console.log.apply(console, ['[ParkerCR]'].concat([].slice.call(arguments))); 60 } 61 62 function cap(v) { 63 if (v === null || v === undefined) return ''; 64 v = String(v); 65 return v.length > MAX_LEN ? v.slice(0, MAX_LEN) : v; 66 } 67 68 /* ---------------- attribution source ---------------- */ 69 70 // Prefer the plugin's public API; fall back to its raw store so the bridge 71 // still works if the API surface changes or the script is deferred past us. 72 function getTouch() { 73 try { 74 if (window.ParkerUtmTracking && typeof window.ParkerUtmTracking.getTouch === 'function') { 75 var t = window.ParkerUtmTracking.getTouch(); 76 if (t) return t; 77 } 78 } catch (e) { log('getTouch() failed', e); } 79 80 try { 81 var key = CONFIG.storageKey || 'parker_utm_data'; 82 var raw = window.localStorage.getItem(key); 83 if (!raw) { 84 var m = document.cookie.match(new RegExp('(?:^|; )' + key + '=([^;]*)')); 85 if (m) raw = decodeURIComponent(m[1]); 86 } 87 if (raw) { 88 var s = JSON.parse(raw); 89 return s.lastAny || s.last || null; 90 } 91 } catch (e) { log('store read failed', e); } 92 93 return null; 94 } 95 96 function getDerived() { 97 try { 98 if (window.ParkerUtmTracking && typeof window.ParkerUtmTracking.derive === 'function') { 99 return window.ParkerUtmTracking.derive() || null; 100 } 101 } catch (e) { log('derive() failed', e); } 102 return null; 103 } 104 105 /* ---------------- payload enrichment ---------------- */ 106 107 function enrich(payload) { 108 var touch = getTouch(); 109 if (!touch) { log('no attribution stored; passing through'); return payload; } 110 111 if (ENRICH_ANS) { 112 // custom_answers is a free-form hash CampusReel already persists (it 113 // carries "Program of Interest"), which makes it the most likely field 114 // to survive their pipeline into Salesforce. 115 var answers = payload.custom_answers; 116 if (!answers || typeof answers !== 'object') answers = {}; 117 118 for (var i = 0; i < TOUCH_KEYS.length; i++) { 119 var k = TOUCH_KEYS[i]; 120 if (touch[k]) answers[PREFIX + k] = cap(touch[k]); 121 } 122 123 // The plugin's own 4-tier lead-source derivation, so CampusReel leads 124 // carry the same values as leads captured by Parker's other forms 125 // rather than a second, differently-shaped source taxonomy. 126 var derived = getDerived(); 127 if (derived && typeof derived === 'object') {
128 Object.keys(derived).forEach(function (d) { 129 if (derived[d]) answers[PREFIX + 'lead_source_' + d] = cap(derived[d]); 130 }); 131 } 132 133 payload.custom_answers = answers; 134 } 135 136 if (ENRICH_URL) { 137 // source_url is window.location.href at submit time. A visitor who 138 // arrived on a tagged URL and converted three pages later is on a clean 139 // one by then, so the tags have to come from the store - this is the 140 // second, independent path in case custom_answers does not map through. 141 // 142 // WRITTEN UNDER THE PREFIX, NEVER AS BARE utm_* KEYS, and that is the 143 // whole correctness of this block. 144 // 145 // The first version set utm_source, utm_medium and the rest directly on 146 // the URL. Because attribution persists across navigations, those values 147 // are usually NOT what was on the address bar - so it recorded a URL the 148 // visitor had never been on, indistinguishable from one they had. Anyone 149 // reading source_url later, on either side of the handoff, would have had 150 // no way to tell a real landing page from one we assembled, and no way to 151 // recover the real one. 152 // 153 // Prefixed, the recorded URL keeps its own query string exactly as it 154 // was - including any genuine utm_* tags, which now survive rather than 155 // being silently preferred over ours or vice versa - and our additions 156 // are obviously ours. Strip the prefixed keys and you have the page the 157 // visitor was actually on, byte for byte.
158 try { 159 var here = (window.location && window.location.href) || ''; 160 var u = here ? new URL(payload.source_url || here, here) 161 : new URL(payload.source_url); 162 163 for (var j = 0; j < TOUCH_KEYS.length; j++) { 164 var key = TOUCH_KEYS[j]; 165 if (!touch[key]) continue; 166 167 var pk = PREFIX + key; 168 // Guarded for idempotency, not for collision: a real page cannot 169 // carry our prefix, but a double-enriched payload could. 170 if (!u.searchParams.has(pk)) u.searchParams.set(pk, cap(touch[key])); 171 } 172 173 payload.source_url = u.toString(); 174 } catch (e) { log('source_url enrich failed', e); } 175 } 176 177 log('enriched payload', payload); 178 return payload; 179 } 180 181 /* ---------------- fetch interception ---------------- */ 182 183 function urlOf(input) { 184 try { 185 if (typeof input === 'string') return input; 186 if (input && typeof input.url === 'string') return input.url; // Request 187 if (input && typeof input.toString === 'function') return input.toString(); // URL 188 } catch (e) {} 189 return ''; 190 } 191 192 // ---------------- installation ---------------- 193 // 194 // WE CANNOT ASSUME WE STAY INSTALLED, and that is not paranoia - it was 195 // observed. The bridge was verifiably on the page, classic, not deferred, 196 // loading 200 - and window.fetch still read as native by the time the popup 197 // submitted. Something later on the page put it back. 198 // 199 // Pages like this one carry several scripts that do exactly that: consent 200 // tools and analytics libraries commonly capture a pristine fetch (often 201 // from a fresh iframe) and restore it to defeat wrappers, and any script 202 // doing `window.fetch = somethingElse` after us wins under a plain 203 // assignment. A single assignment at load time is a wrapper that holds only 204 // until the next script disagrees. 205 // 206 // So the property is DEFINED rather than assigned: reads always return our 207 // wrapper, and a later assignment changes what we delegate to instead of 208 // replacing us. Whoever wrote that assignment still gets their behaviour - 209 // their function is called - they simply cannot remove the interception. 210 // The real one, captured before anything of ours is installed. Kept 211 // separately from `underlying` because it is the only thing guaranteed not 212 // to lead back here. 213 var trueNative = window.fetch.bind(window); 214 var underlying = trueNative; 215 216 // RE-ENTRY GUARD, and it is not hypothetical - a test caught it. 217 // 218 // A script doing `window.fetch = window.fetch.bind(window)` reads our 219 // wrapper through the getter and hands back a BOUND COPY of it. bind() 220 // returns a new function object and copies no custom properties, so neither 221 // an identity check nor a marker property can recognise it: the setter sees 222 // a perfectly ordinary function and delegates to it, which calls us, which 223 // delegates to it, until the stack gives out and the page dies. 224 // 225 // Losing attribution is a bad day. Taking the site down to collect it is 226 // unacceptable, so the depth counter is the real safety net and the checks 227 // in the setter are only a tidiness measure. Re-entered, we stop trying and 228 // go straight to the fetch we captured at install. 229 var depth = 0; 230 231 // CALLED WITH A RECEIVER, ALWAYS. This was a bare `underlying(input, init)`, 232 // which is correct only while `underlying` is the bound copy captured at 233 // install. The setter below stores whatever a later script assigns, and the 234 // scripts that assign to window.fetch are precisely the ones that hand back 235 // a PRISTINE, UNBOUND native fetch - consent tools and analytics libraries 236 // restoring a clean copy, often lifted from a fresh iframe. 237 // 238 // Native fetch called with `this === undefined` throws 239 // "Failed to execute 'fetch' on 'Window': Illegal invocation" - SYNCHRONOUSLY, 240 // and from a line outside inner()'s try/catch, so it is not a rejected 241 // promise anybody can handle. Every fetch on the page dies from that moment. 242 // 243 // .call(window, ...) is right for both shapes: a bound function ignores the 244 // receiver, an unbound one needs exactly this. 245 // 246 // AND IF IT STILL THROWS, we fall back to the fetch captured at install 247 // rather than letting the page lose networking to collect attribution. That 248 // is the same trade the rest of this file makes: a broken bridge must never 249 // cost a lead, and it must certainly never cost the site. 250 function nativeFetch(input, init) { 251 try { 252 return underlying.call(window, input, init); 253 } catch (e) { 254 log('underlying fetch threw; falling back to the captured native', e); 255 return trueNative(input, init); 256 } 257 } 258 259 var ours = function (input, init) { 260 if (depth > 0) { 261 return trueNative(input, init); 262 } 263 // NOTHING BELOW MAY THROW SYNCHRONOUSLY. Callers of fetch expect a promise 264 // and chain .then off it; a synchronous throw skips every .catch they 265 // wrote and surfaces as an unhandled error in somebody else's script. 266 267 depth++; 268 try { 269 return inner(input, init); 270 } finally { 271 // Decremented synchronously: the recursion happens in the call chain 272 // before any promise settles, so this is exactly the window that needs 273 // guarding. 274 depth--; 275 } 276 }; 277 278 var inner = function (input, init) { 279 try { 280 // THIS BLOCK GUARDS THE ENRICHMENT ONLY. The final delegation below is 281 // deliberately outside it - passing the original request through is the 282 // fail-open path and must not be reached by catching its own failure. 283 // What makes that safe is that nativeFetch() cannot throw any more; it 284 // used to, and the throw escaped from here into the caller's code. 285 if (urlOf(input).indexOf(ENDPOINT) !== -1 && 286 init && typeof init.body === 'string') { 287 288 var payload = JSON.parse(init.body); // throws -> caught -> pass through 289 if (payload && typeof payload === 'object') { 290 // Copy init so we never mutate the caller's object. 291 var patched = {}; 292 for (var k in init) { if (Object.prototype.hasOwnProperty.call(init, k)) patched[k] = init[k]; } 293 patched.body = JSON.stringify(enrich(payload)); 294 return nativeFetch(input, patched); 295 } 296 } 297 } catch (e) { 298 log('interception failed; passing original through', e); 299 } 300 return nativeFetch(input, init); 301 }; 302 303 try { 304 Object.defineProperty(window, 'fetch', { 305 configurable: true, 306 get: function () { return ours; }, 307 set: function (next) { 308 // NEVER DELEGATE TO OURSELVES. A script doing 309 // `window.fetch = window.fetch.bind(window)` would otherwise make 310 // underlying point at our own wrapper and recurse until the stack 311 // gives out - taking the page with it, which is far worse than 312 // losing attribution. 313 if (typeof next === 'function' && next !== ours) { 314 log('something replaced window.fetch; keeping the bridge on top'); 315 underlying = next; 316 } 317 } 318 }); 319 } catch (e) { 320 // A non-configurable fetch, or an engine that refuses the redefine. Fall 321 // back to the plain assignment rather than leaving no bridge at all. 322 log('defineProperty refused; falling back to assignment', e); 323 window.fetch = ours; 324 } 325 326 log('bridge armed for', ENDPOINT); 327})(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.