百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

从无到有:前端异常监控系统落地(前端故障)

wxin55 2024-10-25 18:06 11 浏览 0 评论

导火索

有一天一个测试同事的一个移动端页面白屏了,看样子是页面哪里报错了。 我自己打开页面并没有报错,最后发现报错只存在于他的手机,移动端项目又是在微信环境下,调试起来会比较麻烦,最后用他手机调试才发现问题: 是他账户下面有个对话的消息数据有问题导致页面报错了。

一般遇到这种情况只有用他的手机或者账户调试能很快查到问题,如果是外部的用户怎么办,我没法拿他的手机去测试。

其实这个问题很常见,但是这次我觉得这个问题如果不是我们自己同事发现的,那就很恐怖,可能废很大精力才能查出问题,甚至会导致很严重的线上bug,细思极恐,刚好前不久成都FCC的大前端交流会上叶小钗谈到了监控这块,也让我有所启发,这些公共服务才是公司的核心财富,目前公司业务发展处在上升阶段,未来用户肯定会越来越多,对系统的稳定性要求也会越来越高,那既然我们还缺乏这块的服务,现在做正合适。

前期准备

从提出这个想法的一开始就知道,落地才是关键,否则一切空谈。 刚好半个多月以后,我们前端组需要在公司做一次分享,我现在做个题材就挺适合分享的,其他后端和测试同事也容易听进去一点。 最开始我考虑了后端存储和可视化的情况,想找个现成后端集成工具帮我处理后端的工作。

就找后端同事问了一下,同事推荐了 Elasticsearch+Fluentd+Kibana 。然后稍微研究了一下,总觉得哪里不对,反正研究了之后发现可能还是需要做一些定制开发才能解决需求,后端同事听了我的需求也是这么说的。一人之力有限,并且公司业务上的事情也多,找一个后端同事配合极好,利用各自的优势可以更快落地,这样我也可以专注前端的工作和把控整个项目落地。

就这样,我和后端同事商量了一下,他也答应抽空和我一起搞了。抛开后端的事情,我开始思考前端的工作,去调研一下别人的方案和这块的知识。

有一些三方库或者开源项目提供类似的功能的,做了很简单的了解。

最后想着自己开发更容易去适应自身的业务,并且目前第一版的需求功能也并没有那么大的开发量,那就自己做吧。

前期遇见了一些需要解决和实现的功能点:生成sourcemap,监听js报错和信息上报,压缩的js代码上报后sourcemap解析问题,如何更平滑的应用在业务项目中,数据存储优化等。

基本实现

前端

  • js报错事件监听+处理上报

  • 构建工具生成sourcemap文件

  • sourcemap文件上传

后端

  • 提供接口收集报错

  • 读取sourcemap文件,解析上传的报错(解析发生时间:接口收集到后马上处理,后期提取的时候处理)

  • 存储数据

监听js报错和信息上报

通过onerror我们能监听和拿到js的报错信息, 可以拿到如下代码的五个参数。 columnNo, error这两个参数在一些老版本的IE8-9浏览器和opera低版本等浏览器上可能拿不到,但是没有关系,我们在代码上兼容拿不到参数的情况,如果缺少后两个参数,传空值就行了。 也可以通过其他方式拿到这些老版本浏览器的columnNo和error参数,目前监控主要是针对移动端,也没太大必要去兼容老版本的浏览器。

window.onerror = function (msg, fileUrl, lineNo, columnNo, error) {}

onerror方法大致实现如下:

可能存在跨域问题,不同域下的js需要配置script属性 crossorigin="anonymous" 和后端配置 Access-Control-Allow-Origin,但是目前我们的项目不存在js跨域问题。

提示一下onerror并不能拿到所有报错信息,比如网络报错等

现在我们能通过onerror拿到报错信息了,可是线上的代码是经过压缩的,报错的时候我们能拿到的的行列数和变量命都不能告诉我们源代码哪里出错了。这里我们需要用到sourcemap,下面来讲讲它。

sourcemap

sourcemap就是一个信息文件,里面储存着位置信息。

也就是说,sourcemap文件记录了代码转换前的位置和转换后对应的位置。

下面图1是login.js的压缩版本,第二行的注释指定了map文件的相对路径,浏览器根据注释会找到map文件然后自动解析出来,在调试器里就可以看到源码了;

图2是map文件(json格式);

图3图4介绍sourcemap文件。

图2我们生成的map文件sourcesContent字段直接引入了源文件代码(构建工具可以配置是否给map文件引入源文件),这样可以方便后端解析,如果没有源文件对应的话后端是解析不出正确结果的。

(图1)

(图2)

(图3)

(图4)

grunt生成sourcemap:

我们的移动端项目构建工具比较老了,统一用的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文件,并解析报错信息。

如此一来,后端解析存在两个关键问题:

  1. map文件存储在哪里

  2. 什么时候解析

①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企业邮箱是默认开启此功能的。 邮件功能要注意性能和优化问题,不能因为前端报错太多导致服务器挂掉。

实际使用后的优化

  • 我们发现不同的浏览器报错的变量可能不一样,同一个报错在chrome浏览器和firefox上 columnNo 参数一点偏差。 用两种报错解析了一下,如下图,报错的代码都是18行,是没问题的,Firefox报错是下图第一个:console 18 0 true,chrome是testBase 18 0 true,行数没问题,偏差不影响我们最终查错,我的18行源代码是:console.log(testBase)。 testBase是故意没有申明,testBase是undefined,出问题的应该是testBase这个变量,过从报错情况上看,确实是谷歌浏览器更精准一点。 虽然不在意IE,不过IE11报错列数和firefox一致。

  • 页面触发事件报错,用户一直触发按钮,这时就会不停上报错误信息。解决:存储上一个报错信息和时间,进行比对,同一个报错,短时间内避免一直重复发送。

  • 框架模板报错,被框架本身捕获,不会触发window.onerror,需要使用框架本身的全局监听捕获信息后手动上传,这里需要加手动上传错误信息的方法。

  • 引入监控的项目,由于业务原因可能需要上传一些业务信息方便分析,所以预留一个配置字段,上传错误的时候请求会带上业务相关信息。

总结

这种非业务服务,来源于个人兴趣和思考,并没有上层压力需要你做或者什么时候做完。

从最开始有个想法、去调研、去找后端同事求助、 开干到最终落地。

这个过程需要自己坚持做下去,因为害怕自己不能最终落地,所以抓紧时间,一步步去实现每个细节的想法,让事情尽快落地和上线,以免自己对这个事情越拖越久。 作为需求方,更好的把握整个项目,加上自己的兴趣,所以这次自己也学习了一点go语言,保证能看懂后端代码和了解后端逻辑,最好能做一点开发,这次在后端同事代码的基础上,实现了发邮件的小功能,我称之为浅入浅出,装完逼就跑路~ 现在第一版已经上线,并且在刚上线不到两个小时,就收到了报错邮件,吓得我急忙查找bug,很快查出来了问题来,这个bug应该存在很久了,但是因为没有阻塞性,并且没有影响到业务,也一直没被发现,结论是我们这个前端异常监控功能还是很成功! 后期还有很多功能需要开发,统计、数据可视化、智能报警等等。 第一版落地,就为以后的迭代和进化打下了良好基础。

在做这个事情的过程中,我是想尽快把事情落地,时间也很紧张,也并没有做非常充分的调研,比如现成的一些开源项目是怎么做的。 后来从同事那里了解到 sentry 这些三方开源项目之后,也有一点失落过,虽然我也解决了我的需求,但是三方的开源项目是一个非常完善的系统,提供了很多功能,比我这个强大多了,那我做这个到底有什么意义, 感觉完全和别人比拼不上,未来我这个项目会继续迭代吗,有继续迭代的必要吗? 以后有特殊定制化的需求的时候,也许自己开发的才容易更适应业务,可是有那个机会吗? 这一次落地已经达到我最初的要求了,也能帮我解决目前问题,未来还有很多挑战和迭代等待着,我会带着它一路过关斩将,还是半路死掉? 我想说:

最后大力地感谢我司后端同事的大力支持!!~

作者:子慕大诗人 出处:http://www.cnblogs.com/1wen/p/7942608.html

相关推荐

ES6中 Promise的使用场景?(es6promise用法例子)

一、介绍Promise,译为承诺,是异步编程的一种解决方案,比传统的解决方案(回调函数)更加合理和更加强大在以往我们如果处理多层异步操作,我们往往会像下面那样编写我们的代码doSomething(f...

JavaScript 对 Promise 并发的处理方法

Promise对象代表一个未来的值,它有三种状态:pending待定,这是Promise的初始状态,它可能成功,也可能失败,前途未卜fulfilled已完成,这是一种成功的状态,此时可以获取...

Promise的九大方法(promise的实例方法)

1、promise.resolv静态方法Promise.resolve(value)可以认为是newPromise方法的语法糖,比如Promise.resolve(42)可以认为是以下代码的语...

360前端一面~面试题解析(360前端开发面试题)

1.组件库按需加载怎么做的,具体打包配了什么-按需加载实现:借助打包工具(如Webpack的require.context或ES模块动态导入),在使用组件时才引入对应的代码。例如在V...

前端面试-Promise 的 finally 怎么实现的?如何在工作中使用?

Promise的finally方法是一个非常有用的工具,它无论Promise是成功(fulfilled)还是失败(rejected)都会执行,且不改变Promise的最终结果。它的实现原...

最简单手写Promise,30行代码理解Promise核心原理和发布订阅模式

看了全网手写Promise的,大部分对于新手还是比较难理解的,其中几个比较难的点:状态还未改变时通过发布订阅模式去收集事件实例化的时候通过调用构造函数里传出来的方法去修改类里面的状态,这个叫Re...

前端分享-Promise可以中途取消啦(promise可以取消吗)

传统Promise就像一台需要手动组装的设备,每次使用都要重新接线。而Promise.withResolvers的出现,相当于给开发者发了一个智能遥控器,可以随时随地控制异步操作。它解决了三大...

手写 Promise(手写输入法 中文)

前言都2020年了,Promise大家肯定都在用了,但是估计很多人对其原理还是一知半解,今天就让我们一起实现一个符合PromiseA+规范的Promise。附PromiseA+规范地址...

什么是 Promise.allSettled()!新手老手都要会?

Promise.allSettled()方法返回一个在所有给定的promise都已经fulfilled或rejected后的promise,并带有一个对象数组,每个对象表示对应的pr...

前端面试-关于Promise解析与高频面试题示范

Promise是啥,直接上图:Promise就是处理异步函数的API,它可以包裹一个异步函数,在异步函数完成时抛出完成状态,让代码结束远古时无限回掉的窘境。配合async/await语法糖,可...

宇宙厂:为什么前端离不开 Promise.withResolvers() ?

大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发。1.为什么需要Promise.with...

Promise 新增了一个超实用的 API!

在JavaScript的世界里,Promise一直是处理异步操作的神器。而现在,随着ES2025的发布,Promise又迎来了一个超实用的新成员——Promise.try()!这个新方法简...

一次搞懂 Promise 异步处理(promise 异步顺序执行)

PromisePromise就像这个词的表面意识一样,表示一种承诺、许诺,会在后面给出一个结果,成功或者失败。现在已经成为了主流的异步编程的操作方式,写进了标准里面。状态Promise有且仅有...

Promise 核心机制详解(promise机制的实现原理)

一、Promise的核心状态机Promise本质上是一个状态机,其行为由内部状态严格管控。每个Promise实例在创建时处于Pending(等待)状态,此时异步操作尚未完成。当异步操作成功...

javascript——Promise(js实现promise)

1.PromiseES6开始支持,Promise对象用于一个异步操作的最终完成(包括成功和失败)及结果值的表示。简单说就是处理异步请求的。之所以叫Promise,就是我承诺,如果成功则怎么处理,失败怎...

取消回复欢迎 发表评论: