chunks
1 week ago
vendor
8 months ago
admin.build.js
1 week ago
admin.js
3 months ago
analytics-tracker.js
4 weeks ago
analytics.build.js
1 week ago
blocks.build.js
1 week ago
carousel.js
1 year ago
custom-player.build.js
1 week ago
documents-viewer-script.js
3 months ago
ep-pdf-lightbox.js
3 months ago
ep-view-count.js
1 week ago
ep-yt-queue.js
4 weeks ago
feature-notices.js
7 months ago
front.js
4 weeks ago
frontend.build.js
3 months ago
gallery-justify.js
1 week ago
gutneberg-script.js
1 month ago
index.html
7 years ago
initCarousel.js
2 years ago
initplyr.js
1 month ago
instafeed.js
4 weeks ago
instagram-shortcode-generator.js
1 month ago
lazy-load.js
6 months ago
license.js
3 months ago
meetup-timezone.js
3 months ago
onboarding.build.js
1 week ago
pdf-gallery-elementor-editor.js
3 months ago
pdf-gallery.js
3 months ago
preview.js
8 months ago
settings.js
3 months ago
sponsored.js
9 months ago
analytics-tracker.js
479 lines
| 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 | })(); |
| 479 |