线上 JS 报错“TypeError: undefined is not a function”,我用了两天才找到原因

0 阅读12分钟

线上 JS 报错压缩后只剩一行,SourceMap 又没上传。本文记录如何从零搭建前端错误监控体系,配合 AI 反向定位源码,把此类问题的平均排查时间从数小时降到 10 分钟。

下载 (1).jpg

凌晨两点,用户群炸了

“支付页面白屏,点不了。” “我也是,刷新好多次了。” “安卓和 iPhone 都不行。”

我被运营的电话叫醒,迷迷糊糊打开电脑。生产环境没有报错监控,我只好登录服务器,在 Nginx 日志里找到一条请求记录,然后凭着直觉去翻前端代码。直到早上六点,我终于在压缩后的 bundle 里找到了一行代码:

// 压缩混淆后的代码,从线上 Chunk 中扒出来的
var t = n.data.paymentInfo; t.getAmount()

t 是个 undefined,调用 getAmount 报错。为什么是 undefined?不知道。后端接口返回的数据有时确实缺少这个字段,但理论上不应该。

最后发现,是当天下午后端发版,修改了支付接口的返回字段名,paymentInfo 改成了 payment_info。没有通知前端,没有更新接口文档,没有做兼容。前端代码里 3 个地方引用了旧字段,其中 2 个在发布前被检查出来,第 3 个藏在一个很少触发的优惠券计算逻辑里,只有在特定组合下才会执行。测试环境的数据刚好没触发这个组合。

从用户反馈到定位根因,花了将近 4 个小时。其中 3 个小时花在“猜原因”和“找代码”上。

这次之后,我花了一周把前端监控体系彻底搭了起来。现在的排查流程:告警 → 点击链接 → 看到源码级错误堆栈 + 用户操作回放 → 10 分钟内定位。这篇文章是完整的搭建记录。

第一步:梳理前端监控体系需要哪些能力

复盘那次事故,理想的排查体验应该满足:

  1. 报错自动收集:不用等用户反馈,系统自动感知线上错误。
  2. 源码映射:压缩后的报错能直接定位到原始文件的行号。
  3. 上下文信息:报错时用户的页面 URL、操作、设备、网络状态。
  4. 版本关联:能知道是哪个版本的代码引入的。
  5. 操作回放:能复现用户的操作路径(可选但很管用)。

我评估了三个方案:

方案优点缺点
Sentry (SaaS)开箱即用,功能齐全,免费额度够小团队用数据存储在第三方,有隐私顾虑
Sentry (自部署)数据可控,功能相同需要维护服务器和数据库
自研 (window.onerror + 上报)完全可控,无成本需要自己处理 SourceMap、聚合、通知、可视化

最终我选了自部署 Sentry。原因是:我们团队对数据隐私有要求,同时不想从零造轮子。Sentry 自部署版开源,功能完整,社区活跃,踩坑有人帮。

第二步:部署 Sentry 并接入前端项目

部署 Sentry (Docker 方式)

Sentry 官方提供了一键部署脚本。一台 2C4G 的轻量服务器就能跑。

# 克隆自部署仓库
git clone https://github.com/getsentry/self-hosted.git
cd self-hosted

# 安装(会拉取所有依赖镜像)
./install.sh

# 启动
docker compose up -d

启动后访问 http://your-server:9000,创建项目和团队,获取 DSN(Data Source Name)。

踩坑一install.sh 在执行时要求至少 4GB 内存,我的测试服务器只有 2GB,脚本直接退出。临时加了 swap 解决:

sudo fallocate -l 4G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

踩坑二:默认的 Kafka 和 ClickHouse 吃内存厉害。如果是小团队(<10 人),可以在 docker-compose.yml 中限制这些服务的资源,或者直接用 Sentry 的 SaaS 免费版。我最终把 Kafka 的堆内存限制从 1G 降到 512M。

前端接入 Sentry SDK

npm install @sentry/react

在入口文件初始化:

// src/index.tsx
import * as Sentry from '@sentry/react';
import { BrowserTracing } from '@sentry/tracing';

Sentry.init({
  dsn: 'https://xxx@sentry.example.com/1',
  integrations: [
    new BrowserTracing({
      tracePropagationTargets: ['localhost', 'api.example.com'],
    }),
  ],
  // 采样率:生产环境建议 0.1-0.5,避免海量上报
  tracesSampleRate: 0.1,
  // 只上报错误,不上报普通日志
  beforeSend(event) {
    if (event.level === 'error') return event;
    return null;
  },
});

接入后,Sentry 自动捕获未处理的异常和 Promise 拒绝。对于主动 try-catch 的业务错误,手动上报:

try {
  await submitOrder(data);
} catch (error) {
  Sentry.captureException(error, {
    tags: { module: 'order' },
    extra: { orderId: data.orderId },
  });
  // 继续业务降级逻辑
}

到这里,线上报错能自动收集了。但报错信息长这样:

TypeError: undefined is not an object (evaluating 't.getAmount')
  at n (chunk-abc123.js:1:2345)
  at onClick (chunk-abc123.js:1:5678)

还是没法定位源码。因为生产打包后的代码经过了压缩混淆,堆栈里的行号是 bundle 里的位置。

第三步:上传 SourceMap,让报错堆栈还原源码

这是整个流程中最关键也最容易出错的环节。

Webpack 配置生成 SourceMap

// webpack.prod.js
module.exports = {
  devtool: 'hidden-source-map', // 生成 map 但不给 bundle 添加引用,防止浏览器直接下载
  output: {
    sourceMapFilename: 'sourcemaps/[name].[contenthash].js.map',
  },
  plugins: [
    new SentryWebpackPlugin({
      org: 'my-org',
      project: 'my-app',
      authToken: process.env.SENTRY_AUTH_TOKEN,
      release: process.env.RELEASE_VERSION,
      include: './dist',
      ignore: ['node_modules'],
      urlPrefix: '~/', // 对应 CDN 路径前缀
      validate: true,  // 上传后验证 map 是否有效
    }),
  ],
};

关键点

  1. devtool: 'hidden-source-map':生产环境必须用这个,既能生成 SourceMap 又不会暴露源码。如果用 source-map,浏览器控制台可以直接看到原始代码,安全风险极大。
  2. release:每次构建的版本号必须唯一。我用了 git rev-parse --short HEAD + 日期。
  3. urlPrefix:如果 JS 文件部署在 CDN 上(如 https://cdn.example.com/static/js/main.123.js),这里要填 ~/ 或完整的 CDN 地址。Sentry 会拿着报错里的文件 URL 去匹配这个前缀。

CI 集成

# .github/workflows/deploy.yml
jobs:
  deploy:
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
        env:
          RELEASE_VERSION: ${{ github.sha }}-${{ github.run_number }}
          SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
      - run: npx sentry-cli releases new $RELEASE_VERSION
      - run: npx sentry-cli releases files $RELEASE_VERSION upload-sourcemaps ./dist
      - run: npx sentry-cli releases finalize $RELEASE_VERSION
      # 部署到 CDN...

踩坑三:SourceMap 文件必须和对应的 JS 文件同名匹配。我一开始用了 hidden-source-map 但忘了配置 sourceMapFilename,生成的 map 文件名和 JS 不匹配,Sentry 匹配不到。排查了半天才发现是名字里缺了 contenthash

踩坑四:忘记把 .map 文件从 CDN 部署目录中剔除。用 hidden-source-map 生成的 map 虽然在 bundle 中没有引用,但如果 map 文件也上传到了公开 CDN,用户可以通过 Chrome DevTools 直接加载。我写了一个简单的构建后脚本,在 CDN 上传前移动所有 .map 文件到一个私有目录,只传给 Sentry。

验证 SourceMap 是否生效

部署后,故意在代码里写一个 throw new Error('test') 触发上报。在 Sentry Issue 详情页看到堆栈:

TypeError: test
  at getAmount (src/utils/payment.ts:42:15)
  at calculateDiscount (src/components/DiscountCoupon.tsx:88:10)

源码文件名 + 行号都精确还原了。到这里,线上报错的定位速度已经上了一个台阶。

第四步:用 AI 辅助分析报错堆栈

Sentry 解决了收集和还原,但分析仍需人力。对于一个复杂的报错,可能涉及多个文件、多个函数调用,关联到后端接口返回数据。我试了把报错堆栈粘贴给 AI,发现它能大幅加速分析。

提示词模板

你是一位资深前端工程师。以下是一个生产环境的报错信息,包含完整堆栈和上下文数据。

## 错误信息
TypeError: Cannot read properties of undefined (reading 'getAmount')
  at getAmount (src/utils/payment.ts:42:15)
  at calculateDiscount (src/components/DiscountCoupon.tsx:88:10)
  at onClick (src/pages/Checkout.tsx:156:5)

## 用户操作上下文
- 页面: /checkout
- 设备: iPhone 15, iOS 18
- 网络: 4G
- 用户操作: 点击“使用优惠券”按钮

## 额外数据
- orderData: { total: 99.9, items: [...], discount: null }
  注意: paymentInfo 字段不存在

请分析:
1. 这个错误最可能的根本原因是什么?
2. 为什么 paymentInfo 可能缺失?
3. 给出最小修复方案(代码),包含防御性判断。
4. 如何避免类似问题?

AI 的输出通常很精准:

  • 根本原因:orderData 中缺少 paymentInfo 字段,getAmount 未做空值保护。
  • 为什么缺失:后端接口可能在某些订单状态下不返回 paymentInfo,或者字段名变更。
  • 修复方案:在 getAmount 函数入口加可选链 orderData?.paymentInfo?.getAmount?.() 或者在上层调用处保证 paymentInfo 存在。
  • 避免方法:前后端字段变更需要走接口契约测试;前端对后端返回数据做 schema 校验。

我把 AI 的分析直接贴到 Sentry Issue 的评论里,作为排查笔记。 后来团队其他人遇到类似报错时,先看评论就能快速了解历史处理方式。

第五步:配置告警规则,不用半夜被叫醒

有了错误收集和源码分析,下一个痛点是:怎么知道什么时候该看、什么时候可以睡?

Sentry 支持自定义告警规则。我设置了几个关键指标:

# Sentry 告警配置(Web UI 操作或用 Terraform 管理)
Alerts:
  - name: "关键流程错误激增"
    conditions:
      - event.type: error
      - tags.module: [payment, order, login]
      - count: > 50 in 1h
    actions:
      - slack: "#frontend-alerts"
      - pagerduty: critical

  - name: "新错误首次出现"
    conditions:
      - event.type: error
      - issue.age: < 1h
      - event.count: > 3
    actions:
      - slack: "#frontend-alerts"

  - name: "低频但不该出现的错误"
    conditions:
      - event.type: error
      - tags.severity: high
      - count: > 1
    actions:
      - email: frontend-team@example.com

核心原则

  • 高频且关键流程的错误(支付、订单)必须立刻通知。
  • 新错误头三次出现时通知,可能是新发布引入的。
  • 低频但不该发生的错误(如白屏、JS 加载失败)每次都要知道。

不要把所有的 console.error 都上报,否则告警噪音会让人麻木。我只上报了:

  • 未捕获的异常(onerror
  • 未处理的 Promise 拒绝(onunhandledrejection
  • 主动调用的 Sentry.captureException(用于关键业务 try-catch 内)

第六步:关联版本和 Git Commit

Sentry 支持将 release 和 Git commit 关联。这样当一个错误出现时,可以直接看到是哪个 commit 引入的,作者是谁。

# 在 CI 中关联 commit
npx sentry-cli releases set-commits $RELEASE_VERSION --auto

--auto 参数会自动读取当前 Git 仓库的 commit 信息,关联到 Sentry Release。

配置完后,Sentry Issue 详情页会显示:

Release: abc123-42
Commits: 3 (by @zhangsan)
First seen: 2026-05-26 14:32:15

点击 commit 直接跳转到 GitLab,看到代码 diff。这比手动去翻部署记录快太多了。

第七步:可选——用户操作回放

定位到错误后,如果还是难以复现,Sentry 的 Session Replay 功能可以回放用户的操作过程。这能解决“这个按钮只有用户 A 在特定步骤点击时才会报错”的谜题。

import { Replay } from '@sentry/replay';

Sentry.init({
  integrations: [
    new Replay({
      maskAllText: true,      // 脱敏用户输入
      blockAllMedia: true,    // 不录制媒体内容
      maskAllInputs: true,    // 脱敏表单输入
    }),
  ],
  replaysSessionSampleRate: 0.01,  // 1% 的会话录制回放
  replaysOnErrorSampleRate: 1.0,   // 报错时 100% 录制
});

隐私合规:必须开启脱敏选项,并在隐私政策中告知用户。我们法务审核后要求默认关闭,只在用户同意的情况下开启,这是一个权衡。

这套体系的最终效果

接入三个月后,数据如下:

指标之前之后
错误发现时间用户反馈(平均 2 小时)自动告警(<5 分钟)
错误定位时间平均 2 小时(翻源码、查日志)平均 10 分钟(堆栈 + 上下文 + AI 分析)
未复现错误比例约 40%<5%(有操作回放)
同类型错误重复率高(修复后无追踪)低(Release 对比)

最让我满意的不是数字,而是一种“掌控感”。以前面对用户反馈的“页面点不了”,我总是心里没底——不知道是偶发还是批量,不知道影响多少人,不知道什么时候开始。现在打开 Sentry Dashboard,所有信息一目了然。

踩过的坑(完整版)

  1. SourceMap 公开泄露:必须用 hidden-source-map,并把 .map 文件移到非公开路径。验证方法:部署后访问 https://cdn.example.com/static/js/main.xxx.js.map,应该返回 404。

  2. Sentry 自部署的内存占用:Kafka + ClickHouse 是小内存服务器的噩梦。如果团队 <5 人,直接用 Sentry SaaS 免费版更省心(每月 5000 errors 免费)。

  3. 跨域脚本错误:CDN 上的 JS 如果报了错,浏览器会因为跨域限制只给 Script error,没有堆栈。必须给 script 标签加 crossorigin="anonymous",CDN 侧配 Access-Control-Allow-Origin

  4. 不发送重复错误:如果同一个错误在循环里触发,可能瞬间发几十万条,打爆 Sentry 额度。在初始化时设置 beforeSend 过滤,或者用 Sentry 的去重逻辑(默认基于堆栈聚合)。

  5. 性能影响:Sentry SDK 体积约 20KB gzip,对性能影响很小。但 Session Replay 会录屏,对低端机有性能开销,采样率不要设太高。

快速搭建检查清单

## 前端错误监控搭建清单

### 基础接入
- [ ] 部署 Sentry(自部署或 SaaS)
- [ ] 前端接入 @sentry/react SDK
- [ ] 配置 error / unhandledrejection 自动捕获
- [ ] 关键业务逻辑手动 Sentry.captureException

### SourceMap
- [ ] Webpack/Vite 配置 hidden-source-map
- [ ] CI 中上传 .map 文件到 Sentry
- [ ] 验证线上报错是否显示源码堆栈
- [ ] 确认 .map 文件未公开

### 告警
- [ ] 关键流程错误激增 → 即时通知
- [ ] 新错误首次出现 → 即时通知
- [ ] 低频高危错误 → 邮件通知
- [ ] 告警测试,确认能收到

### 版本关联
- [ ] 每次构建生成唯一 release 号
- [ ] 关联 Git commit
- [ ] Release 页面验证 commit 列表

### 隐私合规
- [ ] 脱敏用户数据
- [ ] 隐私政策告知
- [ ] 用户可关闭错误上报

最后

那天的凌晨四点,我对着压缩后的一行 JS 发呆的感觉,现在还记得很清楚。错误的排查成本完全取决于你投入了多少监控基础设施。省掉监控的时间,迟早会在排查时加倍还回来。

如果你现在的项目还没有前端错误监控,从 Sentry 免费版开始,花一个小时接入,下一个线上报错你就能在几分钟内找到原因。


你的项目用的是什么错误监控方案?有没有遇到过特别离谱的线上问题?欢迎评论区聊聊。