实现一个简易的前端错误上报

1,454 阅读4分钟

前言

针对用户线上问题难复现问,大多公司都用上了错误监控,有付费的也有不付费的。sentry ,神策,阿里云,基本都是开箱即用;sentry 是我们公司当下的监控工具,但错误监控都是怎么实现的呢?自己想实现一个简易的怎么做?希望我的文章对正在阅读的你有帮助

准备

初始化

  • npm init -y
  • 入口文件:index.js ;模板文件:html
  • npm i webpack webpack-cli webpack-dev-server html-webpack-plugin user-agent -D
    1. user-agent 用户代理,解析浏览器版本,解析成一个对象
    2. html-webpack-plugin 生成产出 html 文件
    3. webpack-cli 命令行工具
    4. webpack 打包项目
    5. webpack-dev-server 启动webpack服务
  • 目录结构 image.png

脚本命令 script配置

  • package.json
 "scripts": {
  "build": "webpack",
  "dev": "webpack server"//不是webpack5 就用"webpack-dev-server"
  },
  • 遇到的问题 :打包出错 webpack-cli 的版本和 webpack-dev-server 版本的不兼容
  • 解决 npm i webpack-cli@3.3.12 -D 降版本

测试

  • yarn build
  • yarn dev

上报地址配置

我这里参考的是阿里云 Webtracking

  • host,project,logStore的配置方法
    1. 阿里云登陆搜索日志服务(SLS)
    2. 点击开通
    3. 找到这个位置,点击创建 (选离自己最近的地域)

image.png

  • 代码位置:src/utils/tracker.js
  • 代码实现: 保留了我的log 过程,有兴趣可以跟着这些log看看 每个位置都是什么信息
const userAgent = require("user-agent")
let host = "cn-hangzhou.log.aliyuncs.com";
let project = "zoemonitor";
let logStore = "zoemonitor-store";

function getExtractData() {
  return {
    title: document.title,
    url: location.href,
    timestamp: Date.now(),
    userAgent: userAgent.parse(navigator.userAgent).name, //将用户浏览器信息专成对象
  };
}
class SendTracker {
  constructor() {
    this.url = `http://${project}.${host}/logstores/${logStore}/track`; //上报的路径
    this.xhr = new XMLHttpRequest();
  }
  send(data = {}) {
    let extraData = getExtractData();
    let log = { ...extraData, ...data };
    // body 里对象都值不能是数字
    for (let key in log) {
      if (typeof log[key] === "number") {
        log[key] = `${log[key]}`;
      }
    }
    console.log({ log });
    this.xhr.open("POST", this.url, true);
    let body = JSON.stringify({
      __logs__: [log],
    });
    // console.log({ body }, this.xhr, this, new XMLHttpRequest());
    this.xhr.setRequestHeader("Content-Type", "application/json"); //请求体类型
    this.xhr.setRequestHeader("x-log-apiversion", " 0.6.0"); //版本号
    this.xhr.setRequestHeader("x-log-bodyrawsize", body.length); //请求体大小
    this.xhr.onload = function () {
      //   console.log(this.xhr.response);
    };
    this.xhr.onerror = function (error) {
      console.log(error);
    };
    this.xhr.send(body);
  }
}
export default new SendTracker();

实现

src/index.html

  <div id="container">
      <div class="content main">
        <!-- XXX -->
        <!-- --------普通错误和promise错误-------- -->
        <input type="button" value="点击抛出错误" onclick="errorClick()" />
        <input
          type="button"
          value="点击抛出Promise错误"
          onclick="promiseErrorClick()"
        />
       </div>
   </div>

monitor

文件夹对应下图新建文件

image.png

实现思路

  1. 监听用户click事件,捕获最后一个交互事件和元素

  2. 监听全局error 事件 window.addEventListener( "error",function (event) {})

  3. 监听全局promise的rejetc事件 window.addEventListener("unhandledrejection", function (event) {}) 4.对应的错误回调事件里 tracker.send({"上报的name":"上报的value"})

代码实现

  • 捕获最后一个交互事件:src/utils/getLastEvent.js
let lastEvent;
["click", "touchstart", "mousedown", "keydown", "mouseover"].forEach(
  (eventType) => {
    document.addEventListener(
      eventType,
      (event) => {
        lastEvent = event;
      },
      {
        capture: true, //m默认是捕获阶段 ,false 是 冒泡阶段
        passive: true, //默认不阻止默认事件
      }
    );
  }
);
export default function () {
  return lastEvent;
}

  • 获取最后一个操作的元素:src/utils/getSelector.js
function getSelectors(path) {
  return path
    .reverse()
    .filter((element) => {
      return element !== document && element !== window;
    })
    .map((element) => {
      let selector = "";
      if (element.id) {
        return `${element.nodeName.toLowerCase()}#${element.id}`;
      } else if (element.className && typeof element.className === "string") {
        return `${element.nodeName.toLowerCase()}.${element.className}`;
      } else {
        selector = element.nodeName.toLowerCase();
      }
      return selector;
    })
    .join(" ");
}

export default function (pathOrTarget) {
//这里其实要考虑资源加载错误的情况, 如果是一个资源加载的标签 css 和js 这种 就直接是一个对象,就不会数组,偷懒.....
    return getSelectors(pathOrTarget);
}

  • js错误监控(运行错误):src/monitor/lib/jsError.js
  1. 文件内容大致结构
import { getLastEvent, getSelector, tracker } from "../../utils";

export function injectJSError() {
  //监听全局error 事件
  window.addEventListener(
    "error",
    function (event) {
   //普通js执行错误
    },
    true
  );
  window.addEventListener(
    "unhandledrejection",
    function (event) {
     //promise错误
    },
    true
  );
}

  //1.原生报错太长,可读性差: "TypeError: Cannot set property 'error' of undefined\n    at errorClick (http://localhost:8080/:22:29)\n    at HTMLInputElement.onclick (http://localhost:8080/:12:70)"
 // 2:切割数据 去除Cannot set property 'error' of undefined, 将at和at前面的空格去除
  function getLines(stack) {
    return stack
      .split("\n")
      .slice(1)
      .map((item) => item.replace(/^\s+at\s+/g, ""))
      .join("^");
  }
  1. 普通error的回调
 console.log({ event }); //可以直接看 原生的错误回调event打印
 let lastEvent = getLastEvent(); //最后一个交互事件
 let log = {
      kind: "stability", //监指标的大类
      type: "error", //小类型
      errorType: "jsError", //js执行错误
      url: "", //访问哪个路径,报错了
      message: event.message, //报错信息
      filename: event.filename, //报错文件位置
      position: `${event.lineno}:${event.colno}`, //报错的具体行数和列数
      stack: getLines(event.error.stack), //报错代码的前后调用关系 调用栈
      selector: lastEvent ? getSelector(lastEvent.path) : "", //代表最后一个操作的元素,先获取操作方法
        };
      tracker.send(log);
  1. promise错误的回调
  let lastEvent = getLastEvent(); //最后一个交互事件
      let message;
      let filename;
      let line = 0;
      let column = 0;
      let stack = "";
      let reason = event.reason;
      if (typeof reason === "string") {
        message = reason;
      } else if (typeof reason === "object") {
        //说明是一个错误对象
        if (reason.stack) {
          // at http://localhost:8080/:17:11
          let matchResult = reason.stack.match(/at\s+(.+):(\d+):(\d+)/); //at 开头任意字符 : 任意数字:任意数字的就是域名
          console.log({ matchResult });
          filename = matchResult[1];
          line = matchResult[2];
          column = matchResult[3];
        }
        message = reason.reason;
        stack = getLines(reason.stack);
      }
      tracker.send({
        kind: "stability", //监指标的大类
        type: "error", //小类型
        errorType: "promiseError", //prmise执行错误
        message, //报错信息
        filename, //报错文件位置
        position: `${line}:${column}`, //报错的具体行数和列数
        stack, //报错代码的前后调用关系 调用栈
        selector: lastEvent ? getSelector(lastEvent.path) : "", //代表最后一个操作的元素,先获取操作方法
      });

使用

入口文件:src/index.js

import "./monitor";

测试结果

  • 本地控制台 成功发送上报请求 image.png
  • 阿里云 promiseError image.png jsError image.png

小结

  1. 监听 全局 unhandledrejection,error
  2. 根据回调事件的参数event 获取我们的报错位置,报错元素,报错事件等等
  3. 发送 我们format过的tracker 信息
  4. 实际业务场景下 不仅需要监控运行错误,还需要监控网络错误,网页性能等等
  5. 完整的监控
    1. nginx:上报的数据发给nginx,nginx负责记录日志
    2. mysql: 所有的监控,用户,报警,都放在mysql里的
    3. redis: 做缓存,可以实现高速读写,主要用来做报警 最后码字不易,如果你有收获,别忘了点赞鼓励