发布于

使用 Sentry 做前端异常监控 - 被劫持的浏览器 API

作者
  • avatar
    姓名
    Terry
    Twitter

引子:CRM 客户列表页的"静默失败"

那年刚接手一个 CRM 系统,客服团队经常收到用户投诉:"客户列表加载不出来,刷新几次才好"。我在本地复现了很久都没问题,直到有一天生产环境报了一堆同样的错误——控制台里只有一个孤零零的 "Uncaught (in promise)",连错误类型都没有。

打开 Network 面板,发现 /api/customers 接口返回了 401,但前端代码里这个 Promise 没有 .catch(),错误就这么默默地被浏览器吞掉了。用户看到的是空白列表,控制台里只有一行红色警告,服务器日志里什么都没有。

这种"静默失败"最要命——因为太安静了,你根本不知道它在发生。要解决这个问题,我决定接入 Sentry。接入之后的第一天,就捕获了 237 个不同类型的异常,其中 70% 都是 Promise 未捕获错误。

今天就把这次接入的经验分享一下,聊聊 Sentry 是如何通过劫持浏览器 API,把那些"安静"的异常变成"响亮"的告警。

前端异常的类型学

在理解 Sentry 之前,我们得先搞清楚浏览器里都有哪些异常。JavaScript 的异常种类堪比动物园,每种都有自己的脾气:

异常类型典型场景捕获方式能被 window.onerror 捕获吗
代码执行异常TypeError, ReferenceError, RangeErrortry...catch 或全局 error 事件✅ 可以
Promise 未捕获Promise.reject() 没有 .catch()unhandledrejection 事件❌ 不行
静态资源加载失败图片、CSS、JS 404error 事件(捕获阶段)❌ 不行
网络请求错误XHR/fetch 失败XHR 的 onerror、fetch 的 .catch()❌ 不行
跨域脚本异常第三方脚本报错error 事件,但只有 "Script error"✅ 能捕获,但没有详情

最坑的就是 Promise 未捕获异常。像我们 CRM 的那个场景:

// 问题代码
async function loadCustomers() {
  const response = await fetch('/api/customers')
  const customers = await response.json()
  renderCustomerList(customers)
}

// 调用方没有 catch
loadCustomers() // 如果 fetch 失败,这里会静默失败

/api/customers 返回 401 或 500 时,fetch 会抛出异常,但因为调用方没有 .catch(),这个异常就被浏览器丢弃了。用户只能看到"加载失败"的 UI,而开发者什么都不知道。

Sentry 的核心手法:API 劫持

Sentry 做异常监控的原理,用一句话概括:把能劫持的浏览器 API 全部劫持一遍

第一步:劫持全局错误入口

最基础的两招是 window.onerrorwindow.onunhandledrejection

// 保存原始处理器
const oldErrorHandler = window.onerror

// 覆写全局错误处理器
window.onerror = function (msg, url, line, column, error) {
  // 收集异常信息并上报
  triggerHandlers('error', {
    column,
    error,
    line,
    msg,
    url,
  })

  // 调用原始处理器,不破坏原有逻辑
  if (oldErrorHandler) {
    return oldErrorHandler.apply(this, arguments)
  }
  return false
}

// 同样处理 Promise 异常
const oldRejectionHandler = window.onunhandledrejection
window.onunhandledrejection = function (e) {
  triggerHandlers('unhandledrejection', e)

  if (oldRejectionHandler) {
    return oldRejectionHandler.apply(this, arguments)
  }
  return true
}

这个代码看起来简单,但有两个关键点:

  1. 先上报再回调:保证自己的监控逻辑一定执行
  2. 保留原始处理器:不破坏应用可能已经注册的其他错误处理逻辑

在 CRM 系统里接入这个后,我们立马就看到了那个 /api/customers 的 401 错误,错误信息里还有完整的 Promise rejection reason。

第二步:劫持定时器和事件回调

光靠全局错误处理器还不够,因为错误发生时的上下文信息会丢失。比如一个按钮点击事件里抛出异常,你只能看到 "ReferenceError: xxx is not defined",但不知道是哪个按钮触发的。

Sentry 的解法是劫持这些 API,在回调外面包一层 try...catch

劫持 setTimeout / setInterval

const originSetTimeout = window.setTimeout

window.setTimeout = function (callback, delay, ...args) {
  const wrapped = wrap$1(callback, {
    mechanism: {
      data: {
        function: getFunctionName(callback),
      },
      handled: true,
      type: 'setTimeout', // 标记这是定时器回调里的异常
    },
  })

  return originSetTimeout.apply(this, [wrapped, delay, ...args])
}

当回调执行出错时,Sentry 知道这个异常发生在 setTimeout 上下文里,还能记录回调函数的名字。

劫持 addEventListener

DOM 节点的 addEventListener 继承自 Node.prototype,Sentry 直接覆写了这个原型方法:

const proto = window.Node.prototype

fill(proto, 'addEventListener', function (original) {
  return function (eventName, fn, options) {
    // 如果是 handleEvent 方式
    if (typeof fn.handleEvent === 'function') {
      fn.handleEvent = wrap$1(fn.handleEvent.bind(fn), {
        mechanism: {
          data: {
            function: 'handleEvent',
            handler: getFunctionName(fn),
            target: this,
          },
          handled: true,
          type: 'instrument',
        },
      })
    }

    // 普通回调函数包装
    return original.call(this, [
      eventName,
      wrap$1(fn, {
        mechanism: {
          data: {
            function: 'addEventListener',
            handler: getFunctionName(fn),
            target: this,
          },
          handled: true,
          type: 'instrument',
        },
      }),
      options,
    ])
  }
})

这样当一个点击事件里的代码出错时,Sentry 不光能捕获异常,还能上报事件名称、目标节点信息、处理函数名等上下文。

在 CRM 里,我们经常会遇到用户点击"删除客户"按钮时出错,有了这个机制,我们能立即知道是哪个按钮、在哪个页面、由哪个处理函数引发的异常。

第三步:劫持网络请求

XHR 的劫持更复杂,因为它的错误分散在多个生命周期方法里。Sentry 的做法是劫持 XMLHttpRequest.prototype.send,在调用时再劫持实例的 onloadonerroronprogressonreadystatechange

function _wrapXHR(originalSend) {
  return function () {
    const xhr = this
    const xmlHttpRequestProps = ['onload', 'onerror', 'onprogress', 'onreadystatechange']

    // 劫持各个事件处理器
    xmlHttpRequestProps.forEach(function (prop) {
      if (prop in xhr && typeof xhr[prop] === 'function') {
        fill(xhr, prop, function (original) {
          return wrap$1(original, {
            mechanism: {
              data: {
                function: prop,
                handler: getFunctionName(original),
              },
              handled: true,
              type: 'instrument',
            },
          })
        })
      }
    })

    return originalSend.apply(xhr, arguments)
  }
}

// 覆写 XMLHttpRequest.prototype.send
fill(XMLHttpRequest.prototype, 'send', _wrapXHR)

Fetch 的劫持类似,但更简单,因为它是 Promise-based 的:

const originFetch = window.fetch

window.fetch = function (...args) {
  const handlerData = {
    args,
    fetchData: {
      method: getFetchMethod(args),
      url: getFetchUrl(args),
    },
    startTimestamp: Date.now(),
  }

  triggerHandlers('fetch', handlerData)

  return originFetch.apply(window, args).then(
    function (response) {
      triggerHandlers('fetch', {
        ...handlerData,
        endTimestamp: Date.now(),
        response,
      })
      return response
    },
    function (error) {
      triggerHandlers('fetch', {
        ...handlerData,
        endTimestamp: Date.now(),
        error,
      })
      throw error
    }
  )
}

Sentry 的实际接入:从零到一

安装和初始化

在 CRM 项目里,我们用的是 React,所以安装了 Sentry 的 React SDK:

npm install @sentry/react

然后在 src/index.tsxsrc/main.tsx 里初始化:

import * as Sentry from '@sentry/react'
import { BrowserTracing } from '@sentry/tracing'

Sentry.init({
  dsn: 'https://xxx@sentry.io/xxx', // 从 Sentry 控制台获取
  integrations: [
    new BrowserTracing({
      tracePropagationTargets: ['localhost', /^https:\/\/yourdomain\.com/],
    }),
  ],

  // 性能监控采样率(0-1,1 表示 100%)
  tracesSampleRate: 0.1,

  // 错误上报采样率(0-1,1 表示 100%)
  replaysSessionSampleRate: 0.1,
  replaysOnErrorSampleRate: 1.0,

  // 环境信息
  environment: process.env.NODE_ENV,
  release: 'crm-app-v1.2.0',
})

React Error Boundary

对于 React 组件里的错误,Sentry 提供了 Sentry.ErrorBoundary 组件:

import * as Sentry from '@sentry/react'

function CustomerList() {
  return (
    <Sentry.ErrorBoundary fallback={ErrorFallback}>
      <CustomerDataTable />
    </Sentry.ErrorBoundary>
  )
}

function ErrorFallback({ error, resetError }) {
  return (
    <div className="error-container">
      <h2>加载失败</h2>
      <p>{error.message}</p>
      <button onClick={resetError}>重试</button>
    </div>
  )
}

CustomerDataTable 组件内部抛出异常时,这个错误会被自动上报到 Sentry,并展示 ErrorFallback 组件。

手动上报异常

对于业务逻辑里的异常,也可以手动上报:

import * as Sentry from '@sentry/react'

try {
  const response = await fetch('/api/customers')
  if (!response.ok) {
    throw new Error(`请求失败: ${response.status}`)
  }
  const customers = await response.json()
} catch (error) {
  // 手动上报异常
  Sentry.captureException(error)

  // 添加额外的上下文
  Sentry.setContext('customer_load', {
    page: 'customer_list',
    userId: currentUser.id,
  })

  // 展示错误 UI
  showErrorNotification('加载客户列表失败')
}

敏感信息脱敏

CRM 系统里经常有敏感信息(客户电话、邮箱等),在上报前需要过滤:

Sentry.init({
  dsn: 'your-dsn',
  beforeSend(event, hint) {
    // 过滤掉包含密码的错误
    if (event.message && event.message.includes('password')) {
      return null
    }

    // 过滤请求体中的敏感字段
    if (event.request) {
      event.request.data = filterSensitiveData(event.request.data)
    }

    return event
  },
})

function filterSensitiveData(data) {
  const sensitiveFields = ['password', 'phone', 'email', 'token']

  if (!data || typeof data !== 'object') {
    return data
  }

  const filtered = { ...data }
  for (const field of sensitiveFields) {
    if (field in filtered) {
      filtered[field] = '***REDACTED***'
    }
  }

  return filtered
}

用户行为:从旁观者变成目击者

Sentry 的另一个"黑科技"是用户行为追踪。光有异常堆栈还不够,如果能知道"用户在报错前做了什么",排查效率能提升 10 倍。

页面跳转行为

为了记录用户跳转路径,Sentry 劫持了 history.pushStatehistory.replaceStatewindow.onpopstate

const originPushState = window.history.pushState
const originReplaceState = window.history.replaceState
let lastHref = window.location.href

window.history.pushState = function (...args) {
  const url = args.length > 2 ? args[2] : undefined
  if (url) {
    const to = String(url)
    triggerHandlers('history', {
      from: lastHref,
      to,
    })
    lastHref = to
  }
  return originPushState.apply(this, args)
}

window.onpopstate = function () {
  const to = window.location.href
  triggerHandlers('history', {
    from: lastHref,
    to,
  })
  lastHref = to
  if (oldOnPopState) {
    return oldOnPopState.apply(this, arguments)
  }
}

这样 Sentry 就能重建用户的访问路径:登录页 → 客户列表 → 客户详情 → 编辑表单。如果某个页面报错率特别高,结合路径数据就能快速定位问题入口。

在 CRM 系统里,我们通过这个功能发现:80% 的错误都发生在"客户详情 → 编辑表单"这个路径上,原来是因为某些客户数据格式异常,导致表单初始化失败。

鼠标和键盘行为

Sentry 采用了"双保险"策略来监控点击和键盘事件:

  1. 事件代理:在 document 上监听 clickkeypress
  2. API 劫持:覆写 Node.prototype.addEventListener,额外注册一个监听器
function instrumentDOM() {
  const triggerDOMHandler = triggerHandlers.bind(null, 'dom')
  const globalDOMEventHandler = makeDOMEventHandler(triggerDOMHandler, true)

  // 方案 1:事件代理
  document.addEventListener('click', globalDOMEventHandler, false)
  document.addEventListener('keypress', globalDOMEventHandler, false)

  // 方案 2:劫持 addEventListener
  fill(Node.prototype, 'addEventListener', function (original) {
    return function (type, listener, options) {
      // 如果是 click 或 keypress 事件
      if (type === 'click' || type === 'keypress') {
        const handlers = (this.__sentry_instrumentation_handlers__ =
          this.__sentry_instrumentation_handlers__ || {})

        if (!handlers[type]) {
          handlers[type] = {
            refCount: 0,
            handler: makeDOMEventHandler(triggerDOMHandler),
          }

          // 额外注册一个监听器用于追踪
          original.call(this, type, handlers[type].handler, options)
        }

        handlers[type].refCount += 1
      }

      // 正常注册用户提供的监听器
      return original.call(this, type, listener, options)
    }
  })
}

这个设计很聪明。事件代理能捕获大部分点击,但如果用户在事件回调里调用了 event.stopPropagation(),代理监听器就收不到事件了。这时 API 劫持就派上用场——因为劫持发生在代理之前,所以不受 stopPropagation 影响。

而且,Sentry 还会记录点击节点的父级路径(比如 button#delete-customer > div.actions > tr.customer-row),方便快速定位页面元素。

Console 日志行为

最后,Sentry 把 console 的几个方法也劫持了:

;['debug', 'info', 'warn', 'error', 'log', 'assert'].forEach(function (level) {
  const original = console[level]
  console[level] = function (...args) {
    triggerHandlers('console', { args, level })
    return original.apply(console, args)
  }
})

这样开发者在调试时打印的信息也会被记录下来。如果线上出了问题,结合 console 日志和用户行为路径,就能复现当时的场景。

Session Replay

Sentry 还有一个强大的功能:Session Replay(会话回放)。它会录制用户的操作过程,就像视频一样:

Sentry.init({
  dsn: 'your-dsn',
  integrations: [
    new Sentry.Replay({
      // 录制采样率
      sessionSampleRate: 0.1,

      // 错误时 100% 录制
      errorSampleRate: 1.0,

      // 遮蔽敏感元素
      maskAllText: false,
      blockAllMedia: true,
    }),
  ],
})

在 CRM 系统里,我们通过 Session Replay 看到一个用户连续 3 次点击"删除客户"按钮,但每次都失败——原来是因为他鼠标移得太快,点击动画还没完成就移开了,导致事件被取消。这个细节在错误堆栈里根本看不到。

可观测性:从监控到洞察

在深入 Trace ID 之前,我们先聊聊**可观测性(Observability)**这个概念。

很多人把"监控"和"可观测性"混为一谈,但它们是两个层次的概念:

维度监控 (Monitoring)可观测性 (Observability)
核心目标知道"系统是否正常"理解"系统发生了什么"
数据类型预定义的指标(CPU、内存、响应时间)日志、指标、追踪链路 (Logs、Metrics、Traces)
反应方式被动告警(超过阈值就报警)主动分析(可以问任何问题)
问题定位知道"哪里出错了"知道"为什么出错"以及"怎么发生的"

可观测性的三大支柱:

  1. Metrics(指标):数值化的统计数据,比如请求 QPS、错误率、P95 响应时间
  2. Logs(日志):离散的事件记录,比如 "用户 123 登录失败"
  3. Traces(追踪):请求的完整调用链,记录从入口到数据库的每一跳

💡 这篇文章的第一篇详细讨论了如何从系统视角理解可观测性,推荐阅读。

Sentry 本质上是一个前端错误监控系统,它通过劫持浏览器 API 收集日志(错误堆栈)和追踪(trace_id),但在Metrics(指标) 方面比较弱——它无法告诉你"这个接口的 P95 响应时间是多少"、"最近一小时有多少用户访问了这个页面"。

这时候就需要更强的工具

Trace ID 与 ARMS:更强大的链路追踪

Sentry 通过 trace_id 实现了基础的分布式追踪,但如果你想构建完整的可观测体系,**阿里云 ARMS(应用实时监控服务)**是更强大的选择。

ARMS vs Sentry:能力对比

能力SentryARMS 应用监控
核心定位前端错误监控 + 基础追踪全栈 APM(前端 + 后端 + 数据库)
Metrics(指标)基础性能指标(采样率低)丰富的黄金指标(吞吐、错误、延迟)
Logs(日志)错误日志 + 上下文支持接入应用日志、系统日志
Traces(追踪)基础分布式追踪端到端全链路追踪,支持调用拓扑
告警能力错误频率告警多维度告警(指标、日志、链路)
自定义指标不支持支持业务指标(订单数、转化率等)
诊断工具Session Replay代码级持续剖析、Arthas 在线诊断
部署方式开源 SDK,SaaS 或自建阿里云托管,一键接入

为什么需要 ARMS?

回到 CRM 系统的案例。接入 Sentry 后,我们能快速捕获前端错误,但遇到下面这些问题时,Sentry 就力不从心了:

场景一:性能瓶颈定位

用户投诉"客户详情页加载很慢"。Sentry 只能告诉你这个页面花了 2.3 秒,但不知道这 2.3 秒花在了哪里——是前端渲染慢?还是后端接口慢?还是数据库查询慢?

ARMS 的全链路追踪能给出答案:

[浏览器] 页面加载总耗时: 2300ms
   (trace_id: abc123)
[前端资源] bundle.js: 800ms, CSS: 300ms
[后端 API] /api/customers/123: 900ms
  ├─ [Java] 用户鉴权: 50ms
  ├─ [Java] 查询客户基础信息: 200ms
  └─ [PostgreSQL] SELECT * FROM customers: 650ms ⚠️ 慢查询
[前端渲染] React 组件挂载: 300ms

一眼就能看出瓶颈在数据库的慢查询。

场景二:业务指标监控

客服团队问你:"昨天有多少客户删除操作失败了?"Sentry 无法回答这个问题,因为它只记录错误,不记录业务指标。

ARMS 支持自定义指标采集,你可以上报业务数据:

// 在 CRM 系统里上报自定义指标
import '@alicloud/opentelemetry-sdk'

const meter = opentelemetry.getMeter('crm')

// 创建计数器:记录删除客户操作的次数和失败次数
const deleteCounter = meter.createCounter('customer.delete', {
  description: '删除客户操作次数',
})

// 创建直方图:记录删除操作的耗时
const deleteDuration = meter.createHistogram('customer.delete.duration', {
  description: '删除操作耗时',
})

async function deleteCustomer(customerId) {
  const startTime = Date.now()

  try {
    // 业务逻辑
    await api.deleteCustomer(customerId)

    deleteCounter.add(1, { status: 'success' })
  } catch (error) {
    deleteCounter.add(1, { status: 'failed' })

    // 同时上报到 Sentry
    Sentry.captureException(error)
    throw error
  } finally {
    deleteDuration.record(Date.now() - startTime)
  }
}

然后在 ARMS 控制台就能看到:

  • customer_delete_total{status="failed"}:昨天删除失败的次数
  • customer_delete_duration{p95}:删除操作的 P95 耗时
  • 还可以设置告警:失败率超过 5% 时发送钉钉通知

场景三:调用拓扑与依赖分析

Sentry 只能看到单个请求的调用链,无法看到服务之间的依赖关系。ARMS 的服务拓扑功能能自动绘制服务间的调用关系图:

        ┌─────────┐
        │  前端  │
        └────┬────┘
        ┌────▼────────┐
        │  网关 API        └────┬────────┘
    ┌────────┼────────┐
    │        │        │
┌───▼──┐ ┌───▼──┐ ┌─▼────────┐
│用户服务│ │订单服务│ │CRM服务   │
└───┬──┘ └───┬──┘ └─┬────────┘
    │        │        │
    └────────┼────────┘
        ┌────▼────┐
        │PostgreSQL│
        └─────────┘

当某个服务响应变慢时,你能在拓扑图上一眼看出是哪个服务出了问题,还能看到服务之间的 QPS、错误率、延迟分布。

Trace ID 的价值再升级

有了 ARMS,Trace ID 不再只是关联前端和后端日志,而是串联了全链路的可观测数据

// 前端发起请求时携带 trace_id
const response = await fetch('/api/customers/123', {
  headers: {
    'sentry-trace': currentTraceId,
    traceparent: `00-${currentTraceId}-01`, // OpenTelemetry 标准
  },
})

这个 trace_id 会:

  1. 前端:Sentry 捕获错误时自动关联
  2. 后端:ARMS 采集日志时自动注入
  3. 数据库:慢查询日志里自动附加
  4. 消息队列:消息头里自动传递
  5. 第三方服务:HTTP header 里自动携带

你在 ARMS 控制台点开任何一个 trace_id,就能看到:

  • 📊 Metrics:这条请求经过的每个服务的 QPS、延迟、错误率
  • 📝 Logs:每个服务打印的相关日志
  • 🔍 Traces:详细的调用链,包括函数级的性能剖析
  • 📈 Top N:热点 SQL、慢查询、热点 API

这就是完整的可观测体系——不止知道"出错了",还知道"为什么出错"、"影响了谁"、"怎么修"。

接入 ARMS 的三步走

# 1. 安装 ARMS Agent(Java 应用为例)
wget https://arms-apm-{region}.oss-{region}.aliyuncs.com/ArmsAgent/java/arms-agent-{version}.tar.gz

# 2. 配置应用信息
cat > arms-agent.config << EOF
app.name=crm-app
app.id=your-app-id
license.licenseKey=your-license-key
EOF

# 3. 启动应用
java -javaagent:arms-agent.jar -jar crm-app.jar

前端接入也类似,在 HTML 里添加 ARMS 探针:

<script>
  window.__ARMS_CONFIG__ = {
    pid: 'your-pid',
    uid: 'user-' + currentUser.id,
    sampleRate: 0.1,
  }
</script>
<script src="https://arms-retcode-{region}.aliyuncs.com/retcode/bl.js"></script>

⚠️ 注意:如果你的应用已经接入了 Sentry,不要删除。可以同时使用:

  • Sentry 专注于前端错误捕获和 Session Replay
  • ARMS 专注于全链路追踪和性能监控

两者的 trace_id 可以互相透传,实现数据联动。

接入 Sentry 之后的效果

回到开头 CRM 系统的问题。接入 Sentry 一个月后,我们总结了一下效果:

指标接入前接入后
用户反馈到发现问题的平均时间3-5 天实时(< 5 分钟)
定位问题根源的平均时间2-4 小时15-30 分钟
每周修复的 bug 数5-8 个15-20 个
静默失败的错误无法统计每天捕获 30-50 个

最直观的感受是:不再"被动等用户投诉",而是"主动发现问题"

比如有一天早上打开 Sentry,发现 /api/customers 接口在特定时间段(凌晨 2-3 点)出现了大量 503 错误。查看用户行为回放,发现这段时间几乎没有用户操作,错误都是后台定时任务触发的。我们查了一下定时任务的代码,发现是在做批量数据同步时没有做重试机制。当天就修复了这个问题,用户根本不知道出过故障。

最佳实践与踩坑指南

1. 合理设置采样率

性能监控和 Session Replay 都会消耗资源,生产环境要合理设置采样率:

Sentry.init({
  // 性能监控:10% 采样
  tracesSampleRate: 0.1,

  // Session Replay:正常时 10%,出错时 100%
  replaysSessionSampleRate: 0.1,
  replaysOnErrorSampleRate: 1.0,
})

2. 分环境使用 DSN

开发环境、测试环境、生产环境使用不同的 DSN,避免混淆:

const dsnMap = {
  development: 'https://xxx@sentry.io/dev',
  staging: 'https://xxx@sentry.io/staging',
  production: 'https://xxx@sentry.io/prod',
}

Sentry.init({
  dsn: dsnMap[process.env.NODE_ENV],
  environment: process.env.NODE_ENV,
})

3. 设置 Source Maps

生产环境的代码通常是压缩混淆过的,错误堆栈很难看懂。需要上传 Source Maps:

// sentry-cli 上传 source maps
sentry-cli releases files $RELEASE_NAME upload-sourcemaps ./dist/*.js

这样 Sentry 就能把压缩后的代码映射回原始源码,错误堆栈就能显示准确的文件名和行号。

4. 配置告警规则

不要被动地打开 Sentry 看错误,要配置告警:

// 在 Sentry 控制台配置
- 错误频率超过 10/分钟时,发送邮件告警
- 特定错误类型(如 TypeError)出现时,发送 Slack 通知
- 新出现的错误类型,立即推送到值班群

在 CRM 系统里,我们配置了"关键接口 5xx 错误超过 5 次/分钟"的告警,这样能在用户还没投诉之前就发现问题。

5. 过滤无意义的错误

有些错误不需要关心(比如浏览器扩展抛出的错误),可以过滤掉:

Sentry.init({
  beforeSend(event, hint) {
    // 过滤掉浏览器扩展的错误
    if (event.exception) {
      for (const exception of event.exception.values) {
        if (exception.stacktrace) {
          for (const frame of exception.stacktrace.frames) {
            if (frame.filename && frame.filename.includes('extension')) {
              return null
            }
          }
        }
      }
    }

    return event
  },
})

总结:从错误监控到可观测体系

这篇文章我们聊了 Sentry 的异常监控原理,也介绍了 ARMS 的全链路追踪能力。现在可以站在更高层次总结一下:如何构建完整的前端可观测体系

三层可观测体系

层次工具覆盖范围解决的问题
第一层:异常监控Sentry前端错误 + 用户行为快速捕获异常、回放用户操作
第二层:全链路追踪ARMS前端 + 后端 + 数据库定位性能瓶颈、分析调用拓扑
第三层:系统观测Chrome DevTools + 系统工具浏览器 + 服务端 + 基础设施理解系统整体行为、发现根本原因

这三层是互补的,不是替代关系。在 CRM 系统里,我们同时使用了这三层:

  1. Sentry 告诉我们"哪个页面报错了"
  2. ARMS 告诉我们"错误在哪个服务、哪个接口、哪个数据库查询"
  3. Chrome DevTools 告诉我们"前端渲染花了多长时间、哪些资源加载慢"

📖 本系列的第一篇文章《系统视角的观测》详细讨论了如何从浏览器到数据库构建全栈观测,第二篇《Network Initiator 调试》介绍了如何快速定位请求源头。推荐阅读。

可观测性的价值

接入这些工具后,我们发现了一个规律:可观测性不是为了"看",而是为了"问"

当你遇到一个问题时,可以问:

  • ❌ "这个 bug 在哪里?" → 查 Sentry 的错误堆栈
  • ❌ "为什么会慢?" → 查 ARMS 的链路追踪
  • ❌ "影响了多少用户?" → 查 ARMS 的指标大盘
  • ❌ "最近有什么异常趋势?" → 查错误率曲线、慢查询趋势
  • ❌ "这个请求是从哪个页面发起的?" → 查 Network Initiator + trace_id

这些问题在以前需要人工排查、连蒙带猜,现在都能在几分钟内得到答案。

落地建议

最后给几个实用建议:

场景建议
新项目接入Sentry(开发环境就接)+ ARMS(上线后再接)
采样率设置Sentry:性能 10%,Session Replay 1-5%
ARMS:Trace 100%,Metrics 100%(成本可控)
敏感信息脱敏Sentry:beforeSend 过滤
ARMS:配置数据脱敏规则
告警规则Sentry:错误频率告警
ARMS:指标告警(P95 > 3s、错误率 > 1%)
成本控制ARMS 按指标上报量计费,合理设置采样率和数据保留时间
团队协作Sentry + Jira 自动创建 Bug 单
ARMS + 钉钉/飞书推送告警

结语:从"被动救火"到"主动预防"

接入 Sentry 之前,我们的模式是:用户投诉 → 开发排查 → 修复 bug → 等下次投诉

接入 Sentry + ARMS 之后,模式变成了:告警触发 → 分析链路 → 修复 bug → 彻底根除

这就是可观测性的力量——把"救火队"变成"消防系统",从被动应对变成主动预防。当你的系统拥有完整的 Logs、Metrics、Traces,你就不再害怕线上故障,因为你对它了如指掌。

推荐阅读


参考资料