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.
Trong bài này
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.
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 setvàsupabase functions deploy.
1. Thêm tệp dùng chung#
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#
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.captureResultbáoerrorcủ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. Những gì nó xếp hàng được gửi ngay sau phản hồi bằngEdgeRuntime.waitUntil.
3. Báo lỗi bạn tự xử lý#
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#
supabase secrets set BELVADOR_DSN="https://<public-key>@ingest.belvador.dev/<project-id>" APP_ENV=productionSau đó 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:
Belvador errors: Supabase Edge Functions (@sentry/deno)Tested with: @sentry/* 11.9.0Guide: https://belvador.dev/vi/docs/sdk/supabase-edgeMục tiêuBá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ắcChé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```typescriptimport * 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```typescriptimport { 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```typescriptimport { 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 }); }),);``````bashsupabase secrets set BELVADOR_DSN="https://<public-key>@ingest.belvador.dev/<project-id>" APP_ENV=production```Kiểm traChạ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áoLiệ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
awaithoặ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ãyawaitcô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); lỗi chưa gửi vào lúc đó sẽ mất.
EdgeRuntime.waitUntilkhô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/spoolcầ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, không qua công thức này.
Thấy chỗ sai hoặc thiếu? Gửi cho chúng tôi: hello@belvador.dev