1(function () { 2 // ---- SINGLETON GUARD (dev'de script 2 kez yüklenmesin) ---- 3 if (window.__EA_INSTALLED__) { 4 console.warn("[EA] already installed, skipping duplicate load"); 5 return; 6 } 7 window.__EA_INSTALLED__ = true; 8 9 const BACKOFFS = [1000, 2000, 5000, 10000, 30000]; 10 const DEFAULTS = { 11 wsUrl: "ws://localhost:8080/ws/ingest", 12 tokenEndpoint: "/api/analyz/token", 13 batchMax: 20, 14 batchInterval: 250, 15 queueMax: 1500, 16 heartbeatInterval: 12000, 17 hoverThrottleMs: 2500, 18 sessionTimeoutMs: 30 * 60 * 1000, 19 sourceKeyInQuery: true, 20 }; 21 22 const STATE = { 23 ws: null, 24 cfg: null, 25 ready: false, 26 connecting: false, 27 reconnectTimer: null, 28 reconnectAttempt: 0, 29 queue: [], 30 flushTimer: null, 31 pingTimer: null, 32 heartbeatTimer: null, 33 lastHeartbeatAt: null, 34 baseContext: null, 35 hoverLast: new WeakMap(), 36 adState: new WeakMap(), 37 adObserver: null, 38 mutationObserver: null, 39 adImpressionKeys: new Set(), 40 adViewableKeys: new Set(), 41 maxScrollDepth: 0, 42 scrollTicking: false, 43 pendingPageMeta: null, 44 }; 45 46 let inMemoryVisitorId = null; 47 let inMemorySessionId = null; 48 49 // ---- UUID v4 (backend uuid.Parse bekliyor) ---- 50 function uuidv4() { 51 if (window.crypto && crypto.randomUUID) return crypto.randomUUID(); 52 53 const buf = new Uint8Array(16); 54 crypto.getRandomValues(buf); 55 buf[6] = (buf[6] & 0x0f) | 0x40; 56 buf[8] = (buf[8] & 0x3f) | 0x80; 57 58 const hex = Array.from(buf) 59 .map((b) => b.toString(16).padStart(2, "0")) 60 .join(""); 61 62 return ( 63 hex.slice(0, 8) + 64 "-" + 65 hex.slice(8, 12) + 66 "-" + 67 hex.slice(12, 16) + 68 "-" + 69 hex.slice(16, 20) + 70 "-" + 71 hex.slice(20) 72 ); 73 } 74 75 function nowIso() { 76 return new Date().toISOString(); 77 } 78 79 function isObject(val) { 80 return !!val && typeof val === "object" && !Array.isArray(val); 81 } 82 83 function mergeDeep(target, source) { 84 const out = Object.assign({}, target || {}); 85 Object.keys(source || {}).forEach((key) => { 86 const srcVal = source[key]; 87 const dstVal = out[key]; 88 if (isObject(srcVal) && isObject(dstVal)) { 89 out[key] = mergeDeep(dstVal, srcVal); 90 } else { 91 out[key] = srcVal; 92 } 93 }); 94 return out; 95 } 96 97 function safeGet(storage, key) { 98 try { 99 return storage.getItem(key); 100 } catch (_) { 101 return null; 102 } 103 } 104 105 function safeSet(storage, key, val) { 106 try { 107 storage.setItem(key, val); 108 } catch (_) { 109 // ignore 110 } 111 } 112 113 function getVisitorId() { 114 const key = "analyz_vid"; 115 let v = null; 116 117 if (typeof localStorage !== "undefined") { 118 v = safeGet(localStorage, key); 119 if (!v) { 120 v = uuidv4(); 121 safeSet(localStorage, key, v); 122 } 123 return v; 124 } 125 126 if (!inMemoryVisitorId) inMemoryVisitorId = uuidv4(); 127 return inMemoryVisitorId; 128 } 129 130 function getSessionId() { 131 const sidKey = "analyz_sid"; 132 const lastKey = "analyz_sid_last"; 133 const now = Date.now(); 134 const timeoutMs = 135 (STATE.cfg && STATE.cfg.sessionTimeoutMs) || DEFAULTS.sessionTimeoutMs; 136 137 if (typeof sessionStorage !== "undefined") { 138 let sid = safeGet(sessionStorage, sidKey); 139 const lastRaw = safeGet(sessionStorage, lastKey); 140 const last = lastRaw ? Number(lastRaw) : 0; 141 142 if (!sid || !last || now - last > timeoutMs) { 143 sid = uuidv4(); 144 safeSet(sessionStorage, sidKey, sid); 145 } 146 147 safeSet(sessionStorage, lastKey, String(now)); 148 return sid; 149 } 150 151 if (!inMemorySessionId) inMemorySessionId = uuidv4(); 152 return inMemorySessionId; 153 } 154 155 function touchSession() { 156 if (typeof sessionStorage === "undefined") return; 157 safeSet(sessionStorage, "analyz_sid_last", String(Date.now())); 158 } 159 160 function getCanonicalUrl() { 161 const link = document.querySelector('link[rel="canonical"]'); 162 if (link && link.href) return link.href; 163 return window.location.href; 164 } 165 166 function getPage() { 167 return { 168 url: getCanonicalUrl(), 169 hostname: window.location.hostname, 170 path: window.location.pathname, 171 title: document.title || "", 172 referrer: document.referrer || "", 173 search: window.location.search || "", 174 }; 175 } 176 177 function detectDevice(ua) { 178 if (/iPad|Tablet/i.test(ua)) return "tablet"; 179 if (/Mobi|Android/i.test(ua)) return "mobile"; 180 return "desktop"; 181 } 182 183 function detectOS(ua) { 184 if (/Windows NT/i.test(ua)) return "Windows"; 185 if (/Mac OS X/i.test(ua) && !/iPhone|iPad|iPod/i.test(ua)) return "macOS"; 186 if (/Android/i.test(ua)) return "Android"; 187 if (/iPhone|iPad|iPod/i.test(ua)) return "iOS"; 188 if (/Linux/i.test(ua)) return "Linux"; 189 return "Other"; 190 } 191 192 function detectBrowser(ua) {
193 if (/Edg\//i.test(ua)) return "Edge"; 194 if (/Chrome\//i.test(ua) && !/Edg\//i.test(ua)) return "Chrome"; 195 if (/Firefox\//i.test(ua)) return "Firefox"; 196 if (/Safari\//i.test(ua) && !/Chrome\//i.test(ua)) return "Safari"; 197 return "Other"; 198 } 199 200 function getBaseContext() { 201 if (STATE.baseContext) return STATE.baseContext; 202 203 const ua = navigator.userAgent || ""; 204 STATE.baseContext = { 205 device: detectDevice(ua), 206 os: detectOS(ua), 207 browser: detectBrowser(ua), 208 userAgent: ua, 209 language: navigator.language || "", 210 timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "", 211 }; 212 213 return STATE.baseContext; 214 } 215 216 function getContext() { 217 const base = getBaseContext(); 218 const screenSize = 219 typeof screen !== "undefined" && screen.width && screen.height 220 ? `${screen.width}x${screen.height}` 221 : ""; 222 const viewportSize = `${window.innerWidth || 0}x${window.innerHeight || 0}`; 223 224 return Object.assign({}, base, { 225 screen: screenSize, 226 viewport: viewportSize, 227 }); 228 } 229 230 function resolvePageMeta(overrides) { 231 let meta = {}; 232 233 if (STATE.cfg && typeof STATE.cfg.pageMeta === "function") { 234 try { 235 const res = STATE.cfg.pageMeta(getPage()); 236 if (isObject(res)) meta = mergeDeep(meta, res); 237 } catch (err) { 238 console.warn("[EA] pageMeta function failed", err); 239 } 240 } else if (STATE.cfg && isObject(STATE.cfg.pageMeta)) { 241 meta = mergeDeep(meta, STATE.cfg.pageMeta); 242 } 243 244 if (isObject(overrides)) { 245 meta = mergeDeep(meta, overrides); 246 } 247 248 if (!meta.pageType) meta.pageType = "static"; 249 if (!meta.routeKey) meta.routeKey = window.location.pathname || "/"; 250 251 return meta; 252 } 253 254 function buildEvent(typeKey, actionKey, meta) { 255 if (!STATE.cfg || !STATE.cfg.sourceKey) return null; 256 const pageMeta = resolvePageMeta(); 257 const mergedMeta = isObject(meta) ? mergeDeep(pageMeta, meta) : pageMeta; 258 259 const base = { 260 sourceKey: STATE?.cfg?.sourceKey, 261 secret: STATE?.cfg?.secret, 262 token: STATE?.cfg?.token, 263 visitorId: getVisitorId(), 264 sessionId: getSessionId(), 265 eventId: uuidv4(), 266 timestamp: nowIso(), 267 typeKey, 268 actionKey, 269 page: getPage(), 270 context: getContext(), 271 meta: mergedMeta, 272 }; 273 274 touchSession(); 275 return base; 276 } 277 278 function sendPayload(payload) { 279 if (!STATE.ws || STATE.ws.readyState !== 1) return false; 280 STATE.ws.send(JSON.stringify(payload)); 281 return true; 282 } 283 284 function queueEvent(event) { 285 if (!event) return; 286 if (!STATE.cfg) return; 287 288 STATE.queue.push(event); 289 290 if (STATE.queue.length > STATE.cfg.queueMax) { 291 STATE.queue.shift(); 292 } 293 294 scheduleFlush(); 295 } 296 297 function scheduleFlush() { 298 if (STATE.flushTimer) return; 299 STATE.flushTimer = setTimeout(flushQueue, STATE.cfg.batchInterval); 300 } 301 302 function flushQueue() { 303 STATE.flushTimer = null; 304 305 if (!STATE.ready) return; 306 if (!STATE.queue.length) return; 307 308 const maxBatch = STATE.cfg.batchMax; 309 310 while (STATE.queue.length) { 311 const batch = STATE.queue.splice(0, maxBatch); 312 const payload = 313 batch.length === 1 314 ? Object.assign({ op: "event" }, batch[0]) 315 : { op: "batch", events: batch }; 316 317 if (!sendPayload(payload)) { 318 STATE.queue = batch.concat(STATE.queue); 319 break; 320 } 321 } 322 323 if (STATE.queue.length) scheduleFlush(); 324 } 325 326 function clearReconnect() { 327 if (STATE.reconnectTimer) { 328 clearTimeout(STATE.reconnectTimer); 329 STATE.reconnectTimer = null; 330 } 331 } 332 333 function scheduleReconnect(reason) { 334 if (reason && reason.code === 1008) { 335 console.warn("[EA] stop reconnect on 1008 (policy violation)", reason); 336 return; 337 } 338 339 if (STATE.reconnectTimer) return; 340 341 STATE.reconnectAttempt = Math.min( 342 STATE.reconnectAttempt + 1, 343 BACKOFFS.length 344 ); 345 const delay = BACKOFFS[STATE.reconnectAttempt - 1] || BACKOFFS[BACKOFFS.length - 1]; 346 347 STATE.reconnectTimer = setTimeout(() => { 348 STATE.reconnectTimer = null; 349 connect().catch((e) => console.error("[EA] reconnect failed", e)); 350 }, delay); 351 } 352 353 async function fetchToken() { 354 if (!STATE.cfg.tokenEndpoint) return null; 355 const res = await fetch(STATE.cfg.tokenEndpoint, { credentials: "include" }); 356 const text = await res.text(); 357 358 if (!res.ok) { 359 throw new Error("token endpoint failed: " + res.status + " " + text); 360 } 361 362 const data = JSON.parse(text); 363 if (!data.token) throw new Error("token missing in response"); 364 return data.token; 365 } 366 367 function buildWsUrl() { 368 let wsUrl = STATE.cfg.wsUrl || DEFAULTS.wsUrl; 369 370 if (window.location.protocol === "https:" && wsUrl.startsWith("ws://")) { 371 wsUrl = wsUrl.replace("ws://", "wss://"); 372 } 373 374 if (STATE.cfg.sourceKeyInQuery && STATE.cfg.sourceKey) { 375 const hasQuery = wsUrl.includes("?"); 376 const joiner = hasQuery ? "&" : "?"; 377 wsUrl = `${wsUrl}${joiner}sourceKey=${encodeURIComponent(STATE.cfg.sourceKey)}`; 378 } 379 380 return wsUrl; 381 } 382 383 function handleMessage(evt) { 384 let msg = null; 385 try { 386 msg = JSON.parse(evt.data); 387 } catch (_) { 388 return; 389 } 390 391 if (!msg || !msg.op) return; 392 393 if (msg.op === "assign") { 394 if (msg.visitorId) { 395 if (typeof localStorage !== "undefined") { 396 safeSet(localStorage, "analyz_vid", msg.visitorId); 397 } else { 398 inMemoryVisitorId = msg.visitorId; 399 } 400 } 401 if (msg.sessionId) { 402 if (typeof sessionStorage !== "undefined") { 403 safeSet(sessionStorage, "analyz_sid", msg.sessionId); 404 } else { 405 inMemorySessionId = msg.sessionId; 406 } 407 } 408 return; 409 } 410 411 if (msg.op === "error") { 412 console.warn("[EA] server error", msg); 413 return; 414 } 415 } 416 417 function startPing() { 418 stopPing(); 419 STATE.pingTimer = setInterval(() => { 420 if (!STATE.ready) return; 421 sendPayload({ op: "ping" }); 422 }, 30000); 423 } 424 425 function stopPing() { 426 if (STATE.pingTimer) { 427 clearInterval(STATE.pingTimer); 428 STATE.pingTimer = null; 429 } 430 } 431 432 function heartbeatTick(reason) { 433 if (!STATE.ready) return; 434 435 const now = Date.now();
436 const last = STATE.lastHeartbeatAt || now; 437 const delta = Math.max(1, Math.round((now - last) / 1000)); 438 STATE.lastHeartbeatAt = now; 439 440 const meta = { 441 deltaSeconds: delta, 442 visible: document.visibilityState === "visible", 443 }; 444 445 const depth = 446 STATE.maxScrollDepth > 0 ? STATE.maxScrollDepth : getScrollDepthPercent(); 447 if (depth !== null) meta.scrollDepth = depth; 448 if (reason) meta.reason = reason; 449 450 queueEvent(buildEvent("engagement", "duration", meta)); 451 STATE.maxScrollDepth = 0; 452 } 453 454 function startHeartbeat() { 455 stopHeartbeat(); 456 STATE.lastHeartbeatAt = Date.now(); 457 STATE.heartbeatTimer = setInterval(heartbeatTick, STATE.cfg.heartbeatInterval); 458 } 459 460 function stopHeartbeat() { 461 if (STATE.heartbeatTimer) { 462 clearInterval(STATE.heartbeatTimer); 463 STATE.heartbeatTimer = null; 464 } 465 } 466 467 async function connect() { 468 if (!STATE.cfg) { 469 console.warn("[EA] connect called but cfg is empty"); 470 return; 471 } 472 if (STATE.connecting) return; 473 if (STATE.ws && (STATE.ws.readyState === 0 || STATE.ws.readyState === 1)) return; 474 475 STATE.connecting = true; 476 STATE.ready = false; 477 478 try { 479 if (!STATE.cfg.secret && STATE.cfg.tokenEndpoint) { 480 STATE.cfg.token = await fetchToken(); 481 } 482 483 const wsUrl = buildWsUrl(); 484 485 const ws = new WebSocket(wsUrl); 486 STATE.ws = ws; 487 488 ws.onopen = () => { 489 const hello = { 490 op: "hello", 491 sourceKey: STATE?.cfg?.sourceKey, 492 secret: STATE?.cfg?.secret, 493 token: STATE?.cfg?.token, 494 visitorId: getVisitorId(), 495 sessionId: getSessionId(), 496 context: getContext(), 497 }; 498 499 sendPayload(hello); 500 501 STATE.ready = true; 502 STATE.connecting = false; 503 STATE.reconnectAttempt = 0; 504 clearReconnect(); 505 506 startPing(); 507 startHeartbeat(); 508 flushQueue(); 509 }; 510 511 ws.onmessage = handleMessage; 512 513 ws.onerror = (e) => { 514 console.error("[EA] ws error", e); 515 }; 516 517 ws.onclose = (e) => { 518 console.warn("[EA] ws closed", { 519 code: e.code, 520 reason: e.reason, 521 wasClean: e.wasClean, 522 }); 523 524 STATE.ready = false; 525 STATE.connecting = false; 526 STATE.ws = null; 527 stopPing(); 528 stopHeartbeat(); 529 530 scheduleReconnect(e); 531 }; 532 } catch (err) { 533 console.error("[EA] connect failed", err); 534 STATE.connecting = false; 535 STATE.ready = false; 536 scheduleReconnect(); 537 } 538 } 539 540 function getScrollDepthPercent() { 541 const doc = document.documentElement; 542 if (!doc) return null; 543 544 const scrollTop = window.scrollY || doc.scrollTop || 0; 545 const viewport = window.innerHeight || 0; 546 const height = Math.max(doc.scrollHeight || 0, doc.offsetHeight || 0); 547 548 if (!height) return 0; 549 550 const depth = Math.min(1, (scrollTop + viewport) / height); 551 return Math.round(depth * 100); 552 } 553 554 function updateScrollDepth() { 555 const depth = getScrollDepthPercent(); 556 if (depth === null) return; 557 STATE.maxScrollDepth = Math.max(STATE.maxScrollDepth, depth); 558 } 559 560 function parseAnalyzMeta(el) { 561 const raw = el.getAttribute("data-analyz-meta"); 562 if (!raw) return null; 563 564 try { 565 const parsed = JSON.parse(raw); 566 return isObject(parsed) ? parsed : null; 567 } catch (_) { 568 return null; 569 } 570 } 571 572 function toNumberIfNumeric(val) { 573 if (val === null || val === undefined || val === "") return undefined; 574 if (/^\d+$/.test(String(val))) return Number(val); 575 return val; 576 } 577 578 function getAttr(el, name) { 579 return el.getAttribute(name) || ""; 580 } 581 582 function buildElementMeta(el) { 583 const type = getAttr(el, "data-analyz-type"); 584 const meta = {}; 585 586 if (type === "ad") { 587 meta.ad = { 588 slot: getAttr(el, "data-analyz-slot") || undefined, 589 campaignId: getAttr(el, "data-analyz-campaign") || undefined, 590 creativeId: getAttr(el, "data-analyz-creative") || undefined, 591 advertiser: getAttr(el, "data-analyz-advertiser") || undefined, 592 label: getAttr(el, "data-analyz-label") || undefined, 593 url: getAttr(el, "data-analyz-url") || undefined, 594 }; 595 } 596 597 if (type === "content") { 598 meta.content = { 599 id: toNumberIfNumeric( 600 getAttr(el, "data-analyz-content-id") || getAttr(el, "data-analyz-id") 601 ), 602 type: getAttr(el, "data-analyz-content-type") || undefined, 603 category: getAttr(el, "data-analyz-category") || undefined, 604 label: getAttr(el, "data-analyz-label") || undefined, 605 href: getAttr(el, "href") || undefined, 606 }; 607 } 608 609 const elementId = getAttr(el, "data-analyz-id") || undefined; 610 const elementLabel = getAttr(el, "data-analyz-label") || undefined; 611 const elementClasses = 612 typeof el.className === "string" && el.className.trim() 613 ? el.className.trim().slice(0, 120) 614 : undefined; 615 const elementText = 616 typeof el.textContent === "string" 617 ? el.textContent.trim().slice(0, 80) 618 : undefined; 619 620 if (type === "ui") { 621 meta.element = { 622 type: "ui", 623 id: elementId, 624 label: elementLabel, 625 classes: elementClasses, 626 text: elementText, 627 }; 628 } else if (elementId || elementLabel || elementClasses || elementText) { 629 meta.element = { 630 id: elementId, 631 label: elementLabel, 632 classes: elementClasses, 633 text: elementText, 634 }; 635 } 636 637 const extra = parseAnalyzMeta(el); 638 if (extra) { 639 return mergeDeep(meta, extra); 640 } 641 642 return meta; 643 } 644 645 function emitElementEvent(el, actionKey) { 646 const type = getAttr(el, "data-analyz-type"); 647 if (!type) return; 648 649 const meta = buildElementMeta(el); 650 651 if (type === "ad") { 652 queueEvent(buildEvent("ad_event", actionKey, meta)); 653 return; 654 } 655 656 if (type === "content") { 657 queueEvent(buildEvent("content_event", actionKey, meta)); 658 return; 659 } 660 661 if (type === "ui") { 662 queueEvent(buildEvent("ui_interaction", actionKey, meta)); 663 return; 664 } 665 666 queueEvent(buildEvent("ui_interaction", actionKey, meta)); 667 } 668 669 function resolveAnalyzElement(target) { 670 if (!target || !target.closest) return null; 671 return target.closest('[data-analyz="1"]'); 672 } 673 674 function installClickHover() { 675 document.addEventListener( 676 "click", 677 (e) => { 678 const el = resolveAnalyzElement(e.target); 679 if (!el) return; 680 emitElementEvent(el, "click"); 681 }, 682 true 683 ); 684 685 document.addEventListener( 686 "mouseover", 687 (e) => { 688 const el = resolveAnalyzElement(e.target); 689 if (!el) return; 690 691 const now = Date.now();
692 const last = STATE.hoverLast.get(el) || 0; 693 if (now - last < STATE.cfg.hoverThrottleMs) return; 694 STATE.hoverLast.set(el, now); 695 696 emitElementEvent(el, "hover"); 697 }, 698 true 699 ); 700 } 701 702 function adDedupeKey(el) { 703 const slot = getAttr(el, "data-analyz-slot"); 704 const creative = getAttr(el, "data-analyz-creative"); 705 const campaign = getAttr(el, "data-analyz-campaign"); 706 if (!slot && !creative && !campaign) return null; 707 return `${window.location.pathname}|${slot}|${creative}|${campaign}`; 708 } 709 710 function ensureAdState(el) { 711 let state = STATE.adState.get(el); 712 if (!state) { 713 state = { 714 impressionSent: false, 715 viewableSent: false, 716 viewableTimer: null, 717 lastRatio: 0, 718 }; 719 STATE.adState.set(el, state); 720 } 721 return state; 722 } 723 724 function sendAdImpression(el) { 725 const key = adDedupeKey(el); 726 if (key && STATE.adImpressionKeys.has(key)) return; 727 728 emitElementEvent(el, "impression"); 729 730 if (key) STATE.adImpressionKeys.add(key); 731 } 732 733 function sendAdViewable(el) { 734 const key = adDedupeKey(el); 735 if (key && STATE.adViewableKeys.has(key)) return; 736 737 emitElementEvent(el, "viewable"); 738 739 if (key) STATE.adViewableKeys.add(key); 740 } 741 742 function handleAdIntersection(entries) { 743 entries.forEach((entry) => { 744 const el = entry.target; 745 if (!el || getAttr(el, "data-analyz-type") !== "ad") return; 746 747 const state = ensureAdState(el); 748 state.lastRatio = entry.intersectionRatio || 0; 749 750 if (entry.isIntersecting && state.lastRatio > 0 && !state.impressionSent) { 751 state.impressionSent = true; 752 sendAdImpression(el); 753 } 754 755 if (state.viewableSent) return; 756 757 if (state.lastRatio >= 0.5) { 758 if (!state.viewableTimer) { 759 state.viewableTimer = setTimeout(() => { 760 state.viewableTimer = null; 761 if (!state.viewableSent && state.lastRatio >= 0.5) { 762 state.viewableSent = true; 763 sendAdViewable(el); 764 } 765 }, 1000); 766 } 767 } else if (state.viewableTimer) { 768 clearTimeout(state.viewableTimer); 769 state.viewableTimer = null; 770 } 771 }); 772 } 773 774 function registerAdElement(el) { 775 if (!el || getAttr(el, "data-analyz-type") !== "ad") return; 776 if (!STATE.adObserver) return; 777 STATE.adObserver.observe(el); 778 } 779 780 function scanForAds(root) { 781 if (!root || !root.querySelectorAll) return; 782 const nodes = root.querySelectorAll('[data-analyz="1"][data-analyz-type="ad"]'); 783 nodes.forEach((el) => registerAdElement(el)); 784 } 785 786 function installAdObservers() { 787 if (STATE.adObserver) return; 788 if (typeof IntersectionObserver === "undefined") return; 789 790 STATE.adObserver = new IntersectionObserver(handleAdIntersection, { 791 threshold: [0, 0.5], 792 }); 793 794 scanForAds(document); 795 796 STATE.mutationObserver = new MutationObserver((mutations) => { 797 mutations.forEach((mutation) => { 798 mutation.addedNodes.forEach((node) => { 799 if (node.nodeType !== 1) return; 800 if (node.matches && node.matches('[data-analyz="1"][data-analyz-type="ad"]')) { 801 registerAdElement(node); 802 } 803 scanForAds(node); 804 }); 805 }); 806 }); 807 808 if (document.body) { 809 STATE.mutationObserver.observe(document.body, { 810 childList: true, 811 subtree: true, 812 }); 813 } 814 } 815 816 function installLifecycleHandlers() { 817 document.addEventListener("visibilitychange", () => { 818 if (document.visibilityState === "hidden") { 819 heartbeatTick("hidden"); 820 stopHeartbeat(); 821 } else { 822 startHeartbeat(); 823 } 824 }); 825 826 window.addEventListener("beforeunload", () => { 827 heartbeatTick("beforeunload"); 828 flushQueue(); 829 }); 830 831 window.addEventListener("online", () => { 832 connect().catch((e) => console.error("[EA] reconnect on online failed", e)); 833 }); 834 835 window.addEventListener("scroll", () => { 836 if (STATE.scrollTicking) return; 837 STATE.scrollTicking = true; 838 window.requestAnimationFrame(() => { 839 updateScrollDepth(); 840 STATE.scrollTicking = false;
841 }); 842 }); 843 } 844 845 window.ErisimAnalytics = { 846 init(cfg) { 847 const sourceKey = cfg?.sourceKey || cfg?.siteKey || ""; 848 if (!sourceKey) { 849 console.warn("[EA] init missing sourceKey"); 850 } 851 852 const wsUrl = cfg.wsUrl || DEFAULTS.wsUrl; 853 854 STATE.cfg = { 855 sourceKey, 856 secret: cfg.secret || undefined, 857 token: cfg.token || undefined, 858 tokenEndpoint: 859 cfg.tokenEndpoint === null ? null : cfg.tokenEndpoint || DEFAULTS.tokenEndpoint, 860 wsUrl, 861 batchMax: cfg.batchMax || DEFAULTS.batchMax, 862 batchInterval: cfg.batchInterval || DEFAULTS.batchInterval, 863 queueMax: cfg.queueMax || DEFAULTS.queueMax, 864 heartbeatInterval: cfg.heartbeatInterval || DEFAULTS.heartbeatInterval, 865 hoverThrottleMs: cfg.hoverThrottleMs || DEFAULTS.hoverThrottleMs, 866 sessionTimeoutMs: cfg.sessionTimeoutMs || DEFAULTS.sessionTimeoutMs, 867 pageMeta: cfg.pageMeta || null, 868 sourceKeyInQuery: 869 typeof cfg.sourceKeyInQuery === "boolean" 870 ? cfg.sourceKeyInQuery 871 : DEFAULTS.sourceKeyInQuery, 872 }; 873 874 installClickHover(); 875 installAdObservers(); 876 installLifecycleHandlers(); 877 updateScrollDepth(); 878 879 connect().catch((e) => console.error("[EA] connect failed", e)); 880 881 if (cfg.autoPageView) { 882 this.page(); 883 } else if (STATE.pendingPageMeta) { 884 const pending = STATE.pendingPageMeta; 885 STATE.pendingPageMeta = null; 886 this.page(pending); 887 } 888 }, 889 890 page(meta) { 891 if (!STATE.cfg) { 892 STATE.pendingPageMeta = meta || {}; 893 return; 894 } 895 const pageMeta = resolvePageMeta(meta); 896 queueEvent(buildEvent("page_view", "view", pageMeta)); 897 }, 898 899 track(typeKey, actionKey, meta) { 900 if (!typeKey || !actionKey) return; 901 queueEvent(buildEvent(typeKey, actionKey, meta || {})); 902 }, 903 }; 904})();
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.