1/* ImgMin first-party analytics: anonymous, allowlisted and same-origin only. */ 2(function () { 3 'use strict'; 4 5 const SESSION_KEY = 'imgmin_analytics_session_v1'; 6 const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1']); 7 const EVENT_SURFACES = { 8 tool_view: 'compressor', extension_cta_clicked: 'compressor', upload_intent: 'compressor', 9 files_accepted: 'compressor', advanced_settings_opened: 'compressor', task_start: 'compressor', 10 process_success: 'compressor', process_fail: 'compressor', target_met: 'compressor', export_triggered: 'compressor', 11 settings_adjusted: 'compressor', rule_saved: 'compressor', rule_reused: 'compressor', rule_shared: 'compressor', 12 bg_tool_view: 'background_remove', bg_upload_intent: 'background_remove', bg_file_accepted: 'background_remove', 13 bg_run_start: 'background_remove', bg_result: 'background_remove', bg_error: 'background_remove', bg_export: 'background_remove' 14 }; 15 let queue = []; 16 let flushTimer = null; 17 18 function uuid() { 19 if (globalThis.crypto?.randomUUID) return globalThis.crypto.randomUUID(); 20 const bytes = globalThis.crypto?.getRandomValues ? globalThis.crypto.getRandomValues(new Uint8Array(16)) : Array.from({ length: 16 }, () => Math.floor(Math.random() * 256)); 21 bytes[6] = (bytes[6] & 0x0f) | 0x40; 22 bytes[8] = (bytes[8] & 0x3f) | 0x80; 23 return [...bytes].map((value, index) => [4, 6, 8, 10].includes(index) ? `-${value.toString(16).padStart(2, '0')}` : value.toString(16).padStart(2, '0')).join(''); 24 } 25 26 function isOptedOut() { 27 return navigator.globalPrivacyControl === true || navigator.doNotTrack === '1' || window.doNotTrack === '1'; 28 } 29 30 function enabled() { 31 return !isOptedOut() && (!LOCAL_HOSTS.has(location.hostname) || typeof window.__IMGMIN_ANALYTICS_TEST__ === 'function'); 32 } 33 34 function sessionId() { 35 try { 36 let id = sessionStorage.getItem(SESSION_KEY); 37 if (!id) { id = uuid(); sessionStorage.setItem(SESSION_KEY, id); } 38 return id; 39 } catch (_) { 40 return uuid(); 41 } 42 } 43 44 function language() { 45 const value = (document.documentElement.lang || 'en').toLowerCase(); 46 if (value.startsWith('zh-hant')) return 'zh-Hant'; 47 if (value.startsWith('zh')) return 'zh-CN'; 48 if (value.startsWith('fr')) return 'fr-FR'; 49 if (value.startsWith('de')) return 'de-DE'; 50 if (value.startsWith('es')) return 'es-ES'; 51 if (value.startsWith('ja')) return 'ja-JP'; 52 return 'en-US'; 53 } 54 55 function page() { 56 if (location.pathname.includes('remove-background')) return 'remove_background'; 57 if (location.pathname.includes('compress-to-')) return 'target_size'; 58 if (location.pathname.includes('/convert/')) return 'format_conversion'; 59 const params = new URLSearchParams(location.search); 60 if (params.has('targetKB')) return 'target_size'; 61 if (params.get('format') && params.get('format') !== 'auto') return 'format_conversion'; 62 return 'home'; 63 } 64 65 function safeProperties(name, input) { 66 input = input || {}; 67 const task = () => ({ task_type: input.task_type, output_format: input.output_format, target_bucket: input.target_bucket }); 68 if (['tool_view', 'extension_cta_clicked', 'advanced_settings_opened', 'settings_adjusted', 'rule_saved', 'rule_reused', 'rule_shared', 'bg_tool_view'].includes(name)) return {}; 69 if (['upload_intent', 'bg_upload_intent', 'bg_file_accepted'].includes(name)) return { method: input.method }; 70 if (name === 'files_accepted') return { method: input.method, count_bucket: input.count_bucket }; 71 if (name === 'process_success') return { ...task(), duration_bucket: input.duration_bucket }; 72 if (['task_start', 'target_met'].includes(name)) return task(); 73 if (name === 'process_fail') return { ...task(), reason: input.reason }; 74 if (name === 'export_triggered') return { ...task(), method: input.method, export_format: input.export_format, target_met: input.target_met }; 75 if (name === 'bg_run_start') return { tier: input.tier }; 76 if (name === 'bg_result') return { tier: input.tier, downscaled: input.downscaled }; 77 if (name === 'bg_error') return { reason: input.reason }; 78 if (name === 'bg_export') return { method: input.method, tier: input.tier }; 79 return {}; 80 } 81 82 function scheduleFlush() { 83 if (flushTimer || !queue.length) return; 84 flushTimer = window.setTimeout(() => flush(false), 800); 85 } 86 87 function flush(unloading) { 88 window.clearTimeout(flushTimer); 89 flushTimer = null; 90 if (!queue.length) return; 91 const events = queue.splice(0, 20); 92 const body = JSON.stringify({ events }); 93 if (unloading && navigator.sendBeacon) { 94 navigator.sendBeacon('/api/analytics/events', new Blob([body], { type: 'application/json' })); 95 } else { 96 fetch('/api/analytics/events', { method: 'POST', headers: { 'content-type': 'application/json' }, body, keepalive: true, credentials: 'same-origin' }) 97 .catch(() => { /* Analytics must never affect local image processing. */ }); 98 } 99 if (queue.length) scheduleFlush(); 100 } 101 102 function track(name, properties) { 103 if (!enabled() || !Object.prototype.hasOwnProperty.call(EVENT_SURFACES, name)) return; 104 const event = { 105 event_id: uuid(), session_id: sessionId(), name, surface: EVENT_SURFACES[name], 106 page: page(), language: language(), properties: safeProperties(name, properties) 107 }; 108 if (typeof window.__IMGMIN_ANALYTICS_TEST__ === 'function') { 109 window.__IMGMIN_ANALYTICS_TEST__(event); 110 return; 111 } 112 queue.push(event); 113 if (queue.length >= 20) flush(false); else scheduleFlush(); 114 } 115 116 window.addEventListener('pagehide', () => flush(true)); 117 window.imgminAnalytics = { track, flush: () => flush(false), enabled }; 118})();
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.