PageSourceSearch

https://www.parker.edu/wp-content/plugins/parker-salesforce-integr…/js/campusreel-utm-bridge.js?ver=1790091993

js parker.edu collected 2026-09-24 09:46:50 UTC 13,766 bytes, 327 lines download raw bytes

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.