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 try { 231 document.dispatchEvent(new CustomEvent('embedpress:view', { 232 detail: { contentId: data.contentId, embedType: data.embedType } 233 })); 234 } catch (e) { /* IE11 etc. â badge will refresh on next page load */ } 235 }, config.viewDuration); 236 } 237 } 238 239 function trackView(element, data) { 240 // Check if tracking is enabled 241 if (!isTrackingEnabled()) return; 242 243 sendTrackingData({ 244 content_id: data.contentId, 245 interaction_type: 'view', 246 user_id: config.userId, 247 session_id: config.sessionId, 248 page_url: config.pageUrl, 249 view_duration: data.viewDuration || config.viewDuration || 0, 250 interaction_data: { 251 embed_type: data.embedType, 252 embed_url: data.embedUrl, 253 viewport_percentage: data.viewportPercentage, 254 view_duration: data.viewDuration || config.viewDuration || 0, 255 browser_fingerprint: getBrowserFingerprint() 256 } 257 }); 258 } 259 260 function setupClickTracking() {
261 trackedElements.forEach((data, element) => { 262 // Only track actual user clicks 263 element.addEventListener('click', (event) => { 264 // Make sure this is a real user click 265 if (!event.isTrusted) return; 266 267 const now = Date.now(); 268 const last = sessionData.clickedContent.get(data.contentId) || 0; 269 270 // Prevent duplicate clicks within cooldown period 271 if (now - last < config.clickCooldown) return; 272 273 // Check if tracking is enabled 274 if (!isTrackingEnabled()) return; 275 276 sessionData.clickedContent.set(data.contentId, now); 277 278 sendTrackingData({ 279 content_id: data.contentId, 280 interaction_type: 'click', 281 user_id: config.userId, 282 session_id: config.sessionId, 283 page_url: config.pageUrl, 284 interaction_data: { 285 embed_type: data.embedType, 286 embed_url: data.embedUrl, 287 browser_fingerprint: getBrowserFingerprint() 288 } 289 }); 290 }); 291 }); 292 } 293 294 function prepareElementForTracking(element) { 295 if (trackedElements.has(element)) return; 296 297 const embedType = element.getAttribute('data-embed-type'); 298 if (!embedType) return; 299 300 // Get a stable content ID based on embed URL or existing attributes 301 let contentId = element.getAttribute('data-embedpress-content') || 302 element.getAttribute('data-source-id') || 303 element.getAttribute('data-emid'); 304 305 if (!contentId) { 306 // Generate stable ID based on embed URL or iframe src 307 const embedUrl = element.getAttribute('data-embed-url') || 308 element.querySelector('iframe')?.src || 309 element.querySelector('embed')?.src || 310 element.querySelector('object')?.data || 311 window.location.href; 312 313 // Create a hash-based ID that will be consistent for the same content 314 contentId = 'ep-' + embedType + '-' + btoa(embedUrl).replace(/[^a-zA-Z0-9]/g, '').substring(0, 10); 315 } 316 317 element.setAttribute('data-embedpress-content', contentId); 318 319 const data = { 320 contentId, 321 embedType, 322 embedUrl: getEmbedUrl(element), 323 inViewport: false, 324 viewTimer: null, 325 viewTracked: false, 326 lastImpressionTime: 0, 327 lastViewTime: 0, 328 viewportPercentage: 0 329 }; 330 331 trackedElements.set(element, data); 332 observer.observe(element); 333 } 334 335 function getEmbedUrl(element) { 336 const iframe = element.querySelector('iframe'); 337 const video = element.querySelector('video source'); 338 const audio = element.querySelector('audio source'); 339 const embed = element.querySelector('embed'); 340 const object = element.querySelector('object'); 341 342 return iframe?.src || video?.src || audio?.src || embed?.src || object?.data || 343 element.getAttribute('data-url') || 344 element.getAttribute('data-src') || 345 element.getAttribute('href') || ''; 346 } 347 348 function findAndTrackEmbeds() { 349 document.querySelectorAll('[data-embed-type]:not([data-embed-type] [data-embed-type])') 350 .forEach(prepareElementForTracking); 351 } 352 353 354 function setupMutationObserver() { 355 if (!('MutationObserver' in window)) return; 356 const mo = new MutationObserver((mutations) => { 357 mutations.forEach(m => { 358 m.addedNodes.forEach(n => { 359 if (n.nodeType !== 1) return; 360 if (n.getAttribute?.('data-embed-type')) prepareElementForTracking(n); 361 n.querySelectorAll?.('[data-embed-type]').forEach(prepareElementForTracking); 362 }); 363 }); 364 }); 365 mo.observe(document.body, { childList: true, subtree: true }); 366 } 367 368 function sendTrackingData(data) { 369 // Skip tracking if embed type is unknown or empty 370 if (!data.interaction_data?.embed_type || 371 data.interaction_data.embed_type === 'unknown' || 372 data.interaction_data.embed_type === '') { 373 return; 374 } 375 376 const trackingData = { 377 ...data, 378 // Only set these if not already provided in data 379 user_id: data.user_id || config.userId, 380 session_id: data.session_id || config.sessionId, 381 page_url: data.page_url || config.pageUrl, 382 post_id: data.post_id || config.postId, 383 // Include original referrer if available 384 original_referrer: embedpress_analytics?.original_referrer || '' 385 }; 386 387 fetch(config.restUrl + 'track', { 388 method: 'POST', 389 headers: { 390 'Content-Type': 'application/json', 391 'X-WP-Nonce': embedpress_analytics?.nonce || '' 392 }, 393 body: JSON.stringify(trackingData), 394 credentials: 'same-origin' 395 }).catch(err => { 396 if (navigator.sendBeacon) { 397 const blob = new Blob([JSON.stringify(trackingData)], { type: 'application/json' }); 398 navigator.sendBeacon(config.restUrl + 'track', blob); 399 } 400 }); 401 } 402 403 async function getIPLocationData() { 404 try { 405 const res = await fetch('https://ipinfo.io/json'); 406 const data = await res.json(); 407 config.ipLocationData = { 408 country: data.country, 409 city: data.city, 410 timezone: data.timezone, 411 source: 'ip' 412 }; 413 } catch { 414 return null; 415 } 416 } 417 418 function sendBrowserInfo() { 419 const fingerprint = getBrowserFingerprint(); 420 const info = { 421 user_id: config.userId, 422 session_id: config.sessionId, 423 browser_fingerprint: fingerprint, 424 screen_resolution: window.screen.width + 'x' + window.screen.height, 425 language: navigator.language, 426 timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, 427 user_agent: navigator.userAgent 428 }; 429 getIPLocationData().then(() => { 430 if (config.ipLocationData) { 431 info.country = config.ipLocationData.country; 432 info.city = config.ipLocationData.city; 433 } 434 fetch(config.restUrl + 'browser-info', { 435 method: 'POST', 436 headers: { 437 'Content-Type': 'application/json', 438 'X-WP-Nonce': embedpress_analytics?.nonce || '' 439 }, 440 body: JSON.stringify(info), 441 credentials: 'same-origin' 442 }); 443 }); 444 } 445 446 function isTrackingEnabled() { 447 // Check the updated global variable first (updated by React component) 448 if (window.embedpressAnalyticsData?.trackingEnabled !== undefined) { 449 return Boolean(window.embedpressAnalyticsData.trackingEnabled); 450 } 451 // Fallback to original localized value 452 return Boolean(embedpress_analytics?.tracking_enabled); 453 } 454
455 function init() { 456 // Check if tracking is enabled before initializing 457 if (!isTrackingEnabled()) { 458 return; 459 } 460 461 462 // Check if page has embedded content (if provided by server) 463 if (embedpress_analytics?.has_embedded_content === false) { 464 return; 465 } 466 467 findAndTrackEmbeds(); 468 setupClickTracking(); 469 setupMutationObserver(); 470 sendBrowserInfo(); 471 } 472 473 if (document.readyState === 'loading') { 474 document.addEventListener('DOMContentLoaded', init); 475 } else { 476 init(); 477 } 478})();
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.