大家好,我是念念!这是上周问朋友的综合设计题。如果你没用过嵌入式监控系统,或者理解不深,基本cool。本文将明确:埋点监控系统负责处理哪些问题,如何设计api?为什么要用img的src发送请求,什么是sendBeacon?react和vue的错误边界如何处理?什么是例如公司开发并上线了一个网站,但是开发者不可能预知用户实际使用时会发生什么:用户浏览了哪些页面?有多少用户会点击弹出窗口的确认按钮?,多少百分比会点击取消?是否有页面崩溃?所以我们需要一个埋点监控SDK来采集数据,后面再做统计分析。有了分析数据,我们才能有针对性地优化网站:不要在PV很少的页面上浪费大量人力;有bug的页面赶紧修,不然要325。比较知名的嵌入式点监控有GoogleAnalytics。除了网页端,还有适用于iOS和Android的SDK。由于业务需求不同,大部分公司都会开发一套嵌入式监控系统,但基本上都会涵盖这三类功能:用户行为监控负责统计PV(页面访问数)、UV(页面访问数)访客)和用户的点击操作等行为。这种统计是用得最多的,只有有了这些数据才能量化我们工作的结果。虽然页面性能监控开发人员和测试人员在上线前都会对数据进行评估,但是用户的环境和我们是不一样的。它可能是3G网络或非常旧的型号。我们需要了解实际使用场景中的性能数据,比如页面加载时间、白屏时间等,错误告警监控获取错误数据并及时处理,可以避免大量用户受到影响。除了全局捕获的错误信息外,还有代码内部捕获的错误告警,都需要收集。下面将从API的设计入手,对以上三种进一步展开。SDK设计在开始设计之前,我们先来看看如何使用SDK。importStatisticSDKfrom'StatisticSDK';//初始化一次window.insSDK=newStatisticSDK('uuid-12345');{window.insSDK.event('click','confirm');...//其他业务代码}}>确认先将SDK实例挂载到全局,然后在业务代码中调用。这里新建实例的时候,需要传入一个id,因为这个嵌入式监控系统经常被多个业务使用,不同的数据源是通过id来区分的。首先实现实例化部分:classStatisticSDK{constructor(productID){this.productID=productID;}}数据发送数据发送是最基本的api,后续的功能都要以此为基础。通常这种前后端分离的场景都会使用AJAX发送数据,而这里我们使用的是图片的src属性。原因有二:没有跨域限制,srcipt、img等标签可以直接发送跨域GET请求,不需要特殊处理。兼容性好,在某些静态页面上可能会禁用脚本,此时无法使用脚本标签。但需要注意的是,此图非展示用途。我们的目的是“传输数据”。我们只是利用img标签的src属性在url后面拼接参数,服务端收到后会解析。classStatisticSDK{constructor(productID){this.productID=productID;}send(baseURL,query={}){query.productID=this.productID;让queryStr=Object.entries(query).map(([key,value])=>`${key}=${value}`).join('&')让img=newImage();img.src=`${baseURL}?${queryStr}`}}imgtag优点是不需要附加到文档中,只需要设置src属性即可成功发起请求。通常请求的url将是一个1X1px的GIF图片。为什么这里返回的图片是GIF,网上的文章都含糊其辞。这里我查阅了一些资料并进行了测试:1.相同大小,不同格式图片中GIF的大小是最小的,所以选择返回一个GIF,性能损失较小。2.如果返回204,会去img的onerror事件,抛出全局错误;如果它返回200和一个空对象,就会有一个CORB警报。3.当然,如果你不关心这个错误,你可以返回一个空对象。事实上,有一些工具可以做到这一点。有一些隐藏点需要添加到页面中。例如,垃圾邮件发送者会添加这样一个隐藏标志来验证邮件是否被打开。如果返回204或200空对象,则会导致明显的图像占位符。比较优雅的网络信标称为网络信标(webbeacon)。除了gif图片,从2014年开始,浏览器逐渐实现了一个专门的API来更优雅地做这件事:Navigator.sendBeacon。使用起来非常简单。Navigator.sendBeacon(url,data)比图片的src更有优势:不会和主要业务代码抢资源,而是在浏览器空闲的时候发送。并且还可以保证页面卸载时请求发送成功,不会阻塞页面刷新和跳转。目前的埋点监控工具通常优先使用sendBeacon,但由于浏览器兼容性问题,还是需要使用图片的src作为底线。用户行为监控实现了发送数据的API,现在可以用来实现用户行为监控的API。classStatisticSDK{constructor(productID){this.productID=productID;}//发送数据send(baseURL,query={}){query.productID=this.productID;让queryStr=Object.entries(query).map(([key,value])=>`${key}=${value}`).join('&')让img=newImage();img.src=`${baseURL}?${queryStr}`}//自定义事件event(key,val={}){leteventURL='http://demo/'this.send(eventURL,{event:key,...val})}//pv曝光pv(){this.event('pv')}}用户行为包括自定义事件和pv曝光,pv曝光也可以看作是一种特殊的自定义行为事件。页面性能监控页面的性能数据可以通过performance.timingAPI获取,获取的数据是一个毫秒级的时间戳。以上你不需要全部都懂,关键数据如下,根据这些数据可以计算出FP/DCL/Load等关键事件的时间点:页面首次渲染时间:FP(firstPaint)=domLoading-navigationStart。DOM加载完成:DCL(DOMContentEventLoad)=domContentLoadedEventEnd-navigationStart。图片、样式等外部链接资源加载:L(Load)=loadEventEnd-navigationStart。以上数值对应于性能面板中的结果。回到SDK,我们只需要实现一个API来上传所有的性能数据:classStatisticSDK{constructor(productID){this.productID=productID;//初始化并自动调用性能报告this.initPerformance()}//数据发送send(baseURL,query={}){query.productID=this.productID;让queryStr=Object.entries(query).map(([key,value])=>`${key}=${value}`).join('&')让img=newImage();img.src=`${baseURL}?${queryStr}`}//性能报告initPerformance(){letperformanceURL='http://performance/'this.send(performanceURL,performance.timing)}}然后,它会在构造函数中自动调用,因为性能数据要上传,不需要用户每次都手动调用。错误告警监控错误告警监控分为JS原生错误和React/Vue组件错误处理。JS原生错误除了trycatch捕获到的错误,我们还需要上报没有捕获到的错误——通过error事件和unhandledrejection事件来监听。errolerror事件用于监听DOM操作错误DOMException和JS错误告警。具体来说,JS错误分为以下8类:InternalError:内部错误,如递归栈爆炸。RangeError:范围错误,如newArray(-1)。EvalError:使用eval()时出错。ReferenceError:引用错误,比如使用了未定义的变量。SyntaxError:语法错误,例如vara=。TypeError:类型错误,如[1,2].split('.')。URIError:传递给encodeURI或decodeURl()的参数无效,例如decodeURI('%2')。Error:以上7个错误的基类,一般是开发者抛出的。也就是说,以上8种代码运行时出现的错误都可以被检测到。unhandledrejectionPromise内部抛出的错误无法被error捕获,所以需要使用unhandledrejection事件。回到SDK的实现,处理错误告警的代码如下:classStatisticSDK{constructor(productID){this.productID=productID;//初始化错误监听this.initError()}//发送数据send(baseURL,query={}){query.productID=this.productID;让queryStr=Object.entries(query).map(([key,value])=>`${key}=${value}`).join('&')让img=newImage();img.src=`${baseURL}?${queryStr}`}//自定义错误报告error(err,etraInfo={}){consterrorURL='http://error/'const{message,stack}=err;this.send(errorURL,{message,stack,...etraInfo})}//初始化错误监听initError(){window.addEventListener('error',event=>{this.error(error);})window.addEventListener('unhandledrejection',event=>{this.error(newError(event.reason),{type:'unhandledrejection'})})}}和初始性能监控一样,初始化错误监控也是必须做的,所以需要在构造函数中调用。后续开发者只需要在业务代码的trycatch中调用error方法即可。React/Vue组件错误成熟的框架库都会有错误处理机制,React和Vue也不例外。React的errorboundary错误边界是希望当应用程序内部出现渲染错误时,整个页面不会崩溃。我们预先给它设置了一个pocketcomponent,可以细化粒度。只有出现错误的部分被这个“底层组件”替换,这样整个页面就无法正常工作了。它的使用非常简单,是一个具有特殊生命周期的类组件,用来包装业务组件。两个生命周期是getDerivedStateFromError和componentDidCatch。代码如下://定义错误边界classErrorBoundaryextendsReact.Component{state={error:null}staticgetDerivedStateFromError(error){return{error}}componentDidCatch(error,errorInfo){//调用我们实现的SDK实例insSDK。error(error,errorInfo)}render(){if(this.state.error){return出了点问题。
}returnthis.props.children}}...回到SDK的集成,在生产环境中,如果在错误边界包裹的组件内部抛出错误,则无法监听到全局错误事件,因为错误边界本身就相当于一个try抓住。因此,需要在错误边界的组件内进行上报处理。也就是上面代码中的componentDidCatch生命周期。Vue的错误边界vue也有类似的生命周期来做这件事,不再赘述:errorCaptured。Vue.component('ErrorBoundary',{data:()=>({error:null}),errorCaptured(err,vm,info){this.error=`${err.stack}\n\n在${info}ofcomponent`//调用我们的SDK,报错信息insSDK.error(err,info)returnfalse},render(h){if(this.error){returnh('pre',{style:{color:'red'}},this.error)}returnthis.$slots.default[0]}})...现在我们已经实现了Created一个完整的SDK框架,以及在实际开发中如何对接react/vue项目。实际生产中使用的SDK会更加健壮,但是思路是一样的。有兴趣的可以阅读源码。结论文章比较长,但是想要很好的回答这个问题,这些知识储备还是很有必要的。如果我们要设计SDK,首先要了解它的基本使用方法,然后才能知道如何构建下面的代码框架;那么我们需要明确SDK的功能范围:它需要能够处理三种类型的监控:用户行为、页面性能、错误告警;最后,react,vue项目一般都会做错误边界处理,如何接入我们自己的sdk。