日志(二):为什么程序员痴迷于错误信息上报?

5,739 阅读6分钟

前言

上一篇已经聊过日志上报的调度原理,讲述如何处理日志上报堆积、上报失败以及上报优化等方案。 最新的交互日志已发布,新鲜热乎!

从上家公司开始,监控就由我们组身强体壮的同事来负责,而我只能开发AdminH5;经过一系列焦虑的面试后,咸鱼翻身,这辈子我也做上监控了。千万不要以为我是因为监控的重要性才这么执着,人往往得不到的东西才是最有吸引力的。

在写这篇文章时,我也在思考,为什么走到哪里都会有一群程序员喜欢封装监控呢?即使换个公司、换个组,依然可能需要有人来迭代监控。嗯,话不多说,先点关注,正文开始

错误监控的核心价值

如果让你封装一个前端监控,你会怎么设计监控的上报优先级?

对于一个网页来说,能否带给用户好的体验,核心指标就是 白屏时长FMP时长,这两项指标直接影响用户的 留存率体验

下面通过数据加强理解:

  • 白屏时间 > 3秒 导致用户流失率上升47%
  • 接口错误率 > 0.5%  造成订单转化率下降23%
  • JS错误数 > 1/千次访问 预示着系统稳定性风险

设想一下,当你访问页面时白屏等待了3秒,并且页面没有骨架屏或者Loading态时,你会不会觉得这个页面挂了?这时候如果我们的监控优先关注的是性能,可能用户已经退出了,我们的上报还没调用到。

在这个白屏等待的过程中,JS Error可能已经打印在控制台了,接口可能已经返回了错误信息,但是程序员却毫无感知。

优先上报错误信息,本质是为了提升生产环境的错误响应速度、减少生产环境的损失、提高上线流程的规范。以下是错误响应的黄金时间轴:

时间窗口响应动作业务影响
< 1分钟自动熔断异常接口避免错误扩散
1-5分钟触发告警通知值班人员降低MTTR(平均修复时间)
>5分钟生成故障诊断报告优化事后复盘流程

重要章节

一:错误类型,你需要关注的五大场景

技术本质:任何错误收集系统都需要先明确错误边界。前端错误主要分为两类: 显性错误(直接阻断执行)和 隐性错误(资源加载、异步异常等)。

// 显性错误(同步执行阶段)
function criticalFunction() {
  undefinedVariable.access(); // ReferenceError
}

// 隐性错误(异步场景)
fetchData().then(() => {
  invalidJSON.parse(); // 异步代码中的错误
});

关键分类: 通过错误本质将前端常见错误分为5种类型,图示如下。 image.png

  1. 语法层错误(SyntaxError)
    ESLint 可拦截,但运行时需注意动态语法(如 eval,这个用法不推荐)。

  2. 运行时异常
    错误的时机场景大部分是在页面渲染完成后,用户对页面发生交互行为,触发JS执行异常。以下是模拟报错的一个例子,用于学习。 // 典型场景 element.addEventListener('click', () => { throw new Error('Event handler crash'); });

  3. 资源加载失败
    常见的资源比如图片、JS脚本、字体文件、外链引入的三方依赖等。我们可以通过全局监听处理,比如使用document.addEventListener('error', handler, true)来捕获资源加载失败的情况。但需要注意以下几点:

    1. <img><script> 等标签的 onerror ,可能会因为事件冒泡机制导致重复收集,比如:
    <!-- 局部处理 -->
    <img src="invalid.jpg" onerror="handleImageError()">
    
    <script>
    // 全局监听
    document.addEventListener('error', (e) => {
      reportError(e.target); // 全局上报
    }, true);
    </script>
    

    问题

    • 局部 onerror 和全局监听会同时触发,导致重复上报。

    • 若局部事件中调用了 e.stopPropagation(),全局监听将无法捕获事件。

    1. 动态资源的监听失效,通过 JavaScript 动态创建 的资源元素(如 <img><script>),若未显式绑定 onerror,可能无法被全局监听捕获。
    // 动态加载图片
    const img = new Image();
    img.src = 'dynamic.jpg';
    document.body.appendChild(img);
    
    // 全局监听可能无法捕获动态资源的错误(取决于浏览器实现)
    

    原因

    • 部分浏览器对动态创建元素的错误事件冒泡支持不一致。
    • 若资源加载失败时未触发冒泡,全局监听会失效。
  4. Promise 穿透
    这个问题大部分都是编程不规范导致的,常见写法如:

    // 1. 没有使用.catch捕获Promise的异常情况
    // 2. Promise
    new Promise(resolve, reject).then(res => {
    })
    

    未处理的 rejection 需要通过事件监听来捕获:

    // 现代浏览器已默认报告未处理的 rejection
    window.addEventListener('unhandledrejection', e => {
      e.preventDefault(); // 阻止默认打印
      report(e.reason);
    });
    
  5. 框架特异性错误
    Vue/React 等框架的错误边界方案:

    // Vue 3 组合式 API 错误处理
    app.config.errorHandler = (err, instance, info) => {
      sendError({ ...err, component: instance?.$options.name });
    }
    

二:错误数据,不只是 stack trace

信息设计哲学:错误信息需要包含足够上下文,但需避免信息冗余。首先我们可以分析一下异常事件里有哪些核心字段, 比如常见的JS Error,以下图为例:

interface ErrorReport {
  // 错误标识
  fingerprint: string; // 通过 message + stack 生成 hash
  type: 'JS_ERROR' | 'RESOURCE' | 'PROMISE' | 'CUSTOM';

  // 核心信息
  message: string;
  stack?: string;  // 注意 iOS 设备上的 stack 差异
  component?: string; // Vue/React 组件名

  // 环境信息
  meta: {
    userAgent: string;
    url: string;
    timestamp: number;
    sdkVersion: string;
  };

  // 自定义扩展
  extras?: Record<string, any>; // 业务自定义字段
}

序列化技巧:处理不可序列化数据(如循环引用)

function safeStringify(obj) {
  const seen = new WeakSet();
  return JSON.stringify(obj, (k, v) => {
    if (typeof v === 'object' && v !== null) {
      if (seen.has(v)) return '[Circular]';
      seen.add(v);
    }
    return v;
  });
}

三:错误拦截,多维度防御体系

H5 的三层捕获
  1. 全局捕获(最后防线)

    window.onerror = (msg, source, lineno, colno, error) => {
      report({
        message: error?.message ?? msg,
        stack: error?.stack || `${source}:${lineno}:${colno}`
      });
      return true; // 阻止默认控制台打印
    };
    
  2. 资源监听(需注意事件捕获阶段)

    document.addEventListener('error', e => {
      if (e.target.tagName === 'IMG') {
        trackResourceError(e.target.src);
      }
    }, true); // 使用捕获阶段确保触发
    
  3. 代码层 Try/Catch
    建议在关键业务逻辑手动包裹:

    function wrappedFetch(url) {
      try {
        return fetch(url).catch(handleFetchError);
      } catch (e) {
        handleSyncError(e);
      }
    }
    
小程序的双端策略
  1. 全局错误捕获
// 1. APP级捕获
App({
  onError(error) {
    wx.request({
      url: 'https://log.example.com',
      data: { error: error.message }
    });
  }
});

// 2. 页面级错误(Page.onError)
Page({
  onError(msg) {
    trackPageError(this.route, msg);
  }
});

2. API错误处理 这种拦截的方式处理小程序原生API,避免了更改业务逻辑,在独立封装成一个sdk时更具备通用性。

const originRequest = wx.request;
wx.request = function(config) {
  const { fail } = config;
  config.fail = function(err) {
    reportApiError(err);
    fail?.call(this, err);
  };
  return originRequest(config);
};

总结

一起回顾错误监控的四大核心价值:

  1. 生产环境感知器
    通过错误率、白屏时长等硬指标,量化用户体验和系统健康度,使不可见的代码问题转化为可观测的数据流。

  2. 故障止损黄金通道
    建立错误信息→告警响应→热修复的快速闭环,将MTTR(平均修复时间)从小时级压缩至分钟级,如:

    错误上报触发自动熔断机制,10秒内阻断错误扩散

  3. 技术演进指南针
    高频错误类型(如资源加载失败率上升)驱动架构优化,推动从「事件驱动开发」到「数据驱动迭代」的范式转变。

  4. 团队协作润滑剂
    标准化的错误元数据(组件栈、用户轨迹)打破前后端协作壁垒,使问题定位效率提升60%以上。

当错误监控从「防御工具」进化为「业务洞察系统」时,程序员对错误上报的执着,本质上是对「确定性」的技术追求——在混沌的软件世界中建立秩序,在脆弱的数字生态里守护体验。这或许正是工程师文化最诗意的表达:用严谨的逻辑对抗无常,以持续观测抵达信任。

我是11,一个热爱技术的前端开发,关注我,一起成长!