1/* 2 * UTM / lead-source attribution â shared by every lead form on the site 3 * (including the blog section). 4 * 5 * Resolves ALL traffic sources into the SAME three fields â atm_source, 6 * atm_campaign, atm_parameters â used unrenamed as the cookie name, the POST 7 * field name, AND the backend/DB column name, so a value written here needs 8 * no relabeling at any hop before it lands in the database: 9 * 10 * atm_source -> "google" | "meta" | "emailer" | "organic" 11 * atm_campaign -> that source's campaign id, "" if none / not applicable 12 * atm_parameters -> the FULL raw query string verbatim, captured for every 13 * visit that has one, regardless of source 14 * 15 * Source resolution, in priority order (first match wins): 16 * 1. Google Ads : gclid is the authoritative signal â auto-tagging (on by 17 * default) appends it to essentially every click. On iOS, where gclid 18 * is suppressed, Google appends gbraid/wbraid instead, so those are 19 * accepted too. gad_source is a newer, non-guaranteed surface marker 20 * kept only as an extra fallback. Campaign id is gad_campaignid. 21 * 2. Meta Ads : fbclid â auto-appended by Meta's link wrapper on every 22 * click (present on iOS too). Campaign id is campaign_id, falling back 23 * to utm_campaign. 24 * 3. Emailer : an email-campaign link appends its own opaque 25 * per-recipient/per-campaign tracking id as ?atm_source=<md5>, e.g. 26 * https://carlstahlcraftsman.com/?atm_source=6668118113eee9ea016b44a19d51ee45 27 * That id is a LOOKUP KEY, not the label itself â ATM_SOURCE_MAP below 28 * resolves it to a human-readable source name. Today only one id is 29 * known ("...ee45" -> "emailer"), but new ids can be issued for other 30 * sources in future (a different mailer platform, a partner, an SMS 31 * blast, etc.) â just add a line to the map, nothing else changes. An 32 * id that isn't in the map yet still resolves to "emailer" (the 33 * presence of atm_source at all is itself the signal), so tracking 34 * keeps working for new campaigns even before their id is mapped. 35 * Campaign id is the atm_campaign query param, if present. 36 * 4. Organic : none of the above matched. No campaign; atm_parameters 37 * is still captured if the URL had a query string. 38 * 39 * No HTML/template changes needed: no form carries hidden atm_* inputs in 40 * its PHP source. Instead, a single document-level "submit" listener 41 * (capture phase, so it runs before anything else the submit triggers) 42 * injects the three hidden inputs into whichever form was just submitted, 43 * right before the browser serializes it. That covers BOTH styles of form 44 * on this site: 45 * - AJAX forms that build `new FormData(this)` in their own (bubble-phase) 46 * handler â the inputs already exist in the DOM by the time that runs. 47 * - Plain native-POST forms (e.g. the blog's brochure-download popup) â 48 * the inputs exist in the DOM before the browser's default submit action 49 * serializes the form, so they ride along automatically too. 50 * One shared script, loaded once per section, zero per-form wiring. 51 * 52 * Persistence: last-touch, via document.cookie. A visitor can land on any 53 * page (not just the one the ad/email link points to) and browse for days 54 * before submitting an enquiry, so the cookie must survive across the whole 55 * site, not just the landing page. atm_source/atm_campaign are only 56 * overwritten when a NEW recognized source is found in the URL â a later 57 * visit with no ad/emailer signal leaves the previously captured 58 * attribution untouched (organic never overwrites a real source). 30 days 59 * is the standard paid-ads attribution window. 60 */ 61(function () { 62 "use strict"; 63 64 var ATM_TTL_DAYS = 30; 65 var ATM_FIELDS = ["atm_source", "atm_campaign", "atm_parameters"]; 66 67 // id (the raw atm_source query value) -> resolved source label. 68 // Add a line here whenever a new emailer/campaign id is issued. 69 var ATM_SOURCE_MAP = { 70 "6668118113eee9ea016b44a19d51ee45": "emailer", 71 }; 72 var ATM_SOURCE_DEFAULT = "emailer"; 73 74 function resolveAtmSourceName(id) { 75 return ATM_SOURCE_MAP[id] || ATM_SOURCE_DEFAULT; 76 } 77 78 function setCookie(name, value, days) { 79 var expires = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toUTCString(); 80 document.cookie = name + "=" + encodeURIComponent(value) + "; path=/; expires=" + expires + "; SameSite=Lax"; 81 } 82 83 function getCookie(name) { 84 var match = document.cookie.match(new RegExp("(?:^|; )" + name + "=([^;]*)")); 85 return match ? decodeURIComponent(match[1]) : ""; 86 } 87 88 // Returns { atm_source, atm_campaign } for a recognized signal, or null 89 // for organic traffic (i.e. "leave whatever was captured before alone"). 90 function resolveAttribution(params) { 91 var googleClickId = params.get("gclid") || params.get("gbraid") || params.get("wbraid") || params.get("gad_source"); 92 if (googleClickId) { 93 return { atm_source: "google", atm_campaign: params.get("gad_campaignid") || "" }; 94 } 95 96 var fbclid = params.get("fbclid"); 97 if (fbclid) { 98 return { 99 atm_source: "meta", 100 atm_campaign: params.get("campaign_id") || params.get("utm_campaign") || "", 101 }; 102 } 103 104 var atmSourceId = params.get("atm_source"); 105 if (atmSourceId) { 106 return { atm_source: resolveAtmSourceName(atmSourceId), atm_campaign: params.get("atm_campaign") || "" }; 107 } 108 109 return null; 110 } 111 112 function captureAtmAttribution() { 113 var rawQuery = window.location.search.replace(/^\?/, ""); 114 // Persisted for EVERY visit with a query string, any source (google, 115 // meta, emailer, or unrecognized) â this is what gets saved verbatim 116 // against the enquiry. 117 if (rawQuery) {
118 setCookie("atm_parameters", rawQuery, ATM_TTL_DAYS); 119 } 120 121 var attribution = resolveAttribution(new URLSearchParams(window.location.search)); 122 if (!attribution) return; // organic: leave any previously captured source/campaign untouched 123 124 setCookie("atm_source", attribution.atm_source, ATM_TTL_DAYS); 125 setCookie("atm_campaign", attribution.atm_campaign, ATM_TTL_DAYS); 126 } 127 128 function currentAtmValues() { 129 // No source cookie yet => organic, with no campaign. A campaign id is 130 // only meaningful paired with the source that owns it, so it's gated on 131 // a real (non-organic) source â avoids ever pairing a stale campaign 132 // with organic traffic. 133 var atmSource = getCookie("atm_source") || "organic"; 134 return { 135 atm_source: atmSource, 136 atm_campaign: atmSource !== "organic" ? getCookie("atm_campaign") : "", 137 atm_parameters: getCookie("atm_parameters") || "", 138 }; 139 } 140 141 // Creates (or reuses) the three hidden inputs on `form` and sets their 142 // values from the captured cookies. Fields are always written (even as 143 // "" / "organic") so the POST payload has a predictable, consistent shape. 144 function injectAtmFields(form) { 145 var values = currentAtmValues();
146 ATM_FIELDS.forEach(function (name) { 147 var input = form.querySelector('input[name="' + name + '"]'); 148 if (!input) { 149 input = document.createElement("input"); 150 input.type = "hidden"; 151 input.name = name; 152 form.appendChild(input); 153 } 154 input.value = values[name]; 155 }); 156 } 157 158 captureAtmAttribution(); 159 160 document.addEventListener( 161 "submit", 162 function (e) { 163 if (e.target instanceof HTMLFormElement) { 164 injectAtmFields(e.target); 165 } 166 }, 167 true 168 ); 169})();
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.