前端监控:打造极致用户体验的利器

前言

云在前端公众号中发送监控领取源码地址和npm包地址

一、数据埋点

1. 埋点的目的

收集用户行为,反馈页面功能、活动效果,指明产品优化方向

2. 常用属性

属性 描述
uuid 用户id
date 访问日期
pv 页面浏览量
uv 用户访问量
duration 停留时间
preformance 性能信息
error 报错信息
device 设备信息

二、数据采集

1. 行为监控

1. 用户点击

js 复制代码
export default function behavior() {
  ["click"].forEach(function (eventType) {
    let timer: NodeJS.Timeout;
    document.addEventListener(
      eventType,
      (e) => {
        clearTimeout(timer);
        timer = setTimeout(() => {
          const target = e.target;
          //目前只处理button标签的点击事件
          if (target instanceof HTMLButtonElement) {
            emit(eventType, target.textContent);
          }
        }, 300);
      },
      true
    );
  });
}

2. 页面跳转(PV)

(1)hash路由

js 复制代码
function Hash() {
  window.addEventListener("hashchange", function () {
    emit("hashchange");
  });
}

(2)history路由

重写跳转方法,设置拦截器进行监听

js 复制代码
function History() {
  const historyPushState = window.history.pushState;
  const historyReplaceState = window.history.replaceState;
  window.history.pushState = function () {
    historyPushState.apply(window.history, arguments);
    emit("historychange");
  };
  window.history.replaceState = function () {
    historyReplaceState.apply(window.history, arguments);
    emit("historychange");
  };
  window.addEventListener("popstate", function () {
    emit("historychange");
  });
}

3. 页面停留时长

记录一个初始时间,用户离开页面时用当前时间减去初始时间,就是用户停留时长

js 复制代码
let visitTime = Date.now();

export function emit(type, data) {
  const date = Date.now();
  //...
  if (type === "hashchange" || type === "historychange") {
     //停留时间 = 跳转时间 - 访问时间
    Object.assign(info, { duration: date - visitTime });
    visitTime = date;
  }
  //...
}

4. UV

如果是游客,先判断localStorage里是否有id值,没有则为游客生成唯一id,并存储到localStorage中。下一次游客再访问时,直接取存在localStorage中的值。

js 复制代码
export class BaseInfo {
  constructor() {
    //...
    if (!localStorage.getItem(UUID)) {
      this.uuid = uuidv4(); //唯一id;
      localStorage.setItem(UUID, this.uuid); //如果不存在uuid,则进行存储
    }else{
      this.uuid = localStorage.getItem(UUID)
    }
  }
}

2. 异常监控

1. JS错误

js 复制代码
function JSError() {
  //  错误信息 出错文件 行号 列号 Error对象
  window.onerror = (msg, url, line, column, error) => {
    emit("js_error", { msg, url, line, column, error });
  };
}

2. 资源加载错误

js 复制代码
function resourceError() {
  window.addEventListener(
    "error",
    function (e) {
      const target = e.target;
      if (!target) return;
      if (target.src || target.href) {
        const url = target.src || target.href;
        emit("resource_error", url);
      }
    },
    true
  );
}

3. 手动抛出的错误

js 复制代码
//重写console.error方法
function consoleError() {
  var oldError = window.console.error;
  window.console.error = function (errorMsg) {
    emit("console_error", errorMsg);
    oldError.apply(window.console, arguments);
  };
}

4. promise错误

js 复制代码
// 当Promise被reject且没有reject处理器的时候,会触发unhandledrejection事件;
function promiseError() {
  window.addEventListener("unhandledrejection", function (e) {
    emit("promise_error", e.error.stack);
  });
}

5. Vue错误

js 复制代码
//全局捕获Vue错误
app.config.errorHandler = (err, instance, info) => {
  // 处理错误,例如:报告给一个服务
  emit('vue_error',info)
}

6. React错误

使用错误边界,在componentDidCatch中捕获错误

js 复制代码
// 定义错误边界
class ErrorBoundary extends React.Component {
  state = { error: null }
  static getDerivedStateFromError(error) {
    return { error }
  }
  componentDidCatch(error, errorInfo) {
    // 错误捕获
    emit('react_error', errorInfo)
  }
  render() {
    if (this.state.error) {
      return <h2>Something went wrong.</h2>
    }
    return this.props.children
  }
}
...
<ErrorBoundary>
  <BuggyCounter />
</ErrorBoundary>

3. 性能监控

1. FP

首次渲染时间

js 复制代码
function fp() {
  const entryHandler = (list) => {
    for (const entry of list.getEntries()) {
      if (entry.name === "first-paint") {
        observer.disconnect();
        emit("fp", entry.startTime);
      }
    }
  };

  const observer = new PerformanceObserver(entryHandler);
  // buffered:true表示观察缓存数据
  observer.observe({ type: "paint", buffered: true });
}

2. DCL

DOM加载完成时间

js 复制代码
function dcl() {
  window.addEventListener("DOMContentLoaded", function (e) {
    emit("DOMContentLoaded", e.timeStamp);
  });
}

3. load

图片、样式等外链资源加载完成时间

js 复制代码
function load() {
  window.addEventListener("load", function (e) {
    emit("load", e.timeStamp);
  });
}

4. fps

监控requestAnimationFrame在一秒内的执行次数,得到FPS的值,如果存在连续3个小于20的FPS,说明页面存在卡顿

js 复制代码
let count = 0;
let frames = 0;
let lastTimestamp = performance.now();

//timestamp开始执行函数的时间戳
export default function updateFPS(timestamp) {
  frames++;

  const deltaTime = timestamp - lastTimestamp;
  if (deltaTime >= 1000) {
    const fps = Math.round(frames / (deltaTime / 1000));
    if (fps < 20) {
      count++;
      if (count >= 3) {
        //连续3次小于20的fps进行数据上报
        emit("fps", '卡顿');
        count = 0;
      }
    } else {
      count--;
      if (count < 0) count = 0;
    }
    frames = 0;
    lastTimestamp = timestamp;
  }

  requestAnimationFrame(updateFPS);
}

三、数据上报

1. 上报方法

1. sendBeacon

  1. 在浏览器空闲的时候发送

  2. 在页面卸载时,也会异步发送数据

js 复制代码
navigator.sendBeacon(url, JSON.stringify(data)); //发送数据

2. XMLHttpRequest

如果浏览器不支持sendBeacon,则使用XMLHttpRequest进行兜底

js 复制代码
let xhr = new XMLHttpRequest();
xhr.open("POST", url);
xhr.send(JSON.stringify(data));

2. 上报时机

1. 达到缓存上限时上传

2. 达到最大缓存时间上传

js 复制代码
clearTimeout(timer);
events.length >= max
  ? send()
  : (timer = setTimeout(() => {
      send();
    }, 60000)); //如果1分钟内没达到最大缓存数,主动上传

3. 页面关闭或刷新时上传

js 复制代码
window.addEventListener("beforeunload", send, true);

最后

  1. 这里只展示前端监控的一些要点与原理,具体的还是得根据自身的业务去拓展
  2. GitHub上的示例代码由于不断更新,会与文中的略有不同,但大体思路还是一致,可做参考
  3. 文章中如果有什么不对的,或者你有新的思路和建议,可以在评论区留言

文章首发在云在前端公众号,未经许可禁止转载!

相关推荐
山楂树の35 分钟前
图像标注大坑:img图片 + Canvas 叠加标注,同步放大后标注位置偏移、对不齐?详解修复方案及亚像素处理原理
前端·css·学习·canva可画
本山德彪37 分钟前
我做了一个拼豆图纸生成器,把照片秒变图纸
前端
DTrader1 小时前
用TS无法实盘量化? - 实盘均线策略
前端·api
进击的夸父1 小时前
vfojs:Vue 超集架构,外壳React灵魂Vue
前端
编程老船长1 小时前
解决不同项目需要不同 Node.js 版本的问题
前端·vue.js
Wect1 小时前
LeetCode 5. 最长回文子串:DP + 中心扩展
前端·算法·typescript
漫游的渔夫1 小时前
前端开发者做 Agent:别写成一次请求,用 5 步受控循环防止 AI 乱跑
前端·人工智能·typescript
kyriewen3 小时前
Webpack vs Vite:一个是“老黄牛”,一个是“猎豹”,你选谁?
前端·webpack·vite
打小就很皮...3 小时前
html2canvas + jsPDF 生成 PDF 的踩坑与解决方案总结
前端·pdf
全栈前端老曹3 小时前
【前端地图】多地图平台适配方案——高德、百度、腾讯、Google Maps SDK 差异对比、封装统一地图接口
前端·javascript·百度·dubbo·wgs84·gcj-02·bd09