PageSourceSearch

https://www.carlstahlcraftsman.com/js/utm-tracking.js

js carlstahlcraftsman.com collected 2026-10-06 17:36:05 UTC 7,702 bytes, 169 lines download raw bytes

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.