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

前言

云在前端公众号中发送监控领取源码地址和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. 文章中如果有什么不对的,或者你有新的思路和建议,可以在评论区留言

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

相关推荐
Martin -Tang7 分钟前
vite和webpack的区别
前端·webpack·node.js·vite
迷途小码农零零发7 分钟前
解锁微前端的优秀库
前端
王解1 小时前
webpack loader全解析,从入门到精通(10)
前端·webpack·node.js
老码沉思录1 小时前
写给初学者的React Native 全栈开发实战班
javascript·react native·react.js
老码沉思录1 小时前
React Native 全栈开发实战班 - 第四部分:用户界面进阶之动画效果实现
react native·react.js·ui
我不当帕鲁谁当帕鲁1 小时前
arcgis for js实现FeatureLayer图层弹窗展示所有field字段
前端·javascript·arcgis
那一抹阳光多灿烂1 小时前
工程化实战内功修炼测试题
前端·javascript
放逐者-保持本心,方可放逐2 小时前
微信小程序=》基础=》常见问题=》性能总结
前端·微信小程序·小程序·前端框架
毋若成4 小时前
前端三大组件之CSS,三大选择器,游戏网页仿写
前端·css
红中马喽4 小时前
JS学习日记(webAPI—DOM)
开发语言·前端·javascript·笔记·vscode·学习