# Gắn monitor bằng SDK Belvador

> withMonitor, captureCheckIn và heartbeat cho TanStack Start và Cloudflare Workers; tạo monitor trong Belvador hoặc từ code.

Từ `@belvador/*` 0.3.0, các gói phía máy chủ báo việc chạy ngầm bằng ba hàm. Chúng không bao giờ ném lỗi, không đổi giá trị job trả về, và không làm gì khi SDK tắt.

| Hàm | Dùng khi |
|---|---|
| `withMonitor(slug, fn, options?)` | Bọc một lượt chạy: lúc bắt đầu, lúc xong, lỗi và thời lượng được báo giúp bạn |
| `captureCheckIn(checkIn, config?)` | Tự báo lúc bắt đầu hoặc kết thúc, khi việc không nằm gọn trong một lần gọi hàm |
| `heartbeat(slug)` | Báo "vẫn còn sống" cho monitor theo chu kỳ |

Các hàm có trong `@belvador/tanstack-start/server` và `@belvador/cloudflare`. Gói trình duyệt không có: check-in gửi từ trình duyệt bị từ chối.

## Bọc job bằng `withMonitor`

### TanStack Start

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

// Call this route from your scheduler; the run is reported even if it throws.
export const Route = createFileRoute("/api/jobs/nightly-invoices")({
  server: {
    handlers: {
      POST: async () => {
        const result = await withMonitor("nightly-invoices", async (run) => {
          // Your job. Nothing to do? run.skip("nothing_due")
          return { done: true };
        }, { schedule: { type: "crontab", value: "0 2 * * *" }, timezone: "UTC" });
        return Response.json(result);
      },
    },
  },
});
```

Gọi route này từ trình lập lịch của bạn (dịch vụ cron, cron trigger của Workers, GitHub Actions). Bảo vệ nó như mọi endpoint nội bộ.

### Cloudflare Workers

```ts title="src/index.ts"
import { withBelvador, withMonitor } from "@belvador/cloudflare";

export default withBelvador((env: Env) => ({ release: env.BELVADOR_RELEASE }), {
  async scheduled(controller, env, ctx) {
    await withMonitor("nightly-invoices", async (run) => {
      // Your job. Nothing to do? run.skip("nothing_due")
    }, { schedule: { type: "crontab", value: "0 2 * * *" }, timezone: "UTC" });
  },
});
```

Mỗi lượt chạy diễn ra như sau:

1. Gửi `in_progress` khi `fn` bắt đầu.
2. Khi `fn` trả về (hoặc promise hoàn tất), gửi `ok` kèm thời lượng.
3. Khi `fn` ném lỗi, lỗi được ghi như một issue bình thường — liên kết với lượt này — và gửi `error`. Sau đó chính lỗi ấy được ném lại cho code của bạn, không đổi.

### Cho Belvador biết lượt chạy đã làm gì

`fn` nhận một tham số `run`:

- `run.skip("nothing_due")` — job có chạy nhưng không có việc. Lượt chạy là **Bỏ qua**, tính là khoẻ.
- `run.unknown("provider_timeout")` — job kết thúc nhưng không biết việc đã có hiệu lực chưa. Lượt chạy thành **Chưa rõ kết quả** và monitor **Cần xem lại**.

Lý do là mã ngắn: chữ thường, số, `.`, `_`, `-`, tối đa 64 ký tự. Đừng bao giờ đưa dữ liệu khách hàng vào.

### Thử lại

Hàng đợi tự thử lại; Belvador chỉ cần biết những lần thử nào thuộc cùng một lượt:

```ts
await withMonitor("sync-orders", async () => syncOrders(message), {
  runId: message.id,        // cùng một id ở mọi lần thử
  attempt: message.attempts, // 1 cho lần đầu
  willRetry: (error) => message.attempts < 5,
});
```

Lần thử lỗi mà `willRetry` là true hiện là **Sẽ thử lại** và không mở sự cố; kết quả cuối của lượt mới quyết định. Belvador không bao giờ tự chạy lại việc của bạn.

Lỗi của mỗi lần thử hỏng cũng được gửi thành một event của issue, để lượt chạy cho thấy từng lần thử gặp lỗi gì. Ba lần thử của cùng một lỗi là một issue với ba event, và một message chạy được ở lần thứ ba vẫn để lại hai. Khi số event quan trọng hơn (điều kiện archive theo số event, ngưỡng escalating, quota), chỉ báo lần hỏng cuối (SDK 0.5.3):

```ts
await withMonitor("sync-orders", async () => syncOrders(message), {
  runId: message.id,
  attempt: message.attempts,
  willRetry: (error) => message.attempts < 5,
  reportRetries: false, // lần thử còn được thử lại chỉ gửi check-in, không gửi event lỗi
});
```

Lỗi được gửi mang số lần thử trong context `monitor`.

## Tạo monitor

Có hai cách; cả hai cho ra cùng một monitor.

**Trong Belvador.** Mở **Monitor → Tạo monitor**, chọn project, tên, lịch và giới hạn, rồi dùng slug trong code. Lịch và giới hạn sau đó sửa trong Belvador.

**Từ code.** Truyền lịch trong options như các ví dụ trên. Check-in đầu tiên tạo monitor, có nhãn **Định nghĩa trong code**; các check-in sau cập nhật nó. Lịch và giới hạn bị khoá trong Belvador — hãy sửa trong code. Tên và môi trường theo dõi vẫn sửa được trong Belvador.

Các tuỳ chọn định nghĩa monitor từ code:

| Tuỳ chọn | Kiểu | Mặc định |
|---|---|---|
| `schedule` | `{ type: "crontab", value: "0 2 * * *" }` hoặc `{ type: "interval", value: 15, unit: "minute" }` | — (không có thì không tạo gì từ code) |
| `timezone` | tên IANA, ví dụ `"Asia/Ho_Chi_Minh"` | `"UTC"` |
| `checkinMargin` | phút | 5 |
| `maxRuntime` | phút, tối đa 10080 | 30 |
| `failureIssueThreshold` | số lượt liên tiếp | 1 |
| `recoveryThreshold` | số lượt liên tiếp | 1 |
| `environment` | môi trường của lượt chạy này | `environment` của SDK |

### Phát hiện từ code

Project có thể tắt **Cho tạo monitor từ code** (Cài đặt project → Quan sát). Khi đó slug mới hiện ở **Monitor → Phát hiện từ code**; **Duyệt** biến nó thành monitor với cấu hình code đã gửi, **Bỏ qua** ẩn nó 30 ngày. Danh sách này cũng nhận các slug vượt quá 20 monitor mới mỗi project mỗi giờ. Monitor đã xoá không được tạo lại từ code trong 30 ngày.

## `captureCheckIn` và `heartbeat`

Khi lúc bắt đầu và lúc kết thúc nằm ở hai nơi — job chia thành hai hàm, hoặc tiến trình chạy nhiều ngày — hãy tự báo:

```ts
const checkInId = captureCheckIn({ monitorSlug: "import-catalog", status: "in_progress" });
// … sau đó, có thể trong hàm khác …
captureCheckIn({ monitorSlug: "import-catalog", status: "ok", checkInId, duration: 42 });
```

`duration` tính bằng giây. Với monitor theo chu kỳ chỉ cần dấu hiệu còn sống, gọi `heartbeat("queue-worker")` ở mỗi vòng lặp.

## Kiểm tra

1. Chạy job một lần.
2. Mở **Monitor** trong Belvador: lượt chạy hiện sau vài giây, kèm kết quả và thời lượng.
3. Cố ý cho job ném lỗi một lần: lượt chạy ghi **Lỗi** và có liên kết tới issue.

Không thấy gì? Với `debug: true` SDK ghi chẩn đoán ra console; `BLV_MONITOR_INVALID_SLUG` nghĩa là slug không khớp `^[a-z0-9][a-z0-9_-]{0,63}$`. Xem [Khắc phục sự cố](https://belvador.dev/vi/docs/troubleshooting).
