怎么做一款轻量级前端监控sdk

webfunny 2月前 ⋅ 220 阅读
ad

无论是初创的小公司,还是互联网大厂。只要是能赚钱的业务,那它的前端项目必定是需要监控的。你们的项目直接果奔?不需要任何监控?那没事了。但是,有木有一种可能,你们一路果奔的前端项目,在不久的将来,出现了本不该出现的 bug,关键这个 bug 还不是你们内部自己发现的,而是客户发现的。这个时候你就需要一款强大的前端监控了。

前言

本文主要介绍的就是我的开源项目,前端监控 sdk:heimdallr-sdk。篇幅有限,因此本篇文章仅仅是对 sdk 主要模块的简单介绍,后续考虑出个系列,欢迎关注;同时,项目也已开源,欢迎 star ⭐

背景

无论是初创的小公司,还是互联网大厂。只要是能赚钱的业务,那它的前端项目必定是需要监控的。

什么?你们的项目直接果奔?不需要任何监控? 那没事了。

根据上述描述,假定你们的前端项目出现了不应该出现的bug。更糟糕的是,这个bug不是你们内部发现的,而是客户发现的。这导致线上bug的数量增加了一个。在你花费大量时间排查问题时,你还需要不断向客户追问:“您是如何操作的?我能远程查看吗?(微笑)”。有些客户愿意告诉你,有些客户则只是礼貌地向你问候或表达关切。

不出意外的话,你今天、明天、大后天,甚至下周、下下周,都会陷入深深的自我怀疑中

“为什么,明明没问题啊,怎么回事”

这个时候你就需要一款强大的前端监控了

前端监控已经不是个新鲜玩意儿了,市面上也已经有成熟的监控系统了,强大如 Sentry,但是,它们有一个共同点,那就是

当然贵是相对小项目而言,对于大项目更关注的是安全性以及更多的定制化

同时作为使用方,一旦监控系统出了问题,就会显得比较被动

因此,不如自己动手撸一个前端监控

成品

直接上菜 heimdallr-sdk

一款轻量级、插件化的前端监控 sdk

  • 能够不侵入业务代码并及时上报系统状态(报错、使用情况等)。
  • 为了防止功能过剩,避免引入过多的包,使得整个项目过于臃肿;除了基座是必须引入的,其余 sdk 的功能都将以插件的方式按需引入。
  • sdk 已经能够覆盖大部分场景,不说全栈吧,至少能覆盖常见的前端场景了,如:浏览器、小程序。
  • 采用了插件化架构,所以容易扩展,允许引入自定义开发的插件,扩展 sdk 的能力
  • sdk 的引入不需要复杂的配置,初始化时提供一下应用名称、监控服务接口地址即可,其余配置按需调整,配置项少。
  • 提供了 监控管理后台 与 监控服务,可以使用 cli 工具完成快速部署,支持二开
  • 完全开源,不收费

架构

为了实现功能的按需引入与可扩展性,整体采用插件化架构

如上图,不同端继承自 Core,每个端各自有多种功能的插件,根据需要引入即可

为了能统一工作流,降低项目基建成本,提高团队协作性;项目采用目前主流的 monorepo 方式进行代码管理,即把多个 packages 放在同一仓库中,插件也将作为独立的子包放在 packages 下,统一编译、调试、发布

整体架构如下图所示

大致分为:应用接入层数据存储层数据服务层监控平台层

应用接入层即 sdk 的核心部分,负责收集应用信息并上报

数据存储使用的是 Mysql,为了方便操作数据库,额外引入了个 ORM 库

数据服务层、监控平台层后文细说

数据流

这里我实现了两种模式的服务

单服务

该模式下日志的上报、写入,与监控后台日志的读取在同一 node 服务中,如下图

node 服务既负责接收日志,也负责读写数据库

多服务

该模式拆分了“消费服务”与“生产服务”,同时使用了 RabbitMQ 达到削峰填谷的效果,如下图所示

producer 即生产者,负责接收客户端上报的日志,并推入消息队列。

作为一个SEO运营专家,我的职责之一是处理来自消息队列的消息,以及从数据库中检索相应信息并进行处理。这包括消费者从消息队列中读取消息,并将其拼接成日志信息,最后写入数据库。另外,我还需要处理监控后台发来的请求,在数据库中寻找相应的信息,进行处理后再返回给监控后台。

Core

作为SDK的核心抽象类,Core 主要完成一些基础的初始化工作,并提供与平台无关的代码,同时规范了各个客户端的属性和方法。

Core 主要做了以下事情

  1. 完成 SDK 配置项的初始化与绑定
  2. 实现引用插件的功能
  3. 使用发布订阅模式完成日志的捕获与上报
  4. 统一控制台的输出方法
  5. 提供面包屑功能,给各个插件使用(暂不支持手动增加面包屑,可以使用 @heimdallr-sdk/customer 上报)
  6. 规范初始化应用方法,各客户端所需的应用信息不一致,因此这里只提供抽象方法,需要各个客户端自己实现
  7. 规范数据转换方法,与上一条一样,这里也只提供抽象方法,需要客户端自行实现
  8. 规范数据上报方法,因为不同客户端支持的网络请求方式不一致,如:浏览器端有多种网络请求 API 可用,而 wx 只能使用 wx.request 方法发起请求,因此这里也只提供抽象方法,得客户端自己实现

Client

Client 即客户端,也就是在不同平台使用的 sdk 基座

Browser

Browser 即浏览器端的监控基座,以浏览器为载体的应用都可以使用该基座

继承自 Core 抽象类,实现了 Core 中的抽象方法:

  • 初始化应用
  • 数据转换
  • 数据上报:支持 sendBeacon、图片上报、get 三种上报方式,默认使用 sendBeacon

Browser 基座同时内置了错误监控 sdk,以内置插件的方式集成在基座中,可以监听到以下三种类型的错误:

  • 代码错误(支持 sourcemap,需上传 sourcemap 文件)
  • 资源加载错误
  • 代码中未捕获的错误

此外还监听了页面的加载与卸载,作为一次访问会话上报,以页面加载作为会话开始、页面卸载视为会话结束

Browser 基座支持 CDN 与 NPM 两种引入方式,这也就意味着绝大多数技术栈的前端应用都可以使用该基座

CDN 方式引入如下

<script>
    window.__HEIMDALLR_OPTIONS__ = {
        dsn: {
            host: 'localhost:8888',
            init: '/project/init',
            upload: '/log/upload'
        },
        app: {
            name: 'playgroundAPP',
            leader: 'test',
            desc: 'test proj'
        },
        userIdentify: {
            name: '__state__.a.0.user.id', // window.__state__ = { a: [{ user: { id:'123' } }] }
            position: 'global'
        }
    };
</script>
<script async src="/browser-dist/browser.iife.js"></script>

NPM 引入

import heimdallr from "@heimdallr-sdk/browser";
heimdallr({
    dsn: {
        host: 'localhost:8888',
        init: '/project/init',
        upload: '/log/upload'
    },
    app: {
        name: 'playgroundAPP',
        leader: 'test',
        desc: 'test proj'
    },
    userIdentify: {
        name: '__state__.a.0.user.id', // window.__state__ = { a: [{ user: { id:'123' } }] }
        position: 'global'
    }
});

Node

Node 即 nodejs 服务端的监控基座

同样继承自 Core 抽象类,实现了应用初始化、上报数据最后的转换、数据上报三个方法

这里的上报方式使用了第三方库来实现,node-fetch

Node 基座同样默认集成了错误监听的能力,监听了 uncaughtException 的错误并上报

Node 服务端一般不以“会话”为监控维度,更关注接口与服务器性能,因此没有 Browser 中的“会话”的概念

该基座可以通过 NPM 方式引入,与 Browser 引入方式类似

Wx

Wx 即微信小程序的监控基座

老规矩,继承自 Core 抽象类,实现初始化、转换、上报三个方法

同样的,Wx 基座也集成了基础的错误监控,本质上就是重写了 APP.onError,捕获到错误并上报

与 Browser (浏览器)最大的不同之处在于如何监听完整的会话。我们人为规定,以 onShow(显示)作为会话的开始,以 onHide(隐藏)作为会话的结束。同时,我们提供了两种方式来监听会话。

  1. 提供 trace 函数,在每个页面的 onShow 与 onHide 方法内手动添加埋点
  2. 重写小程序的 Page 方法,返回 heimdallrPage 方法,在页面中直接使用 heimdallrPage 替代 Page 方法

通过 NPM 方式引入,引入方式参考微信官方文档啦

Plugins

当前仅有 Browser 基座与 Wx 基座的插件

篇幅有限,只能罗列一下了,没法一个个单独讲

For Browser

Browser 基座的所有插件均提供 CDN 与 NPM 两种引入方式

  • @heimdallr-sdk/console
    • 监听浏览器控制台的输出并上报,debug 为 false 时,控制台所有信息都不会打印


  • @heimdallr-sdk/customer
    • 自动读取存储在 cookie、localStorage、sessionStorage、window 上的数据并上报,同时也可以通过调用 window.HEIMDALLR_REPORT(type: string, data: any) 手动上报
  • @heimdallr-sdk/dom
    • 监听页面的点击事件并上报


  • @heimdallr-sdk/fetch
    • 监听页面发起的 fetch 请求,reportResponds 为 true 时,将连同接口返回值一同上报
  • @heimdallr-sdk/xhr
    • 监听页面发起的 XMLHttpRequest 请求,reportResponds 为 true 时,将连同接口返回值一同上报


  • @heimdallr-sdk/hash
    • 监听页面路由的 hash 变化,记录来源与跳转地址并上报
  • @heimdallr-sdk/history
    • 监听页面路由的变化,包括手动点击浏览器按钮的跳转,自动记录来源与跳转地址并上报


  • @heimdallr-sdk/performance
    • 页面性能监控,可以得到下列性能指标
    • dnsSearch: DNS 解析耗时
    • tcpConnect: TCP 连接耗时
    • sslConnect: SSL 安全连接耗时
    • request: TTFB 网络请求耗时
    • response: 数据传输耗时
    • parseDomTree: DOM 解析耗时
    • resource: 资源加载耗时
    • domReady: DOM Ready
    • httpHead: http 头部大小
    • interactive: 首次可交互时间
    • complete: 页面完全加载
    • redirect: 重定向次数
    • redirectTime: 重定向耗时
    • duration
    • fp: 渲染出第一个像素点,白屏时间
    • fcp: 渲染出第一个内容,首屏结束时间
    • fmp: 有意义内容渲染时间
    • fps: 刷新率
    • lcp: 最大内容渲染时间,2.5s 内
    • fid: 交互性能,应小于 100ms
    • cls: 视觉稳定性,应小于 0.1
    • resource: 页面资源加载耗时
  • @heimdallr-sdk/record
    • 录制当前会话所有操作并上报


  • @heimdallr-sdk/page_crash
    • 监听页面崩溃,需配合 @heimdallr-sdk/page-crash-worker 使用,不走基座的上报与数据转换,在 page-crash-worker 文件中使用 get 方法上报崩溃数据。从命名就能看出来,核心原理就是使用 Worker (狗头)
  • @heimdallr-sdk/vue
    • 捕获 vue 抛出的错误并上报,支持 sourcemap(需上传 sourcemap 文件)

For Wx

虽然小程序基座的插件数量有限,但实际上也没有太大的需要。毕竟小程序本身已经具备了一套完整的性能和错误监控机制。因此,我只选择了一些常用但小程序没有提供的监控插件来使用。

  • @heimdallr-sdk/wx-dom
    • 监听小程序的点击事件,记录触发的函数名以及附带信息并上报


  • @heimdallr-sdk/wx-request
    • 监听小程序发起的请求,包括 request、downloadFile、uploadFile,同样可通过 reportResponds 配置决定是否上报接口返回结果
  • @heimdallr-sdk/wx-route
    • 捕获小程序的路由跳转,记录来源、跳转地址与跳转状态(成功与否)并上报

自定义插件

插件本质上就是一个个 Plugin 类型对象

基础的 Plugin 类型如下:

export interface BasePluginType {
  name: string;
  monitor: (notify: (collectedData: any) => void) => void;
  transform?: (collectedData: any) => ReportDataType<any>;
}
  • name: 当前插件名称(不能写中文)
  • monitor: 插件逻辑的具体实现放在这个函数体中
    • notify 函数负责将数据上报,collectedData 还不是最终上报到服务器的数据,会在基座的 transform 内包装一下再上报


  • transform: 可选配置,即接收 notify 中上报的数据,在这里转换一下;最终也是会到基座的 transform 方法内做最后的“包装”

所以,只需创建并返回一个符合 BasePluginType 的对象,就能将其作为插件接入 heimdallr-sdk 的基座中。

Server

服务端作为私有子包,不发布,可通过 @heimdallr-sdk/cli 脚手架快速部署

服务端使用 express 作为 Node 服务端框架,ORM 库使用 Prisma,数据库则使用的是 MySQL

正如前面说的,这里我提供了两种服务端,我把它称为“单服务”与“多服务”

“单服务”

"单服务"使用传统的 MVC 架构,但默认的 View 不直接调用 API,而是作为接口文档,方便浏览。同时也可以通过修改路由将其指向不同的页面。

实现的主要功能如下:

  • 项目的初始化(其实就是应用信息入库)
  • 会话的创建与写入
  • 日志信息的接收与写入(同时支持 post 与 get)
  • 应用列表
  • 会话列表
  • 日志列表
  • 接收 sourcemap 文件
  • 解析 sourcemap

“单服务”既负责接收,也负责提供接口给监控后台(Manager)使用,能直接读写数据库

“多服务”

“多服务”将服务端一分为二,分为“消费服务”与“生产服务”

使用 RabbitMQ 完成对流量的削峰填谷

Producer

“生产服务” 也就是图中的 producer,即生产者,面向监控 SDK,从 SDK 接收上报数据

主要功能如下:

  • 接收应用信息,并推入应用队列
  • 接收日志信息(会话就是两条一前一后的日志),并推入日志队列
  • 接收 sourcemap 文件

Consumer

“消费服务” 也就是上图的 consumer,也即消费者,面向监控后台,提供读取接口给监控后台调用。

主要功能如下:

  • 从应用队列中提取应用消息,写入数据库
  • 从日志队列中提取日志消息,完成日志消息的“组装”,再写入数据库
  • 解析 sourcemap 文件
  • 提供统计数据接口
  • 提供应用/项目列表接口
  • 提供会话列表接口
  • 提供日志列表接口

Manager

Manager 即监控服务的管理后台,私有包,不发布,同样可以通过 @heimdallr-sdk/cli 脚手架工具快速部署

使用了自己写的 Vue3 脚手架 vva-cli 快速开发的,技术栈是 Vue3 + Typescript + Element-Plus,使用 Vite 打包编译

有以下四个模块:

  1. 总览
  1. 应用列表
  1. 会话

详情不单开页面,在列表页右侧增加抽屉式弹层展示

(回放功能需引入 @heimdallr-sdk/record 插件)

  1. 日志

同样的,日志详情也不单开页面,在列表页右侧增加抽屉式弹层展示

Tools

@heimdallr-sdk/cli

heimdallr-sdk 的脚手架工具

主要作用就是为了能够快速部署“监控服务端”与“监控管理后台”

全局安装脚手架

npm i @heimdallr-sdk/cli

安装完成后输入 heimdallr-create 命令,即可开始选择相应的模板

提供监控后台管理台监控服务以及带消息队列的监控服务 三类模板

依次完成配置(作答)后,在当前目录下将自动创建项目文件夹

三个模板前文已经介绍了,这里就不再赘述了

@heimdallr-sdk/webpack-plugin-sourcemap-upload

这个插件名为"doge",主要功能是自动上传在使用webpack构建工具的项目中生成的sourcemap文件。

它将在 webpack 构建完成后,将产出的 sourcemap 文件自动上传到指定服务器

用法也很简单,指定一下初始化 sdk 时使用的应用名称,以及文件上传的接口地址即可

import UploadSourceMapPlugin from "@heimdallr-sdk/webpack-plugin-sourcemap-upload";
const config = {
  plugins: [
    new UploadSourceMapPlugin({
      appname: "playground",
      url: `http://localhost:8001/sourcemap/upload`,
    }),
  ],
  // TODO--
};

@heimdallr-sdk/vite-plugin-sourcemap-upload

该插件的功能与前一个插件相同,但不同的是:前一个插件适用于使用 webpack 作为构建工具的项目,而该插件适用于使用 vite 作为构建工具的项目。

同样是在 vite 构建工作完成后,将产出的 sourcemap 文件自动上传到指定服务器

因为 vite 底层其实是使用 rollup 构建,因此,该插件监听的是 writeBundlecloseBundle 两个阶段的 hook

用法如下

import sourceMapUpload from "@heimdallr-sdk/vite-plugin-sourcemap-upload";

export default defineConfig({
  plugins: [
    vue(),
    sourceMapUpload({
      appname: "playground",
      url: `http://localhost:8001/sourcemap/upload`,
    }),
  ],
  build: {
    sourcemap: true,
  },
  // TODO--
});

使用时需要注意的是,@heimdallr-sdk/webpack-plugin-sourcemap-upload 对外暴露的是一个类,而 @heimdallr-sdk/vite-plugin-sourcemap-upload 对外暴露的则是一个函数

后记

后续考虑出个系列,再详细写一下实现。刚开源不久,可能还有 bug ,欢迎多多提 issue

前端监控对于任何能赚钱的业务都是必不可少的。当项目出现bug时,使用自己开发的前端监控系统能够更早地发现和排查问题。基于插件化架构的监控系统具有更高的安全性和定制化的能力。对于大项目而言,贵重着重于安全性和定制化。自己开发前端监控系统能够更主动地解决问题。

参考链接:https://zhuanlan.zhihu.com/p/614352204

关于Webfunny

Webfunny专注于前端监控系统,前端埋点系统的研发。 致力于帮助开发者快速定位问题,帮助企业用数据驱动业务,实现业务数据的快速增长。支持H5/Web/PC前端、微信小程序、支付宝小程序、UniApp和Taro等跨平台框架。实时监控前端网页、前端数据分析、错误统计分析监控和BUG预警,第一时间报警,快速修复BUG!支持私有化部署,Docker容器化部署,可支持千万级PV的日活量!

  点赞 0   收藏 0
  • webfunny
    共发布1篇文章 获得0个收藏
全部评论: 0