{ if(window.ZoomzillaAnalytics)return; const config=window.ZoomzillaAnalyticsConfig||{},endpoint=config.endpoint||location.origin; const uuid=()=>{if(crypto.randomUUID)return crypto.randomUUID();const b=crypto.getRandomValues(new Uint8Array(16));b[6]=(b[6]&15)|64;b[8]=(b[8]&63)|128;return Array.from(b,(v,i)=>([4,6,8,10].includes(i)?'-':'')+v.toString(16).padStart(2,'0')).join('')}; const random=()=>btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(32)))).replace(/\+/g,'-').replace(/\//g,'_').replace(/=+$/,''); const read=(storage,key,fallback)=>{try{const raw=storage().getItem(key);return raw&&raw.length<=65536?JSON.parse(raw):fallback}catch(_){return fallback}}; const write=(storage,key,value)=>{try{storage().setItem(key,JSON.stringify(value))}catch(_){}}; const local=()=>localStorage,session=()=>sessionStorage,browserKey='zoomzilla.analytics.browser.v1',queueKey='zoomzilla.analytics.delivery.v1'; let browser=read(local,browserKey,null)||read(session,browserKey,null); if(!/^[0-9a-f-]{36}$/.test(browser||''))browser=uuid(); write(local,browserKey,browser);write(session,browserKey,browser); const page=uuid(),queue=[];let token=null,contextTry=0,timer=null,sending=false,current=null,departureKey=null; const captureCandidate=config.capture||new URL(location.href).searchParams.get('azv'); let capture=/^[A-Za-z0-9_-]{43}$/.test(captureCandidate||'')?captureCandidate:null; const pageCapture=capture; function prune(){for(let i=queue.length-1;i>=0;i--)if(queue[i].tries>=5||Date.now()-queue[i].created>86400000)queue.splice(i,1);while(queue.length>64||JSON.stringify(queue).length>62000)queue.shift()} function persist(){prune();write(session,queueKey,{browser,endpoint,pending:queue})} function restoreQueue(){const saved=read(session,queueKey,null);if(saved?.browser!==browser||saved?.endpoint!==endpoint||!Array.isArray(saved.pending))return;for(const item of saved.pending.slice(-64)){if(!item||!item.event||typeof item.event.id!=='string'||JSON.stringify(item.event).length>1024||!Number.isFinite(item.created)||!Number.isInteger(item.tries)||item.tries<0)continue;const existing=queue.find(x=>x.event.id===item.event.id);if(existing)existing.tries=Math.max(existing.tries,item.tries);else queue.push(item)}prune()} restoreQueue(); const post=async(path,data,unload=false)=>{const controller=new AbortController(),timeout=setTimeout(()=>controller.abort(),5000);try{const response=await fetch(endpoint+'/api/analytics/public/'+path,{method:'POST',credentials:'omit',cache:'no-store',headers:{'Content-Type':unload?'text/plain;charset=UTF-8':'application/json'},body:JSON.stringify(data),signal:controller.signal,keepalive:true});if(!response.ok)throw new Error('analytics');return await response.json()}finally{clearTimeout(timeout)}}; function batch(){prune();const items=queue.slice(0,20);for(const item of items)item.tries++;persist();return items.map(item=>item.event)} function acknowledge(result){const accepted=new Set(result.accepted||[]);for(let i=queue.length-1;i>=0;i--)if(accepted.has(queue[i].event.id))queue.splice(i,1);persist()} const schedule=()=>{if(timer)return;const tries=queue.length?Math.max(...queue.map(item=>item.tries)):0;timer=setTimeout(()=>{timer=null;flush()},Math.min(30000,500*2**tries)+Math.random()*500)}; function contextBody(events,extra={}){const oldCapture=events.find(event=>event.capture)?.capture;return {browser,...((capture||oldCapture)?{capture:capture||oldCapture}:{}),...(current?{handoff:current.handoff,journey:current.journey}:{}),...extra,events}} async function connect(extra={}){ const requestedBrowser=browser,events=batch(); try{const result=await post('context',contextBody(events,extra));if(browser!==requestedBrowser)return;token=result.token;contextTry=0;capture=null;acknowledge(result);flush()}catch(_){if(contextTry++<5)setTimeout(()=>connect(extra),Math.min(30000,500*2**contextTry)+Math.random()*500)} } async function flush(){ if(sending||!token||!queue.length)return;sending=true; try{acknowledge(await post('events',{token,events:batch()}))}catch(_){} sending=false;persist();if(queue.length)schedule(); } function depart(){ persist();if(!queue.length)return; const key=queue.map(item=>item.event.id).join(':');if(key===departureKey)return;departureKey=key; const events=batch(); // fetch keepalive отправляется синхронно; ни pagehide, ни навигация его не ждут. const delivery=token?post('events',{token,events},true):post('context',contextBody(events),true); delivery.then(acknowledge).catch(()=>{}); } function emit(kind,details={},operation=current){ const event={id:uuid(),page_id:page,kind,at:new Date().toISOString(),...(operation?.journey?{journey_id:operation.journey}:(config.operationProof?{operation_proof:config.operationProof}:{})),...(pageCapture?{capture:pageCapture}:{}),...details}; if(JSON.stringify(event).length>1024)return;queue.push({event,tries:0,created:Date.now()});persist();schedule(); } window.ZoomzillaAnalytics={emit,uuid, begin(saved){if(saved?.browser&&saved.browser!==browser){browser=saved.browser;token=null;write(local,browserKey,browser);write(session,browserKey,browser)}current=saved||{journey:uuid(),handoff:random(),browser};if(current.browser===browser)connect({handoff:current.handoff,journey:current.journey});return {...current}}, restore(saved){if(saved)this.begin(saved);return current}, metadata(){return current?{...current}:null}, append(data,operation=current){if(operation?.browser===browser){if(token)data.append('analytics_context',token);data.append('analytics_journey',operation.journey);data.append('analytics_handoff',operation.handoff)}}, headers(operation=current){return token&&operation?.browser===browser?{'X-Analytics-Context':token,'X-Analytics-Journey':operation.journey}:{}}, handoff(operation=current){return operation?.handoff||null} }; let visited=false; function visible(){if(config.observePage===false||visited||document.visibilityState!=='visible'||!document.body)return;visited=true;emit('visit',config.entryProof?{entry_proof:config.entryProof}:{},null);if(config.join)emit('join_visit',{},null);if(config.accountProof)emit('account_visit',{account_proof:config.accountProof},null)} document.addEventListener('visibilitychange',()=>{if(document.visibilityState==='hidden')depart();else{departureKey=null;visible()}}); document.addEventListener('DOMContentLoaded',visible);window.addEventListener('pagehide',depart); window.addEventListener('pageshow',()=>{restoreQueue();visible();flush()});visible();connect(); })(); JS;