# Supabase Edge Functions

> Một tệp dùng chung với SDK Sentry cho Deno: lỗi của mỗi function được gửi trước khi function trả lời, kèm lỗi { data, error } của truy vấn.

Supabase Edge Functions chạy trên Deno, một worker phục vụ nhiều request. Belvador dùng **SDK Sentry chính thức cho Deno** với DSN Belvador và bộ lọc dữ liệu của chính Belvador, đặt trong một tệp chung mà mọi function cùng import. Handler của mỗi function được bọc lại để lỗi được gửi **trước khi** function trả lời — Edge Function có thể bị dừng ngay sau khi phản hồi, và lỗi còn nằm trong bộ nhớ lúc đó sẽ mất.

> [!NOTE]
> `@sentry/deno` là SDK của Sentry, không phải gói của Belvador. Belvador chạy đúng công thức này trên Supabase Edge Runtime 1.77.8 tới một node thật: lỗi do handler ném, lỗi bạn tự xử lý, kết quả `{ data, error }` và lỗi quá thời gian chờ dịch vụ phía sau đều tới thành issue riêng. Belvador chưa chạy nó trên một project Supabase hosted.

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

- Một project có nền tảng **Khác** và DSN của nó (Edge Functions dùng SDK Deno của Sentry).
- Supabase CLI để chạy `supabase secrets set` và `supabase functions deploy`.

## 1. Thêm tệp dùng chung

```ts title="supabase/functions/_shared/belvador.ts"
import * as Sentry from "npm:@sentry/deno@11.9.0";
import { captureResultWith, scrubBreadcrumb, scrubEvent } from "npm:@belvador/core@0.6.1/sentry";

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",
  // One worker serves many requests: no automatic integrations sharing one scope;
  // withBelvador and report put each request's context on its own event.
  defaultIntegrations: false,
  sendDefaultPii: false,
  tracesSampleRate: 0,
  maxBreadcrumbs: 50,
  maxValueLength: 8192,
  // Belvador's scrubbing: request bodies, cookies and credentials never leave the app.
  beforeSend: scrubEvent,
  beforeBreadcrumb: scrubBreadcrumb,
});

export const captureResult = captureResultWith(Sentry);

/** This request's context, on its own event (the worker's scope is shared). */
function context(name: string, req: Request) {
  return {
    tags: {
      function: name,
      region: Deno.env.get("SB_REGION") ?? "local",
      execution_id: Deno.env.get("SB_EXECUTION_ID") ?? "local",
      "http.request.method": req.method,
    },
  };
}

/** Reports an error you handled and waits (2 s at most) until it is sent. */
export async function report(error: unknown, name: string, req: Request): Promise<void> {
  Sentry.captureException(error, context(name, req));
  await Sentry.flush(2000);
}

/** Wraps a handler: an error it throws is reported, then answered with a 500. */
export function withBelvador(name: string, handler: (req: Request) => Response | Promise<Response>) {
  return async (req: Request): Promise<Response> => {
    try {
      const res = await handler(req);
      // Sends what captureResult queued during the request, after the response.
      (globalThis as { EdgeRuntime?: { waitUntil(p: Promise<unknown>): void } }).EdgeRuntime?.waitUntil(Sentry.flush(2000));
      return res;
    } catch (error) {
      await report(error, name, req);
      return Response.json({ error: "Internal Server Error" }, { status: 500 });
    }
  };
}
```

`Sentry.init` chạy một lần cho mỗi worker. Ý nghĩa các tuỳ chọn:

| Tuỳ chọn | Vì sao |
|---|---|
| `defaultIntegrations: false` | Một worker phục vụ nhiều request cùng lúc với một scope chung; nếu để integration tự động, tag và breadcrumb của request này sẽ dính vào lỗi của request khác. `withBelvador` và `report` gắn ngữ cảnh của từng request vào đúng event của nó. |
| `sendDefaultPii: false`, `tracesSampleRate: 0` | SDK không tự thêm địa chỉ IP hay thông tin người dùng; không gửi span |
| `maxBreadcrumbs: 50`, `maxValueLength: 8192` | Cùng giới hạn với SDK của Belvador |
| `beforeSend: scrubEvent`, `beforeBreadcrumb: scrubBreadcrumb` | Bộ lọc của Belvador, đúng đoạn mã SDK của Belvador chạy: body request, cookie, header xác thực và thông tin đăng nhập trong văn bản không rời khỏi function |

Mỗi event mang các tag `function`, `region` và `execution_id` (lấy từ `SB_REGION` và `SB_EXECUTION_ID` của Supabase), để bạn biết function nào và lần chạy nào bị lỗi.

## 2. Bọc từng function

```ts title="supabase/functions/list-invoices/index.ts"
import { createClient } from "npm:@supabase/supabase-js@2";
import { captureResult, withBelvador } from "../_shared/belvador.ts";

Deno.serve(
  withBelvador("list-invoices", async (req) => {
    const supabase = createClient(Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_ANON_KEY")!, {
      global: { headers: { Authorization: req.headers.get("Authorization") ?? "" } },
    });
    const { data, error } = captureResult(await supabase.from("invoices").select("id, total"), {
      operation: "select invoices",
    });
    if (error) return Response.json({ error: "Could not load invoices" }, { status: 503 });
    return Response.json(data);
  }),
);
```

`withBelvador(name, handler)`:

- Lỗi do handler ném được báo, được gửi (chờ tối đa 2 giây), rồi trả `500`.
- `captureResult` báo `error` của truy vấn Supabase, có kiểu theo mã Postgres (`db.permission_denied`, `db.query_canceled`, …), và trả nguyên kết quả — xem [Lỗi cơ sở dữ liệu](https://belvador.dev/vi/docs/sdk/database-errors). Những gì nó xếp hàng được gửi ngay sau phản hồi bằng `EdgeRuntime.waitUntil`.

## 3. Báo lỗi bạn tự xử lý

```ts title="supabase/functions/send-receipt/index.ts"
import { report, withBelvador } from "../_shared/belvador.ts";

Deno.serve(
  withBelvador("send-receipt", async (req) => {
    try {
      await sendReceipt(await req.json(), { signal: AbortSignal.timeout(5000) });
    } catch (error) {
      await report(error, "send-receipt", req); // sent before the function answers
      return Response.json({ error: "Receipt not sent" }, { status: 502 });
    }
    return new Response(null, { status: 204 });
  }),
);
```

`report(error, name, req)` gửi lỗi và chờ gửi xong rồi function mới trả lời. Hãy giới hạn các lời gọi chậm bằng `AbortSignal.timeout` thấp hơn giới hạn của Supabase, để chính code của bạn — không phải nền tảng — quyết định lúc bỏ cuộc và lỗi được gửi trước.

## 4. Đặt DSN và deploy

```bash
supabase secrets set BELVADOR_DSN="https://<public-key>@ingest.belvador.dev/<project-id>" APP_ENV=production
```

Sau đó deploy như thường lệ (`supabase functions deploy <tên>`). Khi chạy máy mình, `supabase functions serve` đọc cùng các biến này từ `supabase/functions/.env`.

## Cài bằng AI agent

Dán đoạn này vào Claude Code, Cursor, Lovable hay một coding agent khác. Nó chứa các tệp ở trên cùng các quy tắc agent phải theo:

````text
Belvador errors: Supabase Edge Functions (@sentry/deno)

Tested with: @sentry/* 11.9.0
Guide: https://belvador.dev/vi/docs/sdk/supabase-edge

Mục tiêu
Báo lỗi của máy chủ này về Belvador bằng SDK Sentry chính thức và bộ lọc của Belvador, đúng như các tệp dưới đây. Không thêm SDK lỗi nào khác: mỗi tiến trình một Sentry.init.

Quy tắc
Chép nguyên các tệp và giữ mọi tuỳ chọn của Sentry.init: sendDefaultPii: false, tracesSampleRate: 0, maxBreadcrumbs: 50, beforeSend: scrubEvent, beforeBreadcrumb: scrubBreadcrumb. Đọc DSN từ BELVADOR_DSN; không tự đặt thêm biến môi trường khác. Không đổi xác thực, định tuyến hay logic nghiệp vụ. Báo những lỗi code đã tự xử lý bằng captureException (report() trên Supabase Edge Functions) và bọc bằng captureResult các kết quả Supabase hay PostgREST trả lỗi thay vì ném, không đổi cách code dùng kết quả đó.

supabase/functions/_shared/belvador.ts
```typescript
import * as Sentry from "npm:@sentry/deno@11.9.0";
import { captureResultWith, scrubBreadcrumb, scrubEvent } from "npm:@belvador/core@0.6.1/sentry";

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",
  // One worker serves many requests: no automatic integrations sharing one scope;
  // withBelvador and report put each request's context on its own event.
  defaultIntegrations: false,
  sendDefaultPii: false,
  tracesSampleRate: 0,
  maxBreadcrumbs: 50,
  maxValueLength: 8192,
  // Belvador's scrubbing: request bodies, cookies and credentials never leave the app.
  beforeSend: scrubEvent,
  beforeBreadcrumb: scrubBreadcrumb,
});

export const captureResult = captureResultWith(Sentry);

/** This request's context, on its own event (the worker's scope is shared). */
function context(name: string, req: Request) {
  return {
    tags: {
      function: name,
      region: Deno.env.get("SB_REGION") ?? "local",
      execution_id: Deno.env.get("SB_EXECUTION_ID") ?? "local",
      "http.request.method": req.method,
    },
  };
}

/** Reports an error you handled and waits (2 s at most) until it is sent. */
export async function report(error: unknown, name: string, req: Request): Promise<void> {
  Sentry.captureException(error, context(name, req));
  await Sentry.flush(2000);
}

/** Wraps a handler: an error it throws is reported, then answered with a 500. */
export function withBelvador(name: string, handler: (req: Request) => Response | Promise<Response>) {
  return async (req: Request): Promise<Response> => {
    try {
      const res = await handler(req);
      // Sends what captureResult queued during the request, after the response.
      (globalThis as { EdgeRuntime?: { waitUntil(p: Promise<unknown>): void } }).EdgeRuntime?.waitUntil(Sentry.flush(2000));
      return res;
    } catch (error) {
      await report(error, name, req);
      return Response.json({ error: "Internal Server Error" }, { status: 500 });
    }
  };
}
```

supabase/functions/list-invoices/index.ts
```typescript
import { createClient } from "npm:@supabase/supabase-js@2";
import { captureResult, withBelvador } from "../_shared/belvador.ts";

Deno.serve(
  withBelvador("list-invoices", async (req) => {
    const supabase = createClient(Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_ANON_KEY")!, {
      global: { headers: { Authorization: req.headers.get("Authorization") ?? "" } },
    });
    const { data, error } = captureResult(await supabase.from("invoices").select("id, total"), {
      operation: "select invoices",
    });
    if (error) return Response.json({ error: "Could not load invoices" }, { status: 503 });
    return Response.json(data);
  }),
);
```

supabase/functions/send-receipt/index.ts
```typescript
import { report, withBelvador } from "../_shared/belvador.ts";

Deno.serve(
  withBelvador("send-receipt", async (req) => {
    try {
      await sendReceipt(await req.json(), { signal: AbortSignal.timeout(5000) });
    } catch (error) {
      await report(error, "send-receipt", req); // sent before the function answers
      return Response.json({ error: "Receipt not sent" }, { status: 502 });
    }
    return new Response(null, { status: 204 });
  }),
);
```

```bash
supabase secrets set BELVADOR_DSN="https://<public-key>@ingest.belvador.dev/<project-id>" APP_ENV=production
```

Kiểm tra
Chạy app trên máy với BELVADOR_DSN đã đặt và gây một lỗi thử (một route hay script ngắn ném lỗi). Xác nhận nó hiện thành issue trong project Belvador. Không gửi lỗi thử lên production nếu người dùng chưa đồng ý.

Báo cáo
Liệt kê các tệp đã đổi, gói đã thêm kèm đúng phiên bản, biến môi trường app giờ cần, lỗi thử bạn thấy đã tới, và những gì chưa kiểm được.
````

## Giới hạn cần biết

- **Chỉ handler được bọc mới báo lỗi.** Lỗi trong timer, promise không `await` hoặc code module nằm ngoài handler sẽ không được báo: runtime của Supabase không có hook toàn cục nào chạy chắc chắn trước khi worker dừng. Hãy `await` công việc bên trong handler.
- **Nền tảng có thể dừng function trước.** Supabase dừng function khi chạm giới hạn CPU, bộ nhớ hoặc thời gian chạy (xem [giới hạn của Supabase](https://supabase.com/docs/guides/functions/limits)); lỗi chưa gửi vào lúc đó sẽ mất. `EdgeRuntime.waitUntil` không kéo dài các giới hạn này.
- **Không có đĩa:** lỗi không được giữ qua lần khởi động lại (`@belvador/spool` cần hệ thống tệp). Nếu node của bạn không liên lạc được, SDK thử lại vài giây rồi bỏ lỗi.
- Log nền tảng của Supabase (Postgres, Auth, Edge Functions) đi qua [log drain](https://belvador.dev/vi/docs/otel/supabase-log-drains), không qua công thức này.
