或许你可以构建属于自己的监控SDK

1,047 阅读8分钟

阅读本文你可以了解到

  1. 简单了解下为什么需要监控
  2. 监控包括哪些方面
  3. 手把手带你构建简单的 SDK

本文不会过多的阐述一个监控系统的架构,前端错误有哪些,如何捕获,后端如何过滤处理信息等等。本文专注于如何构建监控 SDK

为什么需要监控

举几个简单的例子阐述一下可能会发生类似的或者是相同的事件(故事纯属虚构,如有雷同纯属巧合)

场景 1:

老板某天突发奇想,想看下产品的运行情况。突然一个白屏,气冲冲的打电话给小明,对着小明就是一顿暴扣,指责小明产品为啥会白屏。不巧,小明正在跟女朋友进行约会,在一个非常尴尬的时刻,老板的电话把他的美好时光打破了,马上查 bug,女朋友也是一脸懵逼啊。小明最后也费尽心思啊,各种调试,最后终于找到了,并修复了 bug,但是约会却泡汤了。

场景 2:

角色换一下。运营人员很想知道现在的产品的运行情况,想采集一些数据。想找小明协助一下,小明当时写的代码简直无法直视,勉强能运行,再第二遍就头疼的这种。此时再进行产品的埋点,监控,侵入性修改可能会出现问题,此时小明也不知所措,最终解决办法就是,告诉运营人员我们以后再弄,暂时没有时间。

简单总结一下:
可能很多中小企业的产品刚起步,没有一个完善的流程,或者是一些老的项目,没有一个完整的监控体系,在发生错误的时候,寻找和解决 bug 就很头疼了。

在一个稳定运行的线上产品,需要一个简单的监控是很有必要的。你可以做的很简陋,但是不可以没有,一个好的监控能帮助你

  1. 快速找到线上存在的错误
  2. 能够更好的帮助运营人员采集相关的信息
  3. 还可以监控页面的性能,更好的提升用户体验

💡 希望各位同学有一个监控的意识,产品做出来了,也要一个稳定的运行环境,这个环境需要监控,不断的改善环境,才能稳定的运行,产品稳定了,自然业务也随之变好

可以监控些什么呢?

可以简单的从三个方面可以进行监控

  • 用户的运行时错误(必须)

异常类型

异常类型 同步方法 异步方法 资源加载 Promise async/await
try/catch
onError
Error 事件 √(捕获阶段可以捕获)
unhandledrejection

监控用户运行时的错误需要判断用户的运行时环境是什么

(1) 纯 Web 端就直接监听 error 和 unhandledrejection 就可以收集错误信息,假如是使用对应的库(Vue,React 等,有对应的错误捕获的钩子)
(2) 小程序可以监听 onError
(3) 混合开发 RN...

用户一些数据(是否需要)

(1) 用户信息(用户名 Id,userName,sessionId,cookie 等)
(2) 设备信息(什么运行环境,操作系统,应用版本,联网状态等)
(3) 用户行为(用户的访问来源,用户访问路径,用户点击,滑动等)

性能数据(看是否需要)

(1) 一次请求的时间,参数交互
(2) 脚本,样式表资源加载的时间等
(3) 应用交互的时间

手把手环节(提供一种思路,更多只是一个模板)

  • SDK 项目的简单模板可以斟酌进行修改
- ...
- src
  |- __test__                   测试文件
  |- core
    |- error.ts                 监听onError
    |- promise.ts               监听promise
    |- performance.ts           监听性能
    |- resource.ts              监听资源
    |- track.ts                 监听track
    |- ajax.ts                  监听ajax请求
    |- event.ts                 监听一些事件
    |- ...                      其他需要(活动数据,其他自定义埋点等)
  |- data.ts                    定制化数据
  |- utils.ts                   工具类
  |- adaptor.ts                 数据适配层
  |- reported.ts                上报
- jest.config.js
- rollup.config.js              rollup打包
- ...

定制化数据

可以根据需要调整基本数据跟不同类型数据,这里是给出一个简单的上报数据格式及类型

// Data.ts
// 基础数据
class Data{
  ...
  private dataId?:number;       // 数据Id(可有可无,主要是看是否提供删除接口)
  private type:string;          // 数据的类型(对应监听的类型)
  private appId:number;         // 应用的Id
  private userInfo:UserInfo     // 用户信息
  private envInfo:EnvInfo       // 设备信息
  private data:UploadData       // 扩展不同类型对应的数据
  ...
  set(data:UploadData){
      this.data = data;
  }
}
// 针对性的扩展数据
class PerformanceData extends Data{
  private loadPageTime: number;  // 页面加载完成的时间
  private domReady: number;      // 反省下你的 DOM 树嵌套是不是太多了!
  private redirect: number;      // 拒绝重定向!比如,http://example.com/ 就不该写成 http://example.com
  private lookupDomain: number;  // DNS 预加载做了么?页面内是不是使用了太多不同的域名导致域名查询的时间太长?
  private TTFB: number;          // 这可以理解为用户拿到你的资源占用的时间,加异地机房了么,加CDN 处理了么?加带宽了么?加 CPU 运算速度了么?
  private contentReady: number;   // 页面内容经过 gzip 压缩了么,静态资源 css/js 等压缩了么?
  private connect: number;        // TCP 建立连接完成握手的时间
}

其他扩展性数据跟 PerformanceData 差不多,只需要继承 Data 类进行扩展即可

数据的采集

// 举两个例子,其他都差不多
// 监听事件比较简单
window.addEventListener("error",()=>{
    ...
    // 可能是资源加载错误 handleExtractData把核心的方法提取出来再做
    if (!e.cancelable) {
    // 收集报错资源
        const { localName, href, src } = e.target;
        let sourceUrl = "";
        if (localName === "link") {
        sourceUrl = href;
        } else {
        sourceUrl = src;
        }
        handleExtractData("RESOURCE", params, ErrorAdaptor, "resource");
    }else{
    // 收集报错信息
    const {
        lineno,
        filename,
        timeStamp,
        error: { message, stack },
    } = e;
    // 脚本错误
        handleExtractData("ERROR", params, ErrorAdaptor, "script");
    }
    ...
})
// 这里有一个概念 叫做面向切面编程,简单理解 就是在运行时代理原来事件,去做自己想做的事
function MonitorTrack(){
    ...
    const historyArr: HistoryFun[] = ["pushState", "replaceState"];
    historyArr.forEach((val) => {
    // 代理运行时的事件
    const originFun = history[val];
        // 重写这个方法
        history[val] = function (...args) {
            const [, , url] = args;
            const params = {
                url,
            };
            // 记录数据
            handleExtractData("TRACK", params, TrackAdaptor, "history");
            // 最后再执行方法 完成代理
            return originFun && originFun.apply(this, args);
        };
    });
    ...
}

其他都是差不多用类似的方法,进行数据的采集后,统一由适配器进行数据的适配处理(adaptor)

数据适配处理

💡 这里原本是想设计成队列形式,先进先上报。但是后来发现不同数据要维护不同的队列,不同的数据上报的机制也不同,而且设计队列的意义也不大。上报的机制是一并上报,或者是立马上报,没有用到队列的性质,所以就放弃了队列的想法。最终选择做一个适配层。

// 可以设计成方法传参数 也可以设计成类进行拿到不同类型的对象
// 方法
function DataAdaptor(type:AdaptorType){
    // 根据type进行分类处理
    if(type) or switch(type)
}
// 类
class DataAdaptor{
    private type: string;
    push(data:Data){
        if(type) // 做不同事
    }
}

❓这里需要做的是制定合适的上报策略(这里提供一个可参考的上报策略)

  1. 错误类型Error:监听错误马上上报,及时发现及时处理
  2. 资源类型:关于资源加载性能方面的,可以通过页面中空闲时间再进行上报,不阻碍主线程的情况下(requestIdelCallback)。也可以简单的window.onload的时机
  3. 用户资源类型:访问路径,埋点等数据。等用户关闭页面一同上报(sendBeacon)
  4. 其他数据就使用针对其特性做对应的上报策略即可

❓还有一个问题就是用户访问路径和其他数据的存储问题。

客户端存储暂时可用的有两个,WebStorage Or IndexedDB,可以针对场景进行选择(不是说一定得选哪个,看场景)。我这里简单的说明一下我选择WebStorage 的原因

  1. 场景中存到本地只有track 还有 ajax请求的请求 场景没有覆盖广泛 大多需要及时上报
  2. IndexDB是异步的 假如是上报的时候需要同步发送的时候(sendBeacon) 就无法及时发送了
  3. WebStorage大小也有5m,大小应该够用。为了安全起见,可以才有上报完删除已上报的数据,没有上报的数据等下一次一并上报,防止数据大于5m时出现数据的问题

最后打包环节

打包库资源来说,Rollup和Gulp都比Webpack优胜。但Rollup相对Gulp来说tree-shakeing比较友好,对于一个库来说tree-shaking还是比较重要的一环,所以最后采用Rollup来打包

这里贴一份我的Rollup配置

import typescript from "rollup-plugin-typescript2";
import resolve from "rollup-plugin-node-resolve"; // for using third party modules in node_modules
import { terser } from "rollup-plugin-terser"; // Rollup plugin to minify generated es bundle.
import { uglify } from "rollup-plugin-uglify"; // 这个东西不支持ES6的构建的时候 得target是ES5

export default {
  plugins: [
    typescript({
      exclude: "node_modules/**",
      typescript: require("typescript"),
    }),
    resolve(),
    terser(),
    uglify(),
  ],
  input: "./src/core/xxx.ts",
  output: [
    {
      file: "./dist/monitor.umd.js",
      format: "umd", //"amd", "cjs", "system", "esm", "iife" or "umd".
      name: "monitor",
      env: "production",
    },
    {
      file: "./dist/monitor.iife.js",
      format: "iife",
      name:"monitor", // 这一个索引 通过这个去寻找
      env: "production",
    },
  ],
};

总结一下

本文主要是阐述如何去构建一个监控SDK,提供一个简单的架构,提出一些构建时关键的点,如何去考虑。更多的是提供一个模板,后续还需大家去补充优化,构建属于自己的SDK。虽然不建议造轮子,但是可控的库还是就是稳定的提现,稳定了才能让项目更好的运行。

最后贴一下提供给大家的模板链接(MonitorSDK),大家如果觉得写的好,请不要吝啬你们的star喔

参考文章