1(function () { 2 'use strict'; 3 4 // Cookie utility functions 5 function setCookie(name, value, days, isSession = false) { 6 let expires = ''; 7 if (!isSession && days) { 8 const date = new Date(); 9 date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); 10 expires = '; expires=' + date.toUTCString(); 11 } 12 document.cookie = name + '=' + (value || '') + expires + '; path=/; SameSite=Lax'; 13 } 14 15 function getCookie(name) { 16 const nameEQ = name + '='; 17 const ca = document.cookie.split(';'); 18 for (let i = 0; i < ca.length; i++) { 19 let c = ca[i]; 20 while (c.charAt(0) === ' ') c = c.substring(1, c.length); 21 if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length); 22 } 23 return null; 24 } 25 26 // Generate or load user ID from cookie (persistent across sessions) 27 function getOrCreateUserId() { 28 const COOKIE_NAME = 'ep_user_id'; 29 30 // Try to get existing user ID from cookie 31 let userId = getCookie(COOKIE_NAME); 32 33 if (!userId) { 34 // Generate new persistent user ID 35 userId = 'ep-user-' + Date.now() + '-' + Math.random().toString(36).substring(2, 15); 36 // Set cookie to expire in 30 days (1 month) 37 setCookie(COOKIE_NAME, userId, 30); 38 } 39 40 return userId; 41 } 42 43 // Generate session ID for current browser session (for deduplication within session) 44 function getOrCreateSessionId() { 45 const COOKIE_NAME = 'ep_session_id'; 46 let id = getCookie(COOKIE_NAME); 47 if (!id) { 48 id = 'ep-sess-' + Date.now() + '-' + Math.random().toString(36).substring(2, 10); 49 // Set as session cookie (expires when browser closes) 50 setCookie(COOKIE_NAME, id, null, true); 51 } 52 return id; 53 } 54 55 56 // Configuration 57 const config = { 58 viewThreshold: 49, 59 viewDuration: 3000, 60 viewResetCooldown: 60000, // Optional: allow re-counting views after 60s 61 impressionCooldown: 5000, // Throttle repeated impressions 62 clickCooldown: 2000, // Throttle repeated clicks 63 debug: false, 64 restUrl: embedpress_analytics?.rest_url || '/wp-json/embedpress/v1/analytics/', 65 userId: getOrCreateUserId(), 66 sessionId: getOrCreateSessionId(), 67 pageUrl: embedpress_analytics?.page_url || window.location.href, 68 postId: embedpress_analytics?.post_id || 0, 69 ipLocationData: null 70 }; 71 72 const trackedElements = new Map(); 73 const sessionData = { 74 viewedContent: new Set(), 75 clickedContent: new Map(), // contentId -> lastClickTime 76 impressedContent: new Map() // contentId -> lastImpressionTime 77 }; 78 79 // Get browser fingerprint for deduplication 80 function getBrowserFingerprint() { 81 const canvas = document.createElement('canvas'); 82 const ctx = canvas.getContext('2d'); 83 ctx.textBaseline = 'top'; 84 ctx.font = '14px Arial'; 85 ctx.fillText('Browser fingerprint', 2, 2); 86 87 // Create a more comprehensive fingerprint 88 const fingerprintData = { 89 userAgent: navigator.userAgent, 90 language: navigator.language, 91 platform: navigator.platform, 92 screen: `${screen.width}x${screen.height}`, 93 timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, 94 canvas: canvas.toDataURL(), 95 // Add more unique browser characteristics 96 cookieEnabled: navigator.cookieEnabled, 97 doNotTrack: navigator.doNotTrack, 98 hardwareConcurrency: navigator.hardwareConcurrency || 0, 99 maxTouchPoints: navigator.maxTouchPoints || 0, 100 colorDepth: screen.colorDepth, 101 pixelDepth: screen.pixelDepth, 102 availWidth: screen.availWidth, 103 availHeight: screen.availHeight, 104 // Add WebGL fingerprint for better uniqueness 105 webgl: getWebGLFingerprint() 106 }; 107 108 // Use a simple hash function instead of truncating base64 109 return simpleHash(JSON.stringify(fingerprintData)); 110 } 111 112 // Simple hash function to create consistent fingerprints 113 function simpleHash(str) { 114 let hash = 0; 115 if (str.length === 0) return hash.toString(); 116 for (let i = 0; i < str.length; i++) { 117 const char = str.charCodeAt(i); 118 hash = ((hash << 5) - hash) + char; 119 hash = hash & hash; // Convert to 32-bit integer 120 } 121 // Convert to positive hex string and pad to ensure consistent length 122 return Math.abs(hash).toString(16).padStart(8, '0'); 123 } 124 125 // Get WebGL fingerprint for additional uniqueness 126 function getWebGLFingerprint() { 127 try { 128 const canvas = document.createElement('canvas'); 129 const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); 130 if (!gl) return 'no-webgl'; 131 132 const debugInfo = gl.getExtension('WEBGL_debug_renderer_info'); 133 return { 134 vendor: gl.getParameter(gl.VENDOR), 135 renderer: gl.getParameter(gl.RENDERER), 136 version: gl.getParameter(gl.VERSION), 137 shadingLanguageVersion: gl.getParameter(gl.SHADING_LANGUAGE_VERSION), 138 unmaskedVendor: debugInfo ? gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) : 'unknown', 139 unmaskedRenderer: debugInfo ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) : 'unknown' 140 }; 141 } catch (e) { 142 return 'webgl-error'; 143 } 144 } 145 146 const observer = new IntersectionObserver((entries) => {
147 entries.forEach(entry => { 148 const element = entry.target; 149 const data = trackedElements.get(element); 150 if (!data) return; 151 152 let visiblePercentage = Math.floor(entry.intersectionRatio * 100); 153 if (visiblePercentage < config.viewThreshold && entry.isIntersecting) { 154 const rect = entry.boundingClientRect; 155 const vh = window.innerHeight, vw = window.innerWidth; 156 const visibleH = Math.min(rect.bottom, vh) - Math.max(rect.top, 0); 157 const visibleW = Math.min(rect.right, vw) - Math.max(rect.left, 0); 158 const viewportCoverage = (visibleH * visibleW) / (vh * vw) * 100; 159 if (viewportCoverage >= 30) { 160 visiblePercentage = Math.max(visiblePercentage, config.viewThreshold); 161 } 162 } 163 164 data.viewportPercentage = visiblePercentage; 165 data.inViewport = entry.isIntersecting; 166 167 if (entry.isIntersecting) { 168 trackImpression(element, data); 169 } 170 171 handleViewTracking(element, data, visiblePercentage); 172 }); 173 }, { 174 threshold: Array.from({ length: 11 }, (_, i) => i / 10) 175 }); 176 177 function trackImpression(element, data) { 178 // Check if tracking is enabled 179 if (!isTrackingEnabled()) return; 180 181 const now = Date.now(); 182 const last = sessionData.impressedContent.get(data.contentId) || 0; 183 184 if (now - last < config.impressionCooldown) return; 185 sessionData.impressedContent.set(data.contentId, now); 186 data.lastImpressionTime = now; 187 188 189 sendTrackingData({ 190 content_id: data.contentId, 191 interaction_type: 'impression', 192 user_id: config.userId, 193 session_id: config.sessionId, 194 page_url: config.pageUrl, 195 interaction_data: { 196 embed_type: data.embedType, 197 embed_url: data.embedUrl, 198 viewport_percentage: data.viewportPercentage, 199 location_data: config.ipLocationData, 200 browser_fingerprint: getBrowserFingerprint() 201 } 202 }); 203 } 204 205 function handleViewTracking(element, data, visiblePercentage) { 206 if (!data.inViewport || visiblePercentage < config.viewThreshold) { 207 if (data.viewTimer) { 208 clearTimeout(data.viewTimer); 209 data.viewTimer = null; 210 } 211 return; 212 } 213 214 const now = Date.now(); 215 216 // Optional: allow re-tracking views if cooldown passed 217 if (data.viewTracked && now - (data.lastViewTime || 0) > config.viewResetCooldown) { 218 data.viewTracked = false; 219 } 220 221 if (data.viewTracked || sessionData.viewedContent.has(data.contentId)) return; 222 223 if (!data.viewTimer) { 224 data.viewTimer = setTimeout(() => { 225 trackView(element, data); 226 data.viewTracked = true; 227 data.lastViewTime = Date.now(); 228 sessionData.viewedContent.add(data.contentId); 229 data.viewTimer = null; 230 }, config.viewDuration); 231 } 232 } 233 234 function trackView(element, data) { 235 // Check if tracking is enabled 236 if (!isTrackingEnabled()) return; 237 238 sendTrackingData({ 239 content_id: data.contentId, 240 interaction_type: 'view', 241 user_id: config.userId, 242 session_id: config.sessionId, 243 page_url: config.pageUrl, 244 view_duration: data.viewDuration || config.viewDuration || 0, 245 interaction_data: { 246 embed_type: data.embedType, 247 embed_url: data.embedUrl, 248 viewport_percentage: data.viewportPercentage, 249 view_duration: data.viewDuration || config.viewDuration || 0, 250 browser_fingerprint: getBrowserFingerprint() 251 } 252 }); 253 } 254 255 function setupClickTracking() { 256 trackedElements.forEach((data, element) => { 257 // Only track actual user clicks 258 element.addEventListener('click', (event) => { 259 // Make sure this is a real user click 260 if (!event.isTrusted) return; 261 262 const now = Date.now(); 263 const last = sessionData.clickedContent.get(data.contentId) || 0; 264 265 // Prevent duplicate clicks within cooldown period 266 if (now - last < config.clickCooldown) return; 267 268 // Check if tracking is enabled 269 if (!isTrackingEnabled()) return; 270 271 sessionData.clickedContent.set(data.contentId, now); 272 273 sendTrackingData({ 274 content_id: data.contentId, 275 interaction_type: 'click', 276 user_id: config.userId, 277 session_id: config.sessionId, 278 page_url: config.pageUrl, 279 interaction_data: { 280 embed_type: data.embedType, 281 embed_url: data.embedUrl, 282 browser_fingerprint: getBrowserFingerprint() 283 } 284 }); 285 }); 286 }); 287 } 288 289 function prepareElementForTracking(element) { 290 if (trackedElements.has(element)) return; 291 292 const embedType = element.getAttribute('data-embed-type'); 293 if (!embedType) return; 294 295 // Get a stable content ID based on embed URL or existing attributes 296 let contentId = element.getAttribute('data-embedpress-content') || 297 element.getAttribute('data-source-id') || 298 element.getAttribute('data-emid'); 299 300 if (!contentId) { 301 // Generate stable ID based on embed URL or iframe src 302 const embedUrl = element.getAttribute('data-embed-url') || 303 element.querySelector('iframe')?.src || 304 element.querySelector('embed')?.src || 305 element.querySelector('object')?.data || 306 window.location.href; 307 308 // Create a hash-based ID that will be consistent for the same content 309 contentId = 'ep-' + embedType + '-' + btoa(embedUrl).replace(/[^a-zA-Z0-9]/g, '').substring(0, 10); 310 } 311 312 element.setAttribute('data-embedpress-content', contentId); 313 314 const data = { 315 contentId, 316 embedType, 317 embedUrl: getEmbedUrl(element), 318 inViewport: false, 319 viewTimer: null,
320 viewTracked: false, 321 lastImpressionTime: 0, 322 lastViewTime: 0, 323 viewportPercentage: 0 324 }; 325 326 trackedElements.set(element, data); 327 observer.observe(element); 328 } 329 330 function getEmbedUrl(element) { 331 const iframe = element.querySelector('iframe'); 332 const video = element.querySelector('video source'); 333 const audio = element.querySelector('audio source'); 334 const embed = element.querySelector('embed'); 335 const object = element.querySelector('object'); 336 337 return iframe?.src || video?.src || audio?.src || embed?.src || object?.data || 338 element.getAttribute('data-url') || 339 element.getAttribute('data-src') || 340 element.getAttribute('href') || ''; 341 } 342 343 function findAndTrackEmbeds() { 344 document.querySelectorAll('[data-embed-type]:not([data-embed-type] [data-embed-type])') 345 .forEach(prepareElementForTracking); 346 } 347 348 349 function setupMutationObserver() { 350 if (!('MutationObserver' in window)) return; 351 const mo = new MutationObserver((mutations) => { 352 mutations.forEach(m => { 353 m.addedNodes.forEach(n => { 354 if (n.nodeType !== 1) return; 355 if (n.getAttribute?.('data-embed-type')) prepareElementForTracking(n); 356 n.querySelectorAll?.('[data-embed-type]').forEach(prepareElementForTracking); 357 }); 358 }); 359 }); 360 mo.observe(document.body, { childList: true, subtree: true }); 361 } 362 363 function sendTrackingData(data) { 364 // Skip tracking if embed type is unknown or empty 365 if (!data.interaction_data?.embed_type || 366 data.interaction_data.embed_type === 'unknown' || 367 data.interaction_data.embed_type === '') { 368 return; 369 } 370 371 const trackingData = { 372 ...data, 373 // Only set these if not already provided in data 374 user_id: data.user_id || config.userId, 375 session_id: data.session_id || config.sessionId, 376 page_url: data.page_url || config.pageUrl, 377 post_id: data.post_id || config.postId, 378 // Include original referrer if available 379 original_referrer: embedpress_analytics?.original_referrer || '' 380 }; 381 382 fetch(config.restUrl + 'track', { 383 method: 'POST', 384 headers: { 385 'Content-Type': 'application/json', 386 'X-WP-Nonce': embedpress_analytics?.nonce || '' 387 }, 388 body: JSON.stringify(trackingData), 389 credentials: 'same-origin' 390 }).catch(err => { 391 if (navigator.sendBeacon) { 392 const blob = new Blob([JSON.stringify(trackingData)], { type: 'application/json' }); 393 navigator.sendBeacon(config.restUrl + 'track', blob); 394 } 395 }); 396 } 397 398 async function getIPLocationData() { 399 try { 400 const res = await fetch('https://ipinfo.io/json'); 401 const data = await res.json(); 402 config.ipLocationData = { 403 country: data.country, 404 city: data.city, 405 timezone: data.timezone, 406 source: 'ip' 407 }; 408 } catch { 409 return null; 410 } 411 } 412 413 function sendBrowserInfo() { 414 const fingerprint = getBrowserFingerprint(); 415 const info = { 416 user_id: config.userId, 417 session_id: config.sessionId, 418 browser_fingerprint: fingerprint, 419 screen_resolution: window.screen.width + 'x' + window.screen.height, 420 language: navigator.language, 421 timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, 422 user_agent: navigator.userAgent 423 }; 424 getIPLocationData().then(() => { 425 if (config.ipLocationData) { 426 info.country = config.ipLocationData.country; 427 info.city = config.ipLocationData.city; 428 } 429 fetch(config.restUrl + 'browser-info', { 430 method: 'POST', 431 headers: { 432 'Content-Type': 'application/json', 433 'X-WP-Nonce': embedpress_analytics?.nonce || '' 434 }, 435 body: JSON.stringify(info), 436 credentials: 'same-origin' 437 }); 438 }); 439 } 440 441 function isTrackingEnabled() { 442 // Check the updated global variable first (updated by React component) 443 if (window.embedpressAnalyticsData?.trackingEnabled !== undefined) { 444 return Boolean(window.embedpressAnalyticsData.trackingEnabled); 445 } 446 // Fallback to original localized value 447 return Boolean(embedpress_analytics?.tracking_enabled); 448 } 449
450 function init() { 451 // Check if tracking is enabled before initializing 452 if (!isTrackingEnabled()) { 453 return; 454 } 455 456 457 // Check if page has embedded content (if provided by server) 458 if (embedpress_analytics?.has_embedded_content === false) { 459 return; 460 } 461 462 findAndTrackEmbeds(); 463 setupClickTracking(); 464 setupMutationObserver(); 465 sendBrowserInfo(); 466 } 467 468 if (document.readyState === 'loading') { 469 document.addEventListener('DOMContentLoaded', init); 470 } else { 471 init(); 472 } 473})();
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.