在经过两年多的线上沉淀后,将监控代码重新用 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 目录

  在 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 需要自行计算,才能得到,我采用了一套比较简单的规则。

  • 首先,通过 MutationObserver 监听每一次页面整体的 DOM 变化,触发 MutationObserver 的回调。
  • 然后在回调中,为每个 HTML 元素(不包括忽略的元素)打上标记,记录元素是在哪一次回调中增加的,并且用数组记录每一次的回调时间。
  • 接着在触发 load 事件时,先过滤掉首屏外和没有高度的元素,以及元素列表之间有包括关系的祖先元素,再计算各次变化时剩余元素的总分。
  • 最后在得到分数最大值后,从这些元素中挑选出最长的耗时,作为 FMP。

  为了能更好的描述出首屏的时间,将 LCP 和 FMP 两个时间做比较,取最长的那个时间。

shin-monitor源码分析的更多相关文章

  1. RyuBook1.0案例二:Traffic Monitor项目源码分析

    Traffic Monitor源码分析 从simple_switch_13.SimpleSwitch13控制器继承并开发 class SimpleMonitor13(simple_switch_13. ...

  2. .net源码分析 - ConcurrentDictionary<TKey, TValue>

    List源码分析 Dictionary源码分析 ConcurrentDictionary源码分析 继上篇Dictionary源码分析,上篇讲过的在这里不会再重复 ConcurrentDictionar ...

  3. dubbo源码分析6-telnet方式的管理实现

    dubbo源码分析1-reference bean创建 dubbo源码分析2-reference bean发起服务方法调用 dubbo源码分析3-service bean的创建与发布 dubbo源码分 ...

  4. 安卓MonkeyRunner源码分析之启动

    在工作中因为要追求完成目标的效率,所以更多是强调实战,注重招式,关注怎么去用各种框架来实现目的.但是如果一味只是注重招式,缺少对原理这个内功的了解,相信自己很难对各种框架有更深入的理解. 从几个月前开 ...

  5. Robotium源码分析之Instrumentation进阶-attach

    在分析Robotium的运行原理之前,我们有必要先搞清楚Instrumentation的一些相关知识点,因为Robotium就是基于Instrumentation而开发出来的一套自动化测试框架.鉴于之 ...

  6. Robotium源码分析之Instrumentation进阶

    在分析Robotium的运行原理之前,我们有必要先搞清楚Instrumentation的一些相关知识点,因为Robotium就是基于Instrumentation而开发出来的一套自动化测试框架.鉴于之 ...

  7. Storm源码分析--Nimbus-data

    nimbus-datastorm-core/backtype/storm/nimbus.clj (defn nimbus-data [conf inimbus] (let [forced-schedu ...

  8. Memcached源码分析之线程模型

    作者:Calix 一)模型分析 memcached到底是如何处理我们的网络连接的? memcached通过epoll(使用libevent,下面具体再讲)实现异步的服务器,但仍然使用多线程,主要有两种 ...

  9. FiddlerCoreAPI开发(一)源码分析

    1.前言 前一段时间想利用fiddlercore截取本地HTTPS的流量做一些分析,按照样例代码的注释学习了一下,没搞清楚怎么实现,后来又在网上查了些资料,对HTTPS的处理提及很少,都没有解决我的问 ...

  10. Java并发系列[5]----ReentrantLock源码分析

    在Java5.0之前,协调对共享对象的访问可以使用的机制只有synchronized和volatile.我们知道synchronized关键字实现了内置锁,而volatile关键字保证了多线程的内存可 ...

随机推荐

  1. Azure DevOps Server 设置项目管理用户,用户组

    一,引言 Azure DevOps Server 搭建完成后,关于如何进行项目管理,项目成员管理等,我们接着上一篇文章,继续讲解 Azure DevOps Server 的用户,用户组.首先,我们需要 ...

  2. ssh免交互

    sshpass -p the_password ssh -o StrictHostKeyChecking=no root@domainname_or_ip remote_command  #远程执行命 ...

  3. c++ trivial, standard layout和POD类型解析

    目录 1. trivial类型 2. standard layout类型 3. 集大成者,POD(Plain Old Data)类型 4. 测试代码 1. trivial类型 占用一片连续的内存,编译 ...

  4. day32 6 请求转发与重定向的区别、session会话对象 & cookie & 8 应用程序上下文对象ServletContext & 5 请求转发与jsp页面内置对象

    1 请求转发与重定向的区别 2 session与cookie的区别 3 过滤器与监听器的区别 4 web-inf目录 web-inf目录是安全目录,无法从客户端访问,只能通过(服务端的)servlet ...

  5. 微信小程序实战,基于vue2实现瀑布流

    1.什么是瀑布流呢? 瀑布流,又称瀑布流式布局.是比较流行的一种网站页面布局,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部. 瀑布流对于图片的展现, ...

  6. v-model双向绑定原理

    1 <div id="app"> 2 <div>{{msg}}</div> 3 <!-- 写法1 --> 4 <input t ...

  7. CGI、WSGI、uWSGI、ASGI……

    在学习 Python Web 开发时候,可能会遇到诸如 uwsgi.wsgi 等名词,下面通过梳理总结,探究它们之间的关系. CGI CGI(Common Gateway Interface)通用网关 ...

  8. 现代 CSS 高阶技巧,完美的波浪进度条效果!

    本文是 CSS Houdini 之 CSS Painting API 系列第三篇. 现代 CSS 之高阶图片渐隐消失术 现代 CSS 高阶技巧,像 Canvas 一样自由绘图构建样式! 在上两篇中,我 ...

  9. mysql数据库(字段约束条件)

    什么是字段约束 字段约束就是将字段的内容定一个规则,我们要按照规则办事 约束 描述 关键字 非空约束 限制该字段的数据不能为null not null 唯一约束 保证该字段的所有数据都是唯一.不重复的 ...

  10. Django TypeError at /login/ 'bool' object is not callable

    代码: def login(request): if request.POST: username = request.POST.get('username') password = request. ...