(async function () { /* ========================================================= * 1. SETTINGS & LIFESTYLE STATE * ========================================================= */ const RYTICS_SETTINGS = { swUrl: "/rytics_service_worker.js", swScope: "/", sessionTtlMinutes: 30, swMessageTimeoutMs: 2000, swReadyTimeoutMs: 15000, swFlushRetryMs: 500, swDirectFallbackAfterMs: 45000, defaultRoute: "/collect_all", configCdnPath: "/cdn/rytics_config.json", // When config loads, a non-empty allowedEvents list is enforced. Empty list = all events. captureAllEvents: false }; if (window.RYTICS_CONFIG && typeof window.RYTICS_CONFIG === 'object') { Object.assign(RYTICS_SETTINGS, window.RYTICS_CONFIG); } function normalizeHostname(value) { if (!value) return ""; return String(value) .toLowerCase() .replace(/^https?:\/\//, "") .split("/")[0] .split(":")[0]; } function getRyticsScriptOrigin() { if (window.RYTICS_CONFIG?.cdnOrigin) { return String(window.RYTICS_CONFIG.cdnOrigin).replace(/\/$/, ""); } const scripts = document.getElementsByTagName("script"); for (let i = scripts.length - 1; i >= 0; i--) { const src = scripts[i].src; if (src && /\/rytics\.js(\?|$)/.test(src)) { return new URL(src).origin; } } return ""; } // ============================================================================ // GENERATED BY TERRAFORM — configured first-party domains (multi-domain routing) // ============================================================================ const RYTICS_CONFIGURED_DOMAINS = [{"collectOrigin":"https://rytics.slechtziend.nl","hosts":["rytics.slechtziend.nl","slechtziend.nl","www.slechtziend.nl"]},{"collectOrigin":"https://rytics.lowvisiontotaal.nl","hosts":["rytics.lowvisiontotaal.nl","lowvisiontotaal.nl","www.lowvisiontotaal.nl"]},{"collectOrigin":"https://rytics.lowvisionshop.nl","hosts":["rytics.lowvisionshop.nl","lowvisionshop.nl","www.lowvisionshop.nl"]}]; const RYTICS_FIRST_PARTY_ORIGIN = "https://rytics.slechtziend.nl"; const RYTICS_FALLBACK_COLLECT_BASE = "https://ranalytics-cloud-run-578e2115-qaill2z3wq-ez.a.run.app"; const RYTICS_USE_RELATIVE_COLLECT = true; let __rytics_active_collect_origin = null; function hostnameMatches(candidate, hostname) { const h = normalizeHostname(hostname); const c = normalizeHostname(candidate); if (!h || !c) return false; return h === c || h.endsWith("." + c); } function resolveActiveCollectOrigin() { if (__rytics_active_collect_origin) return __rytics_active_collect_origin; const pageHost = normalizeHostname(location.hostname); const scriptHost = normalizeHostname(getRyticsScriptOrigin()); for (const entry of RYTICS_CONFIGURED_DOMAINS) { const origin = String(entry.collectOrigin || "").replace(/\/$/, ""); if (!origin) continue; for (const host of entry.hosts || []) { if (hostnameMatches(host, pageHost) || hostnameMatches(host, scriptHost)) { __rytics_active_collect_origin = origin; return origin; } } } __rytics_active_collect_origin = ( RYTICS_CONFIGURED_DOMAINS[0]?.collectOrigin?.replace(/\/$/, "") || RYTICS_FIRST_PARTY_ORIGIN || getRyticsScriptOrigin() || location.origin ).replace(/\/$/, ""); return __rytics_active_collect_origin; } function getRyticsCdnOrigin() { return resolveActiveCollectOrigin(); } function getConfigUrl() { if (RYTICS_SETTINGS.configUrl) return RYTICS_SETTINGS.configUrl; const path = RYTICS_SETTINGS.configCdnPath || "/cdn/rytics_config.json"; const normalized = path.startsWith("/") ? path : `/${path}`; return `${getRyticsCdnOrigin()}${normalized}`; } async function loadActiveConfig() { const cacheKey = "rytics_config"; try { const stored = sessionStorage.getItem(cacheKey); if (stored) { ACTIVE_CONFIG = JSON.parse(stored); return true; } const response = await fetch(getConfigUrl(), { credentials: "omit", cache: "no-store" }); if (!response.ok) throw new Error(`config_http_${response.status}`); ACTIVE_CONFIG = await response.json(); if (!ACTIVE_CONFIG || typeof ACTIVE_CONFIG !== "object") { throw new Error("config_invalid"); } sessionStorage.setItem(cacheKey, JSON.stringify(ACTIVE_CONFIG)); return true; } catch (e) { ACTIVE_CONFIG = {}; RYTICS_SETTINGS.captureAllEvents = true; return false; } } // UTM helpers (event-scoped) const RYTICS_UTM_KEYS = [ "utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content", ]; let ACTIVE_CONFIG = null; let __rytics_sw_reg_promise = null; let __rytics_sw_reg_failed = false; let __rytics_sw_flush_promise = null; let __rytics_sw_flush_timer = null; const __rytics_sw_pending = []; let __rytics_dl_ready = false; const __rytics_dl_buffer = []; function ensureSwRegistration() { if (!("serviceWorker" in navigator) || __rytics_sw_reg_failed) return null; if (!__rytics_sw_reg_promise) { __rytics_sw_reg_promise = navigator.serviceWorker .register(RYTICS_SETTINGS.swUrl, { scope: RYTICS_SETTINGS.swScope }) .catch((err) => { __rytics_sw_reg_failed = true; __rytics_sw_reg_promise = null; throw err; }); } return __rytics_sw_reg_promise; } function bindSwLifecycleHooks() { if (!("serviceWorker" in navigator) || window.__rytics_sw_hooks_bound) return; window.__rytics_sw_hooks_bound = true; navigator.serviceWorker.addEventListener("controllerchange", () => { scheduleSwFlush(); }); bindPageHideFlush(); } let __rytics_pagehide_flushed = false; function bindPageHideFlush() { if (window.__rytics_pagehide_bound) return; window.__rytics_pagehide_bound = true; document.addEventListener("visibilitychange", () => { if (document.visibilityState === "visible") { __rytics_pagehide_flushed = false; return; } flushSwPendingOnPageHide(); }); window.addEventListener( "pagehide", () => { flushSwPendingOnPageHide(); }, { capture: true } ); } function buildCollectUrl(base, route) { const b = String(base || "").replace(/\/$/, ""); const r = route.startsWith("/") ? route : `/${route}`; return `${b}${r}`; } function collectTargets(route) { const targets = []; const activeOrigin = resolveActiveCollectOrigin(); if (activeOrigin) targets.push(buildCollectUrl(activeOrigin, route)); if (RYTICS_FALLBACK_COLLECT_BASE && RYTICS_FALLBACK_COLLECT_BASE !== activeOrigin) { targets.push(buildCollectUrl(RYTICS_FALLBACK_COLLECT_BASE, route)); } if (RYTICS_USE_RELATIVE_COLLECT) targets.push(buildCollectUrl(location.origin, route)); else if (RYTICS_COLLECT_ENDPOINT) targets.push(buildCollectUrl(RYTICS_COLLECT_ENDPOINT, route)); return [...new Set(targets)]; } /* ========================================================= * 2. UTILITY & IDENTITY HELPERS * ========================================================= */ function xorShift(seed) { let x = seed; return function () { x ^= x << 13; x ^= x >> 17; x ^= x << 5; return x >>> 0; }; } function generateRandomIdXOR() { const seed = Math.floor(Math.random() * 9999); const rng = xorShift(seed); return parseInt(rng().toString().substring(0, 5) + Date.now(), 10); } function getCookie(name) { const match = document.cookie .split(";") .map((c) => c.trim()) .find((c) => c.startsWith(name + "=")); if (!match) return null; return decodeURIComponent(match.slice(name.length + 1)); } const USER_ID_COOKIE = "user_id"; const SESSION_ID_COOKIE = "session_id"; const LEGACY_USER_ID_COOKIE = "custom_user_id"; const LEGACY_SESSION_ID_COOKIE = "custom_session_id"; let __rytics_mem_user_id = null; let __rytics_mem_session_id = null; let __rytics_mem_session_expires_at = 0; function getCandidateCookieDomains(hostname = location.hostname) { const parts = hostname.split(".").filter(Boolean); const candidates = []; if (parts.length >= 2) { for (let i = 1; i < parts.length - 1; i++) candidates.push(`.${parts.slice(i).join(".")}`); candidates.push(`.${parts.slice(-2).join(".")}`); } return [...new Set(candidates)]; } function setCookie(name, value, opts) { if (typeof opts === "number") opts = { minutes: opts }; opts = opts || {}; let expires = ""; if (opts.days || opts.minutes) { const date = new Date(); if (opts.days) date.setTime(date.getTime() + opts.days * 864e5); if (opts.minutes) date.setTime(date.getTime() + opts.minutes * 60e3); expires = `; Expires=${date.toUTCString()}`; } const isHttps = location.protocol === "https:"; const secure = isHttps ? "; Secure" : ""; const baseAttrs = `${expires}; Path=/; SameSite=Lax${secure}`; try { for (const domain of getCandidateCookieDomains()) { document.cookie = `${name}=${encodeURIComponent(value)}; Domain=${domain}${baseAttrs}`; if (getCookie(name) != null) return { ok: true, source: "cookie", scope: `domain:${domain}` }; } } catch {} try { document.cookie = `${name}=${encodeURIComponent(value)}${baseAttrs}`; if (getCookie(name) != null) return { ok: true, source: "cookie", scope: "host-only" }; } catch {} return { ok: false, source: "none", scope: "none" }; } function coercePositiveInt(value) { if (value == null || value === "") return null; const n = typeof value === "number" ? value : parseInt(String(value), 10); return Number.isFinite(n) && n > 0 ? n : null; } function getOrMigrateCookie(name, legacyName) { let value = getCookie(name); if (value == null && legacyName) { const legacyValue = getCookie(legacyName); if (legacyValue != null) { value = legacyValue; const persist = name === USER_ID_COOKIE ? { days: 365 } : RYTICS_SETTINGS.sessionTtlMinutes; setCookie(name, value, persist); } } return value; } function readCookieId(name, legacyName) { return coercePositiveInt(getOrMigrateCookie(name, legacyName)); } function tryPersistCookie(name, value, opts) { return setCookie(name, String(value), opts).ok; } function getOrCreateUserId() { const fromCookie = readCookieId(USER_ID_COOKIE, LEGACY_USER_ID_COOKIE); if (fromCookie) { __rytics_mem_user_id = fromCookie; return fromCookie; } if (__rytics_mem_user_id) return __rytics_mem_user_id; const id = generateRandomIdXOR(); __rytics_mem_user_id = id; tryPersistCookie(USER_ID_COOKIE, id, { days: 365 }); return id; } function getOrCreateSessionId() { const sessionTtlMs = RYTICS_SETTINGS.sessionTtlMinutes * 60 * 1000; const now = Date.now(); const fromCookie = readCookieId(SESSION_ID_COOKIE, LEGACY_SESSION_ID_COOKIE); if (fromCookie) { __rytics_mem_session_id = fromCookie; __rytics_mem_session_expires_at = now + sessionTtlMs; tryPersistCookie(SESSION_ID_COOKIE, fromCookie, RYTICS_SETTINGS.sessionTtlMinutes); return fromCookie; } if (__rytics_mem_session_id && now < __rytics_mem_session_expires_at) { __rytics_mem_session_expires_at = now + sessionTtlMs; tryPersistCookie(SESSION_ID_COOKIE, __rytics_mem_session_id, RYTICS_SETTINGS.sessionTtlMinutes); return __rytics_mem_session_id; } const isNew = !__rytics_mem_session_id || now >= __rytics_mem_session_expires_at; const sid = generateRandomIdXOR(); __rytics_mem_session_id = sid; __rytics_mem_session_expires_at = now + sessionTtlMs; tryPersistCookie(SESSION_ID_COOKIE, sid, RYTICS_SETTINGS.sessionTtlMinutes); if (isNew) { try { const payload = applyMarketingRedaction(buildBasePayload("session_start")); trackEvent("session_start", payload, RYTICS_SETTINGS.defaultRoute); } catch (e) { // swallow errors to avoid breaking page flow } } return sid; } // Bootstrap IDs and SW registration synchronously before async init or dataLayer replay. if ("serviceWorker" in navigator) { ensureSwRegistration(); bindSwLifecycleHooks(); } getOrCreateUserId(); getOrCreateSessionId(); installDataLayerProxyEarly(); function sanitizeUtm(obj) { if (!obj || typeof obj !== "object") return null; let found = false; const out = {}; for (const k of RYTICS_UTM_KEYS) { const v = obj[k]; if (typeof v === "string" && v.trim()) { out[k] = v.trim(); found = true; } } return found ? out : null; } function getUtmFromUrl(urlStr) { try { const u = new URL(urlStr || location.href); const utm = {}; for (const k of RYTICS_UTM_KEYS) { const v = u.searchParams.get(k); if (v && v.trim()) utm[k] = v.trim(); } return sanitizeUtm(utm); } catch { return null; } } let RYTICS_UTM_CURRENT = getUtmFromUrl(location.href); (function patchHistoryForUtm() { try { const _ps = history.pushState; const _rs = history.replaceState; function refresh() { RYTICS_UTM_CURRENT = getUtmFromUrl(location.href); } history.pushState = function () { const r = _ps.apply(this, arguments); refresh(); return r; }; history.replaceState = function () { const r = _rs.apply(this, arguments); refresh(); return r; }; window.addEventListener("popstate", refresh); } catch {} })(); function searchParamInUrl(paramName) { try { return new URL(location.href).searchParams.get(paramName) || null; } catch { return null; } } function getGclidFromUrl() { try { const gclid = searchParamInUrl("gclid"); if (!gclid) return null; return `GCL.${Math.floor(Date.now() / 1000)}.${gclid}`; } catch { return null; } } function getFbclidFromUrl() { try { const fbclid = new URL(location.href).searchParams.get("fbclid"); return fbclid || null; } catch { return null; } } function getAllCookies() { return document.cookie.split('; ').reduce((acc, c) => { const idx = c.indexOf("="); const k = idx >= 0 ? c.slice(0, idx).trim() : c.trim(); const v = idx >= 0 ? c.slice(idx + 1) : ""; acc[k] = decodeURIComponent(v || ""); return acc; }, {}); } /* ========================================================= * 2b. CONSENT HUB (GDPR / Google Consent Mode v2 — June 2026) * * Tracks all standard consent signals. Never blocks SW init or event * transmission — when marketing consent is denied, marketing attribution * fields and cookies are sent empty/null only. * ========================================================= */ const MARKETING_CLICK_IDS = [ "gclid", "wbraid", "gbraid", "fbclid", "msclkid", "ttclid", "twclid", "dclid", "li_fat_id", "epik", "sccid", "scid", ]; const MARKETING_COOKIE_KEYS = new Set([ "_gcl_aw", "_gcl_au", "_gcl_dc", "_gcl_gb", "_gcl_gf", "_gcl_ha", "_wbraid", "_gbraid", "_fbc", "_fbp", "IDE", "test_cookie", ]); let RYTICS_CONSENT = createDefaultConsent(); let __rytics_last_consent_json = null; function createDefaultConsent() { return { necessary_consent: true, analytics_consent: false, functional_consent: false, personalization_consent: false, marketing_consent: false, ad_user_data_consent: false, ad_personalization_consent: false, consent_source: null, consent_updated_at: null, }; } function normalizeConsentInput(input) { if (!input || typeof input !== "object") return {}; const out = {}; const bool = (v) => typeof v === "boolean" ? v : (v === "granted" || v === "yes" || v === "true" || v === 1); if ("necessary_consent" in input || "essential_consent" in input || "security_storage" in input) { out.necessary_consent = bool(input.necessary_consent ?? input.essential_consent ?? input.security_storage ?? true); } if ("analytics_consent" in input || "a_consent" in input || "analytics_storage" in input || "statistics" in input) { out.analytics_consent = bool(input.analytics_consent ?? input.a_consent ?? input.analytics_storage ?? input.statistics); } if ("functional_consent" in input || "f_consent" in input || "functionality_storage" in input || "preferences" in input) { out.functional_consent = bool(input.functional_consent ?? input.f_consent ?? input.functionality_storage ?? input.preferences); } if ("personalization_consent" in input || "p_consent" in input || "personalization_storage" in input) { out.personalization_consent = bool(input.personalization_consent ?? input.p_consent ?? input.personalization_storage); } if ("marketing_consent" in input || "m_consent" in input || "ad_storage" in input || "advertising" in input) { out.marketing_consent = bool(input.marketing_consent ?? input.m_consent ?? input.ad_storage ?? input.advertising); } if ("ad_user_data_consent" in input || "ad_user_data" in input) { out.ad_user_data_consent = bool(input.ad_user_data_consent ?? input.ad_user_data); } if ("ad_personalization_consent" in input || "ad_personalization" in input) { out.ad_personalization_consent = bool(input.ad_personalization_consent ?? input.ad_personalization); } if (input.consent_source || input.source) out.consent_source = String(input.consent_source || input.source); return out; } function hasMarketingConsent() { return !!RYTICS_CONSENT.marketing_consent; } function setConsent(input) { const patch = normalizeConsentInput(input); if (!Object.keys(patch).length) return RYTICS_CONSENT; const next = { ...RYTICS_CONSENT, ...patch, consent_updated_at: new Date().toISOString() }; const json = JSON.stringify(next); if (json === __rytics_last_consent_json) return next; RYTICS_CONSENT = next; __rytics_last_consent_json = json; try { const payload = buildBasePayload("consent_update"); payload.event_data = { event: "consent_update", ...getConsentPayload() }; trackEvent("consent_update", payload, "/collect_all"); } catch {} return next; } function getConsentPayload() { const c = RYTICS_CONSENT; return { necessary_consent: c.necessary_consent, analytics_consent: c.analytics_consent, functional_consent: c.functional_consent, personalization_consent: c.personalization_consent, marketing_consent: c.marketing_consent, ad_user_data_consent: c.ad_user_data_consent, ad_personalization_consent: c.ad_personalization_consent, security_storage: c.necessary_consent, analytics_storage: c.analytics_consent, functionality_storage: c.functional_consent, personalization_storage: c.personalization_consent, ad_storage: c.marketing_consent, ad_user_data: c.ad_user_data_consent, ad_personalization: c.ad_personalization_consent, consent_source: c.consent_source, consent_updated_at: c.consent_updated_at, a_consent: c.analytics_consent, e_consent: c.necessary_consent, f_consent: c.functional_consent, m_consent: c.marketing_consent, p_consent: c.personalization_consent, }; } function getSwConsentFields() { const c = getConsentPayload(); return { consent: c, necessary_consent: c.necessary_consent, analytics_consent: c.analytics_consent, functional_consent: c.functional_consent, personalization_consent: c.personalization_consent, marketing_consent: c.marketing_consent, ad_user_data_consent: c.ad_user_data_consent, ad_personalization_consent: c.ad_personalization_consent, a_consent: c.a_consent, e_consent: c.e_consent, f_consent: c.f_consent, m_consent: c.m_consent, p_consent: c.p_consent, }; } function stripMarketingClickIds(utm) { // UTM campaign params (utm_source, utm_medium, etc.) are always kept. // Only ad-network click identifiers are cleared without marketing consent. if (!utm || hasMarketingConsent()) return utm; const out = { ...utm }; for (const k of MARKETING_CLICK_IDS) delete out[k]; return sanitizeUtm(out); } function redactMarketingCookies(cookies) { if (!cookies || hasMarketingConsent()) return cookies; const out = { ...cookies }; for (const k of Object.keys(out)) { if (MARKETING_COOKIE_KEYS.has(k) || k.startsWith("_gcl_")) delete out[k]; } return out; } function applyMarketingRedaction(payload) { if (!payload || hasMarketingConsent()) return payload; payload.gclid = null; payload.wbraid = null; payload.gbraid = null; payload.fbclid = null; if (payload.utm_current) payload.utm_current = stripMarketingClickIds(payload.utm_current); if (payload.cookies) payload.cookies = redactMarketingCookies(payload.cookies); return payload; } function parseGtmConsentCommand(event) { if (!event || typeof event !== "object") return null; const cmd = event[0] ?? event["0"]; const params = event[2] ?? event["2"]; if (cmd !== "consent" || !params || typeof params !== "object") return null; const granted = (v) => v === "granted" || v === true; return { analytics_consent: granted(params.analytics_storage), marketing_consent: granted(params.ad_storage), ad_user_data_consent: granted(params.ad_user_data), ad_personalization_consent: granted(params.ad_personalization), functional_consent: granted(params.functionality_storage), personalization_consent: granted(params.personalization_storage), necessary_consent: params.security_storage == null ? true : granted(params.security_storage), consent_source: "gtm_consent", }; } function parseUsercentricsConsent(event) { if (!event || event.event !== "consent_status") return null; const cat = event.ucCategory || {}; const marketing = cat.marketing === true; return { necessary_consent: cat.essential !== false, functional_consent: cat.functional === true, marketing_consent: marketing, analytics_consent: cat.functional === true || marketing || cat.essential === true, ad_user_data_consent: marketing, ad_personalization_consent: marketing, consent_source: "usercentrics", }; } function processConsentEvent(event) { if (!event || typeof event !== "object") return; const gtmConsent = parseGtmConsentCommand(event); if (gtmConsent) { setConsent(gtmConsent); return; } const ucConsent = parseUsercentricsConsent(event); if (ucConsent) { setConsent(ucConsent); return; } const cfg = ACTIVE_CONFIG?.consent || {}; const events = cfg.dataLayerEvents || [ "consent_update", "cookie_consent_update", "consent_status", ]; if (!events.includes(event.event)) return; setConsent({ ...event, consent_source: event.consent_source || event.source || "datalayer" }); } function readCookiebotConsent() { const c = window.Cookiebot?.consent; if (!c) return null; return { necessary_consent: !!c.necessary, analytics_consent: !!c.statistics, functional_consent: !!c.preferences, marketing_consent: !!c.marketing, ad_user_data_consent: !!c.marketing, ad_personalization_consent: !!c.marketing, consent_source: "cookiebot", }; } function readOneTrustConsent() { const groups = window.OnetrustActiveGroups || ""; const map = ACTIVE_CONFIG?.consent?.onetrust || { necessary: ["C0001"], analytics: ["C0002"], functional: ["C0003"], marketing: ["C0004"], }; const has = (ids) => (ids || []).some((id) => groups.includes(id)); return { necessary_consent: has(map.necessary), analytics_consent: has(map.analytics), functional_consent: has(map.functional), marketing_consent: has(map.marketing), ad_user_data_consent: has(map.marketing), ad_personalization_consent: has(map.marketing), consent_source: "onetrust", }; } function initConsentAdapters() { window.RYTICS = window.RYTICS || {}; window.RYTICS.setConsent = setConsent; window.RYTICS.getConsent = getConsentPayload; window.RYTICS.getActiveCollectOrigin = resolveActiveCollectOrigin; window.RYTICS.getConfiguredDomains = () => RYTICS_CONFIGURED_DOMAINS.slice(); window.RYTICS_CONSENT = RYTICS_CONSENT; const cfg = ACTIVE_CONFIG?.consent || {}; const adapters = cfg.adapters || ["datalayer", "manual", "cookiebot", "onetrust"]; if (adapters.includes("cookiebot")) { const apply = () => { const mapped = readCookiebotConsent(); if (mapped) setConsent(mapped); }; window.addEventListener("CookiebotOnConsentReady", apply); window.addEventListener("CookiebotOnAccept", apply); window.addEventListener("CookiebotOnDecline", apply); apply(); } if (adapters.includes("onetrust")) { const apply = () => { const mapped = readOneTrustConsent(); if (mapped) setConsent(mapped); }; window.addEventListener("OneTrustGroupsUpdated", apply); apply(); } if (adapters.includes("gtag") && typeof window.gtag === "function") { try { window.gtag("consent", "default", { analytics_storage: "denied", ad_storage: "denied", ad_user_data: "denied", ad_personalization: "denied", functionality_storage: "denied", personalization_storage: "denied", security_storage: "granted", }); } catch {} } } function getSwMessageBase() { return { url: location.href, referrer: document.referrer, visibility: document.visibilityState, utm: stripMarketingClickIds(RYTICS_UTM_CURRENT) || null, ...getSwConsentFields(), }; } /* ========================================================= * 3. SERVICE WORKER & TRANSPORT * ========================================================= */ async function initSw() { if (!("serviceWorker" in navigator)) return; try { bindSwLifecycleHooks(); const reg = await ensureSwRegistration(); if (!reg) return; await navigator.serviceWorker.ready; const worker = navigator.serviceWorker.controller || reg.active; if (worker) { worker.postMessage({ type: "RYTICS_INIT", page_instance_id: Date.now(), ...getSwMessageBase(), }); } scheduleSwFlush(); } catch (e) { __rytics_sw_reg_failed = true; __rytics_sw_reg_promise = null; await flushPendingViaDirect(); } } async function waitForSwWorker(maxWaitMs) { if (!("serviceWorker" in navigator) || __rytics_sw_reg_failed) return null; ensureSwRegistration(); const deadline = Date.now() + (maxWaitMs ?? RYTICS_SETTINGS.swReadyTimeoutMs); while (Date.now() < deadline) { if (navigator.serviceWorker.controller) { return navigator.serviceWorker.controller; } try { await Promise.race([ navigator.serviceWorker.ready, __rytics_sw_reg_promise, new Promise((resolve) => setTimeout(resolve, 100)), ]); } catch {} const reg = await __rytics_sw_reg_promise?.catch(() => null); const worker = navigator.serviceWorker.controller || reg?.active || null; if (worker) return worker; await new Promise((resolve) => setTimeout(resolve, 50)); } return navigator.serviceWorker.controller || null; } function swSendToWorker(worker, msg) { return new Promise((resolve, reject) => { const ch = new MessageChannel(); const t = setTimeout( () => reject(new Error("sw_timeout")), RYTICS_SETTINGS.swMessageTimeoutMs ); ch.port1.onmessage = (e) => { clearTimeout(t); const d = e.data || {}; if (d.ok) resolve(d); else reject(new Error(d.error || "sw_error")); }; worker.postMessage(msg, [ch.port2]); }); } async function swSend(msg) { const worker = await waitForSwWorker(RYTICS_SETTINGS.swMessageTimeoutMs); if (!worker) throw new Error("no_sw_worker"); return swSendToWorker(worker, msg); } function buildSwEnqueueMessage(name, payload) { return { type: "RYTICS_ENQUEUE", event: { type: name, payload }, ...getSwMessageBase(), }; } async function directFetchFallback(payload, path) { const route = path || RYTICS_SETTINGS.defaultRoute; for (const url of collectTargets(route)) { try { if (await postCollect(url, payload)) return true; } catch (err) { /* try next endpoint */ } } return false; } function directFetchKeepalive(payload, path) { const route = path || RYTICS_SETTINGS.defaultRoute; const body = JSON.stringify(payload); for (const url of collectTargets(route)) { try { if (typeof navigator.sendBeacon === "function") { const blob = new Blob([body], { type: "application/json" }); if (navigator.sendBeacon(url, blob)) return true; } fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body, keepalive: true, mode: "cors", credentials: "omit", }); return true; } catch (err) { /* try next endpoint */ } } return false; } function flushSwPendingOnPageHide() { if (__rytics_pagehide_flushed || !__rytics_sw_pending.length) return; __rytics_pagehide_flushed = true; if (__rytics_sw_flush_timer) { clearTimeout(__rytics_sw_flush_timer); __rytics_sw_flush_timer = null; } const batch = __rytics_sw_pending.splice(0); if (!batch.length) return; if ("serviceWorker" in navigator && !__rytics_sw_reg_failed) { const worker = navigator.serviceWorker.controller; if (worker) { for (const item of batch) { try { worker.postMessage(buildSwEnqueueMessage(item.name, item.payload)); } catch (err) { directFetchKeepalive(item.payload, item.path); } } return; } } for (const item of batch) { directFetchKeepalive(item.payload, item.path); } } async function flushPendingViaDirect() { while (__rytics_sw_pending.length) { const item = __rytics_sw_pending.shift(); await directFetchFallback(item.payload, item.path); } } async function flushStaleViaDirect() { const maxAge = RYTICS_SETTINGS.swDirectFallbackAfterMs; const now = Date.now(); const remaining = []; for (const item of __rytics_sw_pending) { if (now - item.enqueued_at >= maxAge) { await directFetchFallback(item.payload, item.path); } else { remaining.push(item); } } __rytics_sw_pending.length = 0; __rytics_sw_pending.push(...remaining); } function scheduleSwFlush() { if (__rytics_sw_flush_promise || !__rytics_sw_pending.length) return; __rytics_sw_flush_promise = flushSwPendingQueue().finally(() => { __rytics_sw_flush_promise = null; if (__rytics_sw_pending.length && !__rytics_sw_reg_failed) { if (__rytics_sw_flush_timer) clearTimeout(__rytics_sw_flush_timer); __rytics_sw_flush_timer = setTimeout(() => { __rytics_sw_flush_timer = null; scheduleSwFlush(); }, RYTICS_SETTINGS.swFlushRetryMs); } }); } async function flushSwPendingQueue() { if (!__rytics_sw_pending.length) return; if (!("serviceWorker" in navigator) || __rytics_sw_reg_failed) { await flushPendingViaDirect(); return; } const worker = await waitForSwWorker(RYTICS_SETTINGS.swReadyTimeoutMs); if (!worker) { await flushStaleViaDirect(); return; } while (__rytics_sw_pending.length) { const item = __rytics_sw_pending[0]; try { await swSendToWorker(worker, buildSwEnqueueMessage(item.name, item.payload)); __rytics_sw_pending.shift(); } catch (e) { await flushStaleViaDirect(); return; } } } /* ========================================================= * 4. PAYLOAD & EVENT TRACKING * ========================================================= */ function buildBasePayload(event_name) { const payload = { timestamp: new Date().toISOString(), event_name: event_name, user_id: getOrCreateUserId(), session_id: getOrCreateSessionId(), custom_event_id: generateRandomIdXOR(), user_agent: navigator.userAgent, referrer: document.referrer, domain: window.location.hostname, page_location: window.location.href, // attribution (marketing fields redacted when marketing_consent is false) client_id: getCookie('_ga'), gclid: getGclidFromUrl() || getCookie('_gcl_aw'), wbraid: searchParamInUrl('wbraid') || getCookie('_wbraid'), gbraid: searchParamInUrl('gbraid') || getCookie('_gbraid'), fbclid: getFbclidFromUrl() || getCookie('_fbc'), // UTMs (event-scoped) utm_current: stripMarketingClickIds(RYTICS_UTM_CURRENT) || null, // GDPR consent flags (always included; never blocks transmission) user_consent: getConsentPayload(), user_consent_level: RYTICS_CONSENT.analytics_consent ? (RYTICS_CONSENT.marketing_consent ? "marketing" : "analytics") : "none", // context cookies: redactMarketingCookies(getAllCookies()), visibility: document.visibilityState, localization: navigator.language || null, // browser/os parsing ...(function parseUA() { try { const ua = navigator.userAgent || ""; let browser_name = null; let browser_version = null; if (/\bEdg\//.test(ua)) { browser_name = "Edge"; browser_version = (ua.match(/Edg\/([\d\.]+)/) || [])[1] || null; } else if (/OPR\//.test(ua) || /Opera\//.test(ua)) { browser_name = "Opera"; browser_version = (ua.match(/OPR\/([\d\.]+)/) || ua.match(/Opera\/([\d\.]+)/) || [])[1] || null; } else if (/FBAN|FBAV/.test(ua)) { browser_name = "Facebook App"; browser_version = (ua.match(/FBAV\/([\d\.]+)/) || [])[1] || null; } else if (/Instagram/.test(ua)) { browser_name = "Instagram App"; browser_version = (ua.match(/Instagram\/([\d\.]+)/) || [])[1] || null; } else if (/TikTok/.test(ua)) { browser_name = "TikTok App"; browser_version = (ua.match(/app_version\/([\d\.]+)/) || [])[1] || null; } else if (/UCBrowser\//.test(ua)) { browser_name = "UC Browser"; browser_version = (ua.match(/UCBrowser\/([\d\.]+)/) || [])[1] || null; } else if (navigator.brave !== undefined) { browser_name = "Brave"; } else if (/Chrome\//.test(ua)) { browser_name = "Chrome"; browser_version = (ua.match(/Chrome\/([\d\.]+)/) || [])[1] || null; } else if (/Firefox\//.test(ua)) { browser_name = "Firefox"; browser_version = (ua.match(/Firefox\/([\d\.]+)/) || [])[1] || null; } else if (/Safari\//.test(ua) && /Version\//.test(ua)) { browser_name = "Safari"; browser_version = (ua.match(/Version\/([\d\.]+)/) || [])[1] || null; } let os_name = null; if (/Windows/.test(ua)) os_name = "Windows"; else if (/Android/.test(ua)) os_name = "Android"; else if (/iPhone|iPad|iPod/.test(ua)) os_name = "iOS"; else if (/Mac OS X/.test(ua)) os_name = "macOS"; else if (/Linux/.test(ua)) os_name = "Linux"; return { browser_name, browser_version, os_name }; } catch { return {}; } })() }; return applyMarketingRedaction(payload); } async function postCollect(url, payload) { const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), keepalive: true, mode: "cors", credentials: "omit", }); return res.ok; } function trackEvent(name, payload, path) { const route = path || RYTICS_SETTINGS.defaultRoute; if (!("serviceWorker" in navigator) || __rytics_sw_reg_failed) { void directFetchFallback(payload, route); return; } __rytics_sw_pending.push({ name, payload, path: route, enqueued_at: Date.now(), }); ensureSwRegistration(); bindSwLifecycleHooks(); scheduleSwFlush(); } /* ========================================================= * 5. DATA LAYER PROXY & INITIALIZATION * ========================================================= */ function ensureFppResponses() { window.fpp_responses = window.fpp_responses || []; } const GTM_INTERNAL_EVENTS = new Set([ "gtm.js", "gtm.dom", "gtm.load", "gtm.init", ]); function isEventAllowed(eventName) { if (!eventName) return false; if (RYTICS_SETTINGS.captureAllEvents) return true; const allowed = ACTIVE_CONFIG?.allowedEvents; if (!Array.isArray(allowed) || allowed.length === 0) return true; return allowed.includes(eventName); } function shouldSkipGtmInternalEvent(eventName) { if (RYTICS_SETTINGS.captureAllEvents) return false; const allowed = ACTIVE_CONFIG?.allowedEvents; if (Array.isArray(allowed) && allowed.length > 0) return false; return GTM_INTERNAL_EVENTS.has(eventName); } function queuePurchaseEvent(event) { const ignoreReferrers = ACTIVE_CONFIG?.ignorePurchaseReferrers || []; if (ignoreReferrers.some((ref) => document.referrer.includes(ref))) return; const ecommerce = event.ecommerce || {}; const transactionId = ecommerce.transaction_id || ""; if (ACTIVE_CONFIG?.dedupePurchases !== false && transactionId) { const storageKey = "rytics_processed_transactions"; let processedIds = []; try { processedIds = JSON.parse(localStorage.getItem(storageKey) || "[]"); } catch {} if (processedIds.includes(transactionId)) return; processedIds.push(transactionId); if (processedIds.length > 50) processedIds.shift(); localStorage.setItem(storageKey, JSON.stringify(processedIds)); } const payload = buildBasePayload("purchase"); Object.assign(payload, { transaction_id: transactionId, value: ecommerce.value, tax: ecommerce.tax || 0, shipping: ecommerce.shipping || 0, currency: ecommerce.currency, items: ecommerce.items, }); trackEvent("purchase", payload, "/collect"); } function queueEvent(event) { processConsentEvent(event); const consentEvents = ACTIVE_CONFIG?.consent?.dataLayerEvents || [ "consent_update", "cookie_consent_update", ]; if (consentEvents.includes(event.event)) return; if (!isEventAllowed(event.event) || shouldSkipGtmInternalEvent(event.event)) return; const payload = buildBasePayload(event.event); payload.event_data = event; trackEvent(event.event, payload, "/collect_all"); } function handleDataLayerEvent(event) { if (!event || typeof event !== "object") return; if (parseGtmConsentCommand(event)) { try { processConsentEvent(event); } catch (error) { ensureFppResponses(); window.fpp_responses.push({ error: error.message, stack: error.stack, event, }); } return; } if (!event.event) return; try { if (event.event === "purchase") queuePurchaseEvent(event); queueEvent(event); } catch (error) { ensureFppResponses(); window.fpp_responses.push({ error: error.message, stack: error.stack, event, }); } } function installDataLayerProxyEarly() { window.dataLayer = window.dataLayer || []; ensureFppResponses(); for (let i = 0; i < window.dataLayer.length; i++) { __rytics_dl_buffer.push(window.dataLayer[i]); } const originalPush = window.dataLayer.push.bind(window.dataLayer); window.dataLayer.push = function (...args) { const result = originalPush(...args); for (const event of args) { if (__rytics_dl_ready) handleDataLayerEvent(event); else __rytics_dl_buffer.push(event); } return result; }; } function activateDataLayerProxy() { if (__rytics_dl_ready) return; __rytics_dl_ready = true; const pending = __rytics_dl_buffer.splice(0); for (const event of pending) { handleDataLayerEvent(event); } } async function init() { // 0. Resolve first-party collect/CDN origin from page hostname vs configured domains resolveActiveCollectOrigin(); // 1. Load event allowlist from LB CDN (/cdn/rytics_config.json); fallback: capture all dataLayer events await loadActiveConfig(); // 2. Consent hub (flags only; never blocks SW or hits) initConsentAdapters(); // 3. Register SW initSw(); // 4. Drain buffered dataLayer events (config + consent adapters are ready) activateDataLayerProxy(); } init(); })();