duration-tracker-test
Version:
页面元素曝光/时长统计插件
2 lines (1 loc) • 8.37 kB
JavaScript
class t{constructor(e={}){this.options=Object.assign({threshold:.5,interval:5e3,callback:null,resetStayTimeKey:"stay_time",resetIdKey:"id",takeValueType:"round",reportType:["expose"]},e),this.el=this.options.el;const s=this.options.reportType;if(!Array.isArray(s)||!s.length)throw new Error("上报类型格式错误");const i=s.includes("expose"),o=s.includes("totalTime");if(i&&o?(this.isWatcheExpose=!0,this.isWatchAll=!0):(this.isWatcheExpose=i,this.isWatchTotalTime=o),i){if(!this.el||0===this.el.length)throw new Error("未获取到元素");this.items=this.el.length?Array.from(this.el):[this.el]}this.tRecordTime=Date.now(),this.timesObject={},this.pTotalTimer=null,this._pageHideHandle=null,this._pageShowHandle=null,this._throttleRestart=null,this._throttlePageHideHandle=null,this._closePage=null,this.isWatchPageStatus=!0,this.pageStatus="show",this.intervalSec=this.options.interval/1e3,this.MathType={ceil:Math.ceil,floor:Math.floor,round:Math.round}[this.options.takeValueType];const{os:r,os_version:a}=t.getOSInfo(),{browser:n,browser_version:h}=t.getBrowserInfo();this.baseExposure={page_url:window.location.href,referrer:document.referrer,device_type:t.getDeviceType(),os:r,os_version:a,browser:n,browser_version:h,screenWidth:screen.width,screenHeight:screen.height,networkType:navigator.connection?.type||"unknown"},this.Init()}Init(){(this.isWatcheExpose||this.isWatchAll)&&(this.eInitObserver(),this.eReportExposeTime()),this.isWatchTotalTime&&this.tPeportTotalTime(),this.initWatchPageStatus()}eInitObserver(){if(!window.IntersectionObserver)return void console.warn("IntersectionObserver not supported, fallback needed.");this.observer=new IntersectionObserver(t=>{try{for(const e of t){const t=e.target.dataset[this.options.resetIdKey];if(!t){console.error("监听元素上需要绑定 data-id 或 data-[resetIdKey] 属性");continue}const{start:s,visible:i}=this.timesObject[t],o=Date.now();e.isIntersecting?(this.timesObject[t].start=o,this.timesObject[t].visible=!0):i&&s&&(this.timesObject[t].stage+=this.MathType((o-s)/1e3),this.timesObject[t].start=null,this.timesObject[t].visible=!1)}}catch(t){console.error("IntersectionObserver error:",t)}},{threshold:this.options.threshold}),this.items.forEach(t=>{const e=t.dataset[this.options.resetIdKey];return this.timesObject[e]={visible:!1,start:null,stage:0,...t.dataset},this.observer.observe(t)})}initWatchPageStatus(){this._pageHideHandle=()=>{this.pageStatus="hide",this.isWatchPageStatus&&this.innerDestroy()},this._pageShowHandle=()=>{this.pageStatus="show",this.isWatchPageStatus&&this.restartWatch()},this._throttleRestart=t._throttle_(this._pageShowHandle,1e3),this._throttlePageHideHandle=t._throttle_(this._pageHideHandle,1e3),document.addEventListener("visibilitychange",this._onVisibilityChange=()=>{"hidden"===document.visibilityState?this._throttlePageHideHandle():this._throttleRestart()}),window.addEventListener("beforeunload",this._closePage=()=>{this._throttlePageHideHandle()}),window.addEventListener("pagehide",this._onPageHide=t=>{t.persisted&&this._throttlePageHideHandle()}),window.addEventListener("pageshow",this._onPageShow=t=>{t.persisted&&this._throttleRestart()})}eCountExposeTime(){const t=[],e={};if(Object.keys(this.timesObject).forEach(e=>{const{start:s,stage:i,visible:o,...r}=this.timesObject[e];o&&s?(t.push({...r,[this.options.resetStayTimeKey]:this.MathType((Date.now()-s)/1e3)+i}),this.timesObject[e].start=Date.now()):i&&t.push({...r,[this.options.resetStayTimeKey]:i}),this.timesObject[e].stage=0}),this.isWatchAll){const s=Date.now(),i=this.MathType((s-this.tRecordTime)/1e3);this.tRecordTime=s,e.totalTime={[this.options.resetStayTimeKey]:i},e.expose=t}this.options.callback&&this.options.callback(this.isWatchAll?e:t,{baseInfo:{...this.baseExposure,timestamp:Date.now()},pageStatus:this.pageStatus})}eReportExposeTime(){clearInterval(this.pTotalTimer),this.pTotalTimer=setInterval(()=>{this.eCountExposeTime()},this.options.interval)}tCountTotalTime(){if(!this.tRecordTime)return;const t=Date.now(),e=this.MathType((t-this.tRecordTime)/1e3);e>0&&(this.options.callback&&this.options.callback({[this.options.resetStayTimeKey]:e},{baseInfo:{...this.baseExposure,timestamp:t},pageStatus:this.pageStatus}),this.tRecordTime=t)}tPeportTotalTime(){clearInterval(this.pTotalTimer),this.pTotalTimer=setInterval(()=>{this.tCountTotalTime()},this.options.interval)}runReport(){(this.isWatcheExpose||this.isWatchAll)&&this.eCountExposeTime(),this.isWatchTotalTime&&this.tCountTotalTime()}restartWatch(t){this.pTotalTimer||this.observer?console.warn("正在监听中"):(t&&(this.items=t.length?Array.from(t):[t],this.items.forEach(t=>{const e=t.dataset[this.options.resetIdKey];return this.timesObject[e]||(this.timesObject[e]={visible:!1,start:null,stage:0,...t.dataset}),this.observer.observe(t)})),this.isWatchPageStatus||(this.initWatchPageStatus(),this.isWatchPageStatus=!0),(this.isWatcheExpose||this.isWatchAll)&&(this.eInitObserver(),this.eReportExposeTime()),this.isWatchTotalTime&&(this.tRecordTime=Date.now(),this.tPeportTotalTime()))}updatedDomWatch(t){if(!t)throw new Error("未获取到新元素");this.items=t.length?Array.from(t):[t],this.items.forEach(t=>{const e=t.dataset[this.options.resetIdKey];return this.timesObject[e]||(this.timesObject[e]={visible:!1,start:null,stage:0,...t.dataset}),this.observer.observe(t)})}destroyWatch(){this.isWatchPageStatus=!1,this.innerDestroy(),document.removeEventListener("visibilitychange",this._onVisibilityChange),window.removeEventListener("beforeunload",this._throttlePageHideHandle),window.removeEventListener("pagehide",this._onPageHide),window.removeEventListener("pageshow",this._onPageShow),this._onVisibilityChange=this._onPageHide=this._onPageShow=this._closePage=null,this._throttleRestart=this._throttlePageHideHandle=null}innerDestroy(){if(!this.observer&&!this.pTotalTimer)return console.warn("监听已销毁");this.runReport(),this.observer&&this.observer.disconnect(),this.pTotalTimer&&clearInterval(this.pTotalTimer),this.timesObject={},this.tRecordTime=this.pTotalTimer=this.observer=null}static getDeviceType(){const t=navigator.userAgent;return/mobile|android|iphone|ipod|blackberry|iemobile|opera mini/i.test(t)?"手机":/ipad|tablet|playbook|silk/i.test(t)?"平板":"电脑"}static getOSInfo(){const t=navigator.userAgent;let e="Unknown",s="";if(/windows phone/i.test(t))e="Windows Phone";else if(/win/i.test(t)){e="Windows";const i=t.match(/Windows NT ([\d.]+)/);i&&(s=i[1])}else if(/android/i.test(t)){e="Android";const i=t.match(/Android ([\d.]+)/);i&&(s=i[1])}else if(/iphone|ipad|ipod/i.test(t)){e="iOS";const i=t.match(/OS ([\d_]+)/i);i&&(s=i[1].replace(/_/g,"."))}else if(/mac/i.test(t)){e="MacOS";const i=t.match(/Mac OS X ([\d_]+)/);i&&(s=i[1].replace(/_/g,"."))}else/linux/i.test(t)&&(e="Linux");return{os:e,os_version:s}}static getBrowserInfo(){const t=navigator.userAgent;let e="Unknown",s="";if(/chrome|crios/i.test(t)&&!/edge|edg|opr|opera/i.test(t)){e="Chrome";const i=t.match(/(?:chrome|crios)\/([\d.]+)/i);i&&(s=i[1])}else if(/edg/i.test(t)){e="Edge";const i=t.match(/edg\/([\d.]+)/i);i&&(s=i[1])}else if(/firefox|fxios/i.test(t)){e="Firefox";const i=t.match(/(?:firefox|fxios)\/([\d.]+)/i);i&&(s=i[1])}else if(/safari/i.test(t)&&!/chrome|crios|android/i.test(t)){e="Safari";const i=t.match(/version\/([\d.]+)/i);i&&(s=i[1])}else if(/opr|opera/i.test(t)){e="Opera";const i=t.match(/(?:opr|opera)[\/ ]([\d.]+)/i);i&&(s=i[1])}else if(/msie|trident/i.test(t)){e="IE";const i=t.match(/(?:msie |rv:)([\d.]+)/i);i&&(s=i[1])}return{browser:e,browser_version:s}}static _debounce_(t,e){let s;return(...i)=>{clearTimeout(s),s=setTimeout(()=>{t.apply(this,i)},e)}}static _throttle_(t,e){let s=0;return(...i)=>{const o=Date.now();o-s>=e&&(t.apply(this,i),s=o)}}}function e(t,e,s){if("object"!=typeof e||null===e)throw new Error("入参数据必须是对象或数组格式");const i={beacon:s=>{const i=new Blob([JSON.stringify(e)],{type:"application/json"});s(navigator.sendBeacon(t,i))},fetch:(s,i)=>{fetch(t,{method:"POST",body:JSON.stringify(e),headers:{"Content-Type":"application/json"},keepalive:!0}).then(t=>{t.ok?s(t.json()):i(new Error("请求失败: "+t.status))}).catch(i)}};return new Promise((t,e)=>{s&&i[s]?i[s](t,e):"function"==typeof navigator.sendBeacon?i.beacon(t,e):i.fetch(t,e)}).catch(t=>{})}export{t as DurationTracker,e as sendTrackingRequest};