阅读本文你可以了解到
- 简单了解下为什么需要监控
- 监控包括哪些方面
- 手把手带你构建简单的 SDK
本文不会过多的阐述一个监控系统的架构,前端错误有哪些,如何捕获,后端如何过滤处理信息等等。本文专注于如何构建监控 SDK。
为什么需要监控
举几个简单的例子阐述一下可能会发生类似的或者是相同的事件(故事纯属虚构,如有雷同纯属巧合)
场景 1:
老板某天突发奇想,想看下产品的运行情况。突然一个白屏,气冲冲的打电话给小明,对着小明就是一顿暴扣,指责小明产品为啥会白屏。不巧,小明正在跟女朋友进行约会,在一个非常尴尬的时刻,老板的电话把他的美好时光打破了,马上查 bug,女朋友也是一脸懵逼啊。小明最后也费尽心思啊,各种调试,最后终于找到了,并修复了 bug,但是约会却泡汤了。
场景 2:
角色换一下。运营人员很想知道现在的产品的运行情况,想采集一些数据。想找小明协助一下,小明当时写的代码简直无法直视,勉强能运行,再第二遍就头疼的这种。此时再进行产品的埋点,监控,侵入性修改可能会出现问题,此时小明也不知所措,最终解决办法就是,告诉运营人员我们以后再弄,暂时没有时间。
简单总结一下:
可能很多中小企业的产品刚起步,没有一个完善的流程,或者是一些老的项目,没有一个完整的监控体系,在发生错误的时候,寻找和解决 bug 就很头疼了。
在一个稳定运行的线上产品,需要一个简单的监控是很有必要的。你可以做的很简陋,但是不可以没有,一个好的监控能帮助你
- 快速找到线上存在的错误
- 能够更好的帮助运营人员采集相关的信息
- 还可以监控页面的性能,更好的提升用户体验
💡 希望各位同学有一个监控的意识,产品做出来了,也要一个稳定的运行环境,这个环境需要监控,不断的改善环境,才能稳定的运行,产品稳定了,自然业务也随之变好
可以监控些什么呢?
可以简单的从三个方面可以进行监控
- 用户的运行时错误(必须)
异常类型
| 异常类型 | 同步方法 | 异步方法 | 资源加载 | 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) // 做不同事
}
}
❓这里需要做的是制定合适的上报策略(这里提供一个可参考的上报策略)
- 错误类型Error:监听错误马上上报,及时发现及时处理
- 资源类型:关于资源加载性能方面的,可以通过页面中空闲时间再进行上报,不阻碍主线程的情况下(requestIdelCallback)。也可以简单的window.onload的时机
- 用户资源类型:访问路径,埋点等数据。等用户关闭页面一同上报(sendBeacon)
- 其他数据就使用针对其特性做对应的上报策略即可
❓还有一个问题就是用户访问路径和其他数据的存储问题。
客户端存储暂时可用的有两个,WebStorage Or IndexedDB,可以针对场景进行选择(不是说一定得选哪个,看场景)。我这里简单的说明一下我选择WebStorage 的原因
- 场景中存到本地只有track 还有 ajax请求的请求 场景没有覆盖广泛 大多需要及时上报
- IndexDB是异步的 假如是上报的时候需要同步发送的时候(sendBeacon) 就无法及时发送了
- 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喔