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

前言

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

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

相关推荐
tuokuac15 分钟前
nginx配置前端请求转发到指定的后端ip
前端·tcp/ip·nginx
程序员爱钓鱼18 分钟前
Go语言实战案例-开发一个Markdown转HTML工具
前端·后端·go
万少40 分钟前
鸿蒙创新赛 HarmonyOS 6.0.0(20) 关键特性汇总
前端
还有多远.1 小时前
jsBridge接入流程
前端·javascript·vue.js·react.js
蝶恋舞者1 小时前
web 网页数据传输处理过程
前端
非凡ghost1 小时前
FxSound:提升音频体验,让音乐更动听
前端·学习·音视频·生活·软件需求
吃饭最爱2 小时前
html的基础知识
前端·html
我没想到原来他们都是一堆坏人2 小时前
(未完待续...)如何编写一个用于构建python web项目镜像的dockerfile文件
java·前端·python
前端Hardy2 小时前
HTML&CSS:有趣的漂流瓶
前端·javascript·css
前端Hardy2 小时前
HTML&CSS :惊艳 UI 必备!卡片堆叠动画
前端·javascript·css