❤️此仓库设计目的
很多公司都是以业务快速开发为主,不注重后续的项目稳定性的监控,每当被提上 Bug 的时候,都需要重新打开项目运行一步步调试才能找到问题,经验丰富的开发也得找上一段时间。这样的时间就是被白白浪费了。但是如果我们在开发的时候就注入监控逻辑,就能让我们在后续的开发中比较快速的发现问题并解决问题。
💡tips
(1)想要自研的同学可以参考后自行研究,下面会给出一些设计该项目时候的一些思考。
(2)本仓库尚未提供后端处理数据的模板,同学上报完之后需要根据实际进行数据的处理
(3)项目是用 Rollup 打包的,如果不太理解的话就去先自行了解一下
(1) 社区活跃的 Sentry fundeg 等都是优秀的开源项目,但是并不能针对公司内部的项目进行深度的定制化,假如后续监控系统需要跟其他系统结合,自研系统更容易集成,但开源就可能需要大费周章了。
(2) 自研可以根据实际情况进行一些调整,不需要完全照搬。自己可以针对性的进行数据的调整,整合以及优化等
(3) 更多的监控系统都是要收费的
(4) 因为社区中集成较多可能项目不太需要的,导致在运行时会发生一些不可预知的问题(例如:并发情况下数据没有上报成功)
- 用户信息(用户 ID,用户名,唯一可以确定的用户信息,sessionId,cookie 等等)
- 设备信息(什么浏览器 操作系统 App 版本号)
- 行为数据(用户访问来源,用户访问路径,用户点击滑动区域等)
- 性能数据(更好的做性能优化的,脚本加载时间,接口响应时间等)
- 异常数据(前端脚本加载错误,脚本运行错误)
- 后端 API 请求超时,返回数据异常,参数交互错误等
- 定制的活动数据
- ...自定义数据
[✔️]Rollup 构建的项目工程
[✔️]TypeScript 的类型检查
[✔️]PV,UV 的简单记录,用户自定义打点
[✔️]监控 Error,Promise,Ajax,Fetch 等错误
[✔️]使用 IndexDB 来进行数据存储
[✔️]使用 WebWorker 来做数据存储和清洗
[✔️]自定义性能指标
[✔️]自定义上报数据
[✔️]强制上报
[✔️]简单集成 Vue,没有集成(React,Angular 等)
[✖️]暂时只实现 Web,还有小程序和 Native 需要集成
[✖️]缺少单元测试
// 本项目只是给出一个模板。如果要使用的话就使用模板里面的数据结构和上报机制
git clone ......
npm install
npm install -g rollup
npm run build
// 然后放在项目的public目录上,在index.html上引入monitor.js即可
// 然后再项目中初始化 initMonitor的配置就行
异常类型 | 同步方法 | 异步方法 | 资源加载 | Promise | async/await |
---|---|---|---|---|---|
try/catch | √ | √ | |||
onError | √ | √ | |||
Error 事件 | √ | √ | √(捕获阶段可以捕获) | ||
unhandledrejection | √ | √ |
主要是针对浏览器的特定事件(onerror 、onunhandledrejection)就可以捕获错误了。
项目可以按照线程来分模块,主线程模块和 WebWorker 模块(如果有环境不支持,可以换成别的)
主线程模块主要负责:
(1) 暴露初始化 Monitor 接口,并接收参数初始化 Context
(2) 核心部分(Core) Patch 相关方法并将数据整合 再传递给 Worker
(3) 初始化监听 Worker 事件,并且与 Worker 进行数据交互
(4) 根据 Worker 传递的数据进行 分发操作
Worker 线程主要负责:
(1) 初始化封装 IndexDB 操作对象
(2) 监听主线程发来的消息并根据对应数据插入对应表格进行数据持久化存储。
(3) 根据划分的时间段进行数据的清洗,然后把数据一并传到主线程
在这里两个线程分工明确:
- 主线程是数据的采集和上报
- Worker 线程主要是进行数据存储和数据清洗
- 页面加载时长:Page Load Time PLT
- 首屏加载时长:Above the fold Time AFT
- 首次渲染时长:First Paint
- 首次内容渲染时长: First Contentful Paint
- 首次有效内容渲染时长: First Meaningful Paint
- 开始渲染 start Render
同学们可以针对需要调整性能指标
- 为什么使用 IndexDB
存储方式 | localStorage | IndexedDB | webSQL |
---|---|---|---|
类型 | key-value | NoSQL | SQL |
数据格式 | string | object | ------- |
容量 | 5M | 500M | 60M |
进程 | 同步 | 异步 | 异步 |
检索 | key | key, index | field |
性能 | 读快写慢 | 读慢写快 | ----- |
综合之后,IndexedDB是最好的选择,它具有容量大、异步的优势,异步的特性保证它不会对界面的渲染产生阻塞。而且IndexedDB是分库的,每个库又分store,还能按照索引进行查询,具有完整的数据库管理思维,比localStorage更适合做结构化数据管理。
缺点也很明显,需要去深入了解Api及其使用,对比localStorage简洁的Api来说,IndexDB较为复杂。
-
上报的机制
对于错误类型的数据,监控出来立马上报,早发现早解决。
对于记录类型的数据,先放入IndexDB中,等一段时间(可设置一周定时任务,然后进行清洗后上报)
对于性能类型的数据,就等主线程空闲的时候再上报
-
上报的方式
对于错误类型是用请求一张1x1的图片实现的,这样做不影响页面的性能,不占用过多的资源。
对于记录类型和性能类型的数据,上报是用Ajax上报:
- 这类数据的数据量通常比较大,上报的时候可能会超过Url的限制
- 可能数据里面有一些“#”的特殊字符使得Get请求失败
- 可能这类数据是在页面关闭或者是其他时机进行上报的,Image同步方式不太合适
-
设计操作IndexDB
因为IndexDB的操作都是异步的,所以在等待的过程和监听到错误的过程可能是有先后顺序的。所以需要一些操作能让它们按照我们的想法中执行
方法一: 观察者模式
操作方式:先把监听的错误全部收集起来。等待过程结束,然后拿到IndexDB对象再进行操作。
出现问题:这样在后续的操作里面没法拿到IndexDB对象,仍需要再等待进行操作(因为IndexDB对象是传进去的),所以会导致复用性不够高。而且调用时机也需要过多的信息通知才行,这里有一个消息通知的耗损,也不划算。假如是在读取操作的时候,无法返回读取后的结果(因为执行的时候是在异步的情况下执行的)
function getDBRequest(){ // 假如在这里拿到DB对象 ... // 这里确实能解决先获取DB对象 再进行操作的问题。 // 假如后续还有该操作的时候 就得(额外的消息)通知 DBRequest进行操作 quene.forEach(val=>{ val(DBRequest) }) }
方法二: 每次操作前先获取DB对象
操作方式:将所有IndexDB的方法全部封装成Promise的形式,每一个方法里面都等待DB对象的初始化,DB对象获取后再进行DB操作。
出现问题:这样的操作就是,每一次都需要过长时间的等待,也会消耗性能。每次都需要一个new 一个新的DB对象,占用过多的内存。还有无法正确的监听到错误,等待的过程可能错误已经发生了,就无法进行数据的封装。
async function add(){ const DBRequest = await getDBRequest() // 这里拿到DB对象 ... // 这里进行Add的操作 }
方法三: 通过Worker操作IndexDB
操作方式:主线程中监听到所有错误,封装成数据后通过消息发送到Worker中。Worker内部会初始化一个局部的的DBRequest,等待消息传递过来。当Worker拿到消息的同时,消息也传递过来,就能顺序执行每一个DB操作了
出现问题:等待的过程也是需要时间,可能刚进去就出现错误了。也是无法正常进行DB操作了。这里有一个解决方法就是,主线程先监听错误后放进错误队列中,等待Worker线程初始化后再把队列中的数据全部传递,后续可以正常传递。
// work.ts async function main(){ const DBRequest = await getDBRequest(); // 这里发通知 告诉主线程 我准备好了 self.addEventListener("message",function(){ // 这里获取到数据后 直接 拿上面已经存在的 局部DBRequest来进行操作 }) } // main.ts function main(){ // 监听到的错误 先push 到一个队列中 const quene = [] quene.push(data); ... // 监听到了 worker已经准备好了以后 if(quene.length){ worker.postMessage(...quene)// 全部发送后 // 还得清空 quene 不然还会进来这个分支 继续发送 quene = [] }else{ // 正常发送 } }
在主线程中操作,也是需要等待DB对象的获取,而且还会占用资源,后续还需要数据进行清洗。所以将数据的操作全部放进Worker中是比较好的选择。这样分工会明确点,通过消息传递,各司其职。后续在别的环境只需要把Worker的操作换成别的操作就能兼容别的环境的监控。
-
设计数据清洗
数据清洗的周期:因为是前端清洗不是后端清洗,后端可以通过定时任务进行数据清洗,而且前端无法这样做。这里设计了一个简单的时间记录。将今天的时间记录到localStorage里面("2020-10-01"),下次再登录如果是超过数据清洗时间,就进行数据的清洗并且上报。
数据清洗的方法:因为收集的数据主要是Data字段(或者是自定义的字段)。对收集数据进行LRU算法的筛选,仅保留最后一次的数据。这样做过滤重复的数据,还能知道这个问题最后还触发的时间是什么时候。
-
performance.loadEventEnd 的问题
这个值需要等待一会才能有值,所以不能直接取。可以通过 setInterval 的手段进行获取。
-
上报后台以后需要对后台上传 sourceMap
报错的信息已经是混淆过后的代码了,所以需要 sourcemap 才能定位到原始的代码中