轻量级埋点sdk搭建

引言

借助埋点监控sdk,我们可以统计用户的点击,页面pv、uv,脚本错误、dom上报等关键信息等。

一:项目初始化

1.技术栈

  • Ts
  • rollup打包工具

2.搭建项目

sql 复制代码
npm init -y 
tsc --init
npm install typescript -D
npm install rollup-plugin-dts -D
npm install rollup-plugin-typescript2 -D
  • dist:打包产物
  • service:后端服务
  • index.html:测试埋点

3.配置package.json打包命令

json 复制代码
"scripts": {
    "dev": "rollup -c"
  },

4.配置rollup.config.js

javascript 复制代码
import ts from 'rollup-plugin-typescript2'
import path from 'path'
import dts from 'rollup-plugin-dts';
export default [{
    //入口文件
    input: "./src/core/index.ts",
    output: [
        //打包esModule
        {
            file: path.resolve(__dirname, './dist/index.esm.js'),
            format: "es",
            name:'tracker'
        },
         //打包common js
        {
            file: path.resolve(__dirname, './dist/index.cjs.js'),
            format: "cjs"
        },
       //打包 AMD CMD UMD
        {
            input: "./src/core/index.ts",
            file: path.resolve(__dirname, './dist/index.js'),
            format: "umd",
            name: "tracker"
        }

    ],
    //配置ts
    plugins: [
        ts(),
    ]

}, {
    //打包声明文件
    input: "./src/core/index.ts",
    output:{
        file: path.resolve(__dirname, './dist/index.d.ts'),
        format: "es",
    },
    plugins: [dts()]
}] 

5.测试

在rollup.config.js配置中,我们将src/core/index.ts作为入口文件。因此随便在index.ts文件写点代码 然后在终端中输入:npm run dev。观察dist目录输出的打包产物。

arduino 复制代码
console.log("test")

二:埋点设计

1. 类型定义:src/type/index.ts

typescript 复制代码
/**
 * @requestUrl 接口地址
 * @historyTracker history上报
 * @hashTracker hash上报
 * @domTracker 携带Tracker-key 点击事件上报
 * @sdkVersionsdk版本
 * @extra透传字段
 * @jsError js 和 promise 报错异常上报
*/
export interface DefaultOptons {
  uuid: string | undefined,
  requestUrl: string | undefined,
  historyTracker: boolean,
  hashTracker: boolean,
  domTracker: boolean,
  sdkVersion: string | number,
  extra: Record<string, any> | undefined,
  jsError:boolean,
  whiteScreen:boolean
}

//用户必传参数
export interface Options extends Partial<DefaultOptons> {
  requestUrl: string
}

//版本号
export enum Trackerversion {
  version = "1.0.0"
}

2. 埋点类设计:src/core/index.ts

埋点监控主要对以下行为进行监听

  1. 指定的dom

  2. js错误

  3. pv

    csharp 复制代码
     export default class Tracker {
         private data:Options
    
         public constructor(options:Options) {
    
         }
    
         //dom上报
         private domTracker() {
    
         }
    
         //js错误上报
         private jsError() {
         }
    
         //pv上报
         private pv(){
         }
    
         //数据上报到后端
         private sendData<T>(data:T) {
         }
     }

3.对dom的监控上报

对dom的监控上报设计规则如下:监控页面html节点中指定dom的操作事件,例如监听此类dom的移入、移出、点击事件等。

typescript 复制代码
export default class Tracker {
        private data:Options
        
        //dom监听的事件列表
        private eventList:string[] = ['click', 'dblclick', 'contextmenu', 'mousedown', 'mouseup', 'mouseenter', 'mouseout', 'mouseover']
        public constructor(options:Options) {

        }

        //dom上报
        private domTracker() {
            this.eventList.forEach(item=>{
              window.addEventListener(item,e=>{
                let element = e.target as HTMLElement
                let isTarget = element.getAttribute('target-key')
                if(isTarget) {
                  this.sendData({type:'dom'})
                }
              })
            })
        }

        //js错误上报
        private jsError() {
        }

        //pv上报
        private pv(){
        }

        //数据上报到后端
        private sendData<T>(data:T) {
        }
   }
   

4.对js错误的监听

js的错误可以划分为

  • 逻辑错误
  • 资源加载错误
  • promise错误

逻辑错误监听

js中的逻辑表达式错误都可以通过window.addEventListener('error',fn)去捕获

javascript 复制代码
window.addEventListener("error",e=>{
    console.log(e)
})
console.log(a)

资源加载错误

资源加载错误最常见的就是页面的图片、图标等资源连接丢失,这种错误也是通过window.addEventListener('error',fn)去捕获,为了区分逻辑错误,可以通过ErrorEvent判断当前错误类型,逻辑错误事件的原型链上存在ErrorEvent

php 复制代码
window.addEventListener('error',e=>{
  e.preventDefault();
  const isErrorEvent:boolean = e instanceof ErrorEvent//判断错误类型
  if(!isErrorEvent) {
    this.sendData({type:'resource',msg:e.message})//资源加载错误
    return 
  }
  this.sendData({type:'js',msg:e.message})//js错误
},true)

Promise错误

promise错误通过unhandledrejection进行捕获,值得注意的是promise的错误分为两种

promise内部产生的错误

promise的reject状态未捕获

javascript 复制代码
window.addEventListener('unhandledrejection',(e:PromiseRejectionEvent)=>{
  e.preventDefault()
  e.promise.catch((error)=>{
    let msg = error?.message || error//区分promise的两种错误消息
    this.sendData({type:'promise',msg})
  })
})

完整代码

typescript 复制代码
    export default class Tracker {
        private data:Options
        
        //dom监听的事件列表
        private eventList:string[] = ['click', 'dblclick', 'contextmenu', 'mousedown', 'mouseup', 'mouseenter', 'mouseout', 'mouseover']
        public constructor(options:Options) {

        }

        //dom上报
        private domTracker() {
            this.eventList.forEach(item=>{
              window.addEventListener(item,e=>{
                let element = e.target as HTMLElement
                let isTarget = element.getAttribute('target-key')
                if(isTarget) {
                  this.sendData({type:'dom'})
                }
              })
            })
        }

        //js错误上报
        private jsError() {
           //1.脚本错误,资源错误
            window.addEventListener('error',e=>{
              e.preventDefault();
              const isErrorEvent:boolean = e instanceof ErrorEvent
              if(!isErrorEvent) {//资源错误
                this.sendData({type:'resource',msg:e.message})
                return 
              }
              this.sendData({type:'js',msg:e.message})
            },true)
            //2.promise错误
            window.addEventListener('unhandledrejection',(e:PromiseRejectionEvent)=>{
              e.preventDefault()
              e.promise.catch((error)=>{
                let msg = error?.message || error
                this.sendData({type:'promise',msg})
              })
            })
        }

        //pv上报
        private pv(){
        }

        //数据上报到后端
        private sendData<T>(data:T) {
        }
   }
   

5.对pv的监听

页面的访问量监听我们可以通过监听history和hash两种路由去实现数据监听上报。

hash路由监听

hash路由的监听直接使用hashchange事件进行监听

javascript 复制代码
window.addEventListener('hashchange',e=>{
    this.sendData({type:'history',msg:e})
 })
 

history路由监听

与hash路由不同的是,history路由模式下无法通过addEventListener去监听其路由改变。因此只能通过自定义事件去监听history路由改变。设计思路如下:首先history路由的跳转只能通过pushState、replaceState去操作,那么我们可以重写history路由的pushState、replaceState,在其在完成路由跳转的同时触发自定义事件进行pv的统计。总结如下

  1. 自定义pushState、replaceState事件,自定义事件名称可以不唯一,此处为了与路由方法保持统一。
  2. 重写history路由的pushState、replaceState方法
  3. 保留原有方法的功能(否则无法完成路由跳转),同时触发步骤0定义的事件,完成数据的上报。

如何自定义事件?

  1. 创建自定义事件对象

  2. 通过addEventListener监听自定义事件

  3. 在执行某些操作时派发自定义事件,此时步骤2可以捕获到。

    javascript 复制代码
     <button>click</button>
     
     const e = new Event('testEvent');
     window.addEventListener('testEvent',e=>{
       console.log('捕获到自定义事件啦');
     })  
     function btnClick() {
       window.dispatchEvent(e)
     }

按照上述步骤我们创建了自定义事件testEvent,当按钮被点击时就可以拦截到自定义事件。

history路由监听代码

typescript 复制代码
    export default class Tracker {
        private data:Options
        
        //dom监听的事件列表
        private eventList:string[] = ['click', 'dblclick', 'contextmenu', 'mousedown', 'mouseup', 'mouseenter', 'mouseout', 'mouseover']
        public constructor(options:Options) {

        }

        //dom上报
        private domTracker() {
            this.eventList.forEach(item=>{
              window.addEventListener(item,e=>{
                let element = e.target as HTMLElement
                let isTarget = element.getAttribute('target-key')
                if(isTarget) {
                  this.sendData({type:'dom'})
                }
              })
            })
        }

        //js错误上报
        private jsError() {
           //1.脚本错误,资源错误
            window.addEventListener('error',e=>{
              e.preventDefault();
              const isErrorEvent:boolean = e instanceof ErrorEvent
              if(!isErrorEvent) {//资源错误
                this.sendData({type:'resource',msg:e.message})
                return 
              }
              this.sendData({type:'js',msg:e.message})
            },true)
            //2.promise错误
            window.addEventListener('unhandledrejection',(e:PromiseRejectionEvent)=>{
              e.preventDefault()
              e.promise.catch((error)=>{
                let msg = error?.message || error
                this.sendData({type:'promise',msg})
              })
            })
        }

        //pv上报
        private pv(){
            //hash监听
            window.addEventListener('hashchange',e=>{
              this.sendData({type:'hash',msg:e})
            })
        
            //history监听
            this.histroryType.forEach((item:keyof History)=>{
            let origin = history[item];
            let eventHistory = new Event(item);
            (window.history[item] as any) = function(this:any){
              origin.apply(this,arguments);
              window.dispatchEvent(eventHistory)
            }
            window.addEventListener(item,()=>{
              this.sendData({type:'history',msg:item})
            })
          })
        }

        //数据上报到后端
        private sendData<T>(data:T) {
        }
   }

6.完整代码

typescript 复制代码
import { DefaultOptons,Options,Trackerversion } from "./../type/index"
export default class Tracker {
  private data:Options
  private histroryType:Partial<keyof History>[]
  private eventList:string[] = ['click', 'dblclick', 'contextmenu', 'mousedown', 'mouseup', 'mouseenter', 'mouseout', 'mouseover']
  public constructor(options:Options) {
    this.histroryType = ['pushState','replaceState']
    this.data = Object.assign(this.initConfig(),options);//初始化配置对象
    this.installExtra()
  }
  private domTracker() {
    this.eventList.forEach(item=>{
      window.addEventListener(item,e=>{
        let element = e.target as HTMLElement
        let isTarget = element.getAttribute('target-key')
        if(isTarget) {
          this.sendData({type:'dom'})
        }
      })
    })
  }
  
  //数据上报
  private sendData<T>(data:T) {
    const params = Object.assign({},data,{time:Date.now()})
    let headers = {
      type: 'application/x-www-form-urlencoded'
    };
    let blob = new Blob([JSON.stringify(params)], headers);
    console.log('blob',blob)
    navigator.sendBeacon(this.data.requestUrl,blob)
  } 

  private jsError() {
    //1.脚本错误,资源错误
    window.addEventListener('error',e=>{
      e.preventDefault();
      const isErrorEvent:boolean = e instanceof ErrorEvent
      if(!isErrorEvent) {//资源错误
        this.sendData({type:'resource',msg:e.message})
        return 
      }
      this.sendData({type:'js',msg:e.message})
    },true)
    //2.promise错误
    window.addEventListener('unhandledrejection',(e:PromiseRejectionEvent)=>{
      e.preventDefault()
      e.promise.catch((error)=>{
        let msg = error?.message || error
        this.sendData({type:'promise',msg})
      })
    })
  }
  //定制功能
  public installExtra() {
    //history
    if(this.data.historyTracker) {
      this.histroryType.forEach((item:keyof History)=>{
        let origin = history[item];
        let eventHistory = new Event(item);
        (window.history[item] as any) = function(this:any){
          origin.apply(this,arguments);
          window.dispatchEvent(eventHistory)
        }
        window.addEventListener(item,()=>{
          this.sendData({type:'history',msg:item})
        })
      })
    }
    //hash
    if(this.data.hashTracker) {
      window.addEventListener('hashchange',e=>{
        this.sendData({type:'hash',msg:e})
      })
    }
    //dom手动上报
    if(this.data.domTracker) {
      this.domTracker();
    }
    //jsError
    if(this.data.jsError) {
      this.jsError()
    }
  }

  //初始化配置项
  private initConfig():DefaultOptons {
    return <DefaultOptons>{
      sdkVersion:Trackerversion.version,
      historyTracker:false,
      hashTracker:false,
      domTracker:false,
      jsError:false,
    }
  }
}

三:测试使用

后端服务

创建后端文件夹service,终端输入

csharp 复制代码
npm init -y
npm install express cors -S

创建index.js

javascript 复制代码
const express = require('express')
const cors = require('cors')

const app = express()

app.use(cors())
app.use(express.urlencoded({ extended: false }))

app.post('/tracker', (req, res) => {
  console.log(req.body);
  res.send(200)
})

app.listen(9000, () => {
  console.log('服务启动在9000');
})

终端输入node index.js启动服务

前端index.html测试

  1. 终端输入:npm run dev进行项目的打包

  2. html文件导入打包的js文件测试

    xml 复制代码
     <!DOCTYPE html>
     <html lang="en">
     <head>
       <meta charset="UTF-8">
       <meta name="viewport" content="width=device-width, initial-scale=1.0">
       <title>Document</title>
       <script src="./dist/index.js"></script>
       <script>
         new tracker({
           requestUrl:'http://127.0.0.1:9000/tracker',
           historyTracker:true,
           domTracker:true,
           jsError:true
         })
       </script>
     </head>
     <body>
       <button target-key="dzp">dzp</button>
       <script>
    
       </script>
    
     </body>
     </html>

四:发布npm

  1. 配置package.json文件

    swift 复制代码
     {
       "name": "dzp-tracker",//包的名称,必须唯一
       "version": "1.0.3",//当前版本,每次发布必须与上一次不同
       "description": "maidian",
       "main": "dist/index.cjs.js",//command模式下导入
       "module": "dist/index.esm.js",//es module模式下导入
       "browser":"dist/index.js",//amd模式下导入使用
       "scripts": {
         "test": "echo \"Error: no test specified\" && exit 1",
         "dev": "rollup -c"
       },
       "files":["dist"],
       "keywords": ["埋点"],
       "author": "dongzeping",
       "license": "ISC",
       "devDependencies": {
         "rollup": "^2.76.0",
         "rollup-plugin-dts": "^6.1.0",
         "rollup-plugin-typescript2": "^0.36.0",
         "typescript": "^5.2.2"
       }
     }
  2. 发布到npm

    复制代码
     npm publish

五:react导入测试

css 复制代码
npm i dzp-tracker

index.js文件初始化埋点sdk,测试并使用正常。

javascript 复制代码
    import React from 'react';
    import ReactDOM from 'react-dom/client';
    import './index.css';
    import App from './App';
    import reportWebVitals from './reportWebVitals';
    import tracker from "dzp-tracker"
    const root = ReactDOM.createRoot(document.getElementById('root'));
    new tracker({
      requestUrl:'http://127.0.0.1:9000/tracker',
      historyTracker:true,
      domTracker:true,
      jsError:true
    })
    root.render(
      <React.StrictMode>
        <App />
      </React.StrictMode>
    );
    reportWebVitals();

六:资料

www.bilibili.com/video/BV1Fa...

相关推荐
卸任7 分钟前
Electron霸屏功能总结
前端·react.js·electron
fengci.7 分钟前
ctfshow黑盒测试前半部分
前端
喵个咪18 分钟前
Headless 架构优势:内容与展示解耦,一套 API 打通全端生态
前端·后端·cms
小江的记录本22 分钟前
【JEECG Boot】 JEECG Boot——数据字典管理 系统性知识体系全解析
java·前端·spring boot·后端·spring·spring cloud·mybatis
喵个咪25 分钟前
传统 CMS 太笨重?试试 Headless 架构的 GoWind,轻量又强大
前端·后端·cms
chenjingming66626 分钟前
jmeter导入浏览器上按F12抓的数据包
前端·chrome·jmeter
张元清26 分钟前
不用 Server Components 也能做 React 流式 SSR —— 实战指南
前端·javascript·面试
前端技术29 分钟前
ArkTS第三章:声明式UI开发实战
java·前端·人工智能·python·华为·鸿蒙
码小瑞33 分钟前
画布文字在不同缩放屏幕上的归一化
前端