1(function () { 2 // Tunables. `repeatEveryMs: 0` means "fire once per clickid". A future 3 // non-zero value makes the conversion re-fire every N ms of active time 4 // (see shouldFire() below, which already tracks lastFiredAtMs/count). 5 var CONFIG = { 6 thresholdMs: 60000, 7 repeatEveryMs: 0, 8 heartbeatMs: 1000, 9 maxDeltaMs: 2000, 10 sessionGapMs: 30000, 11 conversionUrl: 'https://xml.pushub.net/conversion' 12 }; 13 14 var K = { 15 CLICKID: 'bt_tos_clickid', 16 ACCUM: 'bt_tos_accum_ms', 17 LAST_BEAT: 'bt_tos_last_beat', 18 FIRED: 'bt_tos_fired' 19 }; 20 21 // --- Storage helpers (private mode / disabled storage degrade silently) --- 22 23 function safeGetItem(key) { 24 try { 25 return localStorage.getItem(key); 26 } catch (e) { 27 return null; 28 } 29 } 30 31 function safeSetItem(key, value) { 32 try { 33 localStorage.setItem(key, value); 34 } catch (e) { 35 // ignore 36 } 37 } 38 39 function safeRemoveItem(key) { 40 try { 41 localStorage.removeItem(key); 42 } catch (e) { 43 // ignore 44 } 45 } 46 47 function getAccumulatedMs() { 48 var raw = safeGetItem(K.ACCUM); 49 var n = raw !== null ? parseInt(raw, 10) : 0; 50 return isNaN(n) ? 0 : n; 51 } 52 53 function getClickid() { 54 return safeGetItem(K.CLICKID); 55 } 56 57 function getFired() { 58 var raw = safeGetItem(K.FIRED); 59 if (!raw) return null; 60 try { 61 return JSON.parse(raw); 62 } catch (e) { 63 return null; 64 } 65 } 66 67 function setFired(state) { 68 try { 69 safeSetItem(K.FIRED, JSON.stringify(state)); 70 } catch (e) { 71 // ignore 72 } 73 } 74 75 // --- Clickid / attribution session --- 76 77 function getUrlClickid() { 78 try { 79 var params = new URLSearchParams(window.location.search); 80 var val = params.get('clickid'); 81 return val ? val : null; 82 } catch (e) { 83 return null; 84 } 85 } 86 87 function resetAccumulated(now) { 88 safeSetItem(K.ACCUM, '0'); 89 safeSetItem(K.LAST_BEAT, String(now)); 90 } 91 92 // Reads the clickid from the URL (if any) and reconciles it with the 93 // stored clickid. A different incoming clickid starts a brand new 94 // attribution session: stored clickid is overwritten, accumulated time 95 // resets to 0, and the conversion-fired flag is cleared so the new 96 // clickid is free to convert again. 97 function resolveClickid(now) { 98 var urlClickid = getUrlClickid(); 99 var stored = getClickid(); 100 101 if (urlClickid && urlClickid !== stored) { 102 safeSetItem(K.CLICKID, urlClickid); 103 resetAccumulated(now); 104 safeRemoveItem(K.FIRED); 105 return urlClickid; 106 } 107 108 return stored; 109 } 110 111 // --- Conversion postback --- 112 113 function shouldFire(clickid) { 114 var fired = getFired(); 115 116 if (!fired || fired.clickid !== clickid) return true; 117 118 if (CONFIG.repeatEveryMs > 0) { 119 var last = typeof fired.lastFiredAtMs === 'number' ? fired.lastFiredAtMs : 0; 120 return (Date.now() - last) >= CONFIG.repeatEveryMs; 121 } 122 123 return false; 124 } 125 126 function sendConversionPing(clickid) { 127 // Image pixel: always a GET request (sendBeacon would POST). 128 var url = CONFIG.conversionUrl + '?c=' + encodeURIComponent(clickid); 129 try { 130 new Image().src = url; 131 } catch (e) { 132 // ignore 133 } 134 } 135 136 function fireConversion(clickid) { 137 var previous = getFired(); 138 var count = previous && previous.clickid === clickid ? (previous.count || 0) + 1 : 1; 139 140 // Set the flag BEFORE firing so racing tabs can't both fire. 141 setFired({ clickid: clickid, lastFiredAtMs: Date.now(), count: count }); 142 sendConversionPing(clickid); 143 } 144 145 function checkConversion(clickid, accumulatedMs) { 146 if (!clickid) return; 147 148 var accumulated = typeof accumulatedMs === 'number' ? accumulatedMs : getAccumulatedMs(); 149 if (accumulated < CONFIG.thresholdMs) return; 150 if (!shouldFire(clickid)) return; 151 152 fireConversion(clickid); 153 } 154 155 // --- Heartbeat --- 156 157 // Accumulates elapsed time since the last beat (shared across tabs via 158 // localStorage) and always advances lastBeat to "now". Only deltas in 159 // (0, maxDeltaMs] are counted, which both filters out clock weirdness and 160 // prevents double-counting when multiple tabs are open. 161 function beat() { 162 var now = Date.now(); 163 var lastBeatRaw = safeGetItem(K.LAST_BEAT); 164 var lastBeat = lastBeatRaw !== null ? parseInt(lastBeatRaw, 10) : now; 165 if (isNaN(lastBeat)) lastBeat = now; 166 167 var delta = now - lastBeat; 168 if (delta > 0 && delta <= CONFIG.maxDeltaMs) { 169 var accumulated = getAccumulatedMs() + delta; 170 safeSetItem(K.ACCUM, String(accumulated)); 171 checkConversion(getClickid(), accumulated); 172 } 173 174 safeSetItem(K.LAST_BEAT, String(now)); 175 } 176 177 function tick() { 178 if (document.visibilityState !== 'visible') return; 179 beat(); 180 } 181 182 function handleVisibilityChange() { 183 if (document.visibilityState === 'hidden') { 184 beat(); 185 } else if (document.visibilityState === 'visible') { 186 // Move lastBeat up to now BEFORE resuming so the hidden gap isn't 187 // counted as active time. Do not touch the accumulated total. 188 safeSetItem(K.LAST_BEAT, String(Date.now())); 189 } 190 } 191 192 // --- Init --- 193
194 function init() { 195 var now = Date.now(); 196 var clickid = resolveClickid(now); 197 198 var lastBeatRaw = safeGetItem(K.LAST_BEAT); 199 var lastBeat = lastBeatRaw !== null ? parseInt(lastBeatRaw, 10) : NaN; 200 201 if (isNaN(lastBeat)) { 202 // First run ever (or storage unavailable) - nothing to compare against. 203 safeSetItem(K.LAST_BEAT, String(now)); 204 } else if (now - lastBeat > CONFIG.sessionGapMs) { 205 // Gap detection: only run at init. A large gap means the tab was 206 // closed or the user left the domain, so treat this as a fresh 207 // session. Clickid and the fired flag are left untouched. 208 resetAccumulated(now); 209 } 210 211 // Catch the case where the threshold was already crossed before reload. 212 checkConversion(clickid); 213 214 document.addEventListener('visibilitychange', handleVisibilityChange); 215 window.addEventListener('pagehide', beat); 216 217 window.setInterval(tick, CONFIG.heartbeatMs); 218 } 219 220 init(); 221 222 window.BlushTimeTracker = { 223 getAccumulatedMs: getAccumulatedMs, 224 getClickid: getClickid 225 }; 226})();
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.