如何搭建异常捕获平台|数据统计

1,123 阅读20分钟

一、前言

上一篇如何搭建异常捕获平台|场景重现中我们讲述了我们是如何搭建一个异常捕获平台的过程。如果没有看过上一篇文章的同学建议先去看一下,然后再来看这篇文章。

话说回来,有了异常捕获之后,我们拥有了一定的捕获异常能力,并且简单的推送能力。但是那是远远不够的,大量数据收集到了一起,我们该如何筛选出我们需要的数据,用户量大了怎么办。这些是我们面前需要考虑的问题。

这一篇文章我们就讲一下统计后台我们是怎么搭建的,分享一些思路,一些想法。

在讲这些之前,容我先换个输入法,mac自带的输入法真的是不好用,最近码字比较多。

换完了,换成鼠须管好多了,我们继续。

在上期异常捕获代码写完后,我们进行了反思。从最开始的想法到beta版本,写代码的时候基本靠堆,回过头一看,谁都不说是自己写的,实在是不够优雅。所以我们进行了重构

二、代码重构

平常写代码的时候,实现功能先放在第一位,后续来优化代码。我认为这套思维逻辑是没有问题的。写完了之后如果没有后期维护优化的话才是一个比较不好的习惯。

为什么要重构呢,因为遇到了一些问题。

  • 模块混乱堆叠
  • 后期加入的功能越多,代码越大
  • 项目没报错,我们注入的代码报错了怎么办
  • 代码难以维护
  • ...

我们希望做成一个高灵活,可拓展的异常捕获工具。痛定思痛之后我们整理了重构之后我们的拓展功能点有:

  • 拥有生命周期,所有状态可跟踪
  • 提前自我检测来处理内部错误
  • 通过插件的形式上报非错误相关信息
  • 把已有的相关数据上报做成内置插件,选择性接入(如录屏,用户行为堆栈,网络检测等等)
  • 支持自定义插件,在错误信息中增加自定义数据
  • 支持自定义http请求白名单和资源链接白名单,过滤掉不用上报的错误信息
  • 依旧支持cdn和npm两种方式使用

那么我们思路就变得清晰了许多,首先我们来讲一下生命周期方面的设计。

2.1 生命周期

如果程序中有了生命周期,我们就可以更好的把控模块运行状态,执行流程模块化,转为执行策略。先看一下原型设计

如图所示,Edith的生命周期分为如下阶段:

  • init

    该阶段从调用Edith.init方法开始,内部state还是空对象,对全局没有进行任何逻辑处理;但是会对init方法的参数进行判断,如果没有apiKey,就不能开始初始化,如果slienceDev字段为false,代表开发环境下不用上报,因此进入sleep生命周期,不再唤醒。

  • will_mount

    该阶段对参数做初始化配置处理,其中包括对http请求白名单和资源链接白名单进行初始化合并,以及插件字段校验,然后初始化内部state,state里存放的就是上报信息;此前会执行willMount的钩子

  • did_mount

    完成了参数存储和state的初始化,会执行didMount的钩子

  • check_self

    该阶段可以执行一些内部方法,防止在不同运行环境下发生内部错误,完成自我检测,如果没有错误,就继续往下一步,否则直接进入sleep阶段

  • install_plugin

    该阶段根据传入的插件类型,进行异步加载安装,并且初始化插件需要注入的state,然后执行pluginInstalled的钩子

  • listening

    该阶段是Edith正常运行后的常规状态,这个状态里,Edith监听着错误所有错误事件

  • collecting

    该阶段是当检测到异常后,触发handleCollect方法,进入到收集错误状态中,过程中会收集所有插件的功能字段,然后上报,自动回到listening,这个生命周期不会被打断,中途不会触发其他上报

  • sleep

    该阶段,不能触发任何上报,handleCollect方法成为空方法。

原型里并没有任何错误监听和记录的任何逻辑代码,只实现基础功能和提供生命周期钩子,这样可以使我们的代码逻辑更加隔离清晰,易于拓展。

代码方面我们参考了Javascript模板方法设计模式

接下来就可以把功能模块一一安装到证明周期中去,下面挑选重点模块来将一下。后面源码已开源。

2.2 自检模块

为什么需要自检模块呢?

我们是通过js注入的方式去捕获异常,我们没有办法确保这段js在任何浏览器环境下都可以正常的运行。如果我们自身报错了导致项目异常,显然这种情况并不是我们想遇见的。所以我们设计了自检模块。

自检模块就像坐飞机。机票里会自带坠毁保险,很明显这个保险你是享受不到的,但是会让你稍稍安心,并且家里人可以得到补偿。我们的自检模块也是这样。

自检模块可以在坠机的时候让你安全跳伞。还是有点区别的。

自检模块中,会触发内部所有模块内底层方法,如有异常,生命周期状态置为睡眠,不再唤醒。

2.3 插件模块

为什么决定用插件的形式把相关信息收集起来呢?主要是出于两点考量:

  • 让我们的核心代码更加纯粹,其他字段可以根据需要选择性引入并上报
  • 易于拓展,方便后期增加功能
  • 减小核心包大小

就像webpack-plugins 设计模式一样,保留其核心功能,拓展功能通过插件的形式安装进来。这样的设计模式特别适合我们。对于想定制化捕获信息的团队,就可以通过我们开放的插件模板开发自己的捕获信息,然后通过我们的钩子集成到state中。

插件分为内置插件和用户自定义插件两种。

目前内置插件,就是我们beta版本已经开发过的模块。

  • 主动检测模块
  • 用户行为堆栈
  • 录屏和网络信息

只需要在初始化的时候传入对应的字段名即可:

原型中,会根据plugins参数对插件进行初始化,如果识别是内置插件,则会加载对应的模块,得到对应的插件实例。因此先定义好两种加载方式的插件路径,npm模块采用的是动态引入,script标签采用的是加载对应cdn链接:

npm返回的就是promise,而cdn的形式需要我们封装成promise,同时cdn会在Edith实例上挂载对应的构造方法,可是考虑到用es打包成模块的插件而言,动态导入得到的对象是的带有default属性的Module对象实例,因此也统一这样处理:

内部安装插件模块的逻辑如下:

可以注意到,代码中promiseList即存放了动态加载的promise以及用户自定义的其他插件(对象),而getPromiseResult方法就是处理这些数据,得到插件对象列表。想要实现得到多个promise的结果,即用到Promise.all方法,但是这个方法存在一点问题,就是其中一个promise报错,整个都报错了,因此需要封装一下,getPromiseResult的代码如下:

2.3.1开发插件

我们是如何通过插件模板让用户自定义上报信息的呢。

插件即一个实现了apply方法的对象,apply方法的参数为原型注入可以修改state的回调方法,先看看原型内部如何调用收集插件数据的:

compiler作为参数传给插件的apply方法调用,compiler里对拿到的字段名和值,进行了更新,setState到了内部的state里。

插件的一般写法是:

其中,值得注意的是,插件内部如果需要得知是否是第一次加载数据,可以根据判断传入的Edith实例的state是否有自身传递的值,来进行相关初始化逻辑。同时,内部的属性ajaxWhiteList,linkWhiteList会混入到Edith的白名单内,以避免被监控系统监听到。

在生命周期中一共会触发两次插件钩子,第一次为插件装载(checkself),为了初始化数据结构及检查插件语法。第二次是捕获到异常的时候(collected)。除了上述两种状态外插件内部自行收集监听信息即可。

2.4 命令模式。

这边想起一个小的设计模式,在这里分享一下。

异常状态拥有很多种类型,最开始我们写多类型状态处理的时候用了很多if - esle 条件语句。对就是你想象中那种,对象之间高度耦合。这种状况是我们不愿意见到的。

javascript设计模式-命令模式安装命令的方式执行下文,对象之间的耦合度,这种方法很适合这样的使用场景。便于命令拓展。

2.5 其余模块

其余模块都是上一篇文章中的核心异常捕获逻辑,只要遵从生命周期执行即可。

下面我们来说一下统计后台项目的设计思路。

三、统计后台

我们给整个异常监控系统命名为Edith,分为异常捕获(Edith-Script)和数据统计(Edith-Management)两部分

Edith的名称取自漫威电影《蜘蛛侠2:英雄远征》。钢铁侠给蜘蛛侠留下的一副眼镜起名为“Edith”,中文翻译:伊迪丝,英文是“Even Dead,I'm The Hero”,中文翻译为伊人已逝,仍是英雄。这幅眼镜可以跟卫星相连,相当于一个集结战略情报收集、布控、军队调防等一体的指挥部。

3.1 架构

从上图我们可以看出,整个系统设计为四个模块

  • 用户
  • 统计
  • 指标
  • 性能

下面我们讲从这四个模块展开来说一下

3.2 用户

监控平台开放之后,以后一定是给予别人去用的,所以用户模块及用户体验方面一定要做好。

用户这里展开模块来说有如下几点。

3.2.1 权限

考虑到服务业务方的信息隐私性和独立性,我们确定了普通用户-组员-管理员三级权限。

一个应用的创建者默认就是管理员了,他可以通过添加用户,成为组员或者管理员。管理员相比组员,不仅能查看该应用的数据总览和应用管理,还能添加成员、配置警报的方式和规则、项目的其他管理,这样的安排对项目的把控会更合理。

3.2.2 推送规则

每当某个错误有新的事件产生,立即推送警报必然不合理。如果一个错误短时间内出现10000次,意味着邮箱短时间内收到10000封邮件。 因此,按照某种规则来触发警报邮件更加合理。

目前警报规则只有一种,就是指数警报,警报指标有两种用户数或者事件数。当累计出错的事件数刚好是指数序列中的值,就会触发警报系统发送警报邮件。通过设置指数的底数a和起始指数n,来灵活配置警报的间隔和次数。 当底数为10,起始指数为1时,指数序列为10, 100, 1000, ...。那么当异常情况影响1,10,100,1000...以此类推用户数的时候,就会触发警报。

警报方式分为邮箱警报和钉钉警报。当指定的项目触发警报条件的时候,会根据业务方设定的警报方式,将异常信息通知个指定的人处理。

3.2.3 团队

应用的创始者默认为管理员角色。管理员可通过邮箱和钉钉手机号码添加管理员和组员。管理员拥有所有权限,组员只拥有数据总览和应用维度管理的权限。

另外我们还设计了分享模式,可以制定一条异常信息及详情生成二维码分享出去。可设置有效期。以方便非本项目组人排查问题。

3.2.4 应用维度

根据实际业务场景,我们将应用维度管理暂时支持项目管理和域名管理。

项目管理用来配置项目路由路径,通过它我们就可以知道是哪一个项目出现异常,同时它可以做为数据总览的筛选条件,也是警报管理的配置条件之一。

我们有些业务方根据域名区分一般都有测试、灰度、线上三个环境,甚至有些业务方有多个线上域名,所有我们可以通过域名配置来定义不同的环境,它也是可以做为数据总览的筛选条件

3.3 统计

统计方面我们分成了两个模块

  • 数据可视化展示
  • 数据处理

3.3.1 数据可视化

  • 最近异常用户列表,由于爆发同种异常不同用户的操作、设备等各种因素不同,我们保留了最近20个用户的上报信息,尽可能保证数据的准确性。

  • 基础信息模块,主要展示概要信息,堆栈信息,设备信息,位置信息,其他相关信息。其中概要信息展示当前用户异常上报时间,标题,异常类型,访问链接等。

  • 堆栈信息, 展示浏览器异常报错的堆栈详细信息

  • 设备信息,展示浏览器、Js引擎、操作系统的类型和版本,我们的项目一般都有最低兼容的设备操作系统类型和版本,这个可以信息可以忽略到一些异常问题。

  • 场景重现, 模块主要是重现异常前10秒的用户界面画面情况。这是详情最重要的地方,它模拟用户操作,100% 回放用户操作。也就是说,用户做什么我们就回放什么,相当于我们站在用户身后看着用户操作。这部分最主要的能力就是解决偶发 Bug,因为对于偶发 Bug 来讲,用户的操作过程以及依赖数据尤为重要。

  • 用户行为, 模块以卡片的形式将点击,请求,页面跳转的情况进行展示。

  • 页面性能, 模块将当前页面在用户端的性能数据进行展示,性能参数分以下几种:

    DNS查询耗时
    TCP连接耗时
    TTFB
    页面下载耗时:用户浏览器首屏内所有内容都呈现出来所花费的时间
    白屏时间: 用户从打开页面开始到页面开始有东西呈现为止,这过程中占用的时间就是白屏时间
    DOM Ready耗时
    DOM Ready之后继续进行资源下载的耗时
    Load时间:页面所有资源都加载完成并呈现出来所花的时间,即页面 onload 的时间

展示图例如下:

用户行为堆栈

时间趋势图是我们喜欢的可视化工具,我们可以清晰地看出异常上报的时间走势,尤其是当我们解决了某一个错误之后,能够很直观地看到成效。

3.3.2 数据处理

要知道我们的数据上报是面向所有客户端的。同样的一处错误,页面打开了多少次,就会上报多少条。这样一来数据量就很大了,因此做好归类非常重要。因此我们如何归类错误呢?

3.3.2.1 数据清洗

我们目前认为:来自同一个url(忽略search参数和协议)的数据,同样的错误类型,同样的错误信息就认为是同一类错误。关于同样的错误信息的解释是:

  • js异常:错误事件的message相同;unhandledrejection事件的reason相同
  • 网络请求异常:请求的路径URL(忽略search参数和协议),请求的方法method相同
  • 资源加载异常: 资源链接相同

因此我们的数据列表需要把错误归类后,默认根据上报时间顺序,从近到远依次列出来。一列数据,只包含同一类错误。列表项里面包含一些基础信息:页面url,错误类型和名称,请求地址的方法【请求异常】,资源链接【资源异常】,上报时间,事件数,影响用户数等。

其中事件数,即同一类错误上报的次数,这个数据可以直观了解这个错误复现的次数。

而影响用户数,则根据同一类错误的来源IP,userAgen运行环境信息来认定是否为同一用户。这个维度跟事件数有什么差异?

因为事件数可能会有欺骗性。比如一个用户的机型比较特殊,容易触发一个错误,短时间内频繁打开这个页面,导致事件数较大。我们自然会想办法处理这个错误,但是我们并不知道影响域怎么样,无法给这类错误定性,偶发还是必现,并没有参考标准。因此我们需要影响用户数这个维度来协助,了解一个错误的影响范围。

3.3.2.2 数据削峰

什么是数据削峰?

在秒杀系统中,当秒杀时间一到的时候会有一个特别高的流量峰值,它对于资源的消耗是瞬时的。

对于秒杀这个场景来说,最终能够抢购到商品的人数是固定的。也就是说,100个人和1000000个人发起请求的结果都是一样的,最后都只会有100个人买到秒杀的商品。这就意味着,并发量越高,最终的无效请求也就越多。

但是对于业务来说,秒杀活动本身是一种商业推广行为,肯定是希望有更多的人参与进来的。无效的请求越多,也就表示这商业推广越成功。只是在秒杀开始的时候,这些大量的秒杀请求就会给承载秒杀的服务器带来沉重的负担,因此我们可以设计一些规则,让并发的请求延缓或过滤掉这些无效请求,以此来削弱流量高峰对服务器的压力,这就是流量削峰

我们为什么要数据削峰?

我们知道,服务器的处理资源是恒定的,你用或者不用,它就在原地,处理能力不会改变。所以出现峰值的话,就很容易会导致忙到处理不过来,虽然闲的时候也会没什么要处理。但是为了保证服务质量,很多处理资源只能够按照忙的时候来预估,而这样就会导致资源的浪费。

流量削峰就好比因为存在早高峰和晚高峰的问题,所以有了错峰限行的解决方案。

削峰的存在,一是可以让服务端处理变得平稳,二是可以节省服务器的资源成本。

针对这一类场景,削峰从本质上来说就是更多地去延缓用户发出的请求,以便减少和过滤掉一些无效的请求,它遵从【请求数要尽量少】的原则。

流量削峰主要有三种操作思路,一是排队,二是答题,三是分层过滤,这三种方式都是无损(即不会损失用户的请求发出)的实现方案。当然,还有些有损的实现方案,包括后面要介绍的关于稳定性的一些方法,比如限流和机器负载保护等一些强制措施也能达到削峰保护的目的,只是这些都是一些不得已的措施。

监控业务场景业务要削峰,当活动入口开放时后,当时的并发请求量是非常大的,如果入口页面有异常,并发请求场景与上述场景一致。所以我们需要对数据进行削峰处理。

3.4 指标

指标方便也是可视化中的重要一部分,可以很直观的展示出数据的趋势、比率。

我们展示了一些指标数据,利用图标组件进行展示。

错误率折线图、浏览器环境饼图、收集信息、用户平均网速、错误类型饼图等等,因为支持用户插件上报自定义插件,后期打算可以开展动态指标面板,可以配合用户自动上传的数据进行图表自定义展示。

由于数据有些敏感就给大家截取一些echarts上demo的图标观看吧。

图表信息推送方面使用websocket推送实时信息。

3.5 performance

性能检测方面重点分析FCP/FMP,分别统计50%, 75%,90%用户体验数据,以减少统计偏差。

3.6 summary

下个版本会把指标模块和性能模块做成可视化大屏,已经安排上了。

为什么要做城可视化大屏呢?因为出发点,虽然我们做的是异常捕获平台,但是我们希望项目不报错误,捕获到错误只是小概率事件。所以用户更希望看到项目稳定运行时候的状况,而不是噼里啪啦报错的情况。

现在捕获平台已经在团队内部开始接入使用。

  • 捕获了JS异常数20k+
  • 请求异常数量120+
  • 资源异常2k+
  • 异常日志 50k+

异常回放功能立功多次,使用回放 + 用户行为堆栈,定位问题位置非常迅速。

jssdk大小15K,非常轻量,支持插件拓展监控定制。

github开源地址: Edith-Script

四、END

往期文章推荐