首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >官网悄悄挂了三天我们才知道:前端异常采集、日志上报与告警分级的可观测性实践

官网悄悄挂了三天我们才知道:前端异常采集、日志上报与告警分级的可观测性实践

原创
作者头像
数字化落地笔记
发布2026-09-14 09:12:16
发布2026-09-14 09:12:16
310
举报

导读

后端有完善的监控,不代表用户看到的页面是好的。我们给一个企业官网做改造时就闹过笑话:某个活动落地页的表单按钮因为一次发版引入的 JS 报错彻底点不动了,后端接口一切正常、服务器指标一片飘绿,结果整整三天没有一条咨询进来,还是销售在自己手机上点开才发现"提交"按钮是死的。后端监控看不到前端的崩溃,这就是前端可观测性的缺口。这篇讲我们怎么从零搭起一套轻量的前端异常采集、日志上报和告警分级体系,把"用户先发现问题"变成"我们先发现问题"。

一、先分清:前端到底要采哪几类信号

很多人一上来就引一个大而全的监控 SDK,结果日志收了一堆,真正有用的没几条。我们踩过坑之后把信号收敛成四类,每一类解决的问题都不一样:

  • JS 运行时错误:未捕获的异常、Promise 未处理 rejection,这类往往直接让交互失效,优先级最高。
  • 资源加载错误:JS、CSS、图片、接口加载失败,多和 CDN、网络、发版有关。
  • 接口异常:ajax/fetch 返回 4xx、5xx 或者超时,反映前后端联调和依赖问题。
  • 白屏与性能:页面长时间无内容、核心接口慢、首屏指标劣化,属于"没报错但体验坏了"。

把这四类分开采集、分开统计,后面告警才知道该叫醒谁,而不是所有异常都一刀切地半夜打电话。

二、全局采集:用原生接口兜住绝大多数异常

现代浏览器其实已经提供了足够的原生钩子,不依赖重型框架也能把错误兜住。关键是 window.onerror、unhandledrejection 和资源错误的捕获阶段监听,三者覆盖的场景不同,缺一不可:

代码语言:javascript
复制
const report = (type, payload) => {
  const body = JSON.stringify({
    type,
    msg: payload.message,
    src: payload.filename || location.href,
    line: payload.lineno,
    col: payload.colno,
    stack: (payload.error && payload.error.stack) || '',
    ua: navigator.userAgent,
    t: Date.now()
  });
  navigator.sendBeacon('/log/collect', body); // 卸载页面也能发出去
};

// 1. 同步运行时错误
window.addEventListener('error', (e) => {
  // 资源错误没有 error 对象、target 是具体标签,要区分开
  if (e.target && e.target !== window) {
    report('resource', { message: `资源加载失败: ${e.target.src || e.target.href}` });
  } else {
    report('js', e);
  }
}, true); // 捕获阶段,才能拦到资源加载错误

// 2. Promise 未捕获拒绝(fetch 异步错误主要靠它)
window.addEventListener('unhandledrejection', (e) => {
  report('promise', { message: String(e.reason), error: { stack: e.reason && e.reason.stack } });
});

接口异常则包一层 fetch,统一记录状态码和耗时,注意不要把 token、用户手机号这类敏感字段带进日志:

代码语言:javascript
复制
const rawFetch = window.fetch;
window.fetch = async function (...args) {
  const start = performance.now();
  try {
    const res = await rawFetch.apply(this, args);
    const cost = Math.round(performance.now() - start);
    if (!res.ok || cost > 3000) {
      report('api', { message: `${args[0]} ${res.status} ${cost}ms` });
    }
    return res;
  } catch (err) {
    report('api', { message: `${args[0]} 网络失败`, error: err });
    throw err;
  }
};

三、上报通道:为什么用 sendBeacon 而不是普通请求

最早我们直接用 fetch 发日志,结果发现页面跳转、关闭时的最后一批错误全丢了——请求还没发出去页面就卸载了。换成 navigator.sendBeacon 之后问题解决:它由浏览器保证在页面卸载期间异步、可靠地把数据 POST 出去,且不阻塞跳转。

服务端收到后写进消息队列再落库,避免日志高峰直接压数据库。一个简单的接收与聚合逻辑大致是这样:

代码语言:python
复制
def collect(request):
    log = parse(request.body)
    # 按 页面+错误信息+行号 做指纹,相同错误聚合而不是每条都存
    fp = md5(f"{log['src']}|{log['msg']}|{log['line']}".encode()).hexdigest()
    redis.incr(f"err:{fp}:{today()}")  # 计数
    redis.expire(f"err:{fp}:{today()}", 172800)
    redis.hset(f"err:detail:{fp}", mapping={
        "msg": log["msg"][:200], "src": log["src"], "last": log["t"]
    })
    queue.push("frontend_log", log)  # 明细异步落库
    return {"code": 0}

做指纹聚合非常关键:一次发版故障可能瞬间产生上万条一模一样的错误,不聚合的话告警会被刷爆,真正不同的问题反而被淹没。

还有两个容易被忽略的点。一个是采样率:官网流量上来之后,正常情况下的埋点和性能日志没必要全量上报,我们对 P2 类常规信号按 10% 采样,只对 JS 错误和接口失败保持 100% 采集,既不丢关键问题,也把日志成本压了下来。另一个是白屏判定,这是"没报错但页面是空的"这种最隐蔽故障的唯一抓手。我们的做法是在页面加载完成后延迟几秒,检查几个关键挂载点是否真的渲染出了内容:

代码语言:javascript
复制
function detectBlankScreen() {
  setTimeout(() => {
    const roots = ['#app', '#main', '.content'];
    const empty = roots.every(sel => {
      const el = document.querySelector(sel);
      return !el || el.children.length === 0;
    });
    if (empty) report('blank', { message: '核心挂载点无内容,疑似白屏' });
  }, 3000);
}

那次"按钮点不动三天没人知道"的事故,如果当时有白屏和 JS 错误采集,报错第一分钟就会进 P0 通道,而不是等销售自己发现。

四、告警分级:别让所有错误都半夜响铃

我们按"是否影响核心转化 + 影响面 + 是否突增"把告警分成三级,这也是从一次告警疲劳里学来的——最初任何报错都推消息,几天后人就对红点免疫了:

  • P0 立即电话/短信:核心路径(表单提交、下单、登录)的 JS 错误,且 5 分钟内影响用户数超过阈值,或错误率较前一小时突增若干倍。
  • P1 工作时间处理:非核心页面报错、单个接口 5xx 升高、某类资源持续加载失败,推送到值班群即可。
  • P2 只记录不打扰:个别老旧浏览器的兼容报错、个例网络问题,进日报做趋势观察,不单独告警。

判断"突增"要用环比而不是固定阈值,因为官网流量白天晚上差很多。夜里两条报错可能就占了 100%,白天同样两条只是噪声,按错误率环比波动来告警才不会误报。

五、踩坑清单

  • 坑1:只监听 error 漏掉 Promise 异常。 现在大量代码走 async/await,Promise rejection 不会触发 window.onerror,必须单独监听 unhandledrejection,否则一大半异步错误是瞎的。
  • 坑2:资源错误在冒泡阶段抓不到。 script、img 加载失败不冒泡,addEventListener 第三个参数要传 true 走捕获阶段,否则一条资源错误都收不到。
  • 坑3:页面卸载时日志丢失。 普通 fetch 在 unload 时会被浏览器掐断,关键的"最后一跳"错误要用 sendBeacon 兜底。
  • 坑4:不做指纹聚合被日志淹没。 一次故障刷出几万条相同报错,告警直接炸群。先按"页面+信息+行号"聚合计数,再看趋势。
  • 坑5:把敏感信息打进日志。 早期接口日志里带了完整请求体,含手机号和 token,属于合规隐患。上报前必须做字段白名单和脱敏,只留排查需要的最小信息。

结语

前端可观测性的价值,是让团队比用户更早知道页面坏了。落地不用追求一步到位:先用原生钩子把四类信号采全,再用 sendBeacon 保证卸载不丢、用指纹聚合保证不被淹没,最后按影响面做三级告警,把人力留给真正影响转化的 P0。这套东西搭起来不复杂,但它补上的,正是"后端一片绿、用户却点不动"那个最危险的监控盲区。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 导读
  • 一、先分清:前端到底要采哪几类信号
  • 二、全局采集:用原生接口兜住绝大多数异常
  • 三、上报通道:为什么用 sendBeacon 而不是普通请求
  • 四、告警分级:别让所有错误都半夜响铃
  • 五、踩坑清单
  • 结语
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档