const STORAGE_KEY = 'winston_attribution_v1'; const APPROVED_SOURCES = new Set(['instagram', 'facebook']); const APPROVED_CONTENT = new Set(['bio', 'story', 'feed', 'reel', 'carousel', 'comment']); const APPROVED_MEDIUM = 'organic_social'; const MAX_CAMPAIGN_LENGTH = 120; function clean(value, max = 120) { return String(value ?? '').trim().slice(0, max); } function normalizeAttribution(value) { if (!value || typeof value !== 'object' || Array.isArray(value)) return null; const source = clean(value.utm_source, 32).toLowerCase(); const medium = clean(value.utm_medium, 40).toLowerCase(); const campaign = clean(value.utm_campaign, MAX_CAMPAIGN_LENGTH); const content = clean(value.utm_content, 32).toLowerCase(); if (!APPROVED_SOURCES.has(source)) return null; if (medium !== APPROVED_MEDIUM) return null; if (!campaign || /[\u0000-\u001f\u007f]/.test(campaign)) return null; if (!APPROVED_CONTENT.has(content)) return null; return { utm_source: source, utm_medium: medium, utm_campaign: campaign, utm_content: content, }; } export function parseApprovedAttribution(search = '') { const params = new URLSearchParams(String(search || '').replace(/^\?/, '')); return normalizeAttribution({ utm_source: params.get('utm_source'), utm_medium: params.get('utm_medium'), utm_campaign: params.get('utm_campaign'), utm_content: params.get('utm_content'), }); } export function readStoredAttribution(storage) { if (!storage || typeof storage.getItem !== 'function') return null; try { return normalizeAttribution(JSON.parse(storage.getItem(STORAGE_KEY) || 'null')); } catch { return null; } } export function captureAttribution(search, storage, analyticsAllowed) { if (analyticsAllowed !== true || !storage || typeof storage.setItem !== 'function') return null; const existing = readStoredAttribution(storage); if (existing) return existing; const incoming = parseApprovedAttribution(search); if (!incoming) return null; try { storage.setItem(STORAGE_KEY, JSON.stringify(incoming)); } catch { return incoming; } return incoming; } export function classifyOutbound(href, currentOrigin) { try { const base = new URL(String(currentOrigin || 'https://santuariowinston.org')); const url = new URL(String(href || ''), base); if (!/^https?:$/.test(url.protocol) || url.origin === base.origin) return ''; const host = url.hostname.toLowerCase().replace(/^www\./, ''); if (host === 'teaming.net' || host.endsWith('.teaming.net')) return 'teaming'; if (host === 'wa.me' || host === 'whatsapp.com' || host.endsWith('.whatsapp.com')) return 'whatsapp'; if (host === 'amazon.es' || host.endsWith('.amazon.es') || host === 'amazon.com' || host.endsWith('.amazon.com') || host === 'amzn.eu') return 'amazon'; if (host === 'paypal.com' || host.endsWith('.paypal.com')) return 'paypal'; return 'other'; } catch { return ''; } } function browserStorage() { try { return window.sessionStorage; } catch { return null; } } function analyticsAllowed() { try { return window.WinstonConsent?.get?.().analytics === true; } catch { return false; } } function measurementId() { const marker = document.querySelector('[data-winston-ga4-id]'); const value = clean(marker?.getAttribute('data-winston-ga4-id'), 32).toUpperCase(); return /^G-[A-Z0-9]+$/.test(value) ? value : ''; } let pendingAttribution = null; let gaConfigured = false; let pageViewSent = false; const startedForms = new WeakSet(); function currentAttribution() { if (!analyticsAllowed()) return null; const storage = browserStorage(); const stored = readStoredAttribution(storage); if (stored) return stored; const incoming = pendingAttribution || parseApprovedAttribution(window.location.search); if (!incoming || !storage) return incoming; try { storage.setItem(STORAGE_KEY, JSON.stringify(incoming)); } catch { // A browser may block sessionStorage; attribution then remains memory-only. } return incoming; } function ensureGa4() { if (!analyticsAllowed()) return false; const id = measurementId(); if (!id) return false; window.dataLayer = window.dataLayer || []; window.gtag = window.gtag || function gtag() { window.dataLayer.push(arguments); }; if (!gaConfigured) { window.gtag('js', new Date()); window.gtag('config', id, { send_page_view: false }); gaConfigured = true; } return true; } export function trackEvent(name, params = {}) { if (!ensureGa4()) return false; const attribution = currentAttribution(); window.gtag('event', name, { ...params, ...(attribution || {}), }); return true; } function sendPageView() { if (pageViewSent || !ensureGa4()) return; pageViewSent = true; trackEvent('page_view', { page_location: window.location.href, page_path: `${window.location.pathname}${window.location.search}`, page_title: document.title, }); } function formName(form) { return clean(form.id || form.getAttribute('name') || form.className || 'form', 120); } function startForm(form) { if (!(form instanceof HTMLFormElement) || startedForms.has(form)) return; startedForms.add(form); trackEvent('form_start', { form_name: formName(form) }); } function onInteraction(event) { const target = event.target instanceof Element ? event.target : null; const form = target?.closest('form'); if (form instanceof HTMLFormElement) startForm(form); } function onSubmit(event) { const form = event.target instanceof HTMLFormElement ? event.target : null; if (!form) return; startForm(form); trackEvent('form_submit', { form_name: formName(form) }); } function onClick(event) { const target = event.target instanceof Element ? event.target : null; const anchor = target?.closest('a[href]'); if (!(anchor instanceof HTMLAnchorElement)) return; const provider = classifyOutbound(anchor.href, window.location.origin); if (!provider) return; let domain = ''; try { domain = new URL(anchor.href).hostname; } catch { domain = ''; } trackEvent('outbound_click', { link_url: anchor.href, link_domain: domain, link_text: clean(anchor.textContent, 160), outbound_provider: provider, }); } function handleConsent() { if (!analyticsAllowed()) { const storage = browserStorage(); try { storage?.removeItem(STORAGE_KEY); } catch { // Consent remains authoritative even when storage access fails. } return; } currentAttribution(); sendPageView(); } function initBrowserTracking() { pendingAttribution = parseApprovedAttribution(window.location.search); document.addEventListener('winston:consent-changed', handleConsent); document.addEventListener('focusin', onInteraction, true); document.addEventListener('input', onInteraction, true); document.addEventListener('submit', onSubmit, true); document.addEventListener('click', onClick, true); window.WinstonTracking = Object.freeze({ getAttribution: currentAttribution, trackEvent, }); handleConsent(); } if (typeof window !== 'undefined' && typeof document !== 'undefined') { if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', initBrowserTracking, { once: true }); else initBrowserTracking(); }