有一天一个测试同事的一个移动端页面白屏了,看样子是页面哪里报错了。 我自己打开页面并没有报错,最后发现报错只存在于他的手机,移动端项目又是在微信环境下,调试起来会比较麻烦,最后用他手机调试才发现问题: 是他账户下面有个对话的消息数据有问题导致页面报错了。 一般遇到这种情况只有用他的手机或者账户调试能很快查到问题,如果是外部的用户怎么办,我没法拿他的手机去测试。
其实这个问题很常见,但是这次我觉得这个问题如果不是我们自己同事发现的,那就很恐怖,可能废很大精力才能查出问题,甚至会导致很严重的线上 bug,细思极恐,刚好前不久成都 FCC 的大前端交流会上叶小钗谈到了监控这块,也让我有所启发,这些公共服务才是公司的核心财富,目前公司业务发展处在上升阶段,未来用户肯定会越来越多,对系统的稳定性要求也会越来越高,那既然我们还缺乏这块的服务,现在做正合适。前期准备
从提出这个想法的一开始就知道,落地才是关键,否则一切空谈。 刚好半个多月以后,我们前端组需要在公司做一次分享,我现在做个题材就挺适合分享的,其他后端和测试同事也容易听进去一点。 最开始我考虑了后端存储和可视化的情况,想找个现成后端集成工具帮我处理后端的工作。 就找后端同事问了一下,同事推荐了 Elasticsearch+Fluentd+Kibana 。 然后稍微研究了一下,总觉得哪里不对,反正研究了之后发现可能还是需要做一些定制开发才能解决需求,后端同事听了我的需求也是这么说的。 一人之力有限,并且公司业务上的事情也多,找一个后端同事配合极好,利用各自的优势可以更快落地,这样我也可以专注前端的工作和把控整个项目落地。 就这样,我和后端同事商量了一下,他也答应抽空和我一起搞了。 抛开后端的事情,我开始思考前端的工作,去调研一下别人的方案和这块的知识。 有一些三方库或者开源项目提供类似的功能的,做了很简单的了解。 最后想着自己开发更容易去适应自身的业务,并且目前第一版的需求功能也并没有那么大的开发量,那就自己做吧。 前期遇见了一些需要解决和实现的功能点: 生成 sourcemap,监听 js 报错和信息上报,压缩的 js 代码上报后 sourcemap 解析问题,如何更平滑的应用在业务项目中,数据存储优化等。基本实现
前端
后端
通过 onerror 我们能监听和拿到 js 的报错信息, 可以拿到如下代码的五个参数。 columnNo, error 这两个参数在一些老版本的 IE8-9 浏览器和 opera 低版本等浏览器上可能拿不到,但是没有关系,我们在代码上兼容拿不到参数的情况,如果缺少后两个参数,传空值就行了。 也可以通过其他方式拿到这些老版本浏览器的 columnNo 和 error 参数,目前监控主要是针对移动端,也没太大必要去兼容老版本的浏览器。
onerror 方法大致实现如下:
- window.onerror = function(msg, fileUrl, lineNo, columnNo, error) {}
可能存在跨域问题,不同域下的 js 需要配置 script 属性 crossorigin="anonymous" 和后端配置 Access-Control-Allow-Origin,但是目前我们的项目不存在 js 跨域问题。
现在我们能通过 onerror 拿到报错信息了,可是线上的代码是经过压缩的,报错的时候我们能拿到的的行列数和变量命都不能告诉我们源代码哪里出错了。这里我们需要用到 sourcemap,下面来讲讲它。sourcemap
sourcemap 就是一个信息文件,里面储存着位置信息。 也就是说,sourcemap 文件记录了代码转换前的位置和转换后对应的位置( www.ruanyifeng.com/blog/2013/0… )。 下面图 1 是 login.js 的压缩版本,第二行的注释指定了 map 文件的相对路径,浏览器根据注释会找到 map 文件然后自动解析出来,在调试器里就可以看到源码了; 图 2 是 map 文件(json 格式); 图 3 图 4 介绍 sourcemap 文件。 图 2 我们生成的 map 文件 sourcesContent 字段直接引入了源文件代码(构建工具可以配置是否给 map 文件引入源文件),这样可以方便后端解析,如果没有源文件对应的话后端是解析不出正确结果的。
(图 1)
(图 2)
(图 3)
(图 4)我们的移动端项目构建工具比较老了,统一用的 grunt 作为打包工具。 之前没有在压缩代码时使用 sourceMap,因为开发和测试环境没有压缩,所以也不需要在浏览器用 sourceMap 调试。 然后我就再去修改 gruntfile 文件(之前不是我写的),sourceMap 配置感觉和官方文档对不上,老是报错,最后才发现之前的打包工具的依赖版本是 13 年的了,也暂时没必要去折腾版本问题了,把老版本的文档翻出来再配置了一下 sourcemap 文件就成功的生成在源文件的同级目录下了,比如源文件叫 xx.js,map 文件就是 xx.js.map。 我们给 js 文件加上了 md5 版本号,所以实际的文件是 xx.md5.js 和 xx.md5.js.map(md5 是根据内容变化的)。sourcemap 解析问题
思考的时候发现最大的难点应该在 sourcemap 解析。 最开始后端同事以为 sourcemap 是 nodejs 生成的文件,他们后端用的 go 或者 php 似乎不能解析吧,如果知道了 sourcemap 原理就应该知道,它只是一种数据格式和开发语言没关系。 我把 map 文件和报错信息交给后端同事,他们用 go 语言的一个工具成功解析出了答案,实现了本地文件的解析。 但是我们需要的是自动化解析,不可能每次都去把存储的报错信息手动的拿出来再去找对应的 map 文件做人工解析。 所以需要我们后端程序自己去找到 map 文件,并解析报错信息。
如此一来,后端解析存在两个关键问题:
这里只说我们的方案,map 文件和源 js 文件打包到同级目录下,一起上传到服务器(比如 js 的路径是 www.xxx.com/dist/index.md5.js,那 map 文件的地址就是 www.xxx.com/dist/index.md5.js.map),服务端就可以根据报错的 js 路径再加上. map 后缀找到 map 文件。 压缩文件有一段注释描述 sourceMappongURL 指定了 map 文件的位置,打开浏览器之后调试器会找到这个 map 文件,在浏览器里就能看到源代码,为了避免这种情况,需要服务器配置 .js.map 后缀的文件不可访问。 如果这样的话,服务器解析的时候不能直接去下载静态资源. map 文件,而是需要去找到服务器本地对应的 map 文件,这样要单独配置路径和写逻辑很麻烦,而且文件夹结构有变动的话也不灵活。 所以我们的方案是做 token 权限校验,map 文件必须加正确的 token 参数,服务器才会返回资源(xxx.js.map?token=xxxx),否则 nginx 会屏蔽没有 token 或者 token 错误的请求。
两种方法,一种是后端接口收到报错信息之后,马上找到 map 文件,并解析存储到数据库。 一种是先保留上报信息,通过接口查询的时候再去解析。 我们选择了前者,接口收到数据之后,后端根据当前报错文件的 url,去查查本地是否已经下载过当前文件,如果已经存在这个文件,就直接用本地的文件解析,如果本地没有,路径加上. map 和 token 参数,下载对应的 map 文件到本地,然后再去读取当前本地文件并解析,解析的数据和上报的数据就存为一条记录。 如果是后者的方法,存在很多麻烦的问题,这里不多说了。
一张图详细描述我们的解析流程:
有一种情况可能发生: 当前项目已经更新到 1.1 版本了,1.0 版本的一个报错以前没被触发,这个时候有个用户缓存了 1.0 版本的代码,并且触发了一个新的报错,这个时候服务器本地存储的 map 文件里没有这个文件,就会带上 token 去下载 map 文件,因为当前已经是 1.1 版本了,原 js 文件发生过变动,md5 的版本已经对应不上了,这个时候就没法找到 map 文件了,无法解析,所以这种特殊情况只能存储上报的 errorInfo 信息。如何更平滑的应用在业务项目中
目前 js 的 onerror 方法只有代码量不大,后期还会有叠加。现在的想法是尽量不和业务代码做过多接触,只需要直接引入当前 js 到各个业务项目中去,每个项目不用对它太多任何配置,让它尽量单纯一点。存储优化
后期是会做管理后台来查询和统计这些异常日志的,同一个错误可能上传报错数据到服务端,后端查询出来是一条条独立的记录,我们不能区分这条记录的报错是不是有重复数据,也不应该让后端去做字段对比。 后来想到给 报错的文件路径 + 行 + 列 信息拼在一起字段做 md5 生成,根据这个唯一值生成 md5,最后查询的时候只需要查询当前 md5 字段就能知道这一条报错一个有多少条记录。 不过我想的太天真了,不同的浏览器报错行列信息有点不一样,同一报错就可能生成不同的 md5 字符串,即便这里有点问题,我还是继续用这个方案保存了 md5(因为内核原因,移动端的差异还是比较小,当前字段也能有一定的区分性)。
我们第一版存储的主要数据(还有一些常规的就不说) :
- {
- "businessInfo": "{}",//业务项目自定义的数据
- "errorMd5": "80bb86b86da0607c0dc5c3a77e16eab6",//根据报错部分信息生成的md5
- "manualSendError": "{}",//手动上传的报错信息
- "pageUrl": "http://www.xxx.com/xxx.html",//放生报错的页面url
- "parseError": true,//解释是否失败
- "parsed": '{"col":0,"errKey":"list","file":"xxx.js","line":105}',//解析后的行列、文件路径和变量
- "raw": '{"msg":'', "fileUrl":'', "lineNo":'', "columnNo":'', "error":''}',//onerror的五个参数
- "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Redmi 4X Build/MMB29M; wv)..." //navigator.userAgent
- }
邮件提醒是很有必要的一个功能,目前已经实现实时邮件提醒功能。 公司企业邮箱建个单独的邮箱就叫 frontendmonitor@吧,当后端接口收到报错后,把解析数据通过这个邮箱发送给前端,达到提醒效果。 如果是用 QQ 邮箱或者个人邮箱应该需要在账户里开启 smtp 服务,QQ 企业邮箱是默认开启此功能的。 邮件功能要注意性能和优化问题,不能因为前端报错太多导致服务器挂掉。
这种非业务服务,来源于个人兴趣和思考,并没有上层压力需要你做或者什么时候做完。 从最开始有个想法、去调研、去找后端同事求助、 开干到最终落地。 这个过程需要自己坚持做下去,因为害怕自己不能最终落地,所以抓紧时间,一步步去实现每个细节的想法,让事情尽快落地和上线,以免自己对这个事情越拖越久。 作为需求方,更好的把握整个项目,加上自己的兴趣,所以这次自己也学习了一点 go 语言,保证能看懂后端代码和了解后端逻辑,最好能做一点开发,这次在后端同事代码的基础上,实现了发邮件的小功能,我称之为浅入浅出,装完逼就跑路~ 现在第一版已经上线,并且在刚上线不到两个小时,就收到了报错邮件,吓得我急忙查找 bug,很快查出来了问题来,这个 bug 应该存在很久了,但是因为没有阻塞性,并且没有影响到业务,也一直没被发现,结论是我们这个前端异常监控功能还是很成功! 后期还有很多功能需要开发,统计、数据可视化、智能报警等等。 第一版落地,就为以后的迭代和进化打下了良好基础。
在做这个事情的过程中,我是想尽快把事情落地,时间也很紧张,也并没有做非常充分的调研,比如现成的一些开源项目是怎么做的。 后来从同事那里了解到 sentry 这些三方开源项目之后,也有一点失落过,虽然我也解决了我的需求,但是三方的开源项目是一个非常完善的系统,提供了很多功能,比我这个强大多了,那我做这个到底有什么意义, 感觉完全和别人比拼不上,未来我这个项目会继续迭代吗,有继续迭代的必要吗? 以后有特殊定制化的需求的时候,也许自己开发的才容易更适应业务,可是有那个机会吗? 这一次落地已经达到我最初的要求了,也能帮我解决目前问题,未来还有很多挑战和迭代等待着,我会带着它一路过关斩将,还是半路死掉? 我想说:
最后大力地感谢我司后端同事的大力支持!!~
来源: https://juejin.im/post/5a3dca226fb9a04515441686