web异常监控和分析
# 1、前端监控概述与核心价值
前端监控是保障产品质量的“眼睛”。它能帮助你在用户投诉之前发现 Bug,在老板质疑之前量化性能瓶颈。
# 2. 监控的三个核心维度 (监控什么?)
一个完善的前端监控系统通常包含以下三个方面:
# 🛡️ 稳定性监控 (Stability / Error)
- JS 错误: 运行时报错(
TypeError、ReferenceError等)。 - 资源加载错误: 图片、CSS、Script 加载失败(通常是 404)。
- API 请求错误: 接口返回 4xx/5xx,或者业务逻辑上的“成功但报错”(如
{code: -1, msg: 'error'})。 - 白屏检测: 页面关键 DOM 节点缺失,导致用户看到一片白,这是最高优先级的致命故障。
# 🚀 性能监控 (Performance)
- 加载速度: 页面多久能刷出来?多久能交互?
- 接口耗时: 关键业务接口的响应时间。
- 静态资源: 主要是大图、大 JS 文件的加载耗时。
# 👥 用户行为监控 (User Behavior)
- PV/UV: 基础的访问量数据。
- 用户路径: 用户点进了哪个页面,又跳转到了哪里。
- 点击热力图: 用户最喜欢点哪里。
- 场景回溯 (Session Replay): 这是排查 Bug 的神器。通过技术手段(如
rrweb)录制用户操作视频,还原 Bug 发生时的真实场景。
# 3. 关键性能指标 (Core Web Vitals)
Google 制定了一套行业标准,称为 Core Web Vitals (核心 Web 指标),这是目前性能监控的“圣经”。
| 指标 | 全称 | 中文含义 | 衡量什么? | 标准 (好) |
|---|---|---|---|---|
| LCP | Largest Contentful Paint | 最大内容绘制 | 加载速度。视口内最大的图片或文本块渲染出来的时间。 | < 2.5s |
| INP | Interaction to Next Paint | 下次绘制交互 | 交互响应度。用户点击按钮后,页面多久才有反应。(注:INP 已取代 FID 成为核心指标) | < 200ms |
| CLS | Cumulative Layout Shift | 累积布局偏移 | 视觉稳定性。页面加载时元素是否乱跳(导致误触)。 | < 0.1 |
| FCP | First Contentful Paint | 首次内容绘制 | 白屏结束的时间点,用户看到第一个像素的时间。 | < 1.8s |
# 4. 工具选型方案
根据团队规模和预算,通常有三种选择路径:
# 💎 方案 A:SaaS 商业服务 (最省心,但贵)
适合中小团队或快速迭代期,花钱买时间。
- Sentry: 全球最强。错误追踪能力极强,支持 SourceMap 还原代码,现在的性能监控也做得很好。
- LogRocket: 专注于会话回放,能像看电影一样看用户怎么操作出错的,体验极佳。
- 阿里云 ARMS / 腾讯云 RUM: 国内大厂方案,合规性好,网络接入快。
# 🛠️ 方案 B:自研 / 基于开源搭建 (成本可控,数据私有)
适合大厂或对数据隐私要求高的团队。
- Web-Tracing: 国内开发者的开源项目,功能比较全。
- Prometheus + Grafana: 后端常用的监控方案,前端通过埋点 SDK 上报数据,Grafana 做可视化面板。
- Sentry (Self-hosted): Sentry 提供私有化部署版本,但维护成本(服务器资源、数据库)较高。
# 📦 方案 C:简单埋点 (轻量级)
- Google Analytics (GA4): 免费,主要看流量和简单的性能数据,查 Bug 能力弱。
- Lighthouse (本地): 不是实时监控,但适合开发阶段跑分优化。
# 5. 核心技术实现与难点
# 5.1 白屏监控的实现方案
# ✅ 一、什么是白屏?
白屏指页面加载后,用户看到几秒钟“什么都没有渲染”,常见原因:
- JS 报错导致根节点没挂载
- 资源加载失败(CSS/JS)
- 后端返回空 HTML
- 路由跳转异常(SPA)
- 渲染被长任务阻塞
- 第三方脚本卡住
白屏上报的核心: 发现用户看到“全白”的画面并记录上下文信息。
# ✅ 二、白屏检测的 3 种实战方案(按可靠性排序)
# 方案 1:关键 DOM 探测(推荐)
适合所有 SPA/SSR 项目,准确率最高。
原理: 判断首屏是否成功渲染关键内容(主容器、关键节点、LCP 元素等)
# ✔ 可直接用的代码
(function() {
const TIMEOUT = 5000; // 5s 内不出现关键节点 → 白屏
function isWhiteScreen() {
// 关键 DOM 节点
const root = document.getElementById('app') || document.getElementById('root');
if (!root) return true;
// 内容为空、或只有 loading skeleton
const text = root.innerText.trim();
const hasMeaningfulContent = text.length > 0;
// 检测是否有 LCP 元素
const hasLCP = performance.getEntriesByType('largest-contentful-paint').length > 0;
return !(hasMeaningfulContent || hasLCP);
}
setTimeout(() => {
if (isWhiteScreen()) {
reportWhiteScreen();
}
}, TIMEOUT);
function reportWhiteScreen() {
const data = {
type: 'white-screen',
url: location.href,
ua: navigator.userAgent,
timestamp: Date.now(),
html: document.documentElement.outerHTML.slice(0, 500), // 采样
performance: {
fcp: performance.getEntriesByName('first-contentful-paint')[0]?.startTime,
lcp: performance.getEntriesByType('largest-contentful-paint')[0]?.startTime,
}
};
navigator.sendBeacon('/monitor/white-screen', JSON.stringify(data));
console.warn('White Screen Detected', data);
}
})();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
这个版本可直接用于生产环境。
# 方案 2:像素采样检测(更严格,但耗性能)
通过 canvas 截取可视区域像素,检测是否纯白/纯灰。
function detectScreenPixels() {
const canvas = document.createElement('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(document.body, 0, 0);
const data = ctx.getImageData(0, 0, 10, 10).data;
let sum = 0;
for (let i = 0; i < data.length; i += 4) {
sum += data[i] + data[i + 1] + data[i + 2];
}
const avg = sum / (data.length / 4);
return avg > 230; // 过于亮 = 白屏
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
⚠ 不推荐默认启用(成本高、部分浏览器不支持 body 截图)。
# 方案 3:超时 + DOM 无变化(轻量 fallback)
5s 内 DOM 没有变化,可能白屏:
let lastDOMCount = 0;
setTimeout(() => {
const currentCount = document.getElementsByTagName('*').length;
if (currentCount === lastDOMCount || currentCount < 10) {
reportWhiteScreen();
}
}, 5000);
document.addEventListener('DOMContentLoaded', () => {
lastDOMCount = document.getElementsByTagName('*').length;
});
2
3
4
5
6
7
8
9
10
11
12
适合 老项目、轻监控。
# ✅ 三、白屏上报的数据格式(建议)
统一结构便于分析:
{
"type": "white-screen",
"url": "https://xxx.com/page",
"timestamp": 1710000000000,
"ua": "Mozilla/5.0 ...",
"domCount": 4,
"fcp": 2200,
"lcp": 0,
"stack": "Error stack if available",
"network": {
"rtt": 50,
"downlink": 4
},
"htmlSample": "<html>..."
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ✅ 四、如何减少误报?
# 1. 检查是否有 LCP
有 LCP ⇒ 不算白屏 (Chrome 官方建议)
# 2. 检查是否存在 skeleton(骨架屏)
如果项目会先渲染 skeleton,要加入白名单:
if (document.querySelector('.skeleton')) return;
# 3. 检查是否用户主动切走 tab
若页面被后台化,白屏检测应跳过:
document.visibilityState === "visible"
# 4. 分地域 + 设备聚合
例如低端安卓设备白屏率高,不是 bug,是性能限制。
# ✅ 五、白屏监控仪表盘(指标建议)
你可以在 Grafana/Sentry/Datadog 中做 3 个核心指标:
# 1. 白屏率(主指标)
白屏次数 / PV
# 2. LCP = 0 的比例(辅助指标)
# 3. 白屏分布
- 终端:安卓 / iOS / 桌面
- 地域:省份 / 国家
- 网络:4G / WiFi
- 页面:URL pattern
这可以快速定位到具体场景。
白屏诊断流程
- 看 RUM 数据:FCP/LCP/INP 及地域/设备分布,确定是否普遍或个别设备/地区问题。
- 回放/filmstrip:用 Lighthouse lab filmstrip 或 RUM 视频/回放找出“最大元素什么时候出现”。(若 LCP 总是某张大图 → 优化该图)([web.dev][3])
- 查看网络面板:是否资源被阻塞(CSS/JS/Fonts)或第三方慢。
- 查看主线程:是否被长任务阻塞(导致渲染被延后)。
- 施策(最快路径):保证首屏 HTML 直接可渲染 + preload 关键资源 + 异步非关键 JS。
工具推荐(监控 + 本地调试)
- RUM/监控:Sentry / Datadog RUM / NewRelic Browser / 自建收集 + Grafana
- 本地/CI 测试:Lighthouse(或 Lighthouse CI) + PageSpeed Insights + WebPageTest(filmstrip)
- 诊断:Chrome DevTools(Performance 面板 + Lighthouse) + web-vitals(上报)([Chrome for Developers][2])
# 5.2 错误捕获与上报
# 跨域的js运行错误捕获
只能捕获错误但不能拿到具体信息
- 在script标签增加crossorigin属性(在客户端做)
- 设置js资源响应头Access-Control-Allow-Origin: * (这里可以是*也可以是域名。需要在服务端做)
# 错误上报
- 采用ajax通信方式上报(一般不用这种方式)
- 利用Image对象上报(如google的gaa,国内的cnzz都是image方式上报)
- 监听请求进行上报时可以重写xmlHttpRequest的方式,重写send函数,执行完上报,在用老的send函数call执行发送请求
export function injectXHR() {
let XMLHttpRequest = window.XMLHttpRequest;
let oldOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (method, url, async, username, password) {
if (!url.match(/logstores/) && !url.match(/sockjs/)) {
this.logData = {
method, url, async, username, password
}
}
return oldOpen.apply(this, arguments);
}
let oldSend = XMLHttpRequest.prototype.send;
let start;
XMLHttpRequest.prototype.send = function (body) {
if (this.logData) {
start = Date.now();
let handler = (type) => (event) => {
let duration = Date.now() - start;
let status = this.status;
let statusText = this.statusText;
tracker.send({//未捕获的promise错误
kind: 'stability',//稳定性指标
type: 'xhr',//xhr
eventType: type,//load error abort
pathname: this.logData.url,//接口的url地址
status: status + "-" + statusText,
duration: "" + duration,//接口耗时
response: this.response ? JSON.stringify(this.response) : "",
params: body || ''
})
}
this.addEventListener('load', handler('load'), false);
this.addEventListener('error', handler('error'), false);
this.addEventListener('abort', handler('abort'), false);
}
oldSend.apply(this, arguments);
};
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 利用image方式更简单,一行就可以轻松实现上报
(new Image()).src="http://xxx.com/abc?r=yyy"
# 5.3 性能数据采集
- 监测与回归防护* 在 CI/PR 中加入 Lighthouse 或 PSI 检查(关注 LCP/FCP/INP 回归),并在发布后用 RUM 数据验证。
- PerformanceTiming (opens new window)对象包含延迟相关的性能信息
- PerformanceObserver.observe (opens new window)方法用于观察传入的参数中指定的性能条目类型的集合。当记录一个指定类型的性能条目时,性能监测对象的回调函数将会被调用
// 安装:npm i web-vitals
import {getLCP, getFCP, getCLS, getINP} from 'web-vitals';
function sendMetric(name, value, extra = {}) {
navigator.sendBeacon('/rum/metric', JSON.stringify({name, value, extra, url: location.href, ts: Date.now()}));
}
getLCP(metric => sendMetric('LCP', metric.value));
getFCP(metric => sendMetric('FCP', metric.value));
getCLS(metric => sendMetric('CLS', metric.value));
getINP(metric => sendMetric('INP', metric.value));
2
3
4
5
6
7
8
9
10
11
web-vitals 会把浏览器 API 的差异封装好,便于把 LCP/FCP/INP/CLS 可靠地上报到后端进行聚合(CrUX 风格的 75th 百分位判断)。([web.dev])
原生测 LCP(简单示例)
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// entry.startTime 是 LCP 的时间(ms)
console.log('LCP candidate', entry.startTime, entry);
navigator.sendBeacon('/rum/metric', JSON.stringify({name:'LCP', value: entry.startTime}));
}
}).observe({type: 'largest-contentful-paint', buffered: true});
2
3
4
5
6
7
(注意:要结合页面 visibility 和最终候选项来确定最终 LCP,实际库会处理这些细节。)([web.dev])
# 5.4 实施难点与解决方案(SourceMap、采样、过滤)落地实施的难点与技巧
如果你准备开始做监控,注意以下几个“坑”:
SourceMap 安全问题:
- 线上报错通常是压缩后的代码(如
a.js:1:503),看不懂。 - 解决: 需要在构建流程(CI/CD)中把
.map文件上传到监控平台(如 Sentry),但绝对不要部署到公网服务器上,否则源码会泄露。
- 线上报错通常是压缩后的代码(如
海量日志削峰:
- 如果某个 Bug 导致无限循环报错,瞬间上万条日志会打爆服务器或耗尽 SaaS 配额。
- 解决: 前端 SDK 必须做采样 (Sampling) 和 防抖 (Throttle)。例如:同一个错误 1 秒内只报 1 次,或者只采集 10% 的性能数据。
无意义的报错:
Script error.(跨域脚本错误) 或 浏览器插件注入的错误。- 解决: 配置过滤规则(Ignore List),忽略非业务域名的脚本错误和常见的插件错误。
# 6. 总结与实施路线
💡 建议的起步路线:
- 第一阶段 (止血): 接入 Sentry (免费版或私有部署),重点监控 JS 报错 和 白屏。先保证程序不崩。
- 第二阶段 (优化): 引入
web-vitals库,上报 LCP 和 INP 数据,结合性能面板优化首屏速度。- 第三阶段 (洞察): 结合 rrweb 实现用户操作回放,解决那些“我本地复现不出来”的诡异 Bug。
# 7. 参考资料与扩展阅读
# Web Vitals
- TTFB(time to first byte)(首字节时间) 是指浏览器发起第一个请求到数据返回第一个字节所消耗的时间,这个时间包含了网络请求时间、后端处理时间
- FP(First Paint)(首次绘制) 首次绘制包括了任何用户自定义的背景绘制,它是将第一个像素点绘制到屏幕的时刻
- FCP(First Content Paint)(首次内容绘制) 首次内容绘制是浏览器将第一个DOM渲染到屏幕的时间,可以是任何文本、图像、SVG等的时间
- FMP(First Meaningful paint)(首次有意义绘制) (opens new window) 首次有意义绘制是页面可用性的量度标准
- FID(First Input Delay)(首次输入延迟) 用户首次和页面交互到页面响应交互的时间
- LCP (Largest Contentful Paint)(最大内容渲染) (opens new window) 代表在viewport中最大的页面元素加载的时间。
2.5s以内是好的,2.5s到4s需要优化。 - DCL (DomContentLoaded)(DOM加载完成) 当 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,无需等待样式表、图像和子框架的完成加载
- L (onLoad) 当依赖的资源全部加载完毕之后才会触发
- TTI (Time to Interactive) 可交互时间 用于标记应用已进行视觉渲染并能可靠响应用户输入的时间点
- FID First Input Delay(首次输入延迟) 用户首次和页面交互(单击链接,点击按钮等)到页面响应交互的时间。100ms以内是好的,100ms到300ms需要改善。
- TBT(Total blocking time)主线程累计阻塞时间
- CLS(Cummulative layout time)累计布局偏移,小于0.1是好的,0.1s到0.25s需要改善。
- 卡顿 超过50ms的长任务。响应用户交互的响应时间如果大于100ms,用户就会感觉卡顿
- paint timing (opens new window)