当前位置: 首页 > 科技观察

搭建前端监控如何采集异常数据?

时间:2023-03-16 13:50:14 科技观察

本文介绍前端如何采集数据,从采集异常数据开始。什么是异常数据?数据异常是指在页面运行过程中,前端触发的执行异常或加载异常。这时,浏览器会抛出错误信息。例如,如果你的前端代码使用了一个未声明的变量,控制台会打印一个红色的错误来告诉你错误的原因。或者接口请求有错误,在网络面板也可以发现异常情况,是请求发送异常还是接口响应异常。在我们实际的开发场景中,前端捕获的异常主要分为两类,接口异常和前端异常。让我们看看如何捕获这两种类型的异常。接口异常请求时必须触发接口异常。目前大部分的前端请求都是由axios发起的,所以只需要获取axios中可能出现的异常即可。如果使用Promise,使用.catch捕获:axios.post('/test').then((res)=>{console.log(res);}).catch((err)=>{//err是捕获的错误对象handleError(err);});如果你使用async/await,使用try..catch..capture:async()=>{try{letres=awaitaxios.post('/test');控制台日志(资源);}catch(err){//err是捕获的错误对象handleError(err);}};当捕获到异常时,会交给handleError函数处理,该函数会对接收到的异常进行处理,并调用上报接口将异常数据传输给服务器,从而完成收集。我们上面写的异常捕获在逻辑上是正确的。在实践中,我们会发现第一道坎:那么多的页面,是不是每个request都要包裹一层catch呢?是的,如果我们是开发一个新的项目,一开始就规定每个request都包含一层catch是可以理解的,但是如果在一个规模比较小的已有项目中接入前端监控,在这个时候抓到每一个页面或者每一个请求显然是不现实的。因此,为了尽可能降低访问成本,减少入侵,我们采用第二种方案:在axios拦截器中捕获异常。前端项目,为了统一处理请求,比如401跳转,或者全局错误提示,都会在全局写一个axios实例,给这个实例加上拦截器,然后直接引入这个实例在其他页面使用,比如://全局请求:src/request/axios.jsconstinstance=axios.create({baseURL:'https://api.test.com'timeout:15000,headers:{'Content-Type':'application/json',},})exportdefaultinstance然后在特定页面发起请求,如下所示://apage:src/page/a.jsximporthttpfrom'@/src/request/axios.js';async()=>{letres=awaithttp.post('/test');控制台日志(资源);};这样我们发现每次页面请求都会去全局的axios实例,所以我们只需要在全局请求所在的位置捕获异常即可,不需要在每个页面都捕获,所以访问成本会是大大减少。按照这个方案,我们接下来会在文件src/request/axios.js中实现。在拦截器中捕获异常首先我们为axios添加一个响应拦截器://响应拦截器instance.interceptors.response.use((response)=>{returnresponse.data;},(error)=>{//发生异常将转到此处if(error.response){letresponse=error.response;if(response.status>=400){handleError(response);}}else{handleError(null);}returnPromise.reject(error);},);响应拦截器的第二个参数是发生错误时要执行的函数,参数是异常。我们首先需要判断error.response是否存在。如果存在,说明接口有响应,即接口连接上了,但是返回了错误;如果不存在,说明接口没有连接上,请求被挂起。如果有响应,先获取状态码,根据状态码判断什么时候需要收集异常。上面的判断方法简单粗暴。只要状态码大于400,就视为异常,获取响应数据,执行上报逻辑。如果没有响应,可以认为是接口超时异常,在调用异常处理函数的时候传一个null即可。前端异常上面我们介绍了如何在axios拦截器中捕获接口异常,这部分我们将介绍如何捕获前端异常。前端代码捕获异常最常见的方式就是使用try..catch..任何同步代码块都可以放在try块中,只要有异常发生就会执行catch:try{//anysynchronizationcode}catch(err){console.log(err);}说的是“任意同步代码”而不是“任意代码”,主要是普通的Promise写法try..catch..无法捕获,以及只能用.catch()捕获,比如:try{Promise.reject(newError('发生错误')).catch((err)=>console.log('1:',err));}catch(err){console.log('2:',err);}把这段代码扔进浏览器,打印出来的结果是:1:Error:Somethingwentwrong.很明显,.catch捕获了异常。不过和上面接口异常的逻辑是一样的,这样处理当前页面的异常是没有问题的,但是从整个应用的角度来看,这样捕获异常侵入性高,访问成本高,所以我们的思路还是全局捕获。全局捕获js异常也比较简单,使用window.addEventLinstener('error')即可://js错误捕获window.addEventListener('error',(error)=>{//error是js的异常});为什么不使用window.onerror?这里很多朋友有疑问,为什么不用window.onerror来全局监控呢?window.addEventLinstener('error')和window.onerror有什么区别?首先这两个函数的作用基本相同,都是可以全局捕获js异常。但是有一类异常叫资源加载异常,是代码中引用了不存在的图片、js、css等静态资源引起的,如:constloadCss=()=>{letlink=document.createElement('链接')link.type='text/css'link.rel='stylesheet'link.href='https://baidu.com/15.css'document.getElementsByTagName('head')[10].append(link)}render(){return

加载样式
}baidu.com/15上面代码中的.css和bbb.png是不存在的,这里的JS执行肯定会报资源未找到的错误。但默认情况下,上述两个窗口对象上的全局监听函数都无法监听到此类异常。因为资源加载异常只会在当前元素上触发,异常不会冒泡到窗口,所以无法捕捉到监控窗口上的异常。那我们该怎么办呢?如果你熟悉DOM事件,你就会明白,既然冒泡阶段无法监控,那么就必须在捕获阶段进行监控。方法是给window.addEventListene函数指定第三个参数,简单的为true,表示在捕获阶段会执行监听函数,从而监听资源加载异常。//捕获阶段全局监听window.addEventListene('error',(error)=>{if(error.target!=window){console.log(error.target.tagName,error.target.src);}handleError(错误);},真实,);上面的方法可以很方便的监听到图片加载异常,这也是比较推荐window.addEventListene的原因。但是请记住,如果第三个参数设置为true,则可以通过监听事件捕获来全局捕获JS异常和资源加载异常。重要的是要注意window.addEventListene也不能捕获Promise异常。无论是Promise.then()还是async/await,都无法在异常发生时被捕获。因此,我们还需要在全局监控一个unhandledrejection函数来捕获未处理的Promise异常。//承诺错误捕获window.addEventListener('unhandledrejection',(error)=>{//打印异常原因console.log(error.reason);handleError(error);//防止控制台打印error.preventDefault();});unhandledrejection事件会在Promise发生异常且没有指定catch时触发,相当于一个全局的Promise异常解决方案。这个函数会捕获运行时意外发生的Promise异常,这对我们排查问题非常有用。默认情况下,当Promise中发生异常且未被捕获时,该异常将打印在控制台上。如果我们想阻止异常打印,我们可以使用上面的error.preventDefault()方法。异常处理函数在捕获到异常之前,我们调用了一个异常处理函数handleError。所有异常和报告逻辑都在此函数中处理。接下来,我们实现了这个功能。consthandleError=(error:any,type:1|2){if(type==1){//处理接口异常}if(type==2){//处理前端异常}}为了区分异常类型,该函数新增了第二个参数类型,表示当前异常属于前端还是接口。在不同的场景下使用如下:处理前端异常:handleError(error,1)。处理接口异常:handleError(error,2)。处理接口异常处理接口异常,需要对获取到的error参数进行解析,然后获取需要的数据。接口异常一般需要的数据字段如下:code:http状态码。url:接口请求地址。method:接口请求方法。params:接口请求参数。error:接口上报错误信息。这些字段可以在error参数中获取,如下:consthandleError=(error:any,type:1|2){if(type==1){//此时的错误响应,其config字段包含Request信息let{url,method,params,data}=error.configleterr_data={url,method,params:{query:params,body:data},error:error.data?.message||config对象中的JSON.stringify(error.data),})}}params表示GET请求的query参数,data表示POST请求的body参数,所以我在处理参数的时候,把这两个结合起来参数合二为一,用一个属性params来表示。params:{query:params,body:data}还有一个error属性表示错误信息。获取方式取决于你接口的返回格式。避免得到接口可能返回的超长错误信息,大部分是接口没有处理的,可能会导致写数据失败,所以一定要提前和后台约定好。处理前端异常前端异常大部分是js异常,异常对应js的Error对象。在处理之前,我们先看一下错误的类型:ReferenceError:引用错误。RangeError:超出有效范围。TypeError:类型错误。URIError:URI解析错误。这几类异常的引用对象都是Errors,所以可以这样获取:consthandleError=(error:any,type:1|2){if(type==2){leterr_data=null//监听是否错误是标准类型if(errorinstanceofError){let{name,message}=errorerr_data={type:name,error:message}}else{err_data={type:'other',error:JSON.strigify(error)}}}}在上面的判断中,首先判断异常是否是Error的实例。其实大部分代码异常都是标准的JSError,不过这里先做个判断。如果是,直接获取异常类型和异常信息。如果不是,请将异常类型设置为其他。我们随便写个异常代码,看看抓取的结果:functiontest(){console.aaa('ccc');}test();那么捕获到的异常是这样的:consthandleError=(error:any)=>{if(errorinstanceofError){let{name,message}=error;控制台日志(名称,消息);//打印结果:TypeErrorconsole.aaa不是函数}};getenvironmentdatagetenvironmentdata意思是不管是接口异常还是前端异常,除了异常本身的数据外,我们还需要一些其他的信息来帮助我们更快更准确的定位到哪里出错了.我们称这类数据为“环境数据”,也就是触发异常的环境。比如是谁在哪个页面触发了错误,在什么地方触发了错误,有了这些我们就可以马上找到错误的来源,然后根据异常信息来解决错误。环境数据至少包括以下内容:app:应用程序的名称/标识。env:应用环境,一般是开发、测试、生产。版本:应用程序的版本号。user_id:触发异常的用户ID。user_name:触发异常的用户名。page_route:不寻常的页面路由。page_title:异常的页面名称。app和version都是应用配置,可以判断异常发生在应用的哪个版本。对于这两个字段,我建议直接获取package.json下的name和version属性。应用升级时,及时修改version版本号即可。其余字段需要根据框架的配置获取。下面我将分别介绍如何在Vue和React中获取它们。在Vue中,一般直接从Vue中的Vuex获取用户信息。如果你的用户信息没有存储在Vuex中,从localStorage中获取也是一样的。如果是在Vuex中,可以这样实现:importstorefrom'@/store';//vuex导出目录letuser_info=store.state;letuser_id=user_info.id;letuser_name=user_info.name;用户信息存在于状态管理中,页面路由信息一般定义在vue-router中。前端路由地址可以直接从vue-router获取,页面名称可以在meta中配置,如:{path:'/test',name:'test',meta:{title:'testpage'},component:()=>import('@/views/test/Index.vue')}这样配置之后,很容易获取当前页面路由和页面名称:window.vm=newVue({...})letroute=vm.$routeletpage_route=route.pathletpage_title=route.meta.title最后一步,我们再次获取当前环境。当前环境用一个环境变量VUE_APP_ENV表示,它有三个值:dev:开发环境。测试:测试环境。亲:生产环境。然后在根目录下创建三个环境文件,写入环境变量:.env.development:VUE_APP_ENV=dev。.env.staging:VUE_APP_ENV=测试。.env.production:VUE_APP_ENV=专业版。现在获取env环境时,可以这样获取:{env:process.env.VUE_APP_ENV;}最后一步,在进行打包的时候,传入mode来匹配对应的环境文件:#测试环境打包$numrunbuild--modestaging#生产环境打包$numrunbuild--modeproduction获取环境数据,然后将异常数据拼凑起来,我们准备上报数据。在React中,就像在Vue中一样,可以直接从状态管理中获取用户信息。因为在React中全局获取当前行程没有捷径,所以我也会把页面信息放在状态管理中。我使用的状态管理是Mobx,获取方式如下:import{TestStore}from'@/stores';//mobx导出目录let{user_info,cur_path,cur_page_title}=TestStore;//用户信息:user_info//页面信息:以cur_path和cur_page_title为例,每次页面切换都需要更新mobx中的路由信息??。怎么做?其实只要在根路由页面(一般是首页)的useEffect中监听即可:import{useLocation}from'react-router';import{observer,useLocalObservable}from'mobx-react';import{TestStore}from'@/stores';exportdefaultobserver(()=>{const{pathname,search}=useLocation();consttest_inst=useLocalObservable(()=>TestStore);useEffect(()=>{test_inst.setCurPath(路径名,搜索);},[路径名]);});获取到用户信息和页面信息后,接下来就是当前环境了。和Vue一样,通过--mode指定模式,并加载相应的环境变量,只是设置方式略有不同。大多数React项目可能是用create-react-app创建的,我们将以此为例介绍如何修改它。首先打开scripts/start.js文件,这是执行npmrunstart时执行的文件。我们在第6行的开头加入代码:process.env.REACT_APP_ENV='dev';是的,我们指定的环境变量是REACT_APP_ENV,因为只能读取REACT_开头的环境变量。然后修改scripts/build.js文件的第48行,如下:if(argv.length>=2&&argv[0]=='--mode'){switch(argv[1]){case'staging':process.env.REACT_APP_ENV='测试';休息;case'production':process.env.REACT_APP_ENV='pro';休息;default:}}这时候可以这样获取env环境:{env:process.env.REACT_APP_ENV;}小结经过前面一系列的操作,我们已经全面的获取了异常数据,以及当发生异常。接下来就是调用上报接口,将数据传到后台存储。易于查找和跟踪。如果你也需要前端监控,不妨按照文中介绍的方法,花半个小时收集异常数据,相信对你会有很大帮助。