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

前言

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

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

相关推荐
嘤嘤怪呆呆狗9 分钟前
【开发问题记录】执行 git cz 报require() of ES Module…… 错误
前端·javascript·vue.js·git·vue
夜斗(dou)1 小时前
谷歌开发者工具 - 网络篇
前端·网络·chrome devtools
常常不爱学习1 小时前
CSS盒子模型(溢出隐藏,块级元素和行级元素的居中对齐,元素样式重置)
前端·css
风抽过的烟头1 小时前
Python提取字符串中的json,时间,特定字符
前端·python·json
SomeB1oody1 小时前
【Rust自学】6.3. 控制流运算符-match
开发语言·前端·rust
夕水1 小时前
你可能需要避免的5个react的ref错误用法
javascript·react.js
m0_748256782 小时前
【Django自学】Django入门:如何使用django开发一个web项目(非常详细)
前端·django·sqlite
林小白的日常2 小时前
uniapp中wx.getFuzzyLocation报错如何解决
前端·javascript·uni-app
傻小胖2 小时前
React 脚手架配置代理完整指南
前端·react.js·前端框架
EterNity_TiMe_2 小时前
【论文复现】农作物病害分类(Web端实现)
前端·人工智能·python·机器学习·分类·数据挖掘