Skip to content

埋点

概念以及应用场景

  • pv(page view),同一用户对同一页面的访问次数
  • uv ,独立用户访问次数
  • 收集用户的隐私信息,优化性能体验,进行 A/B 业务决策
  • 收集报错信息,进行监控

TIP

埋点肯定是跟用户有关的,需要获取系统里的用户信息

需要返回给后端的数据:

js
export default function user() {
   return {
       id: 1,  // 用户id
       name: '用户名'// 用户名
       data: new Date().getTime(),  // 当前时间
       ua: navigator.userAgent,   // 用户访问的设备信息

       // 其他用户信息
       ...
   }
}

配置上报标记:

配置一个枚举用于标记需要上报的元素属性,当有这个自定义属性则需要上报

js
export enum Token {
   click = 'data-click'  // 标记需要上报的属性
}

模拟一个需要上报的点击按键,在需要上报的元素打上属性,标记需要上报的交互元素

html
<body>
  <button data-click="上报">上报</button>
  <button>不上报</button>
  <script type="module" src="./main.ts"></script>
</body>

封装上报逻辑:

用于获取用户信息,上报埋点数据

js
import user from "./user";
import button from "./event/button";

class Tracker {
  events: Record<string, Function>;
  constructor() {
    this.events = { button }  // 注册方法
    this.init();
  }
  /**
   * 上报埋点
   * @params 埋点信息
   */
  protected sendReport(params = {}) {
    let userInfo = user();
    const body = Object.assign({}, userInfo, params);

    // sendBeacon不支持跨域,不支持 JSON,需要通过Blob进行转换
    let blob = new Blob([JSON.stringify(body)], {
      type: "application/json",
    });
    navigator.sendBeacon("http://localhost:3000/tracker", blob);  
  }

  // 遍历一下注册的方法,将上报埋点的逻辑sendReport传进注册的事件方法中
  private init() {
    Object.keys(this.events).forEach((key) => {
        this.events[key](this.sendReport)
    })
  }
}

WARNING

  • axios,fetch,xhr 都会阻塞页面的关闭,当页面关闭时,接口停止
  • 需要使用浏览器的 sendBeacon 方法,在页面关闭时继续发送请求,不会阻塞页面的关闭。
  • 但是 sendBeacon 不支持跨域,不支持 JSON

定义 send 类型:

js
/**
 * 开启功能 使用该sdk的人可以选择开启的功能
 * button: true
 */
export interface Options {}

export type key<T = never> = "type" | "data" | "text" | T; //  never在联合类型默认会被忽略
export type params = Record<key, any>; //  约束对象
export type send = (params: params) => void; 

// 预期结构
// {
//     type: "",
//     data: {},
//     text: ""
// }

封装点击事件上报的函数:

js
import type { send } from '../type/index'
import { Token } from '../type/enum'

export default function button(send: send) {
    // 获取上报的交换按键元素
    document.addEventListener('click', (e) => {
        const target = e.target as HTMLElement
        const flag = target.getAttribute(Token.click)  // 获取标记的属性值
        // 熟知按钮的位置
        if (flag) {
            send({
                type: 'click',
                text: flag,
                data: target.getBoundingClientRect()  // 获取按钮所在的位置信息
            })
        }
    })
}

监控代码报错,上报错误信息

可以再封装一个监控方法,记录报错信息,当代码遇到报错时,拦截并上报给后端

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

export default function error(send: send) {
  window.addEventListener("error", (e) => {
    send({
      type: e.type,
      data: {
        line: e.lineno,
        file: e.filename,
      },
      text: e.message,
    });
  });
}

封装好报错监控之后,只需要直接在 Tracker 里注册就可以使用了:

js
import user from "./user";
import button from "./event/button";
import error from "./event/error"; 

class Tracker {
  events: Record<string, Function>;
  constructor() {
    this.events = { button } 
    this.events = { button, error } 
    this.init();
  }
  /**
   * 上报埋点
   * @params 埋点信息
   */
  protected sendReport(params = {}) {
    let userInfo = user();
    const body = Object.assign({}, userInfo, params);

    // sendBeacon 不支持跨域,不支持 JSON
    let blob = new Blob([JSON.stringify(body)], {
      type: "application/json",
    });
    navigator.sendBeacon("http://localhost:3000/tracker");
  }
  private init() {
    Object.keys(this.events).forEach((key) => {
        this.events[key](this.sendReport)
    })
  }
}

Promise 报错上报

如果需要拦截 promise,对 promise 报错进行上报,则可以添加以下方法:

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

export default function reject(send: send) {
  window.addEventListener("unhandledrejection", (e) => {
    send({
      type: e.type,
      data: {
        reason: e.reason,
        href: location.href
      },
      text: e.message
    })
  })
}

TIP

当出现以下三种情况时,会触发 options 预检请求:

  1. 出现跨域情况
  2. 自定义请求头
  3. post 并且是 application/json 非普通请求

上报 ajax 或者 fetch 请求

如果需要上报 ajax,则需要重写 ajax,因为 ajax 没有中间件或者拦截器

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

export default function request(send: send) {
  const OriginOpen = XMLHttpRequest.prototype.open;
  const OriginSend = XMLHttpRequest.prototype.send;

  XMLHttpRequest.prototype.open = function (method: string, url:string) {
    send({
      type: 'ajax',
      data: {
        method,
        url
      },
      text: 'ajax'
    })

    OriginOpen.call(this, method,url, async);
  }
  XMLHttpRequest.prototype.send = function (data) {
    send({
      type: 'ajax-send',
      data,
      text: 'ajax-send'
    })
    OriginSend.call(this, data);
  }

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

上报 PV

应用场景:当业务需求需要收集用户从 A 页面跳转到 B 页面的新旧 URL,上报到服务器,用于统计用户从页面 A 到 B 的访问量。

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

export default function page(send: send) {

  window.addEventListener('hashchange', (e) => {
    send({
      type: 'pv-hash',
      data: {
        newURL: e.newURL,
        oldURL: e.oldURL
      },
      text: 'pv-hash'
    })
  })

  // history 模式
  window.addEventListener('popstate', (e) => {
    send({
      type: 'pv-history',
      data: {
        state: e.state,
        url: location.href
      },
      text: 'pv-history'
    })
  })

  // 手写pushState,解决history模式下,跳转其他页面不触发popstate的问题
  const pushState = history.pushState;

  window.history.pushState = function (state, titiel, url) {
    const res = pushState.call(this, state, titiel, url);

    // 注册自定义事件
    const e = new Event('pushState');
    window.dispatchEvent(e);
    return res;
  }
  window.addEventListener('pushState', (e) => {
    send({
      type: 'pv-pushState',
      data: {
        url: location.href
      },
      text: 'pv-pushState',
    })
  })

}

上报首屏加载时间

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

export default function onePage(send: send) {
   let firstScreenTime = 0;

   // 监听dom的变化
   const ob = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      firstScreenTime = performance.now()
    })
    if (firstScreenTime > 0) {
      send({
        type: 'firstScreen',
        data: {
          time: firstScreenTime
        },
        text: 'firstScreen'
      })
      ob.disconnect()
    }

   })

   // subtree监听后代变化,childList监听增删改查
   ob.observe(document.body, {subtree: true, childList: true})
}

打包

在 vite.config.ts 中进行配置

ts
export default defineConfig({
  build: {
    lib: {
      entry: './src/index.ts',  // 入口文件
      name: 'Tracker',  // 全局变量的名称
      fileName: 'tracker'  // 打包后的文件名
      format: ['es', 'cjs', 'iife', 'umd']  // 打包格式
    }
  }
})

灰度发布

灰度发布,是指在发布新版本之前,先发布给部分用户进行测试,以确定新版本是否满足用户的需求或者是否存在 bug,新版本都没问题,再全量发布

  • 如果灰度发布的新版本存在 bug,则通过埋点进行监控上报

TIP

软件版本号 1.0.0 -> 2.0.0 三段式版本号分别对应的含义:

  1. 第一位版本号更新代表整体架构发生了改变
  2. 第二位版本号更新代表功能发生了改变
  3. 第三位版本号更新代表 bug 的修复

小结

通过:通过埋点上报可以收集用户信息,错误反馈,性能数据,以便后续的更新与维护,通过单一职责的设计模式,对需要上报的内容进去封装并注册,即插即用