PluginProbe ʕ •ᴥ•ʔ
EmbedPress – PDF Embedder, Embed PDF viewer, YouTube Videos, 3D FlipBook, Social feeds & more / trunk
EmbedPress – PDF Embedder, Embed PDF viewer, YouTube Videos, 3D FlipBook, Social feeds & more vtrunk
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 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