HOW - 前端团队性能监控上报规范(Sentry)

我们的目标是专门针对性能监控的 Sentry 手动上报规范方案,即如何在前端项目里通过 Sentry 主动收集 性能指标(加载速度、交互耗时、关键链路追踪),并在团队内制定一套可落地的规范。


1. 设计目标

  1. 关键链路透明化:核心业务流程(如下单、支付、搜索)有明确耗时记录。
  2. 页面性能可量化:首屏加载、路由切换、接口请求时延均可度量。
  3. 团队行为一致:统一 API + 上报规范,避免随意埋点。
  4. 数据可分析:所有性能数据必须携带模块信息和上下文,方便聚合。

2. 监控范围

A. 页面级

  • 首屏加载时间(TTFB、FCP、LCP)
  • 路由切换耗时

B. 接口级

  • 核心 API 请求耗时(下单、支付、搜索接口)
  • 请求失败重试次数

C. 交互级

  • 用户关键操作耗时(点击按钮 → 页面渲染完成)
  • 异常性能事件(动画掉帧、渲染超时)

D. 自定义链路

  • 业务自定义事务(如“提交订单流程”)

3. Sentry 性能能力

注意,在 version 9 后已废弃 Transaction 概念和相关 API.

Sentry 支持 APM(应用性能监控),可手动调用:

  • Sentry.startTransaction:开启事务(transaction)
  • transaction.startChild:记录子 span
  • Sentry.startSpan(新 API):简化事务/子任务追踪
  • transaction.finish():结束事务

为什么必须使用 transaction?

  1. 链路化展示:只有把 span 放在 transaction 下,Sentry 才能展示完整的端到端链路(总耗时 + 各 span 耗时)
  2. 错误关联:如果在 transaction 生命周期内发生异常,错误事件会自动关联到该 transaction(scope.setSpan),便于把错误和性能问题关联分析
  3. 聚合与筛选:transaction name/ops/tags 可以作为聚合维度,便于按业务模块或流程统计 P95、错误率等。

4. 封装方案

这里以 Sentry version 9 为例。
Sentry 版本(注意版本差异):
“@sentry/react”: “^9.12.0”,
“@sentry/vite-plugin”: “^3.3.1”,

Sentry v9 中有关性能 tracing 的变动

从 v7 → v8 → v9 的迁移指南中看到:

startTransaction()span.startChild() 在 v8 中被标记为 deprecated (弃用)。文档建议使用新的 span APIs,如 startSpan()startSpanManual()startInactiveSpan()

scope.getSpan(), scope.setSpan() 在 v8+ 中也有行为变动或被 deprecated 的提示。

v9 的新 API 更倾向于「所有 manual instrumentation 都用 span」,transaction 在概念上更多是 UI/展示上的一个 root span + trace tree 的根,但 SDK 内部可能将 root span 当作 transaction 处理。

文档 “New Tracing APIs” 里说:

“The new model does not differentiate between a transaction or a span when manually starting or ending them. Instead, you always use the same APIs to start a new span …”

所以在 v9 里,startTransaction() 虽然可能还在类型里/还有支持,但已被弱化或弃用,官方推荐用新的 span API。

具体封装

在项目新增 utils/sentry/performance.ts

import * as Sentry from "@sentry/react"

/**
 * Start a performance root span (replaces Transaction)
 */
export function startPerfSpan(
    name: string,
    op?: string,
    context?: Record<string, any>
) {
    return Sentry.startSpanManual(
        {
            name,
            op,
            attributes: context,
        },
        (span) => {
            // The span is active within this callback.
            // You can return the span to manage it manually.
            return span
        }
    )
}

/**
 * Create a child span
 */
export function startChildSpan(
    parentSpan: any,
    name: string,
    op?: string,
    context?: Record<string, any>
) {
    // Use startSpanManual with the parent span's scope if you want to attach as a child
    return Sentry.startSpanManual(
        {
            name,
            op,
            attributes: context,
            scope: parentSpan ? parentSpan.getScope() : undefined,
        },
        (span) => span
    )
}

/**
 * End a span
 */
export function finishPerfSpan(span: any) {
    if (span) span.end()
}

/**
 * Convenience: create a root span, run async task, auto end, capture errors
 */
export async function withPerfSpan<T>(
    opts: { name: string; op?: string; data?: Record<string, any> },
    task: (span: any) => Promise<T>
): Promise<T> {
    let result: T
    await Sentry.startSpan(
        {
            name: opts.name,
            op: opts.op,
            attributes: opts.data,
        },
        async (span) => {
            try {
                result = await task(span)
            } catch (err) {
                Sentry.captureException(err)
                throw err
            }
        }
    )
    return result!
}

The reason to use startSpan (not startSpanManual) in a utility like withPerfSpan is that : startSpan is designed for cases where you want to automatically manage the span’s lifecycle within a callback or async function. When you use startSpan, the span is started, set as the active span for the duration of the callback, and then automatically ended when the callback completes—whether it completes normally or throws an error. This makes it ideal for wrapping a block of code or an async operation, as in the pattern of withPerfSpan.

In contrast, startSpanManual is intended for situations where you need manual control over when the span ends, which is useful if the span’s lifetime does not match a single function’s execution or you need to end it at a custom point.


5. 上报规范

5.1 页面级

// 在路由开始时(例如 router.beforeEach / history.listen)
const navigationSpan = startPerfSpan("RouteChange", "navigation", { from: prevPath, to: newPath });

// 路由完成或页面渲染完成时调用 end
if (navigationSpan) {
  navigationSpan.end();
}

5.2 接口级

import { startPerfSpan, startChildSpan } from './utils/sentry/performance';

async function fetchOrder(payload) {
  // 创建根 span(替代 transaction)
  const span = startPerfSpan("OrderAPI", "http.client", { api: "/order/create" });
  try {
    const res = await api.createOrder(payload);
    return res;
  } finally {
    if (span) span.end();
  }
}

async function callCreateOrder(payload) {
  // 创建根 span
  const rootSpan = startPerfSpan('OrderCreateFlow', 'http.client', { api: '/api/order/create', module: 'Order' });

  // 创建子 span:获取 token
  const spanAuth = startChildSpan(rootSpan, 'auth-get-token', 'http.client');
  await getAuthToken();
  if (spanAuth) spanAuth.end();

  // 创建子 span:发起请求
  const spanReq = startChildSpan(rootSpan, 'POST /api/order/create', 'http.client', { url: '/api/order/create' });
  try {
    const res = await fetch('/api/order/create', { method: 'POST', body: JSON.stringify(payload) });
    // ... handle res
  } finally {
    if (spanReq) spanReq.end();
    if (rootSpan) rootSpan.end();
  }
}

把所有子步骤作为 span 放在同一个父级 span 下,Sentry 能展示完整链路(总耗时 + 各步耗时)。

5.3 用户交互

import { withPerfSpan, startChildSpan } from './utils/sentry/performance';

async function onSubmit() {
  await withPerfSpan({ name: 'OrderSubmitUI', op: 'ui.action', data: { module: 'Order' } }, async (rootSpan) => {
    const spanValidate = startChildSpan(rootSpan, 'validateForm', 'function');
    validateForm(); // 同步校验
    if (spanValidate) spanValidate.end();

    const spanApi = startChildSpan(rootSpan, 'submitOrderApi', 'http.client');
    await api.submitOrder();
    if (spanApi) spanApi.end();

    // 可以在 root span 上补充数据/tag
    if (rootSpan) rootSpan.setTag && rootSpan.setTag('flow', 'checkout-v2');
  });
}

5.4 自定义链路

import { withPerfSpan, startChildSpan } from './utils/sentry/performance';

await withPerfSpan({ name: 'FlowUpload', op: 'business.flow', data: { fileCount: files.length } }, async (rootSpan) => {
  const s1 = startChildSpan(rootSpan, 'prepareFiles', 'function');
  await prepareFiles(files);
  if (s1) s1.end();

  const s2 = startChildSpan(rootSpan, 'uploadChunks', 'http.client', { url: '/upload' });
  await uploadChunks(files);
  if (s2) s2.end();

  const s3 = startChildSpan(rootSpan, 'finalize', 'function');
  await finalizeUpload();
  if (s3) s3.end();
});

6. 落地规范

  1. 统一封装

    • 禁止业务代码直接写 Sentry.startSpan 或者 Sentry.startSpanManual
    • 必须通过 utils/sentry/performance.ts
  2. 命名规范

    • 事务 name: 大驼峰 + 业务模块(例:“OrderSubmit”)
    • op: 统一取值集 → navigationhttp.clientui.actionbusiness.flow
  3. 上下文要求

    • 必须传入 moduleactionparamsId(如订单号、用户 ID)
    • 避免上传敏感信息
  4. 采样控制

    • 性能事务可能很多,默认采样率不超过 10%
    • 关键链路可强制 100% 采样(配置 allowList)

7. Code Review 检查清单

  • 是否使用了封装函数?
  • 是否合理命名 nameop
  • 是否包含必要上下文?
  • 是否避免敏感信息?
  • 是否考虑采样率?

最终效果

  • 每个页面切换、接口调用、关键交互都会在 Sentry 生成事务链路

  • Dashboard 可查看:

    • 哪些页面加载慢
    • 哪些 API 延迟大
    • 哪些业务流程瓶颈
    • 团队统一「性能上报点」,保证数据可分析
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@PHARAOH

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值