PageSourceSearch

https://visitcheshire.com/cpresources/c19495c8/ga-event-tracking.js?v=1790772881

js visitcheshire.com collected 2026-10-02 10:18:47 UTC 9,487 bytes, 274 lines download raw bytes

1/**
2 * Event Tracking Framework
3 *
4 * A generic, reusable system for tracking events via GTM's dataLayer.
5 * Supports two trigger types:
6 *   - "impression": fires when an element becomes visible in the viewport (50% threshold)
7 *   - "click": fires when an element is clicked
8 *
9 * Usage:
10 *   window.gaEventTracking.register({
11 *     selector: 'img.my-element',           // CSS selector to find trackable elements
12 *     eventName: 'my_{trigger}',            // event name — {trigger} is replaced with "click" or "impression"
13 *     triggers: ['click', 'impression'],    // which events to track
14 *     getPayload: function(el) {            // returns the event data object pushed to the dataLayer
15 *       return { id: el.dataset.id };
16 *     }
17 *   });
18 *
19 * Events are pushed to GTM's dataLayer. All custom event names should be
20 * prefixed with "in_" so a single GTM trigger (regex: ^in_) can catch them all.
21 * Page-level custom dimensions (pageCategory, pageSection, pageType) from
22 * SEOmatic are automatically included as individual properties in every event.
23 *
24 * Elements are discovered both on registration and dynamically via MutationObserver,
25 * so elements added to the DOM after page load (e.g. via Sprig/htmx) are tracked automatically.
26 *
27 * Wrapped in try/catch to ensure errors never affect other site functionality.
28 */
29try {
30  if (localStorage.getItem("in_debug") === "1") console.log("[Event Tracking] ga-event-tracking.js loaded");
31  // Prefix used to extract custom event attributes from data-event-attribute-* HTML attributes
32  var PREFIX = "data-event-attribute-";
33
34  // Tracks which (element, tracker) pairs have been initialised to prevent duplicate listeners.
35  // Each element maps to a Set of tracker references it has already been bound to,
36  // allowing multiple trackers to independently track the same element.
37  var processed = new WeakMap();
38
39  // Stores all registered tracker configurations
40  var trackers = [];
41
42  // Ensure the dataLayer exists
43  window.dataLayer = window.dataLayer || [];
44
45  /**
46   * Retrieve page-level custom dimensions from the dataLayer.
47   * These are set by SEOmatic/DestinationCore at page load
48   * (e.g. pageCategory, pageSection, pageType) and are explicitly
49   * included in the payload of every event.
50   */
51  var PAGE_DIMENSIONS = ["pageCategory", "entryId", "entryTitle", "pageSection", "pageType", "relatedBusinessId", "relatedBusinessTitle"];
52
53  function getPageDimensions() {
54    var dimensions = {};
55    for (var i = 0; i < window.dataLayer.length; i++) {
56      var entry = window.dataLayer[i];
57      for (var j = 0; j < PAGE_DIMENSIONS.length; j++) {
58        var key = PAGE_DIMENSIONS[j];
59        if (entry.hasOwnProperty(key)) {
60          dimensions[key] = entry[key];
61        }
62      }
63    }
64    return dimensions;
65  }
66
67  /**
68   * Send an event via GTM's dataLayer, with a fallback to gtag() if GTM is not present.
69   * GTM routes these events to GA4 via a trigger matching the "in_" prefix.
70   * If GTM is not loaded but gtag() is available (e.g. via standalone GA4),
71   * the event is sent directly to GA4 via gtag().
72   * Page-level custom dimensions are included as individual properties.
73   */
74  function sendEvent(eventName, attributes) {
75    try {
76      var params = {};
77      var dimensions = getPageDimensions();
78      for (var key in dimensions) {
79        if (dimensions.hasOwnProperty(key)) {
80          params[key] = dimensions[key];
81        }
82      }
83      for (var key in attributes) {
84        if (attributes.hasOwnProperty(key)) {
85          params[key] = attributes[key];
86        }
87      }
88
89      if (window.google_tag_manager) {
90        // GTM is present — push to dataLayer for GTM to route to GA4
91        params.event = eventName;
92        window.dataLayer.push(params);
93
94        // Reset event-specific attributes so they don't bleed into
95        // subsequent events via dataLayer's sticky state
96        var reset = {};
97        for (var attr in attributes) {
98          if (attributes.hasOwnProperty(attr)) {
99            reset[attr] = undefined;
100          }
101        }
102        window.dataLayer.push(reset);
103      } else if (typeof gtag === "function") {
104        // No GTM, but gtag is available — send directly to GA4
105        gtag("event", eventName, params);
106      }
107    } catch (e) {
108        console.error("[Event Tracking]", e);
109    }
110  }
111
112  /**
113   * Extract event attributes from an element's data-event-attribute-* HTML attributes.
114   * e.g. data-event-attribute-ad_id="123" becomes { ad_id: 123 }
115   * Numeric string values are automatically parsed to numbers.
116   */
117  function getEventAttributes(el) {
118    var attributes = {};
119    for (var i = 0; i < el.attributes.length; i++) {
120      var attr = el.attributes[i];
121      if (attr.name.indexOf(PREFIX) === 0) {
122        var key = attr.name.slice(PREFIX.length);
123        var value =
124          isNaN(attr.value) || attr.value === ""
125            ? attr.value
126            : parseFloat(attr.value);
127        attributes[key] = value;
128      }
129    }
130    return attributes;
131  }
132
133  /**
134   * IntersectionObserver for impression tracking.
135   * Fires the impression event once when at least 50% of the element is visible
136   * in the viewport, then stops observing that element.
137   */
138  var impressionObserver = new IntersectionObserver(
139    function (entries) {
140      entries.forEach(function (entry) {
141        if (!entry.isIntersecting) {
142          return;
143        }
144
145        var el = entry.target;
146        var elTrackers = el._gaTrackers || [];
147        for (var t = 0; t < elTrackers.length; t++) {
148          var tracker = elTrackers[t];
149          if (tracker.triggers.indexOf("impression") === -1) {
150            continue;
151          }
152          var payload = tracker.getPayload(el);
153          if (payload !== null) {
154            sendEvent(
155              tracker.eventName.replace("{trigger}", "impression"),
156              payload
157            );
158          }
159        }
160
161        // Only track one impression per element per page load
162        impressionObserver.unobserve(el);
163      });
164    },
165    { threshold: 0.5 }
166  );
167
168  /**
169   * Initialise tracking on a single DOM element.
170   * Attaches the tracker config to the element, sets up the IntersectionObserver
171   * for impressions, and adds a click event listener for clicks.
172   * Each (element, tracker) pair is only initialised once, but multiple
173   * trackers can independently bind to the same element.
174   */
175  function initElement(el, tracker) {
176    var trackerSet = processed.get(el);
177    if (!trackerSet) {
178      trackerSet = new Set();
179      processed.set(el, trackerSet);
180    }
181    if (trackerSet.has(tracker)) {
182      return;
183    }
184    trackerSet.add(tracker);
185
186    // Store tracker configs on the element so the IntersectionObserver callback can access them
187    if (!el._gaTrackers) {
188      el._gaTrackers = [];
189    }
190    el._gaTrackers.push(tracker);
191
192    if (tracker.triggers.indexOf("impression") !== -1) {
193      impressionObserver.observe(el);
194    }
195
196    if (tracker.triggers.indexOf("click") !== -1) {
197      var clickHandler = function () {
198        var payload = tracker.getPayload(el);
199        if (payload === null) {
200          return;
201        }
202        sendEvent(
203          tracker.eventName.replace("{trigger}", "click"),
204          payload
205        );
206      };
207      el.addEventListener("click", clickHandler);
208      el.addEventListener("auxclick", clickHandler);
209    }
210  }
211
212  /**
213   * Scan a DOM subtree (or the full document) for elements matching
214   * any registered tracker's selector, and initialise tracking on them.
215   */
216  function scanForElements(root) {
217    trackers.forEach(function (tracker) {
218      var els = (root || document).querySelectorAll(tracker.selector);
219      els.forEach(function (el) {
220        initElement(el, tracker);
221      });
222    });
223  }
224
225  /**
226   * MutationObserver to detect dynamically added DOM elements.
227   * When new nodes are added (e.g. via Sprig/htmx AJAX swaps), each node
228   * and its descendants are checked against all registered tracker selectors.
229   */
230  var domObserver = new MutationObserver(function (mutations) {
231    for (var i = 0; i < mutations.length; i++) {
232      var addedNodes = mutations[i].addedNodes;
233      for (var j = 0; j < addedNodes.length; j++) {
234        var node = addedNodes[j];
235
236        // Skip non-element nodes (text nodes, comments, etc.)
237        if (node.nodeType !== 1 || !node.querySelectorAll) {
238          continue;
239        }
240
241        // Check if the added node itself, or any of its descendants, match a tracker selector
242        trackers.forEach(function (tracker) {
243          if (node.matches && node.matches(tracker.selector)) {
244            initElement(node, tracker);
245          }
246          var els = node.querySelectorAll(tracker.selector);
247          for (var k = 0; k < els.length; k++) {
248            initElement(els[k], tracker);
249          }
250        });
251      }
252    }
253  });
254
255  domObserver.observe(document.body, { childList: true, subtree: true });
256
257  /**
258   * Public API — exposed on window so other scripts can register tracker configurations.
259   * On registration, immediately scans the current DOM for matching elements.
260   * Any elements added later are picked up automatically by the MutationObserver.
261   */
262  window.gaEventTracking = {
263    register: function (config) {
264      try {
265        trackers.push(config);
266        scanForElements();
267      } catch (e) {
268          console.error("[Event Tracking]", e);
269      }
270    },
271  };
272} catch (e) {
273    console.error("[Event Tracking]", e);
274}

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.