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

前言

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

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

相关推荐
SEO_juper13 分钟前
新独立站冷启动收录全攻略:配置、推送、抓取配额优化完整手册
前端·谷歌·seo·跨境电商·外贸·geo·独立站
TinssonTai18 分钟前
这个 VS Code 插件让我的 AI Coding 又快又稳 - 旧瓶装新酒
前端·人工智能·程序员
体验家18 分钟前
体验家 XMPlus 网页端问卷 SDK 技术解析:用几行 JavaScript 实现精准场景触发与防打扰机制
开发语言·前端·javascript
Maimai1080833 分钟前
Web3 前端交易系统如何落地:从下单 UI 到 Operation 编码、签名与实时状态更新
前端·react.js·ui·架构·前端框架·web3
Cxiaomu33 分钟前
React接入WebRTC实时视频实践
react.js·音视频·webrtc
kidding72342 分钟前
高效备忘清单工具类小程序
前端·计算机网络·微信小程序·小程序
IMPYLH1 小时前
HTML 的 <abbr> 元素
前端·算法·html
李白的天不白1 小时前
Tree-Shaking
前端
Csvn2 小时前
TypeScript:你以为安全的 `JSON.parse` 其实是颗雷 — 运行时类型安全实战
前端·javascript
橘子星2 小时前
深入理解线性数据结构:栈、队列与链表
前端·javascript