# Offline và gửi bền

> Lỗi được giữ lại khi chưa gửi được: thử lại có độ trễ ngẫu nhiên, hàng đợi IndexedDB với offline: true trên trình duyệt, và @belvador/spool ghi xuống đĩa cho Node.js, Bun và Deno.

Lỗi thường xảy ra đúng lúc mạng tệ nhất: đi qua đường hầm, node của bạn đang khởi động lại, máy chủ mất kết nối tới dịch vụ phía trên. Trang này cho biết SDK của Belvador làm gì khi một lỗi chưa gửi được, và cách giữ lỗi lại cho tới khi gửi được. Mọi thứ ở đây có từ SDK 0.6.0.

## Mọi SDK đã làm sẵn

Mỗi lần gửi được thử tối đa 5 lần. Thời gian chờ trước mỗi lần thử lại là ngẫu nhiên, từ 0 tới 1, 2, 4, rồi 8 giây (tối đa 30 giây) — để một nghìn app cùng mất kết nối không quay lại cùng một giây. `503` có `Retry-After` thì chờ đúng như yêu cầu. Mỗi lần thử lại gửi đúng envelope cũ với cùng event id, nên node của bạn chỉ đếm lỗi một lần.

Nếu không bật các tuỳ chọn dưới đây, lỗi vẫn không gửi được sau lần thử cuối sẽ mất, và trang bị đóng sẽ mang theo các lỗi đang chờ. Quy tắc đầy đủ nằm ở [Cách gửi](https://belvador.dev/vi/docs/sdk/configuration#cach-gui).

## Trình duyệt: `offline: true`

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

init({
  dsn: "https://<public-key>@ingest.belvador.dev/<project-id>",
  // Errors that cannot be sent wait in this browser, then go out when it is back online.
  offline: true,
});
```

Với `offline: true`, lỗi gửi hỏng vì mạng được giữ trong trình duyệt này (IndexedDB) và gửi lại:

- ngay khi trình duyệt báo đã có mạng lại,
- hoặc ở lần tải trang kế tiếp, kể cả khi tab đã bị đóng.

| Quy tắc | Giá trị |
|---|---|
| Giữ tối đa | 50 envelope cho mỗi DSN — vượt quá thì bỏ cái cũ nhất và đếm là tràn hàng đợi |
| Giữ trong | 72 giờ — cũ hơn thì bỏ thay vì gửi |
| Thứ tự | cũ nhất trước |
| Câu trả lời không phải lỗi mạng | `4xx` từ node không bao giờ được lưu: gửi lại cũng không đổi được gì |
| Chi phí | khoảng 1,8 KB gzip, tải thành một tệp riêng chỉ khi đặt `offline: true` |

Dùng giống hệt với `@belvador/react` và `@belvador/tanstack-start/client`. Ở chế độ xem trước (`preview: true`) không lưu gì, vì không gửi gì.

Khi trình duyệt không dùng được IndexedDB (một số cửa sổ ẩn danh, chính sách chặn lưu trữ) hoặc không tải được tệp offline, SDK gửi như cũ, không có hàng đợi, và báo [`BLV_OFFLINE_UNAVAILABLE`](https://belvador.dev/vi/docs/sdk/verify#blv-offline-unavailable) một lần.

> [!NOTE]
> Thứ được lưu chính là thứ lẽ ra đã gửi đi: event sau `beforeSend` của bạn, luật của project và bộ che của Belvador. Nó nằm trong bộ nhớ của trình duyệt cho site này tới khi được gửi hoặc hết hạn, và script khác của cùng site đọc được — cẩn thận như với bất cứ thứ gì bạn để trong `localStorage`.

## Node.js, Bun và Deno: `@belvador/spool`

Máy chủ dùng SDK chính thức của Sentry với DSN của Belvador có thể giữ lỗi trên đĩa khi node chưa liên lạc được. Khi node trả lời lại, lỗi được gửi theo thứ tự cũ nhất trước — cả bởi tiến trình kế tiếp, nếu tiến trình này đã khởi động lại hoặc bị kill.

```bash
npm install @belvador/spool@0.6.1
```

**Node.js** (`@sentry/node`):

```js title="instrument.mjs"
import * as Sentry from "@sentry/node";
import { scrubBreadcrumb, scrubEvent } from "@belvador/core/sentry";
import { withSpool } from "@belvador/spool";

Sentry.init({
  dsn: process.env.BELVADOR_DSN ?? "https://<public-key>@ingest.belvador.dev/<project-id>",
  release: process.env.APP_VERSION,
  environment: process.env.NODE_ENV,
  sendDefaultPii: false,
  tracesSampleRate: 0,
  maxBreadcrumbs: 50,
  maxValueLength: 8192,
  // Belvador's scrubbing: request bodies, cookies and credentials never leave the app.
  beforeSend: scrubEvent,
  beforeBreadcrumb: scrubBreadcrumb,
  // Errors that cannot be sent wait on disk and go out after a restart.
  transport: withSpool(Sentry.makeNodeTransport, {
    dir: process.env.BELVADOR_SPOOL_DIR ?? "/var/lib/my-app/belvador",
  }),
});
```

**Bun** (`@sentry/bun`):

```ts title="instrument.ts"
import * as Sentry from "@sentry/bun";
import { scrubBreadcrumb, scrubEvent } from "@belvador/core/sentry";
import { withSpool } from "@belvador/spool";

Sentry.init({
  dsn: process.env.BELVADOR_DSN ?? "https://<public-key>@ingest.belvador.dev/<project-id>",
  release: process.env.APP_VERSION,
  environment: process.env.NODE_ENV,
  sendDefaultPii: false,
  tracesSampleRate: 0,
  maxBreadcrumbs: 50,
  maxValueLength: 8192,
  // Belvador's scrubbing: request bodies, cookies and credentials never leave the app.
  beforeSend: scrubEvent,
  beforeBreadcrumb: scrubBreadcrumb,
  // Errors that cannot be sent wait on disk and go out after a restart.
  transport: withSpool(Sentry.makeFetchTransport, {
    dir: process.env.BELVADOR_SPOOL_DIR ?? "./.belvador-spool",
  }),
});
```

**Deno** (`npm:@sentry/deno`):

```ts title="instrument.ts"
import * as Sentry from "npm:@sentry/deno@11.9.0";
import { scrubBreadcrumb, scrubEvent } from "npm:@belvador/core@0.6.1/sentry";
import { makeFetchTransport, withSpool } from "npm:@belvador/spool@0.6.1";

Sentry.init({
  dsn: Deno.env.get("BELVADOR_DSN") ?? "https://<public-key>@ingest.belvador.dev/<project-id>",
  release: Deno.env.get("APP_VERSION"),
  environment: Deno.env.get("APP_ENV") ?? "production",
  sendDefaultPii: false,
  tracesSampleRate: 0,
  maxBreadcrumbs: 50,
  maxValueLength: 8192,
  // Belvador's scrubbing: request bodies, cookies and credentials never leave the app.
  beforeSend: scrubEvent,
  beforeBreadcrumb: scrubBreadcrumb,
  // Errors that cannot be sent wait on disk and go out after a restart.
  transport: withSpool(makeFetchTransport, {
    dir: Deno.env.get("BELVADOR_SPOOL_DIR") ?? "./.belvador-spool",
  }),
});
```

Spool cần quyền mạng tới host trong DSN, hai biến môi trường và quyền đọc/ghi thư mục của nó:

```bash
deno run --allow-net=ingest.belvador.dev \
  --allow-env=BELVADOR_DSN,BELVADOR_SPOOL_DIR \
  --allow-read=./.belvador-spool --allow-write=./.belvador-spool \
  main.ts
```

| Tuỳ chọn | Mặc định | Tác dụng |
|---|---|---|
| `dir` | — (bắt buộc) | Mỗi app một thư mục, tạo với quyền `0700`; tệp có quyền `0600`. Nhiều tiến trình của cùng app có thể dùng chung. |
| `maxBytes` | 64 MiB | Tổng dung lượng trên đĩa. Envelope cũ nhất bị bỏ trước, đếm là tràn hàng đợi. |
| `ttlHours` | 72 | Envelope cũ hơn bị bỏ thay vì gửi. |
| `key` | chưa đặt | 32 byte ngẫu nhiên của riêng bạn: envelope trên đĩa được mã hoá AES-256-GCM. |

Những gì đã kiểm thử: 200 lỗi trong lúc điểm nhận lỗi ngừng, tiến trình bị kill bằng `SIGKILL`, tiến trình mới khởi động khi điểm nhận chạy lại — đủ 200 lỗi tới nơi, không lỗi nào hai lần, trên Node.js, Bun và Deno. Tiến trình bị kill giữa lúc ghi chỉ để lại tối đa một tệp ghi dở, lần khởi động sau bỏ và đếm nó; mọi envelope hoàn chỉnh vẫn tới đúng một lần. Một đợt lỗi dồn dập lớn hơn bộ đệm trong bộ nhớ của SDK Sentry (64 lần gửi) được ghi xuống đĩa thay vì bị bỏ. Các công thức ở trên, đúng như bản phát hành, cũng đã chạy tới một node thật với `@sentry/node` và `@sentry/bun` 11.9.0: lỗi xảy ra khi node không liên lạc được đều tới sau một lần `SIGKILL` và, với Node.js, sau một lần sập vì hết bộ nhớ.

Đang dùng `makeOfflineTransport` của SDK Sentry? Truyền `createStore: makeFileStore({ dir })` từ `@belvador/spool` thay cho kho mặc định. Dù vậy `withSpool` vẫn là lựa chọn tốt hơn: nó giữ được cả đợt lỗi vượt bộ đệm 64 lần gửi và không đếm lỗi đã lưu là bị mất.

## Môi trường chạy không có đĩa

- **Cloudflare Workers** không có đĩa và isolate có thể dừng bất cứ lúc nào. `@belvador/cloudflare` thử lại như trên bên trong `waitUntil`; lỗi vẫn hỏng thì mất.
- **Supabase Edge Functions** và các function sống ngắn khác: spool trên đĩa tạm sẽ biến mất cùng instance, nên không được lợi gì. Thay vào đó hãy gửi trước khi trả lời — xem [Supabase Edge Functions](https://belvador.dev/vi/docs/sdk/supabase-edge).
- **Python** (`sentry-sdk`) chỉ có hàng đợi trong bộ nhớ; chưa có spool trên đĩa.

## Điều này không hứa hẹn

Cả hàng đợi trình duyệt lẫn spool đều không hứa mọi lỗi sẽ tới nơi. Đĩa hoặc hạn mức lưu trữ đầy, tệp hỏng, hay envelope quá thời hạn sẽ bị bỏ và được đếm trong báo cáo gửi của SDK. App của bạn không bao giờ phải chờ bộ lưu trữ và không bao giờ thấy lỗi từ nó.
