duration-tracker-test
Version:
页面元素曝光/时长统计插件
2 lines (1 loc) • 8.6 kB
JavaScript
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).durationTracker={})}(this,function(t){"use strict";class e{constructor(t={}){this.options=Object.assign({threshold:.5,interval:5e3,callback:null,resetStayTimeKey:"stay_time",resetIdKey:"id",takeValueType:"round",reportType:["expose"]},t),this.el=this.options.el;const i=this.options.reportType;if(!Array.isArray(i)||!i.length)throw new Error("上报类型格式错误");const s=i.includes("expose"),o=i.includes("totalTime");if(s&&o?(this.isWatcheExpose=!0,this.isWatchAll=!0):(this.isWatcheExpose=s,this.isWatchTotalTime=o),s){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}=e.getOSInfo(),{browser:n,browser_version:h}=e.getBrowserInfo();this.baseExposure={page_url:window.location.href,referrer:document.referrer,device_type:e.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:i,visible:s}=this.timesObject[t],o=Date.now();e.isIntersecting?(this.timesObject[t].start=o,this.timesObject[t].visible=!0):s&&i&&(this.timesObject[t].stage+=this.MathType((o-i)/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=e._throttle_(this._pageShowHandle,1e3),this._throttlePageHideHandle=e._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:i,stage:s,visible:o,...r}=this.timesObject[e];o&&i?(t.push({...r,[this.options.resetStayTimeKey]:this.MathType((Date.now()-i)/1e3)+s}),this.timesObject[e].start=Date.now()):s&&t.push({...r,[this.options.resetStayTimeKey]:s}),this.timesObject[e].stage=0}),this.isWatchAll){const i=Date.now(),s=this.MathType((i-this.tRecordTime)/1e3);this.tRecordTime=i,e.totalTime={[this.options.resetStayTimeKey]:s},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",i="";if(/windows phone/i.test(t))e="Windows Phone";else if(/win/i.test(t)){e="Windows";const s=t.match(/Windows NT ([\d.]+)/);s&&(i=s[1])}else if(/android/i.test(t)){e="Android";const s=t.match(/Android ([\d.]+)/);s&&(i=s[1])}else if(/iphone|ipad|ipod/i.test(t)){e="iOS";const s=t.match(/OS ([\d_]+)/i);s&&(i=s[1].replace(/_/g,"."))}else if(/mac/i.test(t)){e="MacOS";const s=t.match(/Mac OS X ([\d_]+)/);s&&(i=s[1].replace(/_/g,"."))}else/linux/i.test(t)&&(e="Linux");return{os:e,os_version:i}}static getBrowserInfo(){const t=navigator.userAgent;let e="Unknown",i="";if(/chrome|crios/i.test(t)&&!/edge|edg|opr|opera/i.test(t)){e="Chrome";const s=t.match(/(?:chrome|crios)\/([\d.]+)/i);s&&(i=s[1])}else if(/edg/i.test(t)){e="Edge";const s=t.match(/edg\/([\d.]+)/i);s&&(i=s[1])}else if(/firefox|fxios/i.test(t)){e="Firefox";const s=t.match(/(?:firefox|fxios)\/([\d.]+)/i);s&&(i=s[1])}else if(/safari/i.test(t)&&!/chrome|crios|android/i.test(t)){e="Safari";const s=t.match(/version\/([\d.]+)/i);s&&(i=s[1])}else if(/opr|opera/i.test(t)){e="Opera";const s=t.match(/(?:opr|opera)[\/ ]([\d.]+)/i);s&&(i=s[1])}else if(/msie|trident/i.test(t)){e="IE";const s=t.match(/(?:msie |rv:)([\d.]+)/i);s&&(i=s[1])}return{browser:e,browser_version:i}}static _debounce_(t,e){let i;return(...s)=>{clearTimeout(i),i=setTimeout(()=>{t.apply(this,s)},e)}}static _throttle_(t,e){let i=0;return(...s)=>{const o=Date.now();o-i>=e&&(t.apply(this,s),i=o)}}}t.DurationTracker=e,t.sendTrackingRequest=function(t,e,i){if("object"!=typeof e||null===e)throw new Error("入参数据必须是对象或数组格式");const s={beacon:i=>{const s=new Blob([JSON.stringify(e)],{type:"application/json"});i(navigator.sendBeacon(t,s))},fetch:(i,s)=>{fetch(t,{method:"POST",body:JSON.stringify(e),headers:{"Content-Type":"application/json"},keepalive:!0}).then(t=>{t.ok?i(t.json()):s(new Error("请求失败: "+t.status))}).catch(s)}};return new Promise((t,e)=>{i&&s[i]?s[i](t,e):"function"==typeof navigator.sendBeacon?s.beacon(t,e):s.fetch(t,e)}).catch(t=>{})}});