PageSourceSearch

https://www.dieroester.at/resources/analytics/track-events-ng.js

js dieroester.at collected 2026-10-01 16:24:35 UTC 9,867 bytes, 342 lines download raw bytes

1/**
2 * Event tracking (2026 / GA4 + GTM + Meta Pixel), no jQuery, no UA.
3 *
4 * Usage:
5 *  - Add class "-js-ga-track" to clickable elements or forms you want tracked.
6 *  - Supported data-attributes on the tracked element (or the form itself):
7 *      data-ga-cat="contact"
8 *      data-ga-label="email"
9 *      data-ga-event="contact_click"      // optional (defaults: "click" or "submit")
10 *      data-ga-name="some_name"           // optional
11 *      data-ga-val="10"                   // optional (integer)
12 *      data-fb-event="Lead"               // optional (Meta Pixel event name override)
13 *
14 * Example:
15 *  <a href="mailto:[email protected]"
16 *     class="-js-ga-track"
17 *     data-ga-cat="contact"
18 *     data-ga-label="email"
19 *     data-ga-event="contact_click">
20 *     [email protected]
21 *  </a>
22 *
23 * Notes:
24 *  - Sends GA4 either via gtag() (if present) OR via GTM dataLayer push (if present) OR both.
25 *  - For forms, prevents default submission and submits after callback or max wait (700ms).
26 *  - For clicks that navigate (normal links), delays navigation briefly to improve delivery.
27 *
28 * Debugging: 
29 *  - Tries to get a configuration object window.trackEventsConfig, e.g.
30 *
31 *  <script>
32 *  window.trackEventsConfig = { "debug": true, preventNavigationOnClicks: false, navigationDelay: 3000 };
33 *  </script>
34 *
35 */
36
37(() => {
38	// Default configuration
39	const defaultConfig = {
40		debug: false,
41		preventNavigationOnClicks: false,
42		navigationDelay: 300,
43		submitDelay: 700
44	};
45
46	const config = Object.assign(
47		defaultConfig,
48		window.trackEventsConfig || {}
49	);
50
51	let debug = config.debug;
52
53	if (config.debug) console.log("[track-events] === Debugging enabled ====");
54	if (config.debug) console.log("[track-events] Configuration", config);
55
56	const state = {
57		hasGA4: false, // gtag
58		hasGTM: false, // dataLayer
59		hasFB: false,  // fbq
60	};
61
62	function init() {
63		state.hasGA4 = typeof window.gtag !== "undefined";
64		state.hasGTM = typeof window.dataLayer !== "undefined" && Array.isArray(window.dataLayer);
65		state.hasFB  = typeof window.fbq !== "undefined";
66
67		if (config.debug) {
68			console.log("[track-events]", "GTM", state.hasGTM, "FB", state.hasFB, "GA4(gtag)", state.hasGA4);
69		}
70	}
71	init();
72
73	function parseIntSafe(v) {
74		if (v === undefined || v === null || v === "") return undefined;
75		const n = Number.parseInt(String(v), 10);
76		return Number.isNaN(n) ? undefined : n;
77	}
78
79	function getTrackingElement(target) {
80		if (!target || !(target instanceof Element)) return null;
81		return target.closest(".-js-ga-track");
82	}
83
84	function buildPayload(elem, actionDefault) {
85		const ds = elem.dataset || {};
86
87		// event name: data-ga-event overrides default
88		const eventName = (ds.gaEvent && String(ds.gaEvent).trim()) ? String(ds.gaEvent).trim() : actionDefault;
89
90		const category = ds.gaCat   ? String(ds.gaCat) : undefined;
91		const label    = ds.gaLabel ? String(ds.gaLabel) : undefined;
92		const name     = ds.gaName  ? String(ds.gaName) : undefined;
93
94		// support both data-ga-val and data-ga-value (legacy mismatch)
95		const value = parseIntSafe(ds.gaVal ?? ds.gaValue);
96
97		// GA4 params (category/label are not “native” GA4 fields, but common custom params)
98		const ga4Params = {
99			event_category: category,
100			event_label: label,
101			event_name: name,
102			value: value,
103		};
104
105		// Strip undefined values (clean payloads help GTM mappings too)
106		Object.keys(ga4Params).forEach((k) => ga4Params[k] === undefined && delete ga4Params[k]);
107
108		return { eventName, ga4Params };
109	}
110
111	function sendGA4(eventName, params, done) {
112		// Prefer both if available: gtag direct + GTM push (lets you wire in GTM-only tags too) {{{
113		let callbackCalled = false;
114		const finish = () => {
115			if (!callbackCalled) {
116				callbackCalled = true;
117				done && done();
118			}
119		};
120
121		// gtag() (GA4)
122		if (state.hasGA4) {
123			try {
124				const gaParams = {
125					...params, 
126					transport_type: "beacon",
127					event_callback: finish,
128					debug_mode: config.debug
129				}; 
130
131				window.gtag("event", eventName, gaParams); 
132
133			} catch (e) {
134				if (config.debug) console.warn("[track-events] gtag failed", e);
135			}
136		}
137
138		// GTM dataLayer push (GA4 tag or other tags can listen)
139		if (state.hasGTM) {
140			try {
141				window.dataLayer.push({
142					event: eventName,
143					...params,
144				});
145				if (config.debug) console.log("[track-events] DATA Layer Params", params);
146				if (config.debug) console.log("[track-events] DATA Layer State", window.dataLayer);
147			} catch (e) {
148				if (config.debug) console.warn("[track-events] dataLayer push failed", e);
149			}
150		}
151
152		// If we didn’t send via gtag (no callback), finish quickly.
153		if (!state.hasGA4) {
154			finish();
155		}
156		// }}}
157	}
158
159	function sendFB(elem, actionDefault) {
160		// {{{
161		if (!state.hasFB) return;
162
163		const ds = elem.dataset || {};
164		const fbEventName =
165			(ds.fbEvent && String(ds.fbEvent).trim())
166			? String(ds.fbEvent).trim()
167			: `${ds.gaCat || "event"}_${ds.gaLabel || "label"}_${actionDefault}`;
168
169		const val = parseIntSafe(ds.gaVal ?? ds.gaValue);
170
171		try {
172			// Pixel expects params object; keep it minimal
173			const params = {};
174			if (val !== undefined) params.value = val;
175			if (config.debug) { console.log("[track-events] fbq eventName", fbEventName)}
176			//window.fbq("track", fbEventName, params);
177			window.fbq("trackCustom", fbEventName, params);
178			if (config.debug) { console.log("[track-events] fbq params", params)}
179		} catch (e) {
180			if (config.debug) console.warn("[track-events] fbq failed", e);
181		}
182		// }}}
183	}
184
185	function isNavigatingClick(elem) {
186		// If it’s a normal link that will navigate, delay to help tracking deliver.
187		if (!(elem instanceof Element)) return false;
188		const a = elem.closest("a[href]");
189		if (!a) return false;
190
191		const href = a.getAttribute("href") || "";
192		const target = a.getAttribute("target");
193
194		if (!href) return false;
195		if (target && target.toLowerCase() === "_blank") return false;
196		if (href.startsWith("#")) return false;
197		if (href.startsWith("javascript:")) return false;
198
199		// mailto/tel often open external handlers; still can delay a tiny bit safely
200		return true;
201	}
202
203	// ---------
204	// FORM SUBMIT TRACKING
205	// ---------
206	document.addEventListener(
207		"submit",
208		(ev) => {
209			// call init to be safe for resources that too longer to load
210			init();
211
212			const form = ev.target;
213			if (!(form instanceof HTMLFormElement)) return;
214			if (form.classList.contains("form-has-errors")) return;
215			if (form.dataset.validator !== undefined && ! form.classList.contains("was-validated")) return;
216
217			// Track forms that have class -js-ga-track
218			if (!form.classList.contains("-js-ga-track")) return;
219
220			const elem = form; // tracking element is the form itself for submit
221
222			const { eventName, ga4Params } = buildPayload(elem, "submit");
223
224			// If any tracking system exists, delay submit (max 700ms)
225			const shouldDelay = state.hasGA4 || state.hasGTM || state.hasFB;
226			if (!shouldDelay) return;
227
228			let submitForm = null;
229
230			if (form.classList.contains("-js-track-no-delay")) {
231				// 
232			} else {
233				ev.preventDefault();
234				//ev.stopPropagation();
235
236				let submitted = false;
237				submitForm = () => {
238					if (!submitted) {
239						submitted = true;
240						form.submit();
241					}
242				};
243
244				// hard timeout
245				setTimeout(submitForm, config.submitDelay);
246			}
247
248			if (config.debug) console.log("[track-events] submit", eventName, ga4Params);
249
250			sendFB(elem, "submit");
251			sendGA4(eventName, ga4Params, submitForm);
252		},
253		true
254	);
255
256	// ---------
257	// CLICK TRACKING
258	// ---------
259	document.addEventListener(
260		"click",
261		(ev) => {
262			// call init to be safe for resources that too longer to load
263			init();
264
265			const elem = getTrackingElement(ev.target);
266			if (!elem) return;
267
268			const { eventName, ga4Params } = buildPayload(elem, "click");
269
270			if (config.debug) console.log("[track-events] click", eventName, ga4Params);
271
272			// If this click will navigate, briefly delay navigation
273			const willNavigate = isNavigatingClick(elem);
274			if (willNavigate && (state.hasGA4 || state.hasGTM || state.hasFB)) {
275				ev.preventDefault();
276				ev.stopPropagation();
277
278				const link = elem.closest("a[href]");
279				const href = link ? link.getAttribute("href") : null;
280
281				let navigated = false;
282
283				const go = () => {
284					if (config.preventNavigationOnClicks) return;
285					if (navigated) return;
286					navigated = true;
287					if (href) window.location.href = href;
288				};
289
290				// max wait
291				if (config.debug) console.log(`[track-events] waiting ${config.navigationDelay}ms for Navigation`);
292				setTimeout(go, config.navigationDelay);
293
294				sendFB(elem, "click");
295				sendGA4(eventName, ga4Params, go);
296				return;
297			}
298
299			// Non-navigating clicks: just fire
300			sendFB(elem, "click");
301			sendGA4(eventName, ga4Params);
302		},
303		true
304	);
305
306	// ---------
307	// PHONE / SKYPE / FACETIME AUTO TRACKING
308	// ---------
309	function trackSimpleClick(selector, eventName, params, fbEventName) {
310		document.addEventListener(
311			"click",
312			(ev) => {
313				init();
314				const el = ev.target instanceof Element ? ev.target.closest(selector) : null;
315				if (!el) return;
316
317				if (config.debug) console.log("[track-events] simple", eventName, params);
318
319				if (state.hasFB && fbEventName) {
320					try {
321						window.fbq("track", fbEventName);
322					} catch (e) {
323						if (config.debug) console.warn("[track-events] fbq failed", e);
324					}
325				}
326
327				sendGA4(eventName, params || {});
328			},
329			true
330		);
331	}
332
333	trackSimpleClick(".-sys-tel-standard", "contact_click", { method: "telephone" }, "contact_click_telephone");
334	trackSimpleClick(".-sys-tel-skype",    "contact_click", { method: "skype" },     "contact_click_skype");
335	trackSimpleClick(".-sys-tel-facetime", "contact_click", { method: "facetime" },  "contact_click_facetime");
336
337	// ---------
338	// ADD TO CART
339	// ---------
340	trackSimpleClick(".add-to-cart-button, .-st-add-to-cart", "add_to_cart", {}, "AddToCart");
341})();
342

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.