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

不要再让你的网页在用户的浏览器中裸奔了

时间:2023-03-12 05:01:07 科技观察

页面在用户端运行。如果10%的用户页面有问题,没有办法在本地复现?如何先了解前端出现的问题,而不是等待用户反馈?能不能像查看服务器日志一样定位前端页面的运行问题?随着前端的业务复杂度越来越高,即使有足够的本地测试和根据caniuse的大量兼容性,页面能不能正常运行,运行得如何,仍然不能让人放心。前端页面发布时,页面运行的设备、浏览器、网络环境、用户的操作习惯等因素都可能是导致页面异常的原因。因此,需要对前端页面进行一些监控,最可行的前端监控方式是将页面的日志选择上报给监控日志服务器。前端日志报告可以非常简单。采集到业务逻辑执行的日志数据后,可以通过参数的形式构造一个url,然后通过Image请求发送给服务器,完成日志上报。(newImage).src=`/r.png?page=${location.href}¶m1=${param1}...`;这行代码完成了日志的上报。然而,在生产环境中,日志上报所引发的问题要复杂得多。日志上报带来的问题日志上报的最终目的是为业务服务,监控业务的运行状态。一般来说,在前端运行场景下,开发者最期望监控的就是页面&API请求是否正常响应,页面的js逻辑是否正常执行。.为了覆盖这两个监控目标,需要覆盖很多类型的日志。在一些特殊场景下,开发者也希望能够与特定业务灵活结合,实现自定义上报。所以常见的日志类型如下-页面&API请求是否正常响应-API调用日志-API调用是否成功,耗时多少-页面性能日志-页面连接时间,首次渲染时间,资源加载时间等–访问统计日志–PV/UV,短时间内的断崖式体积变化很容易反映出问题–页面js逻辑是否正常执行–页面稳定性日志–页面加载和产生的JS错误信息页面交互-业务相关日志-自定义上报-一定随着前端业务的增长,日志监控上报量会快速增加,监控逻辑也会越来越复杂。在生产环境中,前端监控最重要的基本原则是日志的采集和上报本身不能抛出异常或影响页面性能。如此多的日志类型意味着日志获取的逻辑复杂,各种浏览器和环境也会让这个问题变得更加困难。比如你想用console.warn打印异常信息,但是可能会出现warn函数调用错误;比如捕获到错误但是error.message全是Scripterror...浏览器兼容性,前端业务逻辑依赖、日志上报方式、日志上报效率、用户操作习惯、网络环境等因素都可能导致日志上报出现问题,甚至影响业务。这些因素都会给日志上报带来可靠性和性能问题。日志报告的可靠性。浏览器兼容性在不同的终端和浏览器中,由于兼容性不同,日志的获取逻辑和上报方式需要兼容多种,比如fetch方式是否可用,页面性能(performance)计算是否可以使用NT2标准,这些问题可能导致上报逻辑本身报错,污染业务日志统计;上报可靠性日志采集sdk可能因为网络原因没有加载,所以安全的做法是sdk注入的位置合理靠后,那么在页面打开到sdk初始化的这段时间里会出现漏报;后端通常会独立设置一个日志采集服务器进行业务分离,这种情况下日志上报可能会遇到跨域问题;频繁的用户操作和关闭页面可能会导致许多收集的数据丢失。日志上报的性能问题在一个复杂的站点中,可能会有大量的日志数据,上报可能会因为浏览器并发数的限制而阻塞业务网络请求或者影响页面性能。更优雅的上报姿势姿势1业务资源隔离Isolation为了避免影响业务,理所当然的,为了不占用业务计算资源,日志上报需要单独设置一个后端服务。同时,不能使用与企业相同的域名。这类似于页面尽量使用CDN导入资源的原理。浏览器对同一个域名的并发数会有一定的限制。页面性能、资源加载、初始化API、PV/UV、初始化js逻辑错误等日志都是在页面初始化时触发上报的。短时间内如此大量的报告可能会导致网络请求延迟。比如Chrome同域名最大并发连接数为6,如果同时报日志超过6次,就会影响同域名的业务;更糟糕的是,页面上有一些错误,网络连接质量不高。会使日志上报阻碍页面渲染。因此,日志上报可以像使用CDN服务一样使用单独的域名和日志处理服务。由于使用不同的域名,跨域的问题也会随之而来,这就需要前后端的支持。服务器需要允许外部访问Access-Control-Allow-Origin:*;前端在上报日志时要加上跨域标识,比如fetch方法:varurl='https://arms-retcode.aliyuncs.com/r。png';fetch(`${url}?t=perf&page=qar.alibaba-inc.com&load=1168`,{mode:'no-cors'})首先是DNS解析,但是可以通过在页面中加入DNS预解析来避免。异常隔离基于资源隔离。日志上报的异常处理也需要隔离。日志本身抛出的异常一定不能和业务异常一起上报。在充分测试的前提下,最简单粗暴的方式就是在整个监控sdk外加上try...catch...。好处是sdk本身永远不会报错,但是也让开发者失去了发现sdk问题的途径。因此,需要一种同时拥有两者的方法。这里介绍一种埋点关键模块的方法,在整个前端SDK中监控多个关键点的埋点,只在采集到的结果中标记是否埋点成功。话不多说,直接上示例代码://全局标记总结,初始化为36点全1的数组varN=36;varsdkStat=Array.from({length:N},()=>1);/**日志上报功能模块*对应模块错误设置对应点为0,多个点为0帮助查找错误链接*/try{/*sdkmodule0*/}catch(){sdkStat[0]=0;}/*othermodules*/try{/*sdkmodule35*/}catch(){sdkStat[35]=0;}//日志报告发送模块varstatStr=parseInt(sdkStat.join(''),2).toString(36);(newImage).src=`/r.png?¶m1=${param1}&sdkStat=${statStr}...`;姿势2压缩请求响应包之前重新检查(新Image)。src的日志发送方式:HTTP请求:前端日志数据以多组key=value字符串形式连接到Image资源请求的url,前端发送Image请求。HTTPResponce:服务器返回响应结果或空图片。将日志数据直接放在url中的好处是网络传输效率高。但是url长度是有限制的,比如IE浏览器是2083个字符,服务器端也会限制url长度。像下面这样的JS错误信息不能完整报错,$isnotdefined@https://www.example.com.cn/catalog/?spm=a2o4k.customer.0.0.37c1379dmQwdrW&q=pediasure&searchclickposition=hint:3:231...tg@https://www.googletagmanager.com/gtm.js?id=GTM-KTVS7D9&l=shadowDatalayerKi7l:64:32...不仅是js报错的堆栈深度错误还因为特殊字符和汉字的转码通过urlencode,这两个因素会使url长度很容易超过限制。另外,业务逻辑其实并不关注也应该关注日志上报的响应结果,所以这个请求的结果尽量省略。消息压缩有以下几种方法:HTTP/2headercompressionhttp请求,每个请求都会传输一系列请求头来描述请求的资源及其特性,但实际上每个请求都有很多相同的值,比如Host:,user-agent:,Accept等。这些数据可以占用300-800字节的传输量。如果携带大cookie,请求头甚至可以占用1kb的空间,但实际需要上报的日志数据只有10-50字节大小。在HTTP1.x中,每个日志报告请求头都携带了大量的重复数据,造成性能上的浪费。HTTP/2头压缩使用HuffmanCode对请求头进行压缩,并使用动态表为每个请求更新不同的数据,将每个请求的头压缩到很小的尺寸。HTTP/1.1效果HTTP/2.0效果header压缩后,每次日志请求的大小大大减小,响应速度也有提升。压缩日志的长度是最需要压缩的,也就是js错误的错误栈。错误栈占据了大部分错误定位的文件地址,很多错误栈都有很多相同的文件。压缩空间来自栈中js文件url的重复。典型的jserror堆栈通常以这种形式出现:obj0.fn0at(http://loooooooooonnnnnnnnnnnng/loooooong/long.js123:1)obj1.fn1at(http://loooooooooonnnnnnnnnnnng/loooooong/long.js234:1)obj2.fn2at(http://loooooooooonnnnnnnnnnnng/laaaaaang/lang.js345:1)...可以考虑将文件url提取出来,单独作为字典使用,那么报出的内容可以缩减为files={'f1':'http://loooooooooonnnnnnnnnnnng/loooooong/long.js','f2':'...'}obj0.fn0at(f1123:1)obj1.fn1at(f1234:1)obj2.fn2at(f2345:1)...可以大大减少日志长度。抛开responsebody,日志上报本身只关注日志是否上报过,并不关注上报请求的返回内容,甚至根本不需要返回内容。所以使用HTTPHEAD方式上报,返回的responsebody为空,避免responsebody传输资源丢失。这时候你只需要架设一个nginx服务器来记录日志内容,并返回一个200状态码。fetch(`${url}?t=perf&page=lazada-home&load=1168`,{mode:'no-cors',method:'HEAD'})三种姿势合并上报。既然一个页面的报告那么多,一个更容易想到的想法应该是将日志合并上报,减少请求次数。HTTP/2多路复用会在用户的浏览器和日志服务器之间产生多个HTTP请求,但是在HTTP/1.1Keep-Alive下,日志报告会以串行方式传输,这会延迟后续的日志报告。通过HTTP/2多路复用合并报告以节省网络连接开销。HTTPPOST合并了POST请求,因为请求体可以有更多的显示空间。在HTTPPOST中,只要一次包含多条日志的内容,一条日志使用一次HTTPHEAD请求会更经济。在HTTPPOST的基础上,顺便解决了用户关闭或切换页面导致的误报问题。以往常见的解决方案是监听页面的unload或beforeunload事件,通过同步XMLHttpRequest请求或构造特定的src标签延迟上报。window.addEventListener("unload",uploadLog,false);functionuploadLog(){varxhr=newXMLHttpRequest();xhr.open("POST","/r.png",false);//false表示同步xhr.send(logData);}这种上报的缺点是会影响下一页的性能。更优雅的方式是使用navigator.sendBeacon(),它可以异步发送日志数据。window.addEventListener("unload",uploadLog,false);functionuploadLog(){navigator.sendBeacon("/r.png",logData);}合并前和合并后(navigator.sendBeacon)理想情况下,合并n条日志上报totaltimespent可以达到原来的1/n总结。前端业务场景和浏览器的兼容性差异很大,所以日志上报必须兼容多种方式;页面生命周期和业务逻辑会影响日志能否获取,是否遗漏。因此,必须严格把握相应的日志类型和上报时机;前端业务逻辑快速迭代,场景多样,日志上报必须和业务解耦,同时可以自定义。这些大大小小的陷阱促使我们将前端日志监控定为一个独立的系统工程。在打磨这个项目的过程中,我们也在探索是否有更高效稳定的日志上报方式。