当前位置: 首页 > news >正文

如何自己开发一个前端监控SDK

最近在负责团队前端监控系统搭建的任务。因为我们公司有统一的日志存储平台、日志清洗平台和基于 Grafana 搭建的可视化看板,就剩日志的采集和上报需要自己实现了,所以决定封装一个前端监控 SDK 来完成日志的采集和上报。

架构设计

因为想着以后有机会可以把自己封装的 SDK 推广给其他团队使用,所以 SDK 在架构设计上就需要有更多的可拓展性。我的想法是把 SDK 根据职责拆解成几个模块,然后有一个核心模块来管理所有的模块,各团队往不同的模块里添加插件由此实现自身定制化的需求。

我们知道一个前端监控 SDK 它需要完成的任务有:日志采集 =>日志整理 =>日志上报。所以根据这个工作流,我把整个 SDK 分成四个模块:

在这里插入图片描述

  • Plugin:负责原始数据的采集。Plugin 内部采用插件化的方式去实现,不同的插件采集不同的数据。比如如果我们想采集网络请求相关的数据,那个可以封装一个专门采集网络请求的插件。
  • Builder:负责把原始数据封装成我们想要的数据结构。
  • Reporter:负责把数据上报到日志平台。因为考虑到一份数据可能会上报到不同的日志平台,所以 Reporter 我也是采用插件化的方式去实现,不同的插件上报到不同的日志平台。
  • Manager:负责和各模块之间进行通信,以及封装一些公共的方法。

综上,整个 SDK 的工作流程如下:

在这里插入图片描述

  1. Manager 建立和各个模块之间的联系。

  2. Plugin 中的某个插件采集到相应的数据,并把数据发送给 Manager。

  3. Manager 接收到来自 Plugin 的数据,并把数据转发给 Builder。

  4. Builder 接收到数据以后按照预设的数据处理方法对数据进行处理,处理完后再把数据发送给 Manager 。

  5. Manager 接收到来自 Builder 的数据,并把数据转发给 Reporter 。

  6. Reporter 中的每个插件接收到数据以后,会把数据上报到对应的日志平台。

在整个SDK运作的过程中,每个模块专注于自己的职责,全程只和 Manager 通信,不受其他模块的影响。

另外,在模块接收或者发送数据的时候都会对外暴露相应的生命周期,这样开发者就可以拿到不同阶段的数据,并对数据进行自定义处理以及决定是否要中断流程。

模块通信

模块通信我采用的是发布-订阅模式,并定义了3个事件:assign (注册)、receive (接收数据)、next (发送数据)。

Manager 会订阅 next 事件,而其他模块会订阅 assign 事件和 receive 事件。最开始的时候,其他模块通过 assign 事件接收到 Manager 的实例,由此其他模块就可以使用 Manager 上定义的发布-订阅相关的方法。当数据在某个模块处理完毕后,这个模块会发布 next 事件把数据传给 Manager ,Manager 接收到数据后再发布 receive 事件把数据传给下一个模块。

export class Manager<O extends ManagerConfigType> extends EventBus {constructor(config?: O) {super()this.assignPlugins(config.plugins || [])this.assignBuilder(config.builder || {})this.assignReporter(config.reporters || [])this.on('manager:next', this.next.bind(this))this.on('manager:next', this.next.bind(this))}private assignPlugins(plugins: any[]) {plugins.forEach(plugin => {this.on('plugin:assign', plugin.init.bind(plugin))})this.emit('plugin:assign', this)}private assignBuilder(builder: any) {this.on('builder:assign', builder.init.bind(builder))this.emit('builder:assign', this)}private assignReporter(reporters: any[]) {reporters.forEach(reporter => {this.on('reporter:assign', reporter.init.bind(reporter))})this.emit('reporter:assign', this)}// 从上一级模块接收数据,然后发给下一级模块public next(args: { from: 'plugin' | 'builder' | 'reporter', data: any}) {const { from, data } = argsif (from === 'plugin') {this.emit('builder:receive', data)} else if (from === 'builder') {this.emit('reporter:receive', data)}}
}export class PluginA<O extends PluginAConfigType> {public init(manager: any) {this._manager = manager}private handleError(msg) {this._manager.emit('manager:next', { from: 'plugin', data: msg })}
}export class Builder<O extends BuilderConfigType> {public init(manager: any) {this._manager = managerthis._manager.on('builder:receive', this.receive.bind(this))}private receive(data: any) {// 处理数据const newData = this.process(data)this._manager.emit('manager:next', { from: 'builder', data: newData })}
}export class ReporterA<O extends ReporterAConfigType> {public init(manager: any) {this._manager = managerthis._manager.on('reporter:receive', this.receive.bind(this))}public receive(args: any) {this.report(args)}
}

接口请求捕获

在大多数情况下,前端通过HTTP的方式和服务端进行交互。不管是自己封装请求方法,还是直接使用类似于 axios 的 HTTP 请求库,都是需要基于 XHR 和 Fetch 去实现的。所以我们需要重写 XHR 和 Fetch 暴露出来的 Hook 并进行代理,由此获得请求相关的信息。

XMLHttpRequest

XMLHttpRequest.open() 方法用来初始化一个新创建的请求,在这个方法里我们可以拿到请求的 URL 和请求方法。

XMLHttpRequest.send() 方法用来发送HTTP请求,在这个方法里我们可以拿到请求参数。

另外,在 XMLHttpRequest.onreadystatechange 事件里,我们可以监听到请求状态的变化。当 xhr.readyState === XMLHttpRequest.DONE 时表示请求操作已经完成,这时候我们就可以记录请求的状态码和请求结束的时间。

	public overideXHRMethod() {const xhrproto = XMLHttpRequest.prototype;const originalOpen = xhrproto.openconst originalSend = xhrproto.sendxhrproto.send = function (this, ...args: any): void {const xhr = this;msg = {...msg,request: args[0],}return originalSend.apply(xhr, args);}xhrproto.open = function (this, ...args: any): void {const xhr = this;msg = {...msg,url: args[1],method: (args[0] || '').toUpperCase(),statusCode: 0,startTimestamp: Date.now() // 请求开始时间}const onreadystatechangeHandler = function (): void {if (xhr.readyState === XMLHttpRequest.DONE) {try {msg.statusCode = xhr.statusmsg.endTimestamp = Date.now() // 请求结束时间msg.responseHeaders = xhr.getAllResponseHeaders()if (['', 'json', 'text'].indexOf(xhr.responseType) !== -1) {msg.response = typeof xhr.response === 'object' ? JSON.stringify(xhr.response) : xhr.response}} catch (e) {/* do nothing */}}}if ('onreadystatechange' in xhr && typeof xhr.onreadystatechange === 'function') {const original = xhr.onreadystatechangexhr.onreadystatechange = function (...readyStateArgs: any): void {onreadystatechangeHandler();return original.apply(xhr, readyStateArgs);}} else {xhr.addEventListener('readystatechange', onreadystatechangeHandler);}return originalOpen.apply(xhr, args);}}

如果想获取响应头可以使用方法 XMLHttpRequest.getAllResponseHeaders()XMLHttpRequest.getResponseHeader() 。不过这两个方法并不能拿到所有的响应头信息,对于跨域的请求只能拿到以下几个字段:

  • Cache-Control
  • Content-Language
  • Content-Length
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma

如果想拿到其他字段,需要在响应头 Access-Control-Expose-Headers 里指定哪些字段是可以公开的。

Fetch

window.fetch() 方法用来发起获取资源的请求,它的第一个参数为请求的 URL,第二个参数为 Request 对象。它返回一个 promise,这个 promise 会在请求响应后被 resolve,并传回 Response 对象。

	public overideFetchMethod() {const originalFetch = window.fetchif (!originalFetch) {return}window.fetch = function(...args) {const method = String(args[1]?.method || 'get').toUpperCase()const url = String(args[0])return originalFetch.apply(window, args).catch((err: Error) => {let msg: ReportMsgType = {url,method: method}throw err})}}

因为公司的项目里很少用到由 Fetch 发起的请求,所以这里写的比较简单。

JS错误捕获

对于那些可预见的 JS 错误,通常我们通过 try/catch 去捕获。其他的 JS 错误,我们可以通过全局监听 error 事件来捕获。另外值得一提的是,对于 Promise 中的错误,如果我们有用 reject 去处理错误那么会触发 rejectionhandled 事件,否则会触发 unhandledrejection 事件。

	private handleError(event: ErrorEvent | PromiseRejectionEvent) {const error = 'error' in event ? event.error : event.reasonconst msg: ReportMsgType = {name: 'browserError',catchBy: 'PluginBrowser',error}}private initMonitor() {window.addEventListener('error', this.handleError.bind(this))window.addEventListener('unhandledrejection', this.handleError.bind(this))}

资源加载错误捕获

常见的前端资源加载包括图片的渲染和外部文件的引用,我们可以通过监听 img、link 和 script 标签的 error 事件来捕获这些资源的加载错误。

const resourceTagName: string[] = ['img', 'script', 'link']export class PluginResource<O extends ResourceConfigType> {private handleError(event: Event) {const target = event.target as HTMLScriptElement | HTMLLinkElementif (!target) {return}const tagName = (target.tagName || '').toLowerCase()const isResource = resourceTagName.includes(tagName)if (!isResource) {return}const msg: ReportMsgType = {name: 'ResourceLoadError',catchBy: 'PluginResource',url: 'src' in target ? target.src : target.href,tagName: tagName}}private initMonitor() {window.addEventListener('error', this.handleError.bind(this), true)}
}

最后

目前整个 SDK 还处于很初级的阶段,能完成常见错误类型的捕获和上报,后续随着需求的增加 SDK 需要实现更多的功能,希望后续再更新一波~

相关文章:

如何自己开发一个前端监控SDK

最近在负责团队前端监控系统搭建的任务。因为我们公司有统一的日志存储平台、日志清洗平台和基于 Grafana 搭建的可视化看板&#xff0c;就剩日志的采集和上报需要自己实现了&#xff0c;所以决定封装一个前端监控 SDK 来完成日志的采集和上报。 架构设计 因为想着以后有机会…...

node.js笔记

首先&#xff1a;浏览器能执行 JS 代码&#xff0c;依靠的是内核中的 V8 引擎&#xff08;C 程序&#xff09; 其次&#xff1a;Node.js 是基于 Chrome V8 引擎进行封装&#xff08;运行环境&#xff09; 区别&#xff1a;都支持 ECMAScript 标准语法&#xff0c;Node.js 有独立…...

mysql 增量备份与恢复使用详解

目录 一、前言 二、数据备份策略 2.1 全备 2.2 增量备份 2.3 差异备份 三、mysql 增量备份概述 3.1 增量备份实现原理 3.1.1 基于日志的增量备份 3.1.2 基于时间戳的增量备份 3.2 增量备份常用实现方式 3.2.1 基于mysqldump增量备份 3.2.2 基于第三方备份工具进行增…...

9月5日上课内容 第一章 NoSQL之Redis配置与优化

本章结构 关系型数据库和非关系型数据库 概念介绍 ●关系型数据库&#xff1a; 关系型数据库是一个结构化的数据库&#xff0c;创建在关系模型&#xff08;二维表格模型&#xff09;基础上&#xff0c;一般面向于记录。 SQL 语句&#xff08;标准数据查询语言&#xff09;就是…...

QT 第四天

一、设置一个闹钟 .pro QT core gui texttospeechgreaterThan(QT_MAJOR_VERSION, 4): QT widgetsCONFIG c11# The following define makes your compiler emit warnings if you use # any Qt feature that has been marked deprecated (the exact warnings # depend…...

nrf52832 GPIO输入输出设置

LED_GPIO #define LED_START 17 #define LED_0 17 #define LED_1 18 #define LED_2 19 #define LED_3 20 #define LED_STOP 20设置位输出模式&#xff1a; nrf_gpio_cfg_output(LED_0); 输出高电平:nrf_gpio_pin_set(LED_0); 输…...

MyBatis 动态 SQL 实践教程

一、MyBatis动态 sql 是什么 动态 SQL 是 MyBatis 的强大特性之一。在 JDBC 或其它类似的框架中&#xff0c;开发人员通常需要手动拼接 SQL 语句。根据不同的条件拼接 SQL 语句是一件极其痛苦的工作。例如&#xff0c;拼接时要确保添加了必要的空格&#xff0c;还要注意去掉列…...

CSS 斜条纹进度条

效果&#xff1a; 代码&#xff1a; html: <div class"active-line flex"><!-- lineWidth&#xff1a;灰色背景 --><div class"bg-line"><div v-for"n in 30" class"gray"></div></div><div…...

JavaScript(1)每天10个小知识点

​ 目录 1. JavaScript 有哪些数据类型&#xff0c;它们的区别&#xff1f;**2. 数据类型检测的方式有哪些**3. null 和 undefined 区别**4. intanceof 操作符的实现原理及实现**5. 如何获取安全的 undefined 值&#xff1f;**6. Object.is() 与比较操作符 “”、“” 的区别*…...

scanf和scanf_s函数详解

目录 引言&#xff1a; 1.scanf函数的用法&#xff1a; 2.scanf_s函数的用法&#xff1a; 3.scanf和scanf_s的区别&#xff1a; 结论&#xff1a; 引言&#xff1a; 在C语言中&#xff0c;输入函数scanf是非常常用的函数之一&#xff0c;它可以从标准输入流中读取数据并将其…...

基于SSM的在线购物系统

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…...

认识JVM的内存模型

从上一节了解到整个JVM大的内存区域&#xff0c;分为线程共享的heap&#xff08;堆&#xff09;&#xff0c;MethodArea&#xff08;方法区&#xff09;&#xff0c;和线程独享的 The pc Register&#xff08;程序计数器&#xff09;、Java Virtual Machine Stacks&#xff08;…...

Java8实战-总结19

Java8实战-总结19 使用流映射对流中每一个元素应用函数流的扁平化 使用流 映射 一个非常常见的数据处理套路就是从某些对象中选择信息。比如在SQL里&#xff0c;你可以从表中选择一列。Stream API也通过map和flatMap方法提供了类似的工具。 对流中每一个元素应用函数 流支持…...

论文浅尝 | 训练语言模型遵循人类反馈的指令

笔记整理&#xff1a;吴亦珂&#xff0c;东南大学硕士&#xff0c;研究方向为大语言模型、知识图谱 链接&#xff1a;https://arxiv.org/abs/2203.02155 1. 动机 大型语言模型&#xff08;large language model, LLM&#xff09;可以根据提示完成各种自然语言处理任务。然而&am…...

【云计算网络安全】解析DDoS攻击:工作原理、识别和防御策略 | 文末送书

文章目录 一、前言二、什么是 DDoS 攻击&#xff1f;三、DDoS 攻击的工作原理四、如何识别 DDoS 攻击五、常见的 DDoS 攻击有哪几类&#xff1f;5.1 应用程序层攻击5.1.1 攻击目标5.1.2 应用程序层攻击示例5.1.3 HTTP 洪水 5.2 协议攻击5.2.1 攻击目标5.2.2 协议攻击示例5.2.3 …...

64位Linux系统上安装64位Oracle10gR2及Oracle11g所需的依赖包

在64位Linux系统上安装64位Oracle 10gR2,到底需要装哪些包? 这不是一个完整的安装教程,仅仅探讨在64位CentOS 5.8系统上安装64位Oracle 10gR2,到底需要装哪些RPM包. 实验环境VMWare Workstation 8.0 Linux发行版: CentOS 5.8 x86_64 Kernel版本: 2.6.18-308.el5 Oracle Dat…...

Unity InputSystem 基础使用之鼠标交互

资料 官方文档 导入InputSystem包 Package Manager 搜索Input System进行下载启用该包&#xff0c;会重启Unity Editor 注意 InputSystem可以和旧版输入系统一起使用 设置&#xff1a;Project Settings->Player->Other Settings->Configuration->Active Input…...

《算法竞赛·快冲300题》每日一题:“二进制数独”

《算法竞赛快冲300题》将于2024年出版&#xff0c;是《算法竞赛》的辅助练习册。 所有题目放在自建的OJ New Online Judge。 用C/C、Java、Python三种语言给出代码&#xff0c;以中低档题为主&#xff0c;适合入门、进阶。 文章目录 题目描述题解C代码Java代码Python代码 “ 二…...

CnosDB 签约京清能源,助力分布式光伏发电解决监测系统难题。

近日&#xff0c;京清能源采购CnosDB&#xff0c;升级其“太阳能光伏电站一体化监控平台”。该平台可以实现电站设备统一运行监控&#xff0c;数据集中管理&#xff0c;为操作人员、维护人员、管理人员提供全面、便捷、差异化的数据和服务。 京清能源集团有限公司&#xff08;…...

汇编:lea 需要注意的一点

lea和mov的效用上不一样&#xff0c;如果当前%rsi的值是0&#xff0c; lea 0x28(%rsi),%rax &#xff0c;这个只是计算一个地址&#xff0c;而不是去做地址访问。 mov 0x8(%rsi),%rsi&#xff0c;而这个mov&#xff0c;在计算完地址&#xff0c;还要访问内存地址。如果rsi是0&a…...

conda相比python好处

Conda 作为 Python 的环境和包管理工具&#xff0c;相比原生 Python 生态&#xff08;如 pip 虚拟环境&#xff09;有许多独特优势&#xff0c;尤其在多项目管理、依赖处理和跨平台兼容性等方面表现更优。以下是 Conda 的核心好处&#xff1a; 一、一站式环境管理&#xff1a…...

《用户共鸣指数(E)驱动品牌大模型种草:如何抢占大模型搜索结果情感高地》

在注意力分散、内容高度同质化的时代&#xff0c;情感连接已成为品牌破圈的关键通道。我们在服务大量品牌客户的过程中发现&#xff0c;消费者对内容的“有感”程度&#xff0c;正日益成为影响品牌传播效率与转化率的核心变量。在生成式AI驱动的内容生成与推荐环境中&#xff0…...

(二)原型模式

原型的功能是将一个已经存在的对象作为源目标,其余对象都是通过这个源目标创建。发挥复制的作用就是原型模式的核心思想。 一、源型模式的定义 原型模式是指第二次创建对象可以通过复制已经存在的原型对象来实现,忽略对象创建过程中的其它细节。 📌 核心特点: 避免重复初…...

Keil 中设置 STM32 Flash 和 RAM 地址详解

文章目录 Keil 中设置 STM32 Flash 和 RAM 地址详解一、Flash 和 RAM 配置界面(Target 选项卡)1. IROM1(用于配置 Flash)2. IRAM1(用于配置 RAM)二、链接器设置界面(Linker 选项卡)1. 勾选“Use Memory Layout from Target Dialog”2. 查看链接器参数(如果没有勾选上面…...

CMake 从 GitHub 下载第三方库并使用

有时我们希望直接使用 GitHub 上的开源库,而不想手动下载、编译和安装。 可以利用 CMake 提供的 FetchContent 模块来实现自动下载、构建和链接第三方库。 FetchContent 命令官方文档✅ 示例代码 我们将以 fmt 这个流行的格式化库为例,演示如何: 使用 FetchContent 从 GitH…...

关于 WASM:1. WASM 基础原理

一、WASM 简介 1.1 WebAssembly 是什么&#xff1f; WebAssembly&#xff08;WASM&#xff09; 是一种能在现代浏览器中高效运行的二进制指令格式&#xff0c;它不是传统的编程语言&#xff0c;而是一种 低级字节码格式&#xff0c;可由高级语言&#xff08;如 C、C、Rust&am…...

Unity | AmplifyShaderEditor插件基础(第七集:平面波动shader)

目录 一、&#x1f44b;&#x1f3fb;前言 二、&#x1f608;sinx波动的基本原理 三、&#x1f608;波动起来 1.sinx节点介绍 2.vertexPosition 3.集成Vector3 a.节点Append b.连起来 4.波动起来 a.波动的原理 b.时间节点 c.sinx的处理 四、&#x1f30a;波动优化…...

使用Matplotlib创建炫酷的3D散点图:数据可视化的新维度

文章目录 基础实现代码代码解析进阶技巧1. 自定义点的大小和颜色2. 添加图例和样式美化3. 真实数据应用示例实用技巧与注意事项完整示例(带样式)应用场景在数据科学和可视化领域,三维图形能为我们提供更丰富的数据洞察。本文将手把手教你如何使用Python的Matplotlib库创建引…...

Spring Security 认证流程——补充

一、认证流程概述 Spring Security 的认证流程基于 过滤器链&#xff08;Filter Chain&#xff09;&#xff0c;核心组件包括 UsernamePasswordAuthenticationFilter、AuthenticationManager、UserDetailsService 等。整个流程可分为以下步骤&#xff1a; 用户提交登录请求拦…...

6.计算机网络核心知识点精要手册

计算机网络核心知识点精要手册 1.协议基础篇 网络协议三要素 语法&#xff1a;数据与控制信息的结构或格式&#xff0c;如同语言中的语法规则语义&#xff1a;控制信息的具体含义和响应方式&#xff0c;规定通信双方"说什么"同步&#xff1a;事件执行的顺序与时序…...