- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
在经过两年多的线上沉淀后,将监控代码重新用 TypeScript 编写,删除冗余逻辑,正式开源.
根据 shin-monitor 的目录结构可知,源码集中在 src 目录中。关于监控系统的迭代过程,可以参考 专栏 .
入口文件是 index.ts,旁边的 utils.ts 是一个工具库.
在 index.ts 中,将会引入 lib 目录中的 error、action 和 performance 三个文件.
1)defaults 。
声明 defaults 变量,配置了各个参数的默认属性,各个参数的 使用指南 可以查看注释、readme 或 demo 目录中的文件.
const defaults: TypeShinParams = { src: '//127.0.0.1:3000/ma.gif', // 采集监控数据的后台接收地址 psrc: '//127.0.0.1:3000/pe.gif', // 采集性能参数的后台接收地址 pkey: '', // 性能监控的项目key subdir: '', // 一个项目下的子目录 rate: 5, // 随机采样率,用于性能搜集,范围是 1~10,10 表示百分百发送 version: '', // 版本,便于追查出错源 record: { isOpen: true , // 是否开启录像 src: '//cdn.jsdelivr.net/npm/rrweb@latest/dist/rrweb.min.js' // 录像地址 }, error: { isFilterErrorFunc: null , // 需要过滤的代码错误 isFilterPromiseFunc: null , // 需要过滤的Promise错误 }, console: { isOpen: true , // 默认是开启,在本地调试时,可以将其关闭 isFilterLogFunc: null , // 过滤要打印的内容 }, crash: { isOpen: true , // 是否监控页面奔溃,默认开启 validateFunc: null , // 自定义页面白屏的判断条件,返回值包括 {success: true, prompt:'提示'} }, event: { isFilterClickFunc: null , // 在点击事件中需要过滤的元素 }, ajax: { isFilterSendFunc: null // 在发送监控日志时需要过滤的通信 }, identity: { value: '', // 自定义的身份信息字段 getFunc: null , // 自定义的身份信息获取函数 }, };
2)setParams() 。
在 setParams() 函数中,会初始化引入的 3 个类,然后开始监控页面错误、计算性能参数、监控用户行为.
function setParams(params: TypeShinParams): TypeShinParams { if (! params) { return null ; } const combination = defaults; // 只重置 params 中的参数 for (const key in params) { combination[key] = params[key]; } // 埋入自定义的身份信息 const { getFunc } = combination.identity; getFunc && getFunc(combination); // 监控页面错误 const error = new ErrorMonitor(combination); error.registerErrorEvent(); // 注册 error 事件 error.registerUnhandledrejectionEvent(); // 注册 unhandledrejection 事件 error.registerLoadEvent(); // 注册 load 事件 error.recordPage(); shin.reactError = error.reactError.bind(error); // 对外提供 React 的错误处理 shin.vueError = error.vueError.bind(error); // 对外提供 Vue 的错误处理 // 启动性能监控 const pe = new PerformanceMonitor(combination); pe.observerLCP(); // 监控 LCP pe.observerFID(); // 监控 FID pe.registerLoadAndHideEvent(); // 注册 load 和页面隐藏事件 // 为原生对象注入自定义行为 const action = new ActionMonitor(combination); action.injectConsole(); // 监控打印 action.injectRouter(); // 监听路由 action.injectEvent(); // 监听事件 action.injectAjax(); // 监听Ajax return combination; }
函数中做了大量初始化工作,若不需要某些监控行为,可自行删除.
在 lib 目录中,存放着整个监控系统的核心逻辑.
1)Http 。
Http 的主要工作是通信,也就是将搜集起来的监控日志或性能参数,统一发送到后台.
并且在 Http 中,还会根据算法生成身份标识字符串,以及做最后的参数组装工作.
监控日志原先采用的发送方式是 Image,目的是跨域,但是发送的数据量有限,像 Ajax 通信,如果需要记录响应,那么长度就会不够.
因此后期就改成了 fetch() 函数,默认只会上传 8000 长度的数据.
public send(data: TypeSendParams, callback?: ParamsCallback): void { // var ts = new Date().getTime().toString(); // var img = new Image(0, 0); // img.src = shin.param.src + "?m=" + _paramify(data) + "&ts=" + ts; const m = this .paramify(data); // 大于8000的长度,就不在上报,废弃掉 if (m.length >= 8000 ) { return ; } const body: TypeSendBody = { m }; callback && callback(data, body); // 自定义的参数处理回调 // 如果修改headers,就会多一次OPTIONS预检请求 fetch( this .params.src, { method: "POST" , // headers: { // 'Content-Type': 'application/json', // }, body: JSON.stringify(body) }); }
而性能参数的发送采用了 sendBeacon() 方法,在页面关闭时也能上报,这是普通的请求所不具备的特性.
它能将少量数据异步 POST 到后台,并且支持跨域,而少量是指多少并没有特别指明,由浏览器控制,网上查到的资料说一般在 64KB 左右.
public sendPerformance(data: TypeCaculateTiming): void { // 如果传了数据就使用该数据,否则读取性能参数,并格式化为字符串 var str = this .paramifyPerformance(data); var rate = randomNum(10, 1); // 选取1~10之间的整数 if ( this .params.rate >= rate && this .params.pkey) { navigator.sendBeacon( this .params.psrc, str); } }
2)Error 。
在 Error 中,会注册 window 的 error 事件,用于监控脚本或资源错误,在脚本错误中,会提示行号和列号.
不过资源错误是看不到具体的错误原因的,只会给个结果,出现了错误,连错误状态码也没有.
window.addEventListener('error', (event: ErrorEvent): void => { const errorTarget = event.target as (Window | TypeEventTarget); // 过滤掉与业务无关或无意义的错误 if (isFilterErrorFunc && isFilterErrorFunc(event)) { return ; } // 过滤 target 为 window 的异常 if ( errorTarget !== window && (errorTarget as TypeEventTarget).nodeName && CONSTANT.LOAD_ERROR_TYPE[(errorTarget as TypeEventTarget).nodeName.toUpperCase()] ) { this .handleError( this .formatLoadError(errorTarget as TypeEventTarget)); } else { // 过滤无效错误 event.message && this .handleError( this .formatRuntimerError( event.message, event.filename, event.lineno, event.colno, // event.error, ), ); } }, true ); // 捕获
还会注册 window 的 unhandledrejection 事件,用于监控未处理的 Promise 错误,当 Promise 被 reject 且没有 reject 处理器时触发.
在 unhandledrejection 事件中,对于响应信息,其实是做了些扩展的,参考《 SDK中的 unhandledrejection 事件 》.
window.addEventListener('unhandledrejection',(event: PromiseRejectionEvent): void => { // 处理响应数据,只抽取重要信息 const { response } = event.reason; // 若无响应,则不监控 if (!response || ! response.request) { return ; } const desc: TypeAjaxDesc = response.request.ajax; desc.status = event.reason.status || response.status; // 过滤掉与业务无关或无意义的错误 if (isFilterPromiseFunc && isFilterPromiseFunc(desc)) { return ; } this .handleError({ type: CONSTANT.ERROR_PROMISE, desc, // stack: event.reason && (event.reason.stack || "no stack") }); }, true );
这 2 个错误的使用,都在 demo/error.html 中有所记录,另一个重要的错误是白屏.
在白屏时,还会上报录像内容,白屏的迭代过程可以参考 此处 .
对 body 的子元素做深度优先搜索,若已找到一个有高度的元素、或若元素隐藏、或元素有高度并且不是 body 元素,则结束搜索.
为了便于定位白屏原因,在白屏时,还会记录些元素信息,例如元素类型、样式、高度等.
private isWhiteScreen(): TypeWhiteScreen { const visibles = []; const nodes = []; // 遍历到的节点的关键信息,用于查明白屏原因 // 深度优先遍历子元素 const dfs = (node: HTMLElement): void => { const tagName = node.tagName.toLowerCase(); const rect = node.getBoundingClientRect(); // 选取节点的属性作记录 const attrs: TypeWhiteHTMLNode = { id: node.id, tag: tagName, className: node.className, display: node.style.display, height: rect.height }; const src = (node as HTMLImageElement).src; if (src) { attrs.src = src; // 记录图像的地址 } const href = (node as HTMLAnchorElement).href; if (href) { attrs.href = href; // 记录链接的地址 } nodes.push(attrs); // 若已找到一个有高度的元素,则结束搜索 if (visibles.length > 0) return ; // 若元素隐藏,则结束搜索 if (node.style.display === 'none') return ; // 若元素有高度并且不是 body 元素,则结束搜索 if (rect.height > 0 && tagName !== 'body' ) { visibles.push(node); return ; } node.children && [].slice.call(node.children).forEach((child: HTMLElement): void => { const tagName = child.tagName.toLowerCase(); // 过滤脚本和样式元素 if (tagName === 'script' || tagName === 'link') return ; dfs(child); }); }; dfs(document.body); return { visibles: visibles, nodes: nodes }; }
监控白屏的时机,是在 load 事件中,延迟 1 秒触发.
原先是在 DOMContentLoaded 事件内触发,经测试发现,当因为脚本错误出现白屏时,两个事件的触发时机会很接近.
在线上监控时发现会有一些误报,HTML是有内容的,那很可能是 DOMContentLoaded 触发时,页面内容还没渲染好.
对于热门的 React 和 Vue 库,声明了两个方法:reactError() 和 vueError(),将这两个方法分别应用于项目中,就能监控框架错误了.
React 需要在项目中创建一个 ErrorBoundary 类,在类中调用 reactError() 方法.
如果 Vue 是被模块化引入的,那么就得在模块的某个位置调用该方法,因为此时 Vue 不会绑定到 window 中,即不是全局变量.
3)Action 。
在 Action 中会监控打印、路由、点击事件和 Ajax 通信。这 4 种行为都会对原生对象进行注入,它们的使用也都可以在 demo 目录中找到.
以路由为例,不仅要监听 popstate 事件,还要重写 pushState 和 replaceState.
public injectRouter(): void { /* * * 全局监听跳转 * 点击后退、前进按钮或者调用 history.back()、history.forward()、history.go() 方法才会触发 popstate 事件 * 点击 <a href=/xx/yy#anchor>hash</a> 按钮也会触发 popstate 事件 */ const _onPopState = window.onpopstate; window.onpopstate = (args: PopStateEvent): void => { this .sendRouterInfo(); _onPopState && _onPopState.apply( this , args); }; /* * * 监听 pushState() 和 replaceState() 两个方法 */ const bindEventListener = (type: string): TypeStateEvent => { const historyEvent: TypeStateEvent = history[type]; return (...args): void => { // 触发 history 的原始事件,apply 的第一个参数若不是 history,就会报错 const newEvent = historyEvent.apply(history, args); this .sendRouterInfo(); return newEvent; }; }; history.pushState = bindEventListener('pushState' ); history.replaceState = bindEventListener('replaceState' ); }
4)Performance 。
Performance 主要是对性能参数的搜集,大部分的性能参数是通过 performance.getEntriesByType('navigation')[0] 或 performance.timing 获取的.
performance.timing 已被废弃,尽量不要使用,此处只是为了兼容。Performance 的迭代过程可以参考 此处 .
参数的发送时机有两者,第一种是 window.load 事件中,第二种是页面隐藏的事件中.
LCP、FID、FP 等参数可通过浏览器提供的对象获取.
public observerLCP(): void { const lcpType = 'largest-contentful-paint' ; const isSupport = this .checkSupportPerformanceObserver(lcpType); // 浏览器兼容判断 if (! isSupport) { return ; } const po = new PerformanceObserver((entryList): void => { const entries = entryList.getEntries(); const lastEntry = (entries as any)[entries.length - 1 ] as TypePerformanceEntry; this .lcp = { time: rounded(lastEntry.renderTime || lastEntry.loadTime), // 时间取整 url: lastEntry.url, // 资源地址 element: lastEntry.element ? removeQuote(lastEntry.element.outerHTML) : '' // 参照的元素 }; }); // buffered 为 true 表示调用 observe() 之前的也算进来 po.observe({ type: lcpType, buffered: true } as any); // po.observe({ entryTypes: [lcpType] }); /* * * 当有按键或点击(包括滚动)时,就停止 LCP 的采样 * once 参数是指事件被调用一次后就会被移除 */ [ 'keydown', 'click'].forEach((type): void => { window.addEventListener(type, (): void => { // 断开此观察者的连接 po.disconnect(); }, { once: true , capture: true }); }); }
FMP 需要自行计算,才能得到,我采用了一套比较简单的规则.
为了能更好的描述出首屏的时间,将 LCP 和 FMP 两个时间做比较,取最长的那个时间.
。
最后此篇关于shin-monitor源码分析的文章就讲到这里了,如果你想了解更多关于shin-monitor源码分析的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
前言 在上一篇随笔中,我们探讨了如何使用 Netty 处理自定义协议中的粘包和拆包问题。Netty 提供了高度封装的 API,帮助开发者轻松应对这一挑战,因此很多人都对其解决方案非常熟悉。 但如果
前言 在上一篇随笔中,我们探讨了如何实现一套自定义通信协议,其中涉及到的粘包和拆包处理最初是完全自定义实现的,后来则改为了继承 ByteToMessageDecoder 来简化处理。 本篇将重点讨
ACO.Visualization项目 本项目演示蚁群算法求解旅行商问题的可视化过程,包括路径上的信息素浓度、蚁群的运动过程等。项目相关的代码:https://github.com/anycad/A
关闭。这个问题不符合Stack Overflow guidelines .它目前不接受答案。 我们不允许提问寻求书籍、工具、软件库等的推荐。您可以编辑问题,以便用事实和引用来回答。 关闭 7 年前。
我需要用Sql数据库制作并包含的PHP票务系统源码用户客户端和管理员。我需要个人 CMS 的这个来源。谢谢你帮助我。 最佳答案 我在不同的情况下使用了 osticket。 这里: http://ost
我的场景:我想在日志文件中写入发生异常的部分代码(例如,发生异常的行前 5 行和行后 5 行 - 或者至少是该方法的所有代码)。 我的想法是用 C# 代码反编译 pdb 文件,并从该反编译文件中找到一
RocketMQ设定了延迟级别可以让消息延迟消费,延迟消息会使用 SCHEDULE_TOPIC_XXXX 这个主题,每个延迟等级对应一个消息队列,并且与普通消息一样,会保存每个消息队列的消费进度
先附上Hystrix源码图 在微服务架构中,根据业务来拆分成一个个的服务,服务与服务之间可以相互调用(RPC),在Spring Cloud可以用RestTemplate+Ribbon和
此篇博客学习的api如标题,分别是: current_url 获取当前页面的url; page_source 获取当前页面的源码; title 获取当前页面的titl
? 1 2
1、前言 作为一个数据库爱好者,自己动手写过简单的sql解析器以及存储引擎,但感觉还是不够过瘾。<<事务处理-概念与技术>>诚然讲的非常透彻,但只能提纲挈领,不能让你
gory"> 目录 运行时信号量机制 semaphore 前言 作用是什么 几个主要的方法 如何实现
自己写的一个评论系统源码分享给大家,包括有表情,还有评论机制。用户名是随机的 针对某一篇文章进行评论 function subcomment() {
一、概述 StringBuilder是一个可变的字符串序列,这个类被设计去兼容StringBuffer类的API,但不保证线程安全性,是StringBuffer单线程情况下的一个替代实现。在可能的情
一、概述 System是用的非常多的一个final类。它不能被实例化。System类提供了标准的输入输出和错误输出流;访问外部定义的属性和环境变量;加载文件和库的方法;以及高效的拷贝数组中一部分元素
在JDK中,String的使用频率和被研究的程度都非常高,所以接下来我只说一些比较重要的内容。 一、String类的概述 String类的声明如下: public final class Str
一、概述 Class的实例代表着正在运行的Java应用程序的类和接口。枚举是一种类,而直接是一种接口。每一个数组也属于一个类,这个类b被反射为具有相同元素类型和维数的所有数组共享的类对象。八大基本树
一、概述 Compiler这个类被用于支持Java到本地代码编译器和相关服务。在设计上,这个类啥也不做,他充当JIT编译器实现的占位符。 放JVM虚拟机首次启动时,他确定系统属性java.comp
一、概述 StringBuffer是一个线程安全的、可变的字符序列,跟String类似,但它能被修改。StringBuffer在多线程环境下可以很安全地被使用,因为它的方法都是通过synchroni
一、概述 Enum是所有Jav中枚举类的基类。详细的介绍在Java语言规范中有说明。 值得注意的是,java.util.EnumSet和java.util.EnumMap是Enum的两个高效实现,
我是一名优秀的程序员,十分优秀!