1(function () { 2 if (typeof window === 'undefined' || typeof document === 'undefined') return; 3 4 var scriptEl = document.currentScript; 5 var writeKey = scriptEl && scriptEl.dataset && scriptEl.dataset.key; 6 if (!writeKey) return; 7 8 var apiHost; 9 try { 10 apiHost = (scriptEl.dataset && scriptEl.dataset.apiHost) || new URL(scriptEl.src).origin; 11 } catch (e) { return; } 12 if (!apiHost) return; 13 apiHost = apiHost.replace(/\/$/, ''); 14 15 var ANON_KEY = '_opinly_anon'; 16 var FIRST_VISIT_KEY = '_opinly_first_visit_sent'; 17 var UTM_KEY = '_opinly_utm'; 18 var SESSION_KEY = '_opinly_session'; 19 var SESSION_TIMEOUT_MS = 1800000; 20 21 function safeGet(getArea, key) { try { return getArea().getItem(key); } catch (e) { return null; } } 22 function safeSet(getArea, key, val) { try { getArea().setItem(key, val); } catch (e) {} } 23 function lsArea() { return window.localStorage; } 24 function ssArea() { return window.sessionStorage; } 25 function ls(key, val) { return val === undefined ? safeGet(lsArea, key) : safeSet(lsArea, key, val); } 26 function ss(key, val) { return val === undefined ? safeGet(ssArea, key) : safeSet(ssArea, key, val); } 27 28 function uuid() { 29 return (typeof crypto !== 'undefined' && crypto.randomUUID) 30 ? crypto.randomUUID() 31 : Date.now() + '-' + Math.random().toString(36).slice(2); 32 } 33 34 function getAnonId() { 35 var id = ls(ANON_KEY); 36 if (!id) { id = uuid(); ls(ANON_KEY, id); } 37 return id; 38 } 39 40 function getSessionId() { 41 var now = Date.now(); 42 var s = null; 43 try { s = JSON.parse(ss(SESSION_KEY)); } catch (e) {} 44 if (!s || !s.id || (now - s.last) > SESSION_TIMEOUT_MS) { 45 s = { id: uuid(), last: now }; 46 } else { 47 s.last = now; 48 } 49 ss(SESSION_KEY, JSON.stringify(s)); 50 return s.id; 51 } 52 53 var CLICK_ID_PARAMS = ['gclid', 'gbraid', 'wbraid', 'fbclid', 'ttclid', 'msclkid', 'li_fat_id', 'twclid', 'epik', 'rdt_cid', 'dclid']; 54 55 function getFirstTouch() { 56 var p; 57 try { p = new URLSearchParams(window.location.search); } catch (e) { p = { get: function () { return null; } }; } 58 59 var clickIds = {}; 60 for (var i = 0; i < CLICK_ID_PARAMS.length; i++) { 61 var v = p.get(CLICK_ID_PARAMS[i]); 62 if (v) clickIds[CLICK_ID_PARAMS[i]] = v; 63 } 64 65 var UTM_PARAMS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'utm_id']; 66 var hasFreshCampaign = Object.keys(clickIds).length > 0; 67 for (var j = 0; j < UTM_PARAMS.length; j++) { 68 if (p.get(UTM_PARAMS[j])) { hasFreshCampaign = true; break; } 69 } 70 71 if (!hasFreshCampaign) { 72 var stored = ls(UTM_KEY); 73 if (stored) { 74 try { 75 var parsed = JSON.parse(stored); 76 if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed; 77 } catch (e) {} 78 } 79 } 80 81 var touch = { 82 utm_source: p.get('utm_source') || undefined, 83 utm_medium: p.get('utm_medium') || undefined, 84 utm_campaign: p.get('utm_campaign') || undefined, 85 utm_term: p.get('utm_term') || undefined, 86 utm_content: p.get('utm_content') || undefined, 87 utm_id: p.get('utm_id') || undefined, 88 referrer: document.referrer || undefined, 89 click_ids: Object.keys(clickIds).length ? clickIds : undefined 90 }; 91 92 if (touch.utm_source || touch.click_ids || touch.referrer) { 93 ls(UTM_KEY, JSON.stringify(touch)); 94 } 95 return touch; 96 } 97 98 function primaryClickId(touch) { 99 if (!touch.click_ids) return undefined; 100 for (var i = 0; i < CLICK_ID_PARAMS.length; i++) { 101 if (touch.click_ids[CLICK_ID_PARAMS[i]]) return touch.click_ids[CLICK_ID_PARAMS[i]]; 102 } 103 return undefined; 104 } 105 106 var anonId = getAnonId(); 107 108 function safePageUrl() { 109 return window.location.origin + window.location.pathname; 110 } 111 112 function deviceContext() { 113 var tz; 114 try { tz = Intl.DateTimeFormat().resolvedOptions().timeZone; } catch (e) {} 115 return { 116 page_path: window.location.pathname, 117 page_title: document.title || undefined, 118 page_hostname: window.location.hostname, 119 screen: window.screen ? (window.screen.width + 'x' + window.screen.height) : undefined, 120 viewport: window.innerWidth + 'x' + window.innerHeight, 121 language: navigator.language || undefined, 122 timezone: tz 123 }; 124 } 125 126 function send(payload) { 127 try { 128 fetch(apiHost + '/track', { 129 method: 'POST', 130 headers: { 'content-type': 'application/json', 'x-api-key': writeKey },
131 body: JSON.stringify(Object.assign({ anon_id: anonId }, payload)), 132 keepalive: true 133 }).catch(function () {}); 134 } catch (e) {} 135 } 136 137 function emit(eventName, props, extra) { 138 var touch = getFirstTouch(); 139 var payload = { 140 event_name: eventName, 141 page_url: safePageUrl(), 142 utm_source: touch.utm_source, 143 utm_medium: touch.utm_medium, 144 utm_campaign: touch.utm_campaign, 145 referrer: touch.referrer, 146 click_id: primaryClickId(touch), 147 properties: Object.assign({ session_id: getSessionId() }, deviceContext(), props || {}) 148 }; 149 if (extra) Object.assign(payload, extra); 150 send(payload); 151 } 152 153 var lastPath; 154 var currentPageUrl; 155 var pageviewId; 156 var engagedMs = 0; 157 var sentEngagedMs = 0; 158 var engagedStart = 0; 159 var engagementBound = false; 160 161 var currentDocumentHeight = 0; 162 var maxScrollDepthPx = 0; 163 var sentScrollDepthPct = -1; 164 165 function getDocumentHeight() { 166 var body = document.body || {}; 167 var el = document.documentElement || {}; 168 return Math.max( 169 body.scrollHeight || 0, body.offsetHeight || 0, body.clientHeight || 0, 170 el.scrollHeight || 0, el.offsetHeight || 0, el.clientHeight || 0 171 ); 172 } 173 174 function getScrollDepthPx() { 175 var viewportH = window.innerHeight || 0; 176 var scrollTop = window.scrollY || window.pageYOffset || 0; 177 return currentDocumentHeight <= viewportH ? currentDocumentHeight : scrollTop + viewportH; 178 } 179 180 function getScrollDepthPct() { 181 if (!currentDocumentHeight) return 0; 182 return Math.min(100, Math.round((maxScrollDepthPx / currentDocumentHeight) * 100)); 183 } 184 185 function initScrollTracking() { 186 currentDocumentHeight = getDocumentHeight(); 187 maxScrollDepthPx = getScrollDepthPx(); 188 document.addEventListener('scroll', function () { 189 var d = getScrollDepthPx(); 190 if (d > maxScrollDepthPx) maxScrollDepthPx = d; 191 }); 192 if (window.ResizeObserver) { 193 new ResizeObserver(function () { currentDocumentHeight = getDocumentHeight(); }).observe(document.documentElement); 194 } else { 195 window.addEventListener('resize', function () { currentDocumentHeight = getDocumentHeight(); }); 196 } 197 } 198 199 function isEngaged() { 200 return document.visibilityState === 'visible' && (document.hasFocus ? document.hasFocus() : true); 201 } 202 203 function accrueEngagement() { 204 if (engagedStart) { 205 engagedMs += Date.now() - engagedStart; 206 engagedStart = 0; 207 } 208 } 209 210 function flushEngagement() { 211 accrueEngagement(); 212 var delta = engagedMs - sentEngagedMs; 213 var sdPct = getScrollDepthPct(); 214 215 if ((delta < 3000 && sdPct <= sentScrollDepthPct) || !currentPageUrl) return; 216 sentEngagedMs = engagedMs; 217 sentScrollDepthPct = sdPct; 218 var touch = getFirstTouch(); 219 send({ 220 event_name: 'page_leave', 221 page_url: currentPageUrl, 222 utm_source: touch.utm_source, 223 utm_medium: touch.utm_medium, 224 utm_campaign: touch.utm_campaign, 225 referrer: touch.referrer, 226 click_id: primaryClickId(touch), 227 properties: { session_id: getSessionId(), pageview_id: pageviewId, duration_ms: delta, scroll_depth: sdPct } 228 }); 229 } 230 231 function onEngagementChange() { 232 if (isEngaged()) { 233 if (!engagedStart) engagedStart = Date.now(); 234 } else { 235 flushEngagement(); 236 } 237 } 238 239 function bindEngagement() { 240 if (engagementBound) return; 241 engagementBound = true; 242 document.addEventListener('visibilitychange', onEngagementChange, true); 243 window.addEventListener('blur', onEngagementChange, true); 244 window.addEventListener('focus', onEngagementChange, true); 245 window.addEventListener('pagehide', flushEngagement, true); 246 } 247 248 function pageView(isSpa, attribution) { 249 if (isSpa && lastPath === window.location.pathname) return; 250 if (pageviewId) flushEngagement(); 251 lastPath = window.location.pathname; 252 currentPageUrl = safePageUrl(); 253 pageviewId = uuid(); 254 engagedMs = 0; 255 sentEngagedMs = 0; 256 engagedStart = Date.now(); 257 currentDocumentHeight = getDocumentHeight(); 258 maxScrollDepthPx = getScrollDepthPx(); 259 sentScrollDepthPct = -1; 260 var props = { pageview_id: pageviewId, referrer: document.referrer || undefined }; 261 if (attribution) props.first_touch = attribution; 262 emit('page_view', props); 263 bindEngagement(); 264 } 265 266 var CLICKABLE = 'a, button, input[type=submit], input[type=button], [role=button]'; 267 268 function elementText(el) { 269 var t = (el.innerText || el.textContent || '').trim().replace(/\s+/g, ' '); 270 return t ? t.slice(0, 255) : undefined; 271 } 272 273 function serializeElement(el) { 274 var tag = el.tagName.toLowerCase(); 275 var info = { tag: tag }; 276 var text = elementText(el); 277 if (text) info.text = text; 278 if (el.id) info.id = el.id; 279 if (typeof el.className === 'string' && el.className.trim()) { 280 info.classes = el.className.trim().split(/\s+/).slice(0, 10); 281 } 282 if (el.name) info.name = el.name; 283 var type = el.getAttribute && el.getAttribute('type'); 284 if (type) info.type = type; 285 if (tag === 'a') { 286 var href = el.getAttribute('href'); 287 if (href) { 288 try { 289 var u = new URL(href, window.location.href); 290 info.href = u.origin + u.pathname; 291 info.external = !!u.host && u.host !== window.location.host; 292 } catch (e) {} 293 } 294 } 295 return info; 296 } 297 298 function onClick(e) { 299 try { 300 var el = e.target && e.target.closest ? e.target.closest(CLICKABLE) : null; 301 if (!el) return; 302 if (el.closest('[data-opinly-no-capture]')) return; 303 emit('click', { element: serializeElement(el) }); 304 } catch (err) {} 305 } 306 307 var MAX_FIELDS = 50; 308 309 function serializeFormMeta(form) { 310 var info = { method: (form.method || 'get').toLowerCase() }; 311 if (form.id) info.id = form.id; 312 var name = form.getAttribute && form.getAttribute('name'); 313 if (name) info.name = name; 314 var action = form.getAttribute && form.getAttribute('action'); 315 if (action) {
316 try { 317 var u = new URL(action, window.location.href); 318 info.action = u.origin + u.pathname; 319 } catch (e) {} 320 } 321 return info; 322 } 323 324 var SENSITIVE_KEY_RE = /pass|token|secret|csrf|nonce|cvv|cvc|ssn|card.?num|credit.?card/i; 325 326 var IDENTIFIED_KEY = 'opinly_identified_email'; 327 var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 328 var EMAIL_HINT_RE = /e-?mail/i; 329 var NOT_SELF_RE = /friend|recipient|refer|gift|share|colleague/i; 330 331 function fieldText(el) { 332 var label = ''; 333 try { 334 if (el.id) { 335 var sel = window.CSS && CSS.escape ? CSS.escape(el.id) : el.id; 336 var l = document.querySelector('label[for="' + sel + '"]'); 337 if (l) label = l.textContent || ''; 338 } 339 } catch (e) {} 340 return [el.name, el.id, el.getAttribute('placeholder'), el.getAttribute('aria-label'), el.getAttribute('autocomplete'), label].join(' '); 341 } 342 343 function isEmailField(el) { 344 if (!el || el.tagName !== 'INPUT') return false; 345 var text = fieldText(el); 346 if (NOT_SELF_RE.test(text)) return false; 347 if ((el.type || '').toLowerCase() === 'email') return true; 348 if ((el.getAttribute('autocomplete') || '').toLowerCase().indexOf('email') !== -1) return true; 349 return EMAIL_HINT_RE.test(text); 350 } 351 352 function isVisibleInput(el) { 353 if (el.disabled || el.readOnly) return false; 354 if ((el.type || '').toLowerCase() === 'hidden') return false; 355 return !!(el.offsetParent !== null || el.offsetWidth || el.offsetHeight || el.getClientRects().length); 356 } 357 358 function sameSiteEmail(email) { 359 var at = email.lastIndexOf('@'); 360 if (at < 0) return false; 361 var dom = email.slice(at + 1).toLowerCase().replace(/^www\./, ''); 362 var host = (location.hostname || '').toLowerCase().replace(/^www\./, ''); 363 if (!dom || !host) return false; 364 if (dom === host) return true; 365 if (host.length > dom.length && host.slice(-(dom.length + 1)) === '.' + dom) return true; 366 if (dom.length > host.length && dom.slice(-(host.length + 1)) === '.' + host) return true; 367 return false; 368 } 369 370 function tryIdentify(el, viaSubmit) { 371 try { 372 if (!isEmailField(el) || !isVisibleInput(el)) return; 373 if (el.closest && el.closest('[data-opinly-no-capture]')) return; 374 if (!viaSubmit && !el._opinlyDirty) return; 375 var email = String(el.value || '').trim(); 376 if (!EMAIL_RE.test(email)) return; 377 if (sameSiteEmail(email)) return; 378 var key = email.toLowerCase(); 379 if (ls(IDENTIFIED_KEY) === key) return; 380 ls(IDENTIFIED_KEY, key); 381 send({ event_name: 'identify', email: email }); 382 } catch (e) {} 383 } 384 385 function identifyFromForm(form) { 386 try { 387 var els = form.elements; 388 if (!els) return; 389 for (var i = 0; i < els.length; i++) { 390 if (isEmailField(els[i]) && isVisibleInput(els[i]) && els[i].value) { 391 tryIdentify(els[i], true); 392 break; 393 } 394 } 395 } catch (e) {} 396 } 397 398 function serializeFieldNames(form) { 399 var names = []; 400 var els = form.elements; 401 if (!els) return names; 402 for (var i = 0; i < els.length && names.length < MAX_FIELDS; i++) { 403 var el = els[i]; 404 var type = (el.type || '').toLowerCase(); 405 if (type === 'password' || type === 'hidden') continue; 406 if (el.closest && el.closest('[data-opinly-no-capture]')) continue; 407 var key = el.name || el.id; 408 if (!key || SENSITIVE_KEY_RE.test(key)) continue; 409 names.push(key); 410 } 411 return names; 412 } 413 414 function onSubmit(e) { 415 try { 416 var form = e.target; 417 if (!form || form.tagName !== 'FORM') return; 418 if (form.closest && form.closest('[data-opinly-no-capture]')) return; 419 identifyFromForm(form); 420 var meta = serializeFormMeta(form); 421 var names = serializeFieldNames(form); 422 meta.field_count = names.length; 423 emit('form_submit', { form: meta, field_names: names }); 424 } catch (err) {} 425 } 426 427 window.opinly = { 428 anonId: anonId, 429 track: function (event, props, opts) { 430 emit(event, props, opts && opts.externalEventId ? { external_event_id: opts.externalEventId } : undefined); 431 }, 432 identify: function (identity) { 433 if (!identity || !identity.email) return; 434 send({ event_name: 'identify', email: identity.email, user_id: identity.userId }); 435 }, 436 page: function () { pageView(false); } 437 }; 438 439 try { window.dispatchEvent(new Event('opinly:ready')); } catch (e) {} 440 441 var firstTouch = getFirstTouch(); 442 var isFirstVisit = !ls(FIRST_VISIT_KEY); 443 444 if (isFirstVisit) { 445 send({ 446 page_url: safePageUrl(), 447 utm_source: firstTouch.utm_source, 448 utm_medium: firstTouch.utm_medium, 449 utm_campaign: firstTouch.utm_campaign, 450 utm_term: firstTouch.utm_term,
451 utm_content: firstTouch.utm_content, 452 utm_id: firstTouch.utm_id, 453 referrer: firstTouch.referrer, 454 click_id: primaryClickId(firstTouch), 455 click_ids: firstTouch.click_ids 456 }); 457 ls(FIRST_VISIT_KEY, '1'); 458 } 459 460 initScrollTracking(); 461 pageView(false, isFirstVisit ? firstTouch : null); 462 463 function hookHistory(method) { 464 var orig = window.history[method]; 465 if (typeof orig !== 'function') return; 466 window.history[method] = function () { 467 var r = orig.apply(this, arguments); 468 pageView(true); 469 return r; 470 }; 471 } 472 hookHistory('pushState'); 473 hookHistory('replaceState'); 474 window.addEventListener('popstate', function () { pageView(true); }); 475 window.addEventListener('pageshow', function (e) { if (e.persisted) pageView(false); }); 476 477 document.addEventListener('click', onClick, true); 478 document.addEventListener('submit', onSubmit, true); 479 480 document.addEventListener('input', function (e) { 481 var t = e.target; 482 if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA')) t._opinlyDirty = true; 483 }, true); 484 document.addEventListener('focusout', function (e) { tryIdentify(e.target, false); }, true); 485 document.addEventListener('change', function (e) { tryIdentify(e.target, false); }, true); 486})();
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.