Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

可观测性 (Observability)

可观测性是现代 Web 开发的一个关键方面,它使你能够监控、追踪和调试应用程序的性能与错误。TanStack Start 提供了内置的可观测性模式,并能与外部工具无缝集成,为你提供关于应用程序的全方位洞察。

合作伙伴解决方案:Sentry

为了获得全面的可观测性,我们推荐使用 Sentry —— 我们在错误追踪和性能监控领域值得信赖的合作伙伴。Sentry 提供:

快速设置:

// 客户端 (app.tsx)
import * as Sentry from "@sentry/react";

Sentry.init({
  dsn: import.meta.env.VITE_SENTRY_DSN,
  environment: import.meta.env.NODE_ENV,
});

// 服务器函数
import * as Sentry from "@sentry/node";

const serverFn = createServerFn().handler(async () => {
  try {
    return await riskyOperation();
  } catch (error) {
    Sentry.captureException(error);
    throw error;
  }
});

开始使用 Sentry → | 查看集成示例 →

内置可观测性模式

TanStack Start 的架构提供了多种无需外部依赖即可实现的内置可观测性机会:

服务器函数日志记录 (Server Function Logging)

在你的服务器函数中添加日志,以追踪执行情况、性能和错误:

import { createServerFn } from "@tanstack/react-start";

const getUser = createServerFn({ method: "GET" })
  .inputValidator((id: string) => id)
  .handler(async ({ data: id }) => {
    const startTime = Date.now();

    try {
      console.log(`[SERVER] 正在获取用户 ${id}`);

      const user = await db.users.findUnique({ where: { id } });

      if (!user) {
        console.log(`[SERVER] 未找到用户 ${id}`);
        throw new Error("用户未找到");
      }

      const duration = Date.now() - startTime;
      console.log(`[SERVER] 用户 ${id} 获取成功,耗时 ${duration}ms`);

      return user;
    } catch (error) {
      const duration = Date.now() - startTime;
      console.error(`[SERVER] 获取用户 ${id} 出错,耗时 ${duration}ms:`, error);
      throw error;
    }
  });

请求/响应中间件 (Request/Response Middleware)

创建中间件来记录所有的请求和响应:

import { createMiddleware } from "@tanstack/react-start";

const requestLogger = createMiddleware().server(async ({ request, next }) => {
  const startTime = Date.now();
  const timestamp = new Date().toISOString();

  console.log(`[${timestamp}] ${request.method} ${request.url} - 开始执行`);

  try {
    const result = await next();
    const duration = Date.now() - startTime;

    console.log(
      `[${timestamp}] ${request.method} ${request.url} - ${result.response.status} (${duration}ms)`,
    );

    return result;
  } catch (error) {
    const duration = Date.now() - startTime;
    console.error(
      `[${timestamp}] ${request.method} ${request.url} - 错误 (${duration}ms):`,
      error,
    );
    throw error;
  }
});

// 应用于所有服务器路由
export const Route = createFileRoute("/api/users")({
  server: {
    middleware: [requestLogger],
    handlers: {
      GET: async () => {
        return Response.json({ users: await getUsers() });
      },
    },
  },
});

路由性能监控 (Route Performance Monitoring)

在客户端和服务器端追踪路由加载性能:

import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/dashboard")({
  loader: async ({ context }) => {
    const startTime = Date.now();

    try {
      const data = await loadDashboardData();
      const duration = Date.now() - startTime;

      // 记录服务器端 (SSR) 性能
      if (typeof window === "undefined") {
        console.log(`[SSR] Dashboard 加载耗时 ${duration}ms`);
      }

      return data;
    } catch (error) {
      const duration = Date.now() - startTime;
      console.error(`[LOADER] Dashboard 错误,耗时 ${duration}ms:`, error);
      throw error;
    }
  },
  component: Dashboard,
});

function Dashboard() {
  const data = Route.useLoaderData();

  // 追踪客户端渲染时间
  React.useEffect(() => {
    const renderTime = performance.now();
    console.log(`[CLIENT] Dashboard 渲染耗时 ${renderTime}ms`);
  }, []);

  return <div>Dashboard 内容</div>;
}

健康检查端点 (Health Check Endpoints)

创建用于健康监测的服务器路由:

// routes/health.ts
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/health")({
  server: {
    handlers: {
      GET: async () => {
        const checks = {
          status: "healthy",
          timestamp: new Date().toISOString(),
          uptime: process.uptime(), // 运行时间
          memory: process.memoryUsage(), // 内存占用
          database: await checkDatabase(), // 数据库检查
          version: process.env.npm_package_version, // 版本号
        };

        return Response.json(checks);
      },
    },
  },
});

async function checkDatabase() {
  try {
    await db.raw("SELECT 1");
    return { status: "connected", latency: 0 };
  } catch (error) {
    return { status: "error", error: error.message };
  }
}

错误边界 (Error Boundaries)

实现全方位的错误处理:

// 客户端错误边界
import { ErrorBoundary } from "react-error-boundary";

function ErrorFallback({ error, resetErrorBoundary }: any) {
  // 记录客户端错误
  console.error("[客户端错误]:", error);

  // 也可以发送到外部服务
  // sendErrorToService(error)

  return (
    <div role="alert">
      <h2>抱歉,出错了</h2>
      <button onClick={resetErrorBoundary}>重试</button>
    </div>
  );
}

export function App() {
  return (
    <ErrorBoundary FallbackComponent={ErrorFallback}>
      <Router />
    </ErrorBoundary>
  );
}

// 服务器函数错误处理
const riskyOperation = createServerFn().handler(async () => {
  try {
    return await performOperation();
  } catch (error) {
    // 记录带有上下文的服务器错误
    console.error("[服务器错误]:", {
      error: error.message,
      stack: error.stack,
      timestamp: new Date().toISOString(),
      // 如果可用,添加请求上下文
    });

    // 返回用户友好的错误信息
    throw new Error("操作失败,请重试。");
  }
});

性能指标收集 (Performance Metrics Collection)

收集并公开基础性能指标:

// utils/metrics.ts
class MetricsCollector {
  private metrics = new Map<string, number[]>();

  // 记录耗时
  recordTiming(name: string, duration: number) {
    if (!this.metrics.has(name)) {
      this.metrics.set(name, []);
    }
    this.metrics.get(name)!.push(duration);
  }

  // 获取统计信息
  getStats(name: string) {
    const timings = this.metrics.get(name) || [];
    if (timings.length === 0) return null;

    const sorted = timings.sort((a, b) => a - b);
    return {
      count: timings.length,
      avg: timings.reduce((a, b) => a + b, 0) / timings.length, // 平均值
      p50: sorted[Math.floor(sorted.length * 0.5)], // 中位数
      p95: sorted[Math.floor(sorted.length * 0.95)], // 95分位数
      min: sorted[0],
      max: sorted[sorted.length - 1],
    };
  }

  getAllStats() {
    const stats: Record<string, any> = {};
    for (const [name] of this.metrics) {
      stats[name] = this.getStats(name);
    }
    return stats;
  }
}

export const metrics = new MetricsCollector();

// 指标端点
// routes/metrics.ts
export const Route = createFileRoute("/metrics")({
  server: {
    handlers: {
      GET: async () => {
        return Response.json({
          system: {
            uptime: process.uptime(),
            memory: process.memoryUsage(),
            timestamp: new Date().toISOString(),
          },
          application: metrics.getAllStats(),
        });
      },
    },
  },
});

开发环境调试响应头 (Debug Headers)

在响应中添加有助于开发的调试信息:

import { createMiddleware } from "@tanstack/react-start";

const debugMiddleware = createMiddleware().server(async ({ next }) => {
  const result = await next();

  if (process.env.NODE_ENV === "development") {
    result.response.headers.set("X-Debug-Timestamp", new Date().toISOString());
    result.response.headers.set("X-Debug-Node-Version", process.version);
    result.response.headers.set("X-Debug-Uptime", process.uptime().toString());
  }

  return result;
});

环境特定日志记录 (Environment-Specific Logging)

为开发环境和生产环境配置不同的日志策略:

// utils/logger.ts
import { createIsomorphicFn } from "@tanstack/react-start";

type LogLevel = "debug" | "info" | "warn" | "error";

const logger = createIsomorphicFn()
  .server((level: LogLevel, message: string, data?: any) => {
    const timestamp = new Date().toISOString();

    if (process.env.NODE_ENV === "development") {
      // 开发环境:详细的控制台日志
      console[level](`[${timestamp}] [${level.toUpperCase()}]`, message, data);
    } else {
      // 生产环境:结构化的 JSON 日志
      console.log(
        JSON.stringify({
          timestamp,
          level,
          message,
          data,
          service: "tanstack-start",
          environment: process.env.NODE_ENV,
        }),
      );
    }
  })
  .client((level: LogLevel, message: string, data?: any) => {
    if (process.env.NODE_ENV === "development") {
      console[level](`[CLIENT] [${level.toUpperCase()}]`, message, data);
    } else {
      // 生产环境:发送到分析服务
      // analytics.track('client_log', { level, message, data })
    }
  });

// 在应用中任何地方使用
export { logger };

// 使用示例
const fetchUserData = createServerFn().handler(async ({ data: userId }) => {
  logger("info", "开始获取用户数据", { userId });

  try {
    const user = await db.users.findUnique({ where: { id: userId } });
    logger("info", "用户数据获取成功", { userId });
    return user;
  } catch (error) {
    logger("error", "获取用户数据失败", {
      userId,
      error: error.message,
    });
    throw error;
  }
});

简单错误报告 (Simple Error Reporting)

无需外部依赖的基础错误报告机制:

// utils/error-reporter.ts
const errorStore = new Map<
  string,
  { count: number; lastSeen: Date; error: any }
>();

export function reportError(error: Error, context?: any) {
  const key = `${error.name}:${error.message}`;
  const existing = errorStore.get(key);

  if (existing) {
    existing.count++;
    existing.lastSeen = new Date();
  } else {
    errorStore.set(key, {
      count: 1,
      lastSeen: new Date(),
      error: {
        name: error.name,
        message: error.message,
        stack: error.stack,
        context,
      },
    });
  }

  // 立即记录到控制台
  console.error("[已报告错误]:", {
    error: error.message,
    count: existing ? existing.count : 1,
    context,
  });
}

// 错误报告端点
// routes/errors.ts
export const Route = createFileRoute("/admin/errors")({
  server: {
    handlers: {
      GET: async () => {
        const errors = Array.from(errorStore.entries()).map(([key, data]) => ({
          id: key,
          ...data,
        }));

        return Response.json({ errors });
      },
    },
  },
});

外部可观测性工具 (External Observability Tools)

虽然 TanStack Start 提供了内置的可观测性模式,但外部工具可以提供更全面的监控:

其他流行工具

应用性能监控 (APM):

错误追踪:

分析与用户行为:

New Relic 集成

New Relic 是一款流行的应用性能监控工具。以下是如何将其与 TanStack Start 集成的方法。

服务端渲染 (SSR)

要为服务端渲染启用 New Relic,你需要执行以下操作:

在 New Relic 上创建一个类型为 Node 的新集成。你将获得一个许可密钥 (license key),我们将在下方使用它。

// newrelic.js - New Relic 代理配置
exports.config = {
  app_name: ["YourTanStackApp"], // 你在 New Relic 中的应用名称
  license_key: "YOUR_NEW_RELIC_LICENSE_KEY", // 你的 New Relic 许可密钥
  agent_enabled: true,
  distributed_tracing: { enabled: true },
  span_events: { enabled: true },
  transaction_events: { enabled: true },
  // 其他默认设置
};
// server.tsx
import newrelic from "newrelic"; // 确保这是第一个导入项
import {
  createStartHandler,
  defaultStreamHandler,
  defineHandlerCallback,
} from "@tanstack/react-start/server";
import type { ServerEntry } from "@tanstack/react-start/server-entry";

const customHandler = defineHandlerCallback(async (ctx) => {
  // 我们这样做是为了将事务按路由 ID 分组,而不是按唯一的 URL 分组
  const matches = ctx.router?.state?.matches ?? [];
  const leaf = matches[matches.length - 1];
  const routeId = leaf?.routeId ?? new URL(ctx.request.url).pathname;

  newrelic.setControllerName(routeId, ctx.request.method ?? "GET");
  newrelic.addCustomAttributes({
    "route.id": routeId,
    "http.method": ctx.request.method,
    "http.path": new URL(ctx.request.url).pathname,
    // 你想添加的任何其他自定义属性
  });

  return defaultStreamHandler(ctx);
});

export default {
  fetch(request) {
    const handler = createStartHandler(customHandler);
    return handler(request);
  },
} satisfies ServerEntry;
# 启动时预加载 New Relic 代理
node -r newrelic .output/server/index.mjs

服务器函数与服务器路由

如果你想为服务器函数和服务器路由添加监控,需要按照上述步骤操作,然后添加以下内容:

// newrelic-middleware.ts
import newrelic from "newrelic";
import { createMiddleware } from "@tanstack/react-start";

export const nrTransactionMiddleware = createMiddleware().server(
  async ({ request, next }) => {
    const reqPath = new URL(request.url).pathname;
    newrelic.setControllerName(reqPath, request.method ?? "GET");
    return await next();
  },
);
// start.ts
import { createStart } from "@tanstack/react-start";
import { nrTransactionMiddleware } from "./newrelic-middleware";

export const startInstance = createStart(() => {
  return {
    requestMiddleware: [nrTransactionMiddleware],
  };
});

SPA 与浏览器 (SPA & Browser)

在 New Relic 上创建一个类型为 React 的新集成。

设置完成后,你需要将 New Relic 提供的集成脚本添加到你的根路由(root route)中。

// __root.tsx
export const Route = createRootRoute({
  head: () => ({
    scripts: [
      {
        id: "new-relic",

        // 方式一:直接在此处复制并粘贴你的 New Relic 集成脚本内容
        children: `...`,

        // 方式二:也可以将其创建在 public 文件夹中(例如 /newrelic.js),然后在此引用
        src: "/newrelic.js",
      },
    ],
  }),
});

OpenTelemetry 集成 (实验性)

OpenTelemetry 是可观测性的行业标准。以下是将其与 TanStack Start 集成的实验性方法:

// instrumentation.ts - 在应用启动前初始化
import { NodeSDK } from "@opentelemetry/sdk-node";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { Resource } from "@opentelemetry/resources";
import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions";

const sdk = new NodeSDK({
  resource: new Resource({
    [SemanticResourceAttributes.SERVICE_NAME]: "tanstack-start-app",
    [SemanticResourceAttributes.SERVICE_VERSION]: "1.0.0",
  }),
  instrumentations: [getNodeAutoInstrumentations()],
});

// 务必在导入应用其他部分之前初始化
sdk.start();
// 服务器函数追踪 (Tracing)
import { trace, SpanStatusCode } from "@opentelemetry/api";

const tracer = trace.getTracer("tanstack-start");

const getUserWithTracing = createServerFn({ method: "GET" })
  .inputValidator((id: string) => id)
  .handler(async ({ data: id }) => {
    return tracer.startActiveSpan("get-user", async (span) => {
      span.setAttributes({
        "user.id": id,
        operation: "database.query",
      });

      try {
        const user = await db.users.findUnique({ where: { id } });
        span.setStatus({ code: SpanStatusCode.OK });
        return user;
      } catch (error) {
        span.recordException(error);
        span.setStatus({
          code: SpanStatusCode.ERROR,
          message: error.message,
        });
        throw error;
      } finally {
        span.end();
      }
    });
  });
// 用于自动追踪的中间件
import { createMiddleware } from "@tanstack/react-start";
import { trace, SpanStatusCode } from "@opentelemetry/api";

const tracer = trace.getTracer("tanstack-start");

const tracingMiddleware = createMiddleware().server(
  async ({ next, request }) => {
    const url = new URL(request.url);

    return tracer.startActiveSpan(
      `${request.method} ${url.pathname}`,
      async (span) => {
        span.setAttributes({
          "http.method": request.method,
          "http.url": request.url,
          "http.route": url.pathname,
        });

        try {
          const result = await next();
          span.setAttribute("http.status_code", result.response.status);
          span.setStatus({ code: SpanStatusCode.OK });
          return result;
        } catch (error) {
          span.recordException(error);
          span.setStatus({
            code: SpanStatusCode.ERROR,
            message: error.message,
          });
          throw error;
        } finally {
          span.end();
        }
      },
    );
  },
);

注意:上述 OpenTelemetry 集成仍处于实验阶段,需要手动设置。我们正在探索原生支持(first-class support),未来将为服务器函数、中间件和路由加载器(Loaders)提供自动插桩功能。

快速集成模式

大多数可观测性工具与 TanStack Start 的集成模式都非常相似:

// 在应用入口点进行初始化
import { initObservabilityTool } from "your-tool";

initObservabilityTool({
  dsn: import.meta.env.VITE_TOOL_DSN,
  environment: import.meta.env.NODE_ENV,
});

// 服务器函数中间件
const observabilityMiddleware = createMiddleware().handler(async ({ next }) => {
  return yourTool.withTracing("server-function", async () => {
    try {
      return await next();
    } catch (error) {
      yourTool.captureException(error);
      throw error;
    }
  });
});

最佳实践

开发环境 vs 生产环境

// 不同环境下的不同策略
const observabilityConfig = {
  development: {
    logLevel: "debug",
    enableTracing: true,
    enableMetrics: false, // 开发环境下指标数据可能干扰视线
  },
  production: {
    logLevel: "warn",
    enableTracing: true,
    enableMetrics: true,
    enableAlerting: true, // 启用告警
  },
};

性能监控检查清单 (Checklist)

安全注意事项

未来的 OpenTelemetry 支持

TanStack Start 即将迎来直接的 OpenTelemetry 支持,届时无需上述手动设置,即可为服务器函数、中间件和路由加载器提供自动插桩。

相关资源