PageSourceSearch

https://ai-mealplan.com/blog/assets/scripts/analytics.js?v=4b3fbb802fab

js ai-mealplan.com collected 2026-09-28 06:58:43 UTC 8,915 bytes, 256 lines download raw bytes

1(function () {
2	'use strict';
3
4	var loader = document.currentScript;
5	var endpoint = loader && loader.dataset ? loader.dataset.endpoint : '';
6	var site = loader && loader.dataset ? loader.dataset.site || '' : '';
7	if (!endpoint || typeof window.fetch !== 'function') return;
8
9	var queue = [];
10	var flushTimer = null;
11	var maxEvents = 20;
12	var sessionTimeout = 30 * 60 * 1000;
13	var maxEngagement = 30 * 60 * 1000;
14	var sessionStorageKey = 'blogmaker_analytics_session_v1';
15	var fallbackSession = null;
16	var visibleSince = null;
17	var blockedProperties = {
18		blog_id: true,
19		blog_url: true,
20		client_id: true,
21		clientid: true,
22		domain: true,
23		email: true,
24		hostname: true,
25		namespace: true,
26		referrer: true,
27		site: true,
28		url: true,
29		user_id: true,
30		userid: true,
31		website_hash: true,
32		website_id: true
33	};
34
35	function uuid() {
36		if (window.crypto && typeof window.crypto.randomUUID === 'function') {
37			return window.crypto.randomUUID();
38		}
39
40		var bytes = new Uint8Array(16);
41		if (window.crypto && typeof window.crypto.getRandomValues === 'function') {
42			window.crypto.getRandomValues(bytes);
43		} else {
44			for (var index = 0; index < bytes.length; index++) bytes[index] = Math.floor(Math.random() * 256);
45		}
46		bytes[6] = (bytes[6] & 15) | 64;
47		bytes[8] = (bytes[8] & 63) | 128;
48		var hex = Array.prototype.map.call(bytes, function (value) {
49			return value.toString(16).padStart(2, '0');
50		}).join('');
51		return hex.slice(0, 8) + '-' + hex.slice(8, 12) + '-' + hex.slice(12, 16) + '-' +
52			hex.slice(16, 20) + '-' + hex.slice(20);
53	}
54
55	function properties(value) {
56		var normalized = {};
57		var accepted = 0;
58		Object.keys(value || {}).some(function (key) {
59			var property = value[key];
60			if (blockedProperties[key.toLowerCase()]) return false;
61			if (['string', 'number', 'boolean'].indexOf(typeof property) === -1) return false;
62			if (typeof property === 'number' && !Number.isFinite(property)) return false;
63			normalized[key] = property;
64			accepted++;
65			return accepted >= 20;
66		});
67		return normalized;
68	}
69
70	function sessionId(now) {
71		var stored = null;
72		try {
73			var value = window.sessionStorage.getItem(sessionStorageKey);
74			stored = value ? JSON.parse(value) : fallbackSession;
75		} catch (error) {
76			stored = fallbackSession;
77		}
78		var reusable = stored && typeof stored.id === 'string' &&
79			/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(stored.id) &&
80			typeof stored.lastActivityAt === 'number' && Number.isFinite(stored.lastActivityAt) &&
81			now >= stored.lastActivityAt && now - stored.lastActivityAt <= sessionTimeout;
82		var current = { id: reusable ? stored.id : uuid(), lastActivityAt: now };
83		fallbackSession = current;
84		try {
85			window.sessionStorage.setItem(sessionStorageKey, JSON.stringify(current));
86		} catch (error) {
87			// The in-memory fallback still scopes the token to this page. The server stores only a
88			// site/day HMAC of this short-lived token.
89		}
90		return current.id;
91	}
92
93	function event(eventName, eventProperties, engagementMs) {
94		var now = Date.now();
95		var search = new URLSearchParams(window.location.search);
96		return {
97			engagementMs: Math.max(0, Math.min(Math.floor(engagementMs || 0), maxEngagement)),
98			eventId: uuid(),
99			eventName: eventName,
100			occurredAt: new Date(now).toISOString(),
101			path: window.location.pathname,
102			referrer: document.referrer,
103			sessionId: sessionId(now),
104			utmSource: search.get('utm_source') || '',
105			utmMedium: search.get('utm_medium') || '',
106			utmCampaign: search.get('utm_campaign') || '',
107			utmContent: search.get('utm_content') || '',
108			utmTerm: search.get('utm_term') || '',
109			properties: properties(eventProperties)
110		};
111	}
112
113	function flush(keepalive) {
114		if (flushTimer !== null) {
115			window.clearTimeout(flushTimer);
116			flushTimer = null;
117		}
118		var events = queue.splice(0, maxEvents);
119		if (!events.length) return;
120
121		try {
122			var payload = { events: events };
123			if (site) payload.site = site;
124			window.fetch(endpoint, {
125				method: 'POST',
126				headers: { 'Content-Type': 'application/json' },
127				body: JSON.stringify(payload),
128				credentials: 'omit',
129				keepalive: !!keepalive
130			}).catch(function () {});
131		} catch (error) {
132			// Analytics is best-effort and must never interrupt the reader experience.
133		}
134	}
135
136	function enqueue(eventName, eventProperties, engagementMs) {
137		queue.push(event(eventName, eventProperties || {}, engagementMs));
138		if (queue.length > maxEvents) queue = queue.slice(-maxEvents);
139		if (flushTimer === null) flushTimer = window.setTimeout(function () { flush(false); }, 250);
140	}
141
142	function trackCustomerInteractions() {
143		// This shared client also runs on Blogmaker's own product surfaces. Interaction reporting is
144		// intentionally limited to customer blogs, whose script tag has no fixed `data-site` value.
145		if (site) return;
146
147		var playedVideos = [];
148		var downloadPattern = /\.(?:7z|csv|docx?|epub|gz|mp3|mp4|pdf|pptx?|rar|txt|xlsx?|zip)(?:$|[?#])/i;
149
150		document.addEventListener('click', function (clickEvent) {
151			var target = clickEvent.target;
152			if (!target || typeof target.closest !== 'function') return;
153			var link = target.closest('a');
154			if (!link) return;
155
156			var eventName = '';
157			if (link.matches('.single-image-gallery')) {
158				eventName = 'gallery_image_click';
159			} else if (link.closest('.pagination')) {
160				eventName = 'pagination_click';
161			} else if (link.closest('.post-tags, .tags-top')) {
162				eventName = 'tag_click';
163			} else if (link.matches('.cta-button, .content-button')) {
164				eventName = 'cta_click';
165			} else {
166				var href = link.getAttribute('href') || '';
167				if (link.hasAttribute('download') || downloadPattern.test(href)) {
168					eventName = 'download_click';
169				} else {
170					try {
171						var destination = new URL(href, window.location.href);
172						if (/^https?:$/.test(destination.protocol) && destination.hostname !== window.location.hostname) {
173							eventName = 'outbound_link_click';
174						}
175					} catch (error) {}
176				}
177			}
178
179			if (eventName) enqueue(eventName, {});
180		});
181
182		// Native media `play` does not bubble, so listen during capture. Repeated pause/play actions
183		// on the same element count as one play for this page view.
184		document.addEventListener('play', function (playEvent) {
185			var video = playEvent.target;
186			if (!video || video.tagName !== 'VIDEO' || playedVideos.indexOf(video) !== -1) return;
187			playedVideos.push(video);
188			enqueue('video_play', {});
189		}, true);
190
191		// Search terms are deliberately excluded; only a valid search submission is counted.
192		document.addEventListener('keypress', function (keyEvent) {
193			var input = keyEvent.target;
194			if (keyEvent.key !== 'Enter' || !input || input.id !== 'search-js') return;
195			if (String(input.value || '').trim().length >= 2) enqueue('search', {});
196		});
197
198		// The newsletter UI submits through the shared fetch endpoint. Observe only its success code
199		// from a cloned response; the email itself is never read or copied into analytics.
200		var originalFetch = window.fetch;
201		window.fetch = function (resource, options) {
202			var responsePromise = originalFetch.apply(this, arguments);
203			try {
204				var body = options && options.body;
205				var resourceUrl = typeof resource === 'string' ? resource : (resource && resource.url) || '';
206				var isNewsletterRequest = /\/internal\/function(?:[?#]|$)/.test(resourceUrl) &&
207					body && typeof body.has === 'function' && body.has('token') && body.has('email');
208				if (isNewsletterRequest) {
209					responsePromise.then(function (response) {
210						if (!response || typeof response.clone !== 'function') return;
211						response.clone().json().then(function (data) {
212							if (data && Number(data.code) === 3) enqueue('newsletter_subscription', {});
213						}).catch(function () {});
214					}).catch(function () {});
215				}
216			} catch (error) {}
217			return responsePromise;
218		};
219	}
220
221	function takeVisibleEngagement() {
222		if (visibleSince === null) return 0;
223		var now = Date.now();
224		var engagementMs = Math.max(0, Math.min(now - visibleSince, maxEngagement));
225		visibleSince = now;
226		return Math.floor(engagementMs);
227	}
228
229	function heartbeat() {
230		if (document.visibilityState === 'visible') enqueue('analytics_heartbeat', {}, takeVisibleEngagement());
231	}
232
233	function finishVisiblePeriod() {
234		var engagementMs = takeVisibleEngagement();
235		visibleSince = null;
236		if (engagementMs > 0) enqueue('analytics_engagement', {}, engagementMs);
237	}
238
239	trackCustomerInteractions();
240	enqueue('page_view', { page_title: document.title });
241	if (document.visibilityState === 'visible') visibleSince = Date.now();
242	document.addEventListener('visibilitychange', function () {
243		if (document.visibilityState === 'hidden') {
244			finishVisiblePeriod();
245			flush(true);
246		} else {
247			visibleSince = Date.now();
248			heartbeat();
249		}
250	});
251	window.addEventListener('pagehide', function () {
252		finishVisiblePeriod();
253		flush(true);
254	});
255	window.setInterval(heartbeat, 30000);
256})();

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.