HEX
Server: Apache
System: Linux p3plzcpnl506847.prod.phx3.secureserver.net 4.18.0-553.54.1.lve.el8.x86_64 #1 SMP Wed Jun 4 13:01:13 UTC 2025 x86_64
User: slfopp7cb1df (5698090)
PHP: 8.1.34
Disabled: NONE
Upload Files
File: /home/slfopp7cb1df/www/shaneconrad.me/wp-content/plugins/embedpress/assets/js/analytics-tracker.js
(function () {
    'use strict';

    // Cookie utility functions
    function setCookie(name, value, days, isSession = false) {
        let expires = '';
        if (!isSession && days) {
            const date = new Date();
            date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
            expires = '; expires=' + date.toUTCString();
        }
        document.cookie = name + '=' + (value || '') + expires + '; path=/; SameSite=Lax';
    }

    function getCookie(name) {
        const nameEQ = name + '=';
        const ca = document.cookie.split(';');
        for (let i = 0; i < ca.length; i++) {
            let c = ca[i];
            while (c.charAt(0) === ' ') c = c.substring(1, c.length);
            if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
        }
        return null;
    }

    // Generate or load user ID from cookie (persistent across sessions)
    function getOrCreateUserId() {
        const COOKIE_NAME = 'ep_user_id';

        // Try to get existing user ID from cookie
        let userId = getCookie(COOKIE_NAME);

        if (!userId) {
            // Generate new persistent user ID
            userId = 'ep-user-' + Date.now() + '-' + Math.random().toString(36).substring(2, 15);
            // Set cookie to expire in 30 days (1 month)
            setCookie(COOKIE_NAME, userId, 30);
        }

        return userId;
    }

    // Generate session ID for current browser session (for deduplication within session)
    function getOrCreateSessionId() {
        const COOKIE_NAME = 'ep_session_id';
        let id = getCookie(COOKIE_NAME);
        if (!id) {
            id = 'ep-sess-' + Date.now() + '-' + Math.random().toString(36).substring(2, 10);
            // Set as session cookie (expires when browser closes)
            setCookie(COOKIE_NAME, id, null, true);
        }
        return id;
    }


    // Configuration
    const config = {
        viewThreshold: 49,
        viewDuration: 3000,
        viewResetCooldown: 60000, // Optional: allow re-counting views after 60s
        impressionCooldown: 5000, // Throttle repeated impressions
        clickCooldown: 2000, // Throttle repeated clicks
        debug: false,
        restUrl: embedpress_analytics?.rest_url || '/wp-json/embedpress/v1/analytics/',
        userId: getOrCreateUserId(),
        sessionId: getOrCreateSessionId(),
        pageUrl: embedpress_analytics?.page_url || window.location.href,
        postId: embedpress_analytics?.post_id || 0,
        ipLocationData: null
    };

    const trackedElements = new Map();
    const sessionData = {
        viewedContent: new Set(),
        clickedContent: new Map(), // contentId -> lastClickTime
        impressedContent: new Map() // contentId -> lastImpressionTime
    };

    // Get browser fingerprint for deduplication
    function getBrowserFingerprint() {
        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');
        ctx.textBaseline = 'top';
        ctx.font = '14px Arial';
        ctx.fillText('Browser fingerprint', 2, 2);

        // Create a more comprehensive fingerprint
        const fingerprintData = {
            userAgent: navigator.userAgent,
            language: navigator.language,
            platform: navigator.platform,
            screen: `${screen.width}x${screen.height}`,
            timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
            canvas: canvas.toDataURL(),
            // Add more unique browser characteristics
            cookieEnabled: navigator.cookieEnabled,
            doNotTrack: navigator.doNotTrack,
            hardwareConcurrency: navigator.hardwareConcurrency || 0,
            maxTouchPoints: navigator.maxTouchPoints || 0,
            colorDepth: screen.colorDepth,
            pixelDepth: screen.pixelDepth,
            availWidth: screen.availWidth,
            availHeight: screen.availHeight,
            // Add WebGL fingerprint for better uniqueness
            webgl: getWebGLFingerprint()
        };

        // Use a simple hash function instead of truncating base64
        return simpleHash(JSON.stringify(fingerprintData));
    }

    // Simple hash function to create consistent fingerprints
    function simpleHash(str) {
        let hash = 0;
        if (str.length === 0) return hash.toString();
        for (let i = 0; i < str.length; i++) {
            const char = str.charCodeAt(i);
            hash = ((hash << 5) - hash) + char;
            hash = hash & hash; // Convert to 32-bit integer
        }
        // Convert to positive hex string and pad to ensure consistent length
        return Math.abs(hash).toString(16).padStart(8, '0');
    }

    // Get WebGL fingerprint for additional uniqueness
    function getWebGLFingerprint() {
        try {
            const canvas = document.createElement('canvas');
            const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
            if (!gl) return 'no-webgl';

            const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
            return {
                vendor: gl.getParameter(gl.VENDOR),
                renderer: gl.getParameter(gl.RENDERER),
                version: gl.getParameter(gl.VERSION),
                shadingLanguageVersion: gl.getParameter(gl.SHADING_LANGUAGE_VERSION),
                unmaskedVendor: debugInfo ? gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) : 'unknown',
                unmaskedRenderer: debugInfo ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) : 'unknown'
            };
        } catch (e) {
            return 'webgl-error';
        }
    }

    const observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
            const element = entry.target;
            const data = trackedElements.get(element);
            if (!data) return;

            let visiblePercentage = Math.floor(entry.intersectionRatio * 100);
            if (visiblePercentage < config.viewThreshold && entry.isIntersecting) {
                const rect = entry.boundingClientRect;
                const vh = window.innerHeight, vw = window.innerWidth;
                const visibleH = Math.min(rect.bottom, vh) - Math.max(rect.top, 0);
                const visibleW = Math.min(rect.right, vw) - Math.max(rect.left, 0);
                const viewportCoverage = (visibleH * visibleW) / (vh * vw) * 100;
                if (viewportCoverage >= 30) {
                    visiblePercentage = Math.max(visiblePercentage, config.viewThreshold);
                }
            }

            data.viewportPercentage = visiblePercentage;
            data.inViewport = entry.isIntersecting;

            if (entry.isIntersecting) {
                trackImpression(element, data);
            }

            handleViewTracking(element, data, visiblePercentage);
        });
    }, {
        threshold: Array.from({ length: 11 }, (_, i) => i / 10)
    });

    function trackImpression(element, data) {
        // Check if tracking is enabled
        if (!isTrackingEnabled()) return;

        const now = Date.now();
        const last = sessionData.impressedContent.get(data.contentId) || 0;

        if (now - last < config.impressionCooldown) return;
        sessionData.impressedContent.set(data.contentId, now);
        data.lastImpressionTime = now;


        sendTrackingData({
            content_id: data.contentId,
            interaction_type: 'impression',
            user_id: config.userId,
            session_id: config.sessionId,
            page_url: config.pageUrl,
            interaction_data: {
                embed_type: data.embedType,
                embed_url: data.embedUrl,
                viewport_percentage: data.viewportPercentage,
                location_data: config.ipLocationData,
                browser_fingerprint: getBrowserFingerprint()
            }
        });
    }

    function handleViewTracking(element, data, visiblePercentage) {
        if (!data.inViewport || visiblePercentage < config.viewThreshold) {
            if (data.viewTimer) {
                clearTimeout(data.viewTimer);
                data.viewTimer = null;
            }
            return;
        }

        const now = Date.now();

        // Optional: allow re-tracking views if cooldown passed
        if (data.viewTracked && now - (data.lastViewTime || 0) > config.viewResetCooldown) {
            data.viewTracked = false;
        }

        if (data.viewTracked || sessionData.viewedContent.has(data.contentId)) return;

        if (!data.viewTimer) {
            data.viewTimer = setTimeout(() => {
                trackView(element, data);
                data.viewTracked = true;
                data.lastViewTime = Date.now();
                sessionData.viewedContent.add(data.contentId);
                data.viewTimer = null;
            }, config.viewDuration);
        }
    }

    function trackView(element, data) {
        // Check if tracking is enabled
        if (!isTrackingEnabled()) return;

        sendTrackingData({
            content_id: data.contentId,
            interaction_type: 'view',
            user_id: config.userId,
            session_id: config.sessionId,
            page_url: config.pageUrl,
            view_duration: data.viewDuration || config.viewDuration || 0,
            interaction_data: {
                embed_type: data.embedType,
                embed_url: data.embedUrl,
                viewport_percentage: data.viewportPercentage,
                view_duration: data.viewDuration || config.viewDuration || 0,
                browser_fingerprint: getBrowserFingerprint()
            }
        });
    }

    function setupClickTracking() {
        trackedElements.forEach((data, element) => {
            // Only track actual user clicks
            element.addEventListener('click', (event) => {
                // Make sure this is a real user click
                if (!event.isTrusted) return;

                const now = Date.now();
                const last = sessionData.clickedContent.get(data.contentId) || 0;

                // Prevent duplicate clicks within cooldown period
                if (now - last < config.clickCooldown) return;

                // Check if tracking is enabled
                if (!isTrackingEnabled()) return;

                sessionData.clickedContent.set(data.contentId, now);

                sendTrackingData({
                    content_id: data.contentId,
                    interaction_type: 'click',
                    user_id: config.userId,
                    session_id: config.sessionId,
                    page_url: config.pageUrl,
                    interaction_data: {
                        embed_type: data.embedType,
                        embed_url: data.embedUrl,
                        browser_fingerprint: getBrowserFingerprint()
                    }
                });
            });
        });
    }

    function prepareElementForTracking(element) {
        if (trackedElements.has(element)) return;

        const embedType = element.getAttribute('data-embed-type');
        if (!embedType) return;

        // Get a stable content ID based on embed URL or existing attributes
        let contentId = element.getAttribute('data-embedpress-content') ||
            element.getAttribute('data-source-id') ||
            element.getAttribute('data-emid');

        if (!contentId) {
            // Generate stable ID based on embed URL or iframe src
            const embedUrl = element.getAttribute('data-embed-url') ||
                element.querySelector('iframe')?.src ||
                element.querySelector('embed')?.src ||
                element.querySelector('object')?.data ||
                window.location.href;

            // Create a hash-based ID that will be consistent for the same content
            contentId = 'ep-' + embedType + '-' + btoa(embedUrl).replace(/[^a-zA-Z0-9]/g, '').substring(0, 10);
        }

        element.setAttribute('data-embedpress-content', contentId);

        const data = {
            contentId,
            embedType,
            embedUrl: getEmbedUrl(element),
            inViewport: false,
            viewTimer: null,
            viewTracked: false,
            lastImpressionTime: 0,
            lastViewTime: 0,
            viewportPercentage: 0
        };

        trackedElements.set(element, data);
        observer.observe(element);
    }

    function getEmbedUrl(element) {
        const iframe = element.querySelector('iframe');
        const video = element.querySelector('video source');
        const audio = element.querySelector('audio source');
        const embed = element.querySelector('embed');
        const object = element.querySelector('object');

        return iframe?.src || video?.src || audio?.src || embed?.src || object?.data ||
            element.getAttribute('data-url') ||
            element.getAttribute('data-src') ||
            element.getAttribute('href') || '';
    }

    function findAndTrackEmbeds() {
        document.querySelectorAll('[data-embed-type]:not([data-embed-type] [data-embed-type])')
            .forEach(prepareElementForTracking);
    }


    function setupMutationObserver() {
        if (!('MutationObserver' in window)) return;
        const mo = new MutationObserver((mutations) => {
            mutations.forEach(m => {
                m.addedNodes.forEach(n => {
                    if (n.nodeType !== 1) return;
                    if (n.getAttribute?.('data-embed-type')) prepareElementForTracking(n);
                    n.querySelectorAll?.('[data-embed-type]').forEach(prepareElementForTracking);
                });
            });
        });
        mo.observe(document.body, { childList: true, subtree: true });
    }

    function sendTrackingData(data) {
        // Skip tracking if embed type is unknown or empty
        if (!data.interaction_data?.embed_type ||
            data.interaction_data.embed_type === 'unknown' ||
            data.interaction_data.embed_type === '') {
            return;
        }

        const trackingData = {
            ...data,
            // Only set these if not already provided in data
            user_id: data.user_id || config.userId,
            session_id: data.session_id || config.sessionId,
            page_url: data.page_url || config.pageUrl,
            post_id: data.post_id || config.postId,
            // Include original referrer if available
            original_referrer: embedpress_analytics?.original_referrer || ''
        };

        fetch(config.restUrl + 'track', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-WP-Nonce': embedpress_analytics?.nonce || ''
            },
            body: JSON.stringify(trackingData),
            credentials: 'same-origin'
        }).catch(err => {
            if (navigator.sendBeacon) {
                const blob = new Blob([JSON.stringify(trackingData)], { type: 'application/json' });
                navigator.sendBeacon(config.restUrl + 'track', blob);
            }
        });
    }

    async function getIPLocationData() {
        try {
            const res = await fetch('https://ipinfo.io/json');
            const data = await res.json();
            config.ipLocationData = {
                country: data.country,
                city: data.city,
                timezone: data.timezone,
                source: 'ip'
            };
        } catch {
            return null;
        }
    }

    function sendBrowserInfo() {
        const fingerprint = getBrowserFingerprint();
        const info = {
            user_id: config.userId,
            session_id: config.sessionId,
            browser_fingerprint: fingerprint,
            screen_resolution: window.screen.width + 'x' + window.screen.height,
            language: navigator.language,
            timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
            user_agent: navigator.userAgent
        };
        getIPLocationData().then(() => {
            if (config.ipLocationData) {
                info.country = config.ipLocationData.country;
                info.city = config.ipLocationData.city;
            }
            fetch(config.restUrl + 'browser-info', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'X-WP-Nonce': embedpress_analytics?.nonce || ''
                },
                body: JSON.stringify(info),
                credentials: 'same-origin'
            });
        });
    }

    function isTrackingEnabled() {
        // Check the updated global variable first (updated by React component)
        if (window.embedpressAnalyticsData?.trackingEnabled !== undefined) {
            return Boolean(window.embedpressAnalyticsData.trackingEnabled);
        }
        // Fallback to original localized value
        return Boolean(embedpress_analytics?.tracking_enabled);
    }

    function init() {
        // Check if tracking is enabled before initializing
        if (!isTrackingEnabled()) {
            return;
        }


        // Check if page has embedded content (if provided by server)
        if (embedpress_analytics?.has_embedded_content === false) {
            return;
        }

        findAndTrackEmbeds();
        setupClickTracking();
        setupMutationObserver();
        sendBrowserInfo();
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();