# TanStack Start và Lovable

> Nối @belvador/tanstack-start cho cả trình duyệt và máy chủ Cloudflare Workers: router, server entry và middleware.

`@belvador/tanstack-start` báo lỗi từ cả hai nửa của một ứng dụng TanStack Start: trình duyệt và máy chủ chạy trên Cloudflare Workers. Ứng dụng làm bằng Lovable là ứng dụng TanStack Start chạy trên Workers, nên đây cũng là hướng dẫn cho chúng.

Một gói, nhưng **nối vào ba chỗ**: router (trình duyệt), server entry và middleware. Mỗi phần báo những gì chỉ nó mới thấy.

## Trước khi bắt đầu

- Một project có nền tảng **TanStack Start / Lovable** và DSN của nó. Phần trình duyệt và phần máy chủ dùng chung project.
- `@tanstack/react-router` và `@tanstack/react-start` **từ 1.168 trở lên**, React 18 hoặc 19.
- Máy chủ chạy trên **Cloudflare Workers** với cờ tương thích **`nodejs_compat`**. Ứng dụng Lovable đã có sẵn.
- Một server entry riêng: `vite.config.ts` đặt `tanstackStart: { server: { entry: "server" } }`. Ứng dụng Lovable đã có sẵn.

> [!IMPORTANT]
> Giữ nguyên những gì ứng dụng đang làm. Nếu đã có `src/server.ts` hoặc `src/start.ts`, hãy thêm vào — đừng thay thế. Middleware hiện có (header bảo mật, CSRF, xác thực, trang lỗi) giữ nguyên chỗ và thứ tự.

## 1. Cài đặt

```bash
npm install @belvador/tanstack-start@0.6.1
```

## 2. Trình duyệt — `src/instrument.ts` và `src/router.tsx`

Tạo `src/instrument.ts`. File này khởi động Belvador trên trình duyệt:

```ts title="src/instrument.ts"
import { init } from "@belvador/tanstack-start/client";

// Imported first by src/start.ts and src/router.tsx. Runs only in the browser.
if (typeof window !== "undefined") {
  init({
    dsn: "https://<public-key>@ingest.belvador.dev/<project-id>",
    release: import.meta.env.VITE_APP_VERSION, // your build or commit id
    environment: import.meta.env.MODE,
  });
}
```

Import nó ở **dòng đầu tiên** của `src/router.tsx`, trước route tree:

```ts title="src/router.tsx"
import "./instrument"; // first line, before the route tree
import { withBelvadorRouter } from "@belvador/tanstack-start/client";

// In getRouter(): wrap the options you already pass to createRouter.
// createRouter(withBelvadorRouter({ routeTree, ...yourOptions }))
// It reports route errors, calls your own defaultOnCatch after that, and adds
// TanStack's error component only if you set no defaultErrorComponent.
```

- `init` chỉ chạy trên trình duyệt (`typeof window !== "undefined"`); phần máy chủ cấu hình riêng ở dưới.
- Các import chạy trước phần còn lại của file. Nếu `init` nằm trong thân `src/router.tsx`, lỗi xảy ra lúc một module route hoặc `src/start.ts` đang nạp sẽ đến trước `init` và không bao giờ được báo; trang vẫn hiện nhờ server render nhưng không bấm được gì. Bước 4 import cùng file này ở đầu `src/start.ts`, vì trình duyệt nạp file đó trước router.
- `withBelvadorRouter(options)` bọc các tuỳ chọn bạn đang truyền cho `createRouter`. Hàm này đặt `defaultOnCatch` để báo lỗi mà error boundary của route bắt được, rồi gọi `defaultOnCatch` của bạn nếu có. TanStack Router **chỉ gọi `defaultOnCatch` ở nơi có error component** (lớp dự phòng ngoài cùng không gọi), nên khi bạn chưa đặt `defaultErrorComponent`, helper dùng `ErrorComponent` có sẵn của TanStack. `errorComponent` riêng của từng route vẫn được ưu tiên. Trong thí nghiệm của mình, một route bị crash trong ứng dụng không có error component nào được báo 0/3 lần chỉ với `defaultOnCatch: belvadorOnCatch`, và 3/3 lần với helper.
- Trước 0.5.0, cách cài là `defaultOnCatch: belvadorOnCatch` cộng một error component; cách đó vẫn chạy và `belvadorOnCatch` vẫn được export.
- Lỗi chưa bắt và promise bị từ chối không xử lý trên trình duyệt được bắt ngay khi `init` chạy.
- Đừng đặt tên file là `instrument.client.ts`: cơ chế import protection của TanStack Start chặn file `.client` được import từ `src/router.tsx`, vì file đó cũng chạy trên server.

## 3. Máy chủ — `src/server.ts`

```ts title="src/server.ts"
import handler from "@tanstack/react-start/server-entry";
import { withBelvador } from "@belvador/tanstack-start/server";

// Requires Cloudflare Workers with nodejs_compat enabled.
// If src/server.ts already exists, wrap its default export instead of `handler`.
// vite.config.ts needs tanstackStart: { server: { entry: "server" } } (Lovable apps have it).
export default withBelvador(
  (env: { BELVADOR_DSN?: string; BELVADOR_RELEASE?: string }) => ({
    dsn: env.BELVADOR_DSN ?? "https://<public-key>@ingest.belvador.dev/<project-id>",
    release: env.BELVADOR_RELEASE,
  }),
  handler,
);
```

Nếu ứng dụng đã có `src/server.ts` — ứng dụng Lovable có sẵn — hãy bọc default export **của file đó** thay cho `handler`, và giữ nguyên mọi thứ nó đang làm, ví dụ:

```ts
import { withBelvador } from "@belvador/tanstack-start/server";

const app = {
  async fetch(request: Request, env: unknown, ctx: unknown) {
    // …server entry hiện có, giữ nguyên…
  },
};

export default withBelvador(
  (env: { BELVADOR_DSN?: string; BELVADOR_RELEASE?: string }) => ({
    dsn: env.BELVADOR_DSN ?? "https://<public-key>@ingest.belvador.dev/<project-id>",
    release: env.BELVADOR_RELEASE,
  }),
  app,
);
```

`withBelvador` cho mỗi request một phạm vi riêng, báo lỗi rồi ném lại lỗi, và trả về response của bạn không đổi. Việc gửi hoàn tất sau khi trả response, trong `waitUntil`.

Cấu hình phía máy chủ lấy từ biến của Worker nếu có: `BELVADOR_DSN`, `BELVADOR_RELEASE` (nếu không có thì dùng `CF_VERSION_METADATA.id`), `BELVADOR_ENVIRONMENT` (mặc định `production`) và `BELVADOR_DISABLED=1`. SDK không bao giờ đọc biến `SENTRY_*`.

## 4. Middleware — `src/start.ts`

```ts title="src/start.ts"
import "./instrument"; // first line: the browser loads start.ts before the router
import { createStart } from "@tanstack/react-start";
import { belvadorFunctionMiddleware, belvadorRequestMiddleware } from "@belvador/tanstack-start";

export const startInstance = createStart(() => ({
  // Last in each list, after your own middleware.
  requestMiddleware: [/* …your middleware, */ belvadorRequestMiddleware],
  functionMiddleware: [/* …your middleware, */ belvadorFunctionMiddleware],
}));
```

Dòng đầu tiên import `./instrument`, giống bước 2: trên trình duyệt, `src/start.ts` và các import của nó nạp trước router. Đặt cả hai middleware **cuối cùng** trong danh sách — gần handler nhất — để chúng thấy lỗi trước khi middleware trang lỗi biến lỗi thành response. Nếu đã có `src/start.ts`, nối thêm vào các mảng hiện có và giữ phần còn lại.

## 5. CSP

```text
connect-src 'self' https://ingest.belvador.dev;
```

Gộp vào chính sách hiện có của ứng dụng — ví dụ nơi tạo header bảo mật — thay vì thay cả chính sách.

## Những gì được báo

| Ở đâu | Được báo | Không được báo |
|---|---|---|
| Trình duyệt | Lỗi chưa bắt, promise bị từ chối không xử lý, error boundary của route (`defaultOnCatch`) | Lỗi code của bạn đã bắt và xử lý; `AbortError` của request bị huỷ |
| Server function | Lỗi được ném ra, kèm stack phía máy chủ | `redirect()`, `notFound()`, lỗi có mã trạng thái dưới 500 (kể cả 499), request người dùng đã huỷ |
| Server route | Lỗi được ném ra, kèm stack phía máy chủ | Response bạn tự trả về, kể cả mã 500 — chỉ lỗi được ném ra mới được thấy; request người dùng đã huỷ |
| Loader của route | Lỗi được ném ra — khi render trên máy chủ thì báo từ máy chủ, kèm stack máy chủ; sau khi chuyển trang trên trình duyệt thì báo từ trình duyệt. Mỗi lỗi một lần | `redirect()`, `notFound()` và các lỗi khác như với server function |

Request bị huỷ là khi người dùng đóng tab hoặc chuyển trang: một `AbortError`, hoặc bất kỳ lỗi nào ném ra sau khi client đã ngắt kết nối. Đây không phải lỗi của app nên từ 0.2.0 chúng được bỏ qua; đặt [`captureAborted: true`](https://belvador.dev/vi/docs/sdk/configuration#tuy-chon-chung) để báo lại.

Việc báo lỗi loader cần `withBelvadorRouter({ routeTree, ... })` ở bước 2 (từ 0.5.1) và nửa máy chủ ở bước 3. Nếu chưa có nửa máy chủ, lỗi loader khi render trên máy chủ vẫn được báo một lần — từ trình duyệt, không có stack máy chủ. Nếu bạn tự đặt `dehydrate`/`hydrate`, hãy truyền chúng bên trong `withBelvadorRouter`: SDK thêm một mã băm ngắn cho mỗi lỗi máy chủ đã báo (không bao giờ gửi nội dung lỗi), và `hydrate` của bạn nhận đúng những gì `dehydrate` của bạn trả về. Các tích hợp bọc chúng sau `createRouter`, như tích hợp SSR của TanStack Query, vẫn chạy bình thường.

## Kiểm tra

Chạy phép kiểm kết nối cho cả hai nửa — tab **Kết nối** của project hiện từng nửa chuyển sang **Đã xác minh**.

Trình duyệt, một lần (ví dụ từ một nút debug):

```ts
import { verifyIntegration } from "@belvador/tanstack-start/client";

// Run once after setup or deploy (for example behind a debug flag), never on every page load.
// The console shows one expected "Belvador verification (uncaught)" error — that is the test.
const result = await verifyIntegration();
// result.outcome: "verified" | "problem" | "unconfirmed"; the console prints one summary line.
```

Máy chủ — một route tạm phải chạy trong middleware:

```ts title="src/routes/api/verify-belvador.ts"
import { createFileRoute } from "@tanstack/react-router";
import { verifyIntegration } from "@belvador/tanstack-start/server";

// Temporary: open /api/verify-belvador once after deploying, then delete this file.
// It must run inside belvadorRequestMiddleware (src/start.ts) — that is part of the check.
export const Route = createFileRoute("/api/verify-belvador")({
  server: {
    handlers: {
      GET: async () => Response.json(await verifyIntegration()),
    },
  },
});
```

Sau đó đọc lại kết quả, hoặc xem tab Kết nối:

```bash
curl -s "https://ingest.belvador.dev/api/<project-id>/verification/bv1_<id printed by your app>/?sentry_key=<public-key>"
```

Xoá code tạm sau đó. Mọi kết quả và mã đều được giải thích trong [Kiểm tra kết nối](https://belvador.dev/vi/docs/sdk/verify).

## Ứng dụng Lovable

Nhờ agent của Lovable cài giúp: trên trang project trong Belvador, sao chép mục **Dán vào AI agent** và gửi vào khung chat Lovable. Lời nhắc chứa đúng các bước ở trên kèm DSN của bạn. [Hướng dẫn cho AI agent](https://belvador.dev/vi/docs/ai-agents) liệt kê những gì agent phải giữ nguyên.

## Khắc phục nhanh

- **Chỉ thấy lỗi trình duyệt** — kiểm tra middleware nằm cuối cả hai danh sách, `src/server.ts` bọc đúng entry thật, và đã bật `nodejs_compat`.
- **Lỗi về hai lần** — gỡ mọi cài đặt `@sentry/*` cũ và lời gọi `Sentry.init`.
- Thêm ở [Khắc phục sự cố](https://belvador.dev/vi/docs/troubleshooting).
