跳到主要内容

项目一:监控体系

异常 / 性能 初始化逻辑

新开页面 / 刷新当前页面时:

  1. 页面初次加载和渲染,此时浏览器通过 html 模版中的地址,加载并执行异常 SDK。
  2. 页面执行 ERWA.install(),初始化 erwa 基础配置信息,如 pageId、白屏插件参数。
  3. 异常 SDK 中,利用插件动态加载性能 SDK,并在全局 window 环境中注入 pageId 信息;
  4. 性能 SDK 加载完成,等带页面触发 onload,此时开始全局初始化性能 sdk。
  5. 加载首屏性能采样 + SPA 性能采样。当页面触发路由跳转时,开始上报 SPA 采样数据。

如果页面发生跳转:

  1. 如果是新的子应用,则模板中触发 insall 方法,异常 SDK 登记 pageid - router 的影射;
  2. 如果是已经加载过,则在上报异常 / 上报性能时,会调用暴露的 getPageId 方法,获取 pageid;

性能 SDK

Observer API

整体用法:

// 1. 创建 Observer 实例,传入回调函数
const observer = new XXXObserver((entries) => {
// 2. 回调:响应观察到的变化
});

// 3. 调用 .observe 监听一个或多个 DOM 元素
observer.observe(domElement, options);

// 4. 如果不再需要监听,可以使用:
observer.disconnect(); // 停止所有观察

Observer API 的设计原则:

  1. 单一观察器:创建一个观察器实例,可以通过 .observe() 调用观察多个元素;
    • performance 不需要,入参通过 entryTypes:[] 就支持,观察的是事件类型而非元素。
  2. 统一批通知:所有被观察的元素,都会触发同一个回调函数,以数组形式批处理返回。
  3. 微任务队列:不是同步立即触发,多个连续变化事件会被收集并在当前宏任务执行完后触发。
    1. 批处理优化:减少重排重绘,等所有同步代码执行完毕,收集变化一次处理,减少 DOM 操作,提高性能。
    2. 避免递归问题:如果在 Observer 的回调中再次修改 DOM,会在下一个微任务循环中执行。

列表:

  1. MutationObserver:监控 DOM 树变化,包括节点添加/删除、属性修改和文本内容变化。
    • 场景:性能监控中跟踪 DOM 变化,追加 elementtiming 标记。
  2. IntersectionObserver:监控元素与视口 / 指定容器的交叉情况,可根据设定 0~1.0 的可视阈值。
    • 场景:图片懒加载,容器可视后才加载图片资源;LCP 测量,获取元素实际可视面积。
  3. ResizeObserver:监控元素尺寸变化,无需监听窗口 resize 事件。
    • 场景:响应式布局组件、文本区域自动增高
  4. PerformanceObserver:监控浏览器性能相关事件和指标。
    • 场景:获取 Web-vitals 指标、长任务;
    • 被增加 elementtiming 属性的对象会纳入监控,当其渲染完成时会触发回调,判断 LCP;
    • 可使用 .mark('A') 创建时间戳标记,并通过 .measure(A,B) 测量标记点的时间间隔,以此测量用户点击 button,到得到响应的事件。

其他补充:

  1. requestIdleCallback:不是 Observer,监听浏览器空闲状态。当前帧在主线程任务执行完后,如果还有空余时间,则会处理 idle 队列中的回调函数,如果当前标签页被隐藏后台,则无法触发。
    • 场景:非关键任务,预加载内容。空闲时间执行 spa 遍历对象逻辑,防止页面阻塞。
  2. reqestAnimationFrame
    • rAF,下一帧绘制前触发。常见于对动画(淡入淡出)、进度条/倒计时的处理,让动画和浏览器的刷新节奏基本一致,动画流畅和节省性能。每秒接近 60 次调用自动节流。页面在后台时停止。
    • setTimeout,不会和刷新时间对齐,有卡顿或者跳帧的可能性。

首屏性能指标

页面加载:

  • 导航阶段:navigationStart,DNS 查询时间段;
    • 可优化:减少重定向、DNS 预解析、HTTP/2 多路复用、CDN 资源加速、SSR;
  • 资源请求:TCP 连接时间段、SSL 建立段、请求响应(TTFB)段、内容下载段、DOM 解析段(DOMContentLoad)、资源加载端、触发 load(此时 script 可能并未执行完,不可交互)。
    • 可优化:组件懒加载、资源压缩、分包、资源缓存、CSS 内联;
  • 解析与渲染:基本是 web-vitals 的用户体验:FCP 首次渲染、LCP 最大渲染、CLS 页面偏移情况、FID 首次交互-响应的延迟时间、TTI;
    • TTI 完全可交互:页面有内容(FCP 后)+主线程出现空闲+交互响应<50ms 延迟
    • 可优化:减少 JS/CSS 阻塞、减少重排和回流(字体/图片加载优化)、设置宽高避免 CLS

其他自定义:

  • 快开比:0.5、1、2 快开比,2s 慢开比(基于 DOM Ready → LCP)
  • 用户交互:(1)performance.mark('clickStart') + .mark('clickEnd') 进行打点;
    • (2)然后通过 .measure('clickTime', 'clickStart', 'clickEnd') 计算耗时;
    • (3)最后从 performance.getEntriesByName('clickTime')[0] 获取结果。

spa 算法整体流程

  • 有限状态机:初始化 → 页面加载 → 监测活动 → 数据上报 → 重置(循环);
  • 算法采样了三性能指标:spa_load、spa_fcp、spa_lcp;

spa_load:以监听到路由发生变更为信息采集起点,持续监听 DOM 变化、网络请求、资源加载、长任务等页面变动信息。直到在一段时间内,页面不再有上述波动变化,即判断页面达到稳定状态。

spa_paint:spa_lcp、spa_fcp,以 spa_load 或者用户触发点击/按键截止时间,

  • 采用 MutationObserve 监听 DOM 变化,获取一批新增的 DOM 列表。
    • 对元素进行遍历。对于符合标准的元素,进行 performance elementtiming 打标。
  • 在 PerformanceObserve 中监听被打标的元素,采样它们的加载完成时间;
  • 采样截止后,IntersectionObserve 采样这些元素的可见面积,统计:
    • 取渲染面积大于 0,且渲染时间最短的元素,为 fcp 元素;
    • 取渲染面积最大的元素,为 lcp 元素。

发布-订阅模式:在确定 spa_load 截止时间方面,采用了多维度观察器监听:

  • 网络请求:XHR(代理了 open、header、send 方法),fetch(代理 fetch 方法);
  • 用户行为:增加监听器:click, keydown;
  • 页面生命周期:mutation Observer,Router(代理 history,监听 hash);
  • 性能监控类:长任务(performance Observer)

spa 计算优化

  1. DOM 遍历限制:
    • 采用 DFS 深度遍历,防止栈溢出;
    • 设置最大递归深度 + 最大检查元素数量;
    • 剪枝:快速过滤掉不相关元素,重复 TD 表格、非 Element 节点等;
  2. 采样延迟处理、计算延迟处理:
    • whenIdle 空闲时间处理,尽可能在 requestIdleCallback 时机处理问题,如果页面是 hidden 状态,则用 setTimeout 异步执行,防止线程阻塞。
    • 待确认元素的 MapList 设置最大值,多余丢弃;并且利用 whenIdle 延迟分析;
  3. 内存优化:
    • 一次 spa 采样结束后,相关观察器,以及 Map 全部重置并清除内存;

异常类型

  • JS 执行异常:DOMException、ECMAScript Excption
    • 运行时错误:通过 window.addEventListener(’error’) / window.error 可监听到;
    • Promise 的未捕获异常 unhandledrejection,通过 addEventListener 可监听到;
    • 框架:Vue errorHandler(劫持),React ErrorBoundary(HOC);
  • 资源加载异常
    • 插件,通过 addEventListener(’error’),监听 target 类型进行判断。
      • 混合内容错误 Mixed Content:页面协议和资源请求协议不一致;
      • 资源获取失败:资源请求失败;
  • 接口请求异常
    • 插件,代理 Xhr 和 Fetch,获取请求异常和非 200-300 的异常;
      • 实现思路是对 Sentry 暴露的 handler 方法直接增加监听逻辑。
      • 内部代理 XMLHttpRequest open/send 方法,捕获 readystatechange 看 404 ;
      • 内部代理 Fetch 请求,捕获 Promise 的 then/catch。
  • 白屏异常
    • 插件,自定义白屏算法,在 load 后通过 3 条交叉线取点的方式,判断是否达到阈值
  • 自定义异常:对外暴露方法,业务端调用后会上报异常。

异常数据采集问题

问题:异常问题定位困难

  • 原因:代码构建压缩与混淆
  • 解决:sourcemap 构建。在上线时构建 Source Map,但不上传至 CDN。将 .map 文件存储在内网服务器中,仅可在异常监控系统中访问,确保数据安全。

问题:采样数据丢失

  • 原因:页面关闭/刷新、网络延迟,如何将数据上传到服务器
  • 解决 1:上传方式
    1. 使用同步 XML + beforeunload 时机,同步上传阻塞页面卸载,可能影响用户体验;
    2. (首选)navigator.sendBeacon() :异步发送不阻塞页面卸载,且可以保障页面卸载前发送;但是有数据格式和体积限制,且兼容性不高;
    3. (次选)fetch + keepalive,不阻塞页面卸载,且卸载后可执行;但兼容性比较差。
    4. (降级)使用 img.src,不阻塞主线程,无需依赖 Xhr / fetch,兼容性好;图片请求天然规避跨域。但 get 请求有体积限制;无法得知是否上传成功,没有响应处理;无法上复杂的数据结构
  • 解决 2:发送失败挽回数据
    • 持久化缓存:数据产生时,先存入 localStorage 中。每发送成功一组异常数据,就从 localStorage 中标记已发送,然后定时批处理清空。每次打开页面,都先尝试重发。

问题:海量日志成本过高

  • 随机采样:直接增加 0.25 采样随机值,确保样本能真实反映情况。
  • 权重采样:根据错误级别、页面重要性、用户身份等,设置不同采样率;
  • 自适应采样:根据灰度分组、动态配置,进行动态调整采样率。

Sentry-javascrpit 架构

  1. Integration 集成层:各种插件,有 GlobalHandlers 全局错误监听、面包屑、堆栈重写;
  2. Client 客户端实例:控制 Sentry 生命周期,事件处理中枢:错误捕获、自定义消息等;
  3. Transport 传输层:负责发送数据,有事件序列化、重试机制(退避算法)、速率限制;
  4. Scope 作用域:上下文信息管理,比如用户操作轨迹、环境信息、元数据、数据隔离;

报警策略

  1. 每 10 分钟执行一次定时任务,获取最近 10 分钟、前一天 10 分钟、上一周 10 分钟,三个时间窗口的数据。
  2. 具体的数据有:项目的性能 PV / UV 总数,项目的异常(分类)PV / UV 总数;
  3. 告警的错误有:
    • 环比报错率超过阈值:(当前错误率 - 上一个窗口错误率) > 阈值;考虑绝对值 > 50;
    • 同比报错率超过阈值:同上,是上一周。
    • 新增错误:这个错误不在 同比和环比的 TOP3 列表中;
      • TOP3:当前时间段内,排名前 3 的错误;考虑绝对值:错误数量必须 > uv 的 1%

查询优化

  1. 缓存热数据:每日定时任务会把昨日性能数据进行缓存:
    • 分钟维度的数据,缓存至 redis 中,有 14 天有效期;
    • 小时维度的数据,缓存至 mongoDB 中,有 60 天有效期;
  2. 构建索引:之前性能数据拉取 MongoDB 很慢,数据库观察后发现有慢查询,使用 .explain("executionStats")分析慢查询,发现查询使用(全表扫描),导致扫描全部 82 万+文档。
    • 复合索引创建:根据查询条件创建多字段联合索引
    • 针对查询模式设计索引:分析常用查询字段组合(id+key,projectId+create_time)
    • 考虑排序方向:为 create_time 创建了降序(-1)索引以匹配 sort 操作
  3. 优化效果:查询方式从 (全表扫描) 变为 (索引扫描),扫描文档数量从 82 万+ 降至 10 个。响应时间从 1000ms 降至 <1ms。

MCP

角色说明
Host(客户端)你的业务系统,负责 UI、交互、调 LLM 前的链路管理
MCP Server能力管理中心,维护 Prompt、Tool、Resource、权限校验
MCP Client(客户端 SDK / 接口适配层)Host 内嵌的一层适配,负责与 MCP Server 通信
LLM 服务大模型推理接口,生成文本、图像、音频等结果

提供工具

  • Resource 资源:一段可被读取或嵌入上下文的数据。它由一个 URI 唯一标识,数据可能是纯文本、二进制 Blob,甚至嵌入式 JSON;
  • Tool 工具:是“可被模型调用的外部功能”,类似函数或者微服务的 API  端点;
  • Prompt 提示词:可复用的提示模板库,输入参数后,返回一个构建好的提示词;
  • Roots 顶级入口:可以调用底层的能力。

报文结构 / 通信

  • 基于 JSON-RPC 的消息框架

通信:Stdio 标准输入输出、StreamableHTTP、(SSE)。

  • Stdio(标准输入输出):用于进程间通信,通过标准输入/输出流传输 JSON-RPC 消息。该方式更适合本地插件,下载导入本地 MCP Hosts 后,离线使用。特点:实现简单,无需网络连接。
  • StreamableHTTP:基于 HTTP 长连接的双向通信机制,使用 HTTP/1.1 /2 协议传输,通过长连接保持会话;允许客户端和服务端在同一连接上互相发送请求和响应;适用场景:跨网络的服务间通信,需要双向能力协商的场景;

tips 1:如果需要服务器 -> 客户端的主动信息推送,可额外增加 SSE 连接,使用 text/event-stream 内容类型 进行单向通信。

tips 2:StreamableHTTP 的长连接,是以 HTTP/1.1 Connection: keep-alive、或者 HTTP/2 的连接复用为基础;采用分块传输编码(Chunked Transfer Encoding),每个 JSON-RPC 消息作为一个独立的分块发送,接收方通过分块识别完整消息;同时使用 Session Id + 心跳消息进行会话管理,以确保连接活跃来实现的。

生命周期与交互流程

(1)初始化握手(initialize → initialized)

每次 MCP 会话都以一段"自我介绍"开始。客户端首先发送  initialize  请求,告知服务器:"我是谁,我懂什么,我想用哪个版本的协议"。具体而言,这个请求包含三个关键部分:

  • clientInfo: 客户端的身份证,记录名称和版本号
  • protocolVersion: 客户端支持的协议版本,如  "0.5"
  • capabilities: 客户端能力清单,如是否支持流式输出、用户输入引导等

服务器收到后,会回复一个  initialize  响应,同样包含三要素:

  • serverInfo: 服务器的身份与版本
  • protocolVersion:  双方实际要使用的协议版本(可能低于客户端请求的版本)
  • capabilities: 服务器能力清单,如是否提供工具、资源、提示词模板等
  • instructions: 可选的使用说明,客户端可将其添加到系统提示中

握手完成后,客户端发送一个无需回复的  initialized  通知,标志着初始化阶段结束,正式会话开始。这种"能力协商"机制保证了不同版本、不同实现的客户端和服务器能够找到共同语言,互不干扰地工作。

(2)Resources 读取流程

资源是 MCP 中的"知识载体",通常用于向 LLM 提供上下文。一次典型的资源读取流程如下:

  1. 发现可用资源

    • 客户端发送  listResources  请求,可选带上  filter  参数筛选特定类型。服务器返回资源列表,每项包含 URI、名称、标题等元数据。若结果较多,会附带  cursor  供分页获取。
  2. 读取具体内容

    • 客户端选择感兴趣的资源,发送  readResource  请求,指定目标 URI。服务器返回资源内容,可能是文本、二进制数据或结构化 JSON。
  3. 订阅更新(可选)

    • 若服务器支持  resources.subscribe  能力,客户端可发送  subscribe  请求,监听特定  URI 或模式的变化。当资源更新时,服务器会主动推送  resourceUpdated  通知,客户端据此刷新缓存。

这种"先列举后读取"的模式让客户端能够智能地决定哪些资源对当前任务有价值,避免盲目加载全部内容。同时,订阅机制确保了长时间运行的会话能够保持数据新鲜度,无需频繁轮询。

(3)Tools 调用流程

工具是 MCP 中的"能力扩展",让 LLM  能够与外部系统交互。工具调用通常遵循以下步骤:

  1. 获取工具列表
    • 客户端发送  listTools  请求,服务器返回可用工具清单,每项包含名称、描述、输入输出 Schema 等。这些信息会被客户端整合到系统提示或工具定义中,指导 LLM 正确调用。
  2. 执行工具调用
    • 当 LLM  决定使用某个工具时,客户端构造  callTool  请求,包含工具名称和参数。服务器执行对应操作后返回结果,通常包含:
      • content: 文本、图像等展示给用户的内容
      • structuredContent: 符合输出 Schema 的结构化数据,供 LLM 解析
      • isError: 布尔值,标记是否执行失败
  3. 监听工具变化(可选)
    • 若服务器支持  tools.listChanged  能力,当工具集合发生变化时(如新增、移除、修改),会主动发送  toolListChanged  通知。客户端可据此更新系统提示或重新获取工具列表。

开发细节

case 1: 让 AI 获取 Mcp 提供的服务

  1. 清单文件和 OpenAPI 规范
    • mcp.manifest.json:定义服务元数据、工具列表和参数信息
    • mcp.openapi.yaml:详细的 API 规范(openapi 3.x 协议),包含 x-mcp 扩展
    • 这两个文件使客户端能够自动发现可用功能,并理解如何使用它们。
  2. 工具注册机制
    • registerTool 中注册的各种功能,通过 title、description、schema 描述提供使用方式指南和接口间依赖关系。
  3. 错误处理和引导机制
    • 接口检测到错误参数,返回的信息中提供指导性错误消息,引导客户端正确调用。

case 2:让 AI 自动找到 pageid

在多个位置引导客户端去获取正确的页面 id,从而查询页面的基础信息:

  1. 同上,在清单文件和 OpenAPI 规范文件中,定义 find_project_id 方法和调用顺序,应当首先在本地获取到工程的 pageId,并通过举例引导客户端指导获取方法。
  2. 兼容处理:支持客户端传递 pageId,或者 HTML 内容。遇到 HTML 内容则尝试进行解析,获取其中的 pageId。
  3. 异常处理:当缺少项目 ID 时返回异常,并提供引导解决方案:
    • message:请提供 HTML 文件内容,或按照下文查找项目 ID
    • guide:查询步骤,写清如何全文搜索到 HTML 文件,然后找到 Erwa.install 代码片段。
    • example: Erwa.install({id: 149}); 中的 id 值为 1498,

case 3:让 AI 获取正确的时间

MCP 客户端在获取近期的异常和性能数据时,往往无法正确的获取到当前的准确时间,比如 cursor 上出现会拉取 2024 年同月同日的数据,数据拉取不精确。

解决方法:

  1. 提供一个 get_server_time 方法,并在注册时 description 内提供描述:'获取服务器当前时间和常用时间范围,用于时间相关 API 调用’。
    • 方法返回:13 位时间戳的当前时间、最近 24 小时 / 最近 7 天 / 最近 14 天 三个时间段。每个参数都通过 description 进行注释。并通过 usage 参数指导 MCP 客户端具体的使用方法。
  2. 在获取异常 / 性能数据列表的接口的注册中,server.registerTool()
    1. description 字段:增加使用注意事项:”时间参数需要使用 13 位毫秒级时间戳,并请先调用 get_server_time 工具获取当前精确时间戳”。
    2. inputSchema 字段:校验入参格式,信息也导出到 manifest.jsonopenapi.yaml 中,以供 MCP 客户端去获取规范内容。同时,schema 的 describe() 提供描述信息,也会传递给客户端,使客户端"理解"了 API 的使用流程和依赖关系。
    3. 在格式不规范时抛出异常,并描述正确的参数格式。
  3. manifest.json 的 descriptionForModel 字段中

这样,MCP 客户端可以在使用异常和性能列表接口查询时,能提前获取使用方法。

🍊 case 4:让 AI 获取精确的异常排查 prompt

通过 MCP 的 Prompt 构建一套完整的异常分析交互系统:

  1. MCP 客户端将异常 id,或完整的异常信息传递给 MCP 服务端;
  2. MCP 服务端负责生成专业的分析提示模板;
  3. MCP 客户端获取这些提示并与大模型交互获得解决方案。

具体来说,通过 server.registerPrompt() 注册一个 ”异常分析提示生成词“ 方法:

  • 调用该方法需传入:异常 id、定位异常代码片段、可传完整的异常信息上下文。
  • 如果没有传异常信息上下文,MCP 服务器会首先根据 id 获取完整的异常信息,然后拼接成 prompt,整体结构:
  • 任务名称:前端错误分析和排查;
  • 角色定位:你是一位前端工程专家,严谨专业,对前端和相关代码纠错非常精通。不确定的知识你会坦诚表示不知道。
  • 错误信息:错误消息、文件、堆栈、用户行为轨迹、本地代码等详细信息;
  • 分析任务:给出合理的分析流程,如找根本原因、参考代码片段和错误信息、自行根据错误信息检索项目内容。
  • 输出格式:严格让 LLM 按照规定格式输出:错误概述、根本原因分析、相关代码、修复建议、进一步调查。

这样做确保了:

  1. 质量保证:服务端提前约定好 LLM 的角色定位和任务细节,确保分析质量的可靠;
  2. 上下文整合:自动整合异常信息,一次 prompt 输入。减少 LLM 对历史信息的丢失、手动收集工作。
  3. 一致性:抹平不同客户端(cursor / vscode)、不同 LLM(claude / gpt)的查询和回复差异,让用户前后体验一致。

性能优化

(1)TTFB 首字节时间(150ms 内)

用户发起请求后,到浏览器收到“服务器响应的第一个字节”之间所经历的总时间。主要收到 DNS 查询、TCP 连接、服务器响应三个节点。

  • h5 页面可接入离线包。通过 APP 预先下载 h5 页面的 html、js、css 等资源,实现首屏加载直接读取本地资源,TTFB 接近于 0 的效果。
  • 资源请求走协商缓存,尽可能减少从 CDN 拉取资源,缩短资源加载时间。
  • 后端方面:负载均衡效率低,可能流量分配不均匀;
  • 引入服务端渲染 SSR 技术,减少阻塞渲染的操作。

(2)LCP 最大内容绘制(600ms 内)

方向一:资源优化

  1. 页面级预加载:客户端开辟一个 webview 容器。用来预先加载下一个可能打开的页面。如果加载完成后用户再打开这个页面,可实现 0 延迟秒开。
  2. 首屏资源过大:js、css 资源过大,加载资源的时间变长,延迟渲染开始的时间。
    • 合理的分包策略,非首屏资源、非首屏路由、非首屏组件,进行分包和懒加载处理;同时,对于都在首屏加载的资源,尽可能不要分散打包。
    • CSS 优化:关键 CSS 内联到 HTML,非关键 CSS 异步加载;
    • 对于第三方组件库:定期检查是否有未用到的组件依然导入,影响打包体积。
  3. CSS 资源优化:使用原子化 CSS,可能会消除重复声明,多个组件可以共享公共的原子类,最终打包时可清除减少未用到的原子 CSS 规则,最终体积会小很多。
  4. 合理预加载模块: 图片/CSS:< link rel="preload" > 、JS < link rel="modulepreload" > ,利用浏览器预加载,提升首屏资源加载速度。
  5. LCP 的图片资源过大:背景图可使用前端 css 绘制的渐变色、由前端拼接成的简单背景图;
    • 图片压缩,比如 webp 自适应图片尺寸。
  6. 字体打包优化。对于中仅需要个别字体的页面,针对性只按需打包字体库,缩小打包体积。

方向二:业务接口

前端依赖后端接口较多,造成页面内容不能及早渲染,需等待数据返回。

  1. 对于非页面核心功能的接口,可以滞后请求,不要占用首屏加载时的请求带宽,核心接口响应后可先驱动页面完成渲染。
  2. 对于需前置判断的接口(如用户身份、路由切换等),必须要串行加载,这些接口最先请求;
    • 请求响应后,可先展示整体骨架屏,用户感知页面正在加载中。
  3. 对于影响数据展示的接口,尽可能减少接口请求个数,或前端并行请求。
    • 增加接口响应时长监控,推动后端对响应时长较大的接口升级。

方向三:SDK 加载

  • 暂时用不到的 SDK,尽可能异步加载,不要占用首屏带宽。如页面的聊天模块在默认状态仅提供交互入口,需点击展开后才可交互,那可以异步加载该 SDK 文件。