PageSourceSearch

https://blushtales.com/assets/js/time-tracker.js

js blushtales.com collected 2026-10-08 17:36:18 UTC 6,186 bytes, 226 lines download raw bytes

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.