PluginProbe ʕ •ᴥ•ʔ
EmbedPress – PDF Embedder, Embed PDF viewer, YouTube Videos, 3D FlipBook, Social feeds & more / 4.5.4
EmbedPress – PDF Embedder, Embed PDF viewer, YouTube Videos, 3D FlipBook, Social feeds & more v4.5.4
4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 trunk 1.0.0 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.5.0 1.6.0 1.6.1 1.6.2 1.6.3 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.7.5 2.0.0 2.0.1 2.0.2 2.0.3 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.2.0 2.2.1 2.2.2 2.3.0 2.3.1 2.3.2 2.3.3 2.4.0 2.4.1 2.5.0 2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.1.2 3.1.3 3.2.0 3.2.1 3.3.0 3.3.1 3.3.2 3.3.3 3.3.4 3.3.5 3.3.6 3.3.7 3.4.0 3.4.1 3.4.2 3.4.3 3.5.0 3.5.1 3.5.2 3.5.3 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.6.8 3.7.0 3.7.1 3.7.2 3.7.3 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.9.0 3.9.1 3.9.10 3.9.11 3.9.12 3.9.13 3.9.14 3.9.15 3.9.16 3.9.17 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 3.9.7 3.9.8 3.9.9 4.0.0 4.0.1 4.0.10 4.0.11 4.0.12 4.0.13 4.0.14 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.0.9 4.1.0 4.1.1 4.1.10 4.1.2 4.1.3 4.1.4 4.1.5 4.1.6 4.1.7 4.1.8 4.1.9 4.2.0 4.2.1 4.2.2 4.2.3 4.2.4 4.2.5 4.2.6 4.2.7 4.2.8 4.2.9 4.3.0 4.3.1 4.4.0 4.4.1 4.4.10 4.4.11 4.4.2 4.4.3 4.4.4 4.4.5 4.4.6 4.4.7 4.4.8 4.4.9 4.5.0 4.5.1
embedpress / assets / js / analytics-tracker.js
embedpress / assets / js Last commit date
chunks 1 month ago vendor 8 months ago admin.build.js 1 month ago admin.js 3 months ago analytics-tracker.js 9 months ago analytics.build.js 1 month ago blocks.build.js 1 month ago carousel.js 1 year ago custom-player.build.js 1 month ago documents-viewer-script.js 3 months ago ep-pdf-lightbox.js 3 months ago feature-notices.js 7 months ago front.js 3 months ago frontend.build.js 3 months ago gallery-justify.js 9 months ago gutneberg-script.js 2 months ago index.html 7 years ago initCarousel.js 2 years ago initplyr.js 2 months ago instafeed.js 1 month 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 month 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
474 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 }, 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 })();
474