我是怎么在家用前端监控工具救了线上项目的命

专业之先知
2025-12-21 00:43
阅读 672

去年双11前两周,我正窝在出租屋的床上,一手握着泡面桶,一手在 VSCode 里疯狂敲代码。作为刚毕业三个月的大专生,能靠自学前端进到这家远程办公的小公司,说实话我自己都还有点不敢信。入职没多久就赶上大促,压力直接拉满——而就在这节骨眼上,用户反馈页面白屏、按钮点不动、订单提交失败的 bug 突然暴增。

更离谱的是,本地和测试环境一切正常,一上线就“薛定谔式崩溃”。我和后端兄弟对骂了三天,连运维都甩锅说“可能是 CDN 缓存问题”,直到我偶然在控制台瞥见一行报错:

Uncaught TypeError: Cannot read property 'map' of undefined

但问题是……这个错误压根没上报!我们项目里连个像样的错误监控都没有。

那一刻我真想砸了这台用了五年的 ThinkPad(虽然它已经快散架了)。也是从那天起,我下定决心:必须给项目加上前端监控工具。不然下次再出事,产品经理怕是要把我钉在需求墙上。


面试题里总考,工作中却没人提?

其实早在面试时,我就被问过:“你们项目怎么处理前端错误上报?”当时支支吾吾说了个 window.onerror,面试官笑了笑没再追问——我知道,他看出来我根本没实战经验。

现在回头看,很多初级前端(包括我)以为监控就是加个 Sentry 就完事了。但现实是:工具只是手段,关键是怎么用、怎么集成、怎么让数据真正有用

我们公司用的是 Vue3 + Vite + TypeScript 技术栈,团队只有三个前端(包括我),老板对“可观测性”这种词嗤之以鼻,只关心“能不能少出 bug”。所以我的任务很明确:选一个轻量、易集成、成本低、能快速见效的监控方案


工具选型:别被大厂方案吓到

一开始我想上 Sentry,毕竟名气大、文档全。但一看到要自己搭服务端(或者每月几百刀的 SaaS 费用),直接劝退。我们这种小团队,连专门的 DevOps 都没有,怎么可能维护一个监控后台?

于是我开始对比市面上的开源/免费方案:

工具 是否需要自建服务 免费额度 集成难度 自定义能力
Sentry 是(或付费 SaaS) 有限 中等
Fundebug 否(SaaS) 5k 次/月
Badjs(腾讯开源) 无限制
自研简易上报 0 成本 弱(初期)

考虑到时间紧、人手少、预算为零,我决定先走“自研简易上报 + 第三方日志平台”的混合路线。核心思路是:先收集,再分析,最后告警

我把目标拆解成三步:

  1. 捕获 JS 错误、资源加载失败、Promise reject
  2. 上报到后端(或第三方)
  3. 能查、能看、能告警

动手干:50 行代码搞定基础监控

别被“监控系统”这个词吓到。其实核心逻辑就那么几行。我在项目里新建了个 monitor.js,用 Vue 的插件形式注入:

// monitor.ts
const MONITOR_URL = 'https://log.mycompany.com/frontend-error';

interface ErrorLog {
  message: string;
  stack?: string;
  url: string;
  userAgent: string;
  timestamp: number;
  type: 'js' | 'resource' | 'promise';
}

function sendLog(log: ErrorLog) {
  // 使用 navigator.sendBeacon 保证页面卸载时也能上报
  if (navigator.sendBeacon) {
    navigator.sendBeacon(MONITOR_URL, JSON.stringify(log));
  } else {
    // fallback: image 打点
    const img = new Image();
    img.src = `${MONITOR_URL}?data=${encodeURIComponent(JSON.stringify(log))}`;
  }
}

export default {
  install(app: any) {
    // 1. 捕获全局 JS 错误
    window.addEventListener('error', (e) => {
      sendLog({
        message: e.message,
        stack: e.error?.stack || '',
        url: location.href,
        userAgent: navigator.userAgent,
        timestamp: Date.now(),
        type: 'js'
      });
    });

    // 2. 捕获未处理的 Promise rejection
    window.addEventListener('unhandledrejection', (e) => {
      sendLog({
        message: e.reason?.message || String(e.reason),
        stack: e.reason?.stack || '',
        url: location.href,
        userAgent: navigator.userAgent,
        timestamp: Date.now(),
        type: 'promise'
      });
      e.preventDefault(); // 防止控制台红字(其实没用,但显得专业)
    });

    // 3. 捕获资源加载失败(图片、脚本等)
    window.addEventListener('error', (e) => {
      const target = e.target as HTMLElement;
      if (target.tagName === 'IMG' || target.tagName === 'SCRIPT') {
        sendLog({
          message: `Resource load failed: ${target.src || target.getAttribute('href')}`,
          url: location.href,
          userAgent: navigator.userAgent,
          timestamp: Date.now(),
          type: 'resource',
          stack: ''
        });
      }
    }, true); // useCapture: true,确保在冒泡前捕获
  }
};

然后在 main.ts 里注册:

import Monitor from './utils/monitor';
app.use(Monitor);

搞定!前后不到一小时。虽然简陋,但至少能把错误“吐”出来。


但光有上报不够,得让人看得懂

第一次上线后,我在后端写了个简单的 Node.js 接口接收日志,存到 MongoDB。结果第二天打开一看,全是这种:

message: "Cannot read property 'name' of undefined"
stack: "at eval (order.vue:45)"

这玩意儿谁看得懂?第 45 行?可我们的代码是打包压缩过的啊!

问题来了:生产环境的 sourcemap 不公开,怎么还原错误位置?

这时候我才意识到,真正的难点不是“上报”,而是“可读性”。

解决方案有两个:

  1. 在构建时生成 sourcemap,并上传到监控平台
  2. 在本地用 sourcemap 反解错误堆栈

我们选了后者——因为不想把 sourcemap 暴露给外网(安全风险)。于是我在本地写了个小脚本,用 source-map 库自动解析:

// decode-stack.js
const fs = require('fs');
const { SourceMapConsumer } = require('source-map');

async function decodeStack(stack, mapPath) {
  const rawSourceMap = JSON.parse(fs.readFileSync(mapPath, 'utf8'));
  const consumer = await new SourceMapConsumer(rawSourceMap);

  return stack.split('\n').map(line => {
    const match = line.match(/at (\S+) \((.+):(\d+):(\d+)\)/);
    if (match) {
      const [, func, file, line, col] = match;
      const pos = consumer.originalPositionFor({
        line: parseInt(line),
        column: parseInt(col)
      });
      if (pos.source) {
        return `at ${func} (${pos.source}:${pos.line}:${pos.column})`;
      }
    }
    return line;
  }).join('\n');
}

虽然每次都要手动跑脚本有点麻烦,但至少能定位到具体组件和行号了。后来我们甚至把这个脚本集成到了内部管理后台,输入错误 ID 就能自动反解——产品经理都惊了:“你这 bug 定位速度比我提需求还快?”


让监控“活”起来:从被动接收到主动预警

有一次凌晨三点,用户大量投诉支付失败。我睡得正香,手机突然震动——是企业微信机器人发来的告警:

【前端监控告警】
过去5分钟内,TypeError: Cannot read property 'status' of null 错误超过 100 次
影响页面:/checkout
相关 commit:a1b2c3d

我一个鲤鱼打挺坐起来,打开电脑,十分钟后定位到是后端接口返回了 null 而不是 { status: ... }。临时加了个空值判断,热更新上线,搞定。

这个告警是怎么来的?其实就是后端加了个简单的聚合规则:

  • 每分钟统计各错误类型的出现次数
  • 如果某错误在 5 分钟内超过阈值(比如 50 次),就触发 webhook
  • webhook 调用企业微信机器人 API 发消息

虽然糙,但救命。


动画交互爱好者的额外彩蛋

作为一个对前端动画和交互特别感兴趣的码农,我还偷偷加了个“用户体验监控”功能:记录用户点击无效区域的次数

比如,某个按钮点了没反应(可能因为状态未加载完成),用户狂点十次。这种行为本身说明交互有问题。我在全局加了个 click 监听:

let lastClickTime = 0;
document.addEventListener('click', (e) => {
  const now = Date.now();
  if (now - lastClickTime < 300) {
    // 300ms 内重复点击,可能是在“狂点”
    sendLog({
      message: 'Rapid click detected',
      detail: { target: e.target.tagName, class: e.target.className },
      type: 'ux',
      // ...
    });
  }
  lastClickTime = now;
});

后来发现,首页“立即抢购”按钮在活动开始前经常被狂点——于是产品加了个倒计时 loading 态,用户满意度直线上升。老板居然夸我“有产品思维”,笑死。


踩过的坑,都是未来的简历亮点

当然,过程没那么顺利。比如:

  • 重复上报:同一个错误,用户刷新十次,上报十次。解决办法:加指纹去重(用错误信息 + 行号 + 页面 URL 做 hash)
  • 性能影响:频繁上报拖慢页面。改用 sendBeacon + 批量发送(每 30 秒发一次队列)
  • 隐私合规:不能随便传用户信息。我们把所有敏感字段(如 token、手机号)过滤掉,只保留技术上下文

最惨的一次是我把监控日志打到了生产数据库,结果半夜 DB CPU 100%,运维差点把我拉黑。从此以后,所有日志都走单独的日志服务,和业务库隔离。


现在回头看:监控不是奢侈品,是必需品

很多人觉得,小项目没必要搞监控。但我想说:越是小团队,越需要监控。因为你没有人力去一个个复现用户反馈的 bug。

现在的我,每次接手新项目,第一件事就是问:“有没有前端监控?”如果没有,我会花半天时间把基础监控搭起来——就当是给未来的自己买份保险。

上周五晚上,我又收到了一条告警,但这次只是个小 warning。我喝了口冰可乐,改了两行代码,提交,睡觉。那种“一切尽在掌握”的感觉,真的爽。

如果你也在自学前端,或者刚入行不久,别被“高大上”的架构吓住。从一个 window.onerror 开始,慢慢迭代,你也能搭建出属于自己的监控体系。

毕竟,能在线上救火的前端,才是真·高级前端(虽然我工资还没涨)。


后记:最近在准备跳槽,面试官又问监控相关的问题。这次我不仅能说出原理,还能掏出自己项目的监控截图和优化数据。他说:“你这经验,比很多三年经验的都扎实。”
——你看,当初那个被 bug 逼疯的大专生,终于也能笑着讲“事故”了。

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝