第 17 章:性能监控与可观测性
到目前为止,我们的 SAAS 应用已经功能完备,并通过了严格的 CI/CD 流程(第 14 章)和自动化测试(第 16 章)。它已经部署到了 Vercel 上的生产环境。但是,一旦应用“走出”了我们的开发和测试环境,进入了真实用户的、不可预测的设备和网络中,我们如何知道它是否运行良好?
第 17 章:性能监控与可观测性
到目前为止,我们的 SAAS 应用已经功能完备,并通过了严格的 CI/CD 流程(第 14 章)和自动化测试(第 16 章)。它已经部署到了 Vercel 上的生产环境。但是,一旦应用“走出”了我们的开发和测试环境,进入了真实用户的、不可预测的设备和网络中,我们如何知道它是否运行良好?
“在我的机器上是好的”在生产环境中毫无意义。
这就是可观测性 (Observability) 的用武之地。对于 Python/Django 开发者来说,你可能熟悉 Sentry (sentry-python)、New Relic 或 Datadog。这些工具帮助你回答三个核心问题:
- 它慢吗? (性能监控)
- 它崩溃了吗? (错误追踪)
- 用户在用它做什么? (产品分析)
在 Next.js/Vercel 生态中,我们有更现代、更集成的工具来回答这些问题。本章,我们将为我们的 SAAS 应用构建一个三位一体的可观测性堆栈。
17.1. 性能监控:Vercel Analytics 与 Web Vitals
我们首先要关心的是用户感知的性能。如果你的 SAAS 加载缓慢或在交互时卡顿,用户会在点击“升级”按钮之前就流失了。
核心 Web Vitals (Core Web Vitals) 是 Google 定义的一组关键指标,用于衡量真实用户的页面体验:
- LCP (Largest Contentful Paint):最大内容绘制。衡量_加载性能_。(例如:你的仪表盘图表需要多快才能显示出来?)
- FID (First Input Delay) / INP (Interaction to Next Paint):首次输入延迟 / 下次绘制交互。衡量_交互性_。(例如:用户点击“保存”按钮后,页面需要多快才能响应?)
- CLS (Cumulative Layout Shift):累积布局偏移。衡量_视觉稳定性_。(例如:页面加载时,按钮是否会突然“跳动”,导致用户点错地方?)
Vercel Analytics 是 Vercel 平台提供的、与 Next.js 深度集成的零配置解决方案。
你不需要安装任何 SDK 或编写任何代码。你只需要在 Vercel 项目的仪表盘上点击“启用”按钮。
启用后,Vercel 会自动:
- 在你的 Next.js 应用中注入一个极轻量级(~1KB)的脚本。
- 从访问你网站的真实用户那里收集上述 Web Vitals 数据(这被称为 RUM - Real User Monitoring)。
- 在你的 Vercel 仪表盘中为你提供一个漂亮的、按页面和国家/地区划分的性能报告。
为什么选择 Vercel Analytics?
- 零配置,零性能影响:它被设计为对你的应用性能(尤其是 INP)完全没有负面影响。
- 隐私优先:它不使用 Cookie,不跟踪个人用户,完全符合 GDPR 等隐私法规。
- RSC 感知:它能很好地理解 Next.js 的路由(App Router),并为你提供有意义的页面级报告。
对于测量和保障核心 Web Vitals,Vercel Analytics 是 Next.js 开发者的首选。
17.2. 错误追踪:集成 Sentry
Vercel Analytics 告诉我们页面_慢不慢_,但它不会告诉我们页面_为什么会崩溃_。当用户遇到一个白屏、一个 Server Action 失败或一个客户端组件崩溃时,我们需要立即知道,并且需要完整的上下文(Context)。
Sentry 是现代应用监控和错误追踪的行业标准。从 Python 到 JavaScript,它提供了全栈覆盖。
对于我们的 Next.js 15+ SAAS 应用,@sentry/nextjs SDK 是一个“大杀器”。因为它能自动捕获来自所有执行上下文的错误:
- 客户端组件 (
"use client"):例如,Zustand store 中的一个 null 引用。 - 服务器组件 (RSC):例如,在 RSC 中直接访问数据库(
db.query...)时 Drizzle 抛出了异常。 - Server Actions:例如,我们的
consumeCreditsaction 失败。 - Route Handlers (API):例如,我们的 Stripe Webhook 处理器崩溃。
[代码解析]:集成 Sentry
集成 Sentry 非常简单,Sentry 团队提供了一个向导:
npx @sentry/wizard -i nextjs这个向导会自动执行以下操作:
- 安装
@sentry/nextjs依赖。 - 在你的
.env.local中添加SENTRY_DSN和SENTRY_AUTH_TOKEN。 - 创建 Sentry 配置文件:
sentry.client.config.ts(用于客户端)sentry.server.config.ts(用于服务器端 - Node.js 运行时)sentry.edge.config.ts(用于 Edge 运行时,例如 Middleware)
- 修改你的
next.config.mjs,使用withSentryConfig包裹它,以便在构建时自动上传 Source Maps(这对于调试压缩过的生产代码至关重要)。
为什么 Sentry 如此关键?
当一个错误发生时,Sentry 不只是记录一个 console.error。它会捕获一个完整的“事件”,包括:
- 堆栈跟踪 (Stack Trace):精确到你的 TSX 源代码中的行号(得益于 Source Maps)。
- 用户上下文:我们可以(并且应该)配置 Sentry,告诉它当前登录的
userId。这样我们就能看到“用户 A 遇到了这个错误 5 次”。 - 面包屑 (Breadcrumbs):用户在崩溃前执行了哪些操作(例如“点击了按钮 A”,“导航到了页面 B”)。
- RSC & Server Action 标签:它会自动标记错误是发生在哪个 Server Action 或 RSC 页面。
一个配置了 userId 的 Sentry setTag 示例:
// src/actions/payment.actions.ts
'use server';
import { auth } from '@/lib/auth';
import * as Sentry from '@sentry/nextjs';
export async function createCheckoutSession(priceId: string) {
const session = await auth();
if (!session?.user?.id) throw new Error('Unauthorized');
// 向 Sentry 添加上下文!
Sentry.setUser({ id: session.user.id, email: session.user.email });
Sentry.setTag("action", "createCheckoutSession");
try {
// ... 我们的 Stripe 逻辑 ...
} catch (error) {
// 手动捕获并报告错误
Sentry.captureException(error, {
extra: { priceId: priceId },
});
return { error: 'Failed to create session.' };
}
}17.3. [代码解析]:分析 src/analytics/ 中的多提供商集成
我们现在有了性能(Vercel)和错误(Sentry)。但我们还缺少产品分析(Product Analytics)。
Vercel Analytics 不会告诉我们:
- “有多少用户点击了 Pro 计划的‘升级’按钮?”
- “用户在仪表盘页面的平均停留时间是多久?”
- “从首页到注册页的转化率是多少?”
这些是产品经理和增长团队需要的数据。传统上,我们会使用 Google Analytics (GA),但 GA 越来越受隐私法规(GDPR)的限制,并且可能拖慢网站速度。
现代 SAAS 倾向于使用隐私优先的分析工具,例如 Plausible 或 Umami。它们是轻量级的、不使用 Cookie 的替代品。
问题是:我们如何集成它们,同时又不让我们的代码库与_特定_的提供商绑定?答案是:创建一个抽象层。
目录结构
src/
└── analytics/
├── AnalyticsProvider.tsx (客户端组件, 用于加载脚本)
└── events.ts (全局事件跟踪函数)文件 1:src/analytics/AnalyticsProvider.tsx
这是一个客户端组件,负责根据环境变量_动态加载_分析脚本。
// src/analytics/AnalyticsProvider.tsx
'use client';
import Script from 'next/script';
import { usePathname, useSearchParams } from 'next/navigation';
import { useEffect } from 'react';
// 从 .env.local 或 Vercel 环境变量中读取
const PLAUSIBLE_DOMAIN = process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN;
const UMAMI_SITE_ID = process.env.NEXT_PUBLIC_UMAMI_SITE_ID;
const UMAMI_SCRIPT_URL = process.env.NEXT_PUBLIC_UMAMI_SCRIPT_URL;
export function AnalyticsProvider() {
const pathname = usePathname();
const searchParams = useSearchParams();
// 这个 Effect 确保了在 Next.js 路由(App Router)切换时
// Umami 也能正确捕获到页面浏览
useEffect(() => {
if (!UMAMI_SITE_ID || !window.umami) return;
// `usePathname` 和 `useSearchParams` 确保了每次 URL 变化时
// 都会重新运行此 Effect,从而发送页面浏览事件
const url = pathname + (searchParams.toString() ? '?' + searchParams.toString() : '');
window.umami.track(url);
}, [pathname, searchParams]);
return (
<>
{/* 1. Plausible Analytics */}
{PLAUSIBLE_DOMAIN && (
<Script
defer
data-domain={PLAUSIBLE_DOMAIN}
src="[https://plausible.io/js/script.js](https://plausible.io/js/script.js)"
/>
)}
{/* 2. Umami Analytics */}
{UMAMI_SITE_ID && UMAMI_SCRIPT_URL && (
<Script
async
defer
data-website-id={UMAMI_SITE_ID}
src={UMAMI_SCRIPT_URL}
// Umami 默认会自动跟踪页面浏览
// 但在 Next.js App Router 中,我们使用上面的 useEffect 手动跟踪
data-auto-track="false"
data-do-not-track="true" // 禁用自动跟踪,由 Effect 接管
/>
)}
</>
);
}然后,我们将这个 AnalyticsProvider 添加到我们的根布局 src/app/[locale]/layout.tsx 中。
文件 2:src/analytics/events.ts
这个文件是我们的抽象层。应用中的任何其他组件(例如定价表)都将调用这个文件中的函数,而_不需要知道_是 Plausible 还是 Umami 在处理它。
// src/analytics/events.ts
// 扩展全局 window 类型以包含 Plausible 和 Umami
declare global {
interface Window {
plausible?: (event: string, options?: { props: Record<string, any> }) => void;
umami?: (event: string, data?: Record<string, any>) => void;
}
}
type AnalyticsEvent =
| 'click_upgrade'
| 'register_success'
| 'start_checkout';
interface EventProps {
planId?: string;
source?: string;
}
/**
* 跟踪一个自定义事件
* @param eventName 事件名称 (例如 'click_upgrade')
* @param props 附加属性 (例如 { planId: 'pro' })
*/
export function trackEvent(eventName: AnalyticsEvent, props: EventProps = {}) {
// 1. 发送到 Plausible
if (window.plausible) {
window.plausible(eventName, { props });
}
// 2. 发送到 Umami
if (window.umami) {
// Umami 的格式是 'event_name', { data: { ... } }
window.umami(eventName, { data: props });
}
// 3. (可选) 打印到控制台 (仅限开发环境)
if (process.env.NODE_ENV === 'development') {
console.log(`[ANALYTICS] Event: ${eventName}`, props);
}
}现在,在我们的 PricingTable.tsx(来自第 12 章)中,我们可以这样做:
// src/components/payment/PricingTable.tsx
'use client';
import { trackEvent } from '@/analytics/events'; // 导入我们的抽象函数
// ...
function PlanCard({ plan }: { plan: PricePlan }) {
// ...
const handleUpgrade = async () => {
//...
// 在启动结账前,跟踪这个关键的转化事件!
trackEvent('click_upgrade', {
planId: plan.id,
source: 'pricing_page'
});
// ... (调用 createCheckoutSession 等)
};
// ...
}第 17 章总结
在本章中,我们为我们的 SAAS 应用构建了一个全面的、三位一体的可观测性堆栈,确保我们对生产环境有 360 度的可见性。
- 性能监控 (Vercel Analytics):我们利用 Vercel 的零配置 RUM 来自动收集核心 Web Vitals。这确保了我们的应用对真实用户来说始终保持高性能,保障了用户体验和 SEO。
- 错误追踪 (Sentry):我们集成了 Sentry,并利用其
@sentry/nextjsSDK 自动捕获来自全栈(RSC, Server Actions, 客户端)的错误。通过添加用户上下文(userId),我们极大地提高了调试生产环境 Bug 的能力。 - 产品分析 (Plausible/Umami):我们构建了一个抽象的分析层(
AnalyticsProvider和trackEvent)。这使我们能够集成一个或多个隐私优先的分析工具,以跟踪关键的用户行为(如“click_upgrade”),而无需将我们的代码库与任何特定提供商锁定。
有了这个堆栈,我们不仅知道我们的应用是否在工作,还知道它工作得有多好,有多少人在使用它,以及他们是如何使用它的。
更多文章
第 19 章:集成 AI 能力 (Vercel AI SDK)
在本书的前 18 章中,我们已经构建了一个极其坚实的 SAAS 基础。我们拥有了支付 (Stripe)、认证 (better-auth)、数据库 (Drizzle)、DevOps (GitHub Actions) 和功能发布 (Feature Flags) 的所有核心组件。现在,是时候为我们的 SAAS 注入“智...
第 16 章:SAAS 测试策略与质量保障
在第 14 章中,我们建立了一个 CI/CD 流水线,它充当了我们 SAAS 应用的“守门员”。在第 15 章中,我们使用 Biome (Linter) 确保了代码的“静态质量”。然而,一个只检查语法的守门员是远远不够的。我们的 CI 流程中还有一个 pnpm test 命令——这才是质量保障的核心。
第 6 章:Server Actions:现代 SAAS 的后端“突变” (Mutation)
从 Django 或 Flask 迁移过来,我们最习惯的模式是什么?为 'Create', 'Update', 'Delete' (C/U/D) 操作定义 REST/GraphQL API 路由。