Skip to content

埋点监控

为什么要做埋点监控? 埋点sdk

专业名词解释

这里对一些专业名词进行解释

一般都是c端用的比较多 就是为了收集用户的浏览隐私 优化A/B业务

pv (page view) 同一用户对同一页面访问的次数

uv 是不同用户对页面的访问次数 是通过用户的ip地址来进行区分

灰度 软件app用的比较多 就是我们的软件用100万用户 现在有个1.0.0 => 2.0.0的转换 我们先从 5%的用户发布 如果没问题再 30% 最后再全量发布

埋点&监控能做什么

从单个页面的常规数据角度出发我们可以通过埋点获取:访问次数(UV/PV)、地域数据(IP)、在线时长、区域点击次数等数据。

当我们将这些单点数据按照特定的纬度进行数据聚合,就可以获得全流程视角下的数据如:用户留存率/流转率、用户转化率、用户访问深度等数据。

而在埋点数据进行上报的同时,我们也可以同步收集页面基础数据/接口相关数据如:页面加载/渲染时长、页面异常、请求接口等数据。

同时对于前端监控来说,大致可以分成三个方向:数据监控、性能监控、异常监控。

具体操作案例代码:

1.先确认我们需要安装的依赖包

package.json

json
{
  "name": "my-point-monitor",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "vite",
    "build": "vite build"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/express": "^4.17.21",
    "@types/nodemailer": "^6.4.14",
    "vite": "^5.1.6"
  },
  "dependencies": {
    "@vitejs/plugin-vue": "^5.0.4",
    "express": "^4.18.3", // 通过express搭建一个小型服务器 接收发送请求
    "ioredis": "^5.3.2", //一个100%用TypeScript编写的redis插件,做缓存使用
    "nodemailer": "^6.9.12", // 通过这个插件 来进行监控信息发送到邮箱
    "vue": "^3.4.21"
  }
}

2.演示demo项目结构

index.html文件

html
index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app"></div>
    <button data-click="上报">上报</button> // 首先 我们假设页面中有点击事件进行监控上报
    <button>不上报</button>
    <script type="module" src="./main.ts"></script> // 模块化导入方式
</body>
</html>

3.创建index.ts文件

  • 这一部分是入口 我们编写的代码文件的总入口
  • 编写Tracker类 写上报埋点的方法
ts
import user from "./user";
import button from "./event/button";
import error from "./monitor/error";
import reject from "./monitor/reject";
import request from "./monitor/request";
import pv from "./pv";
import page from "./page";
class Tracker {
  events: Record<string, any>;
  constructor() {
    this.events = { button, error, reject, request, pv, page };
    this.init();
  }

  /**
   * 上报埋点
   * @param params 这就是我们需要传递的埋点信息
   */
  //这里就是上报埋点
  protected sendRequest(params = {}) {
    let userInfo = user();

    const body = Object.assign({}, userInfo, params);

    //当我们想到要上报埋点的时候 我们就需要想到navigator.sendBeacon  为什么?
    // 因为如果我们使用xhr, ajax, axios,fetch等方法的话 都是不行的 为什么
    // 因为这些方法都是异步的, 当页面关闭的时候, 这些异步请求就会被终止, 所以我们需要使用navigator.sendBeacon
    //navigator.sendBeacon 他会在页面关闭的时候, 依然会发送请求

    /**
     * sendBeacon的缺点是 只能用post发送我们的信息 并且sendBeacon不能接受json类型的参数 并且不支持跨越
     * 但是sendBeacon可以发送blob类型的参数
     * 而我们的blob类型的参数 可以用一个 JSON 字符串构造一个 blob 那里面装的就是json字符串
     */
    const blob = new Blob([JSON.stringify(body)], {
      type: "Application/json", //其实我们正常的数据类型是URLSearchParams querystring 像Application/json这种是非正式的格式  这是我们自己定义的
    });
    navigator.sendBeacon("http://localhost:3000/tracker", blob);
  }

  private init() {
    Object.keys(this.events).forEach((key) => {
      this.events[key](this.sendRequest);
    });
  }
}

export default Tracker;

4.创建user/index.ts 模拟公司返回用户的身份信息以及id

ts

export default function user() {
  return {
    id: 1,
    name: "xiaoliao",
    age: 18,
    ua: navigator.userAgent,
  };
}

5.创建pv/index.ts

ts

import type { send } from "../type";

export default function pv(send: send) {
  /**
   * 这里我们监听的就是 当我们的页面跳转的时候 那么这里我们就会用到跳转的方法
   * 原生的js里面 涉及到页面跳转的方法有两个
   * 1.hashChange
   * 2.pushState 这两个方法都是让页面可以进行跳转的方法
   */

  //hashChange可以通过hashChange事件来进行监听
  window.addEventListener("hashchange", (e) => {
    send({
        type: e.type,
        data: {
            newURL: e.newURL,
            oldURL: e.oldURL,
        },
        text: e.type
    })
  });

  //还有一种方式  就是通过history进行跳转
  //但是目前只有前进和后退可以监听到 我们不能直接去监听到pushState的方法
  window.addEventListener('popstate', (e) => {
    send({
        type: e.type,
        data: {
            state: e.state,
            url: location.href    
        },
        text: e.type
    })
  })

  /**
   * 那我们就该重写pushState方法 去让pushState可以监听到
   */
  const OriginPushState = history.pushState
  history.pushState = function (params, unsed, url) {
    const res = OriginPushState.call(this, params, unsed, url)
    send({
      type: 'history-push',
      data: {
        params, unsed, url
      },
      text: 'history-push'
    })
    return res
  }
}

6.创建page/index.ts

  • 计算首屏加载时间
ts
import { send } from './../type/index';
/**
 * 这个方法就是去计算首屏加载时间 并且发送到后端 而且我们可以通过获取的首屏加载时间做一个统计图 去给领导呈现项目的效率
 */

export default function page(send: send) {
    //我们如何去监听 项目的首屏什么时候加载完呢? 就是当我们的项目启动的时候 vue会往我们的dom元素去插入东西 
    //意思就是vue把所有的js执行完后 就会开始渲染dom到我们的<div id=""></div>盒子里面
    //所以我们只需要去监听 我们的html盒子是否已经有元素变化就行了

    /**
     * 监听元素的变化就是用mutationObserver
     */
    let firstTime = 0 //首屏加载时间
    const observer = new MutationObserver((mutations) => {
        
        mutations.forEach(item => {
            firstTime = performance.now()
            if (firstTime > 0) {
                send({
                    type: 'page',
                    data: {
                        firstTime
                    },
                    text: 'page'
                })
                observer.disconnect()
            }
        })
    })
    observer.observe(document, {subtree: true, childList: true}) // 但是我们这里监听不到这个document.body 因为我们目前还没有创建vue文件
    // 所以我们可以监听document 因为vue会强制性的在document去插入一个结构
}

7.创建monitor文件夹

分别创建error.ts reject.ts request.ts文件

error.ts

ts
import type { send } from "../type";
export default function error(send: send) {
  window.addEventListener("error", (e) => {
    send({
      type: e.type,
      data: {
        lineno: e.lineno,
        filename: e.filename,
      },
      text: e.message,
    });
  });
  /**
   * 当我们已经接受到了错误的信息的时候 我们就需要去给部门或者是前端发送一个消息
   */
}

reject.ts

ts
import type { send } from "./../type/index";
export default function reject(send: send) {
    /**
     * Promise.reject('error') 是通过unhandledrejection
     */
    window.addEventListener('unhandledrejection', (e) => {
        console.dir(e)
        send({
            type: e.type,
            data: {
                reason: e.reason,
                href: window.location.href //我们尽可能的去添加一些报错信息  这里是我们自己添加的当前页面的报错信息
            },
            text: e.reason
        })
    })
}

request.ts

ts
import type { send } from "./../type/index";
/**
 * 我们这里的目的是为了去监控我们发起请求的时候 但是我们这里可以自己去做一些操作 去判断什么请求需要去发送一个埋点信息
 * 这个页面的作用是  让我们知道每个接口都发了什么做了什么
 */
export default function request(send: send) {
  //但是我们没有可以获取ajax请求的监听事件  所以我们需要重写方法
  const OriginOpen = XMLHttpRequest.prototype.open;
  const OriginSend = XMLHttpRequest.prototype.send;

  XMLHttpRequest.prototype.open = function (
    method: string,
    url: string | URL,
    async: boolean = true
  ) {
    send({
      type: "ajax",
      data: {
        method,
        url,
      },
      text: "ajax",
    });
    OriginOpen.call(this, method, url, async);
  };

  XMLHttpRequest.prototype.send = function (data: any) {
    send({
      type: "ajax-send",
      data,
      text: "ajax-send",
    });
    OriginSend.call(this, data);
  };

  const OriginFetch = window.fetch;

  window.fetch = function(...args: any[]) {
    send({
      type: 'fetch',
      data: args,
      text: 'fetch'
    })
    return OriginFetch.apply(this, args);
  }
  
}

8.创建event文件夹下再创建button.ts

ts
import type { send } from "../type";
import { Token } from "../type/enum";
export default function button(send: send) {
    window.addEventListener('click', (e) => {
        const target = e.target as HTMLElement
        console.dir(target)

        //我们需要去把当前按钮的位置信息也给传递过去 这样我们才能知道是哪一个页面或者是哪一个位置点击的最多
        //通过什么过去一个元素的位置呢?
        //通过getBoundingClientRect去获取

        //可以通过target去获取 
        if (target.getAttribute(Token.click)) {
            send({
                type: 'click',
                data: {
                    rect: target.getBoundingClientRect(),
                    href: location.href
                },
                text: Token.click
            })
        }
    })
}

9.创建server文件夹index.ts

根据server文件夹的index.ts 创建一个服务器

并且根据nodemailer创建邮件传输

ts
import express from "express";
// import Redis from "ioredis";
import mailer from "nodemailer";

const transporter = mailer.createTransport({
    service: "qq",
    host: "smtp.qq.com",
    port: 465, 
    
    auth: {
        user: "708880503@qq.com",
        pass: "iqarvetzmtpfbbed"
    }
})

// 创建redis实例
const redis = new Redis({
  host: "127.0.0.1",
  port: 6379,
});

const app = express();
//但是我们这里就是express其实是不允许我们发起post请求的  所以这个时候我们需要使用一个中间件

//除了在前端可以设置跨域以外  我们可以在服务器中设置以允许跨域请求
/**
 * 这里就是我们用来use * 代表所有的请求都会往这里面走
 * *是不允许上传cookies (准确的来说 是因为上传了cookies 浏览器的同源政策会管理的更严格 就要指定 不能允许所有跨域地址)
 */

/**
 * options预检请求(只有三种方式才会出现)
 * 1.跨域
 * 2.自定义请求头
 * 3.post请求并且传的是application/json的方式  非普通请求
 */
app.use("*", (req, res, next) => {
  res.setHeader("Access-Control-Allow-Origin", "http://127.0.0.1:5500"); //不能用*  因为当前的navigator.sendBeacon是会携带cookies进行发起请求
  res.setHeader("Access-Control-Allow-Credentials", "true");
  res.setHeader("Access-Control-Allow-Headers", "Content-type, Authorization"); //因为application/json是我们自己定义的  我们就要给请求头 设置content-type 并且为
  /**
   * 但是我们的cors只允许普通请求 例如URLSearchParams text/plain query formData 这几类  而application/json不是普通请求 是我们人为定义的类型
   * 并且跨域资源共享(cors)是不允许携带凭证的  凭证是什么?  凭证就是  cookies 以及 https证书等等
   * 所以我们要允许让请求携带cookies 前端的cookies就是后端的session 后端会种cookies到浏览器 不同的cookies代表唯一标识 代表是哪个用户
   */
  next(); //这里必须要用next  不然不往下走
});
app.use(express.json());

app.listen(3000, () => {
  console.log("server start");
});

app.post("/tracker", (req, res) => {
  console.log(req.body);
  //这个lpush 是队列的意思
  redis.lpush("tracker", JSON.stringify(req.body)); //这个当我们重启服务 内存里面的东西就会丢失 那要
  if (req.body.type === 'error' || req.body.type === 'unhandledrejection') {
    transporter.sendMail({
        from: '708880503@qq.com',
        to: '708880503@qq.com',
        subject: '错误监控',
        text: JSON.stringify(req.body)
    })
  }
  return res.send("ok"); //这里发送的消息越少越好  为什么? 因为我们一个项目有很多地方都需要埋点 如果每一个都返回大量的信息 那我们的项目得有多少信息返回啊 所以说返回的信息越少越好
});