Supabase Edge Functions
One shared file on Sentry's Deno SDK: each function's errors are sent before it answers, query { data, error } results included.
On this page
Supabase Edge Functions run on Deno, one worker serving many requests. Belvador uses the official Sentry SDK for Deno with your Belvador DSN and Belvador's own scrubbing, in one shared file that every function imports. Each function's handler is wrapped so an error is sent before the function answers — an Edge Function can be stopped as soon as it has responded, and an error still waiting in memory would be lost.
Before you start#
A project with platform Other and its DSN (Edge Functions use Sentry's Deno SDK).
The Supabase CLI for
supabase secrets setandsupabase functions deploy.
1. Add the shared file#
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 runs once per worker. What the options do:
| Option | Why |
|---|---|
defaultIntegrations: false | A worker serves many requests at once with one shared scope; with automatic integrations, one request's tags and breadcrumbs would land on another request's error. withBelvador and report put each request's context on its own event instead. |
sendDefaultPii: false, tracesSampleRate: 0 | No IP addresses or user details added by the SDK; no spans |
maxBreadcrumbs: 50, maxValueLength: 8192 | Same bounds as Belvador's own SDKs |
beforeSend: scrubEvent, beforeBreadcrumb: scrubBreadcrumb | Belvador's scrubbing, the same code its SDKs run: request bodies, cookies, auth headers and credentials in text never leave the function |
Every event carries the tags function, region and execution_id (from Supabase's SB_REGION and SB_EXECUTION_ID), so you can tell which function and which run failed.
2. Wrap each 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):
An error the handler throws is reported, sent (waiting at most 2 seconds), then answered with a
500.captureResultreports theerrorof a Supabase query typed by its Postgres code (db.permission_denied,db.query_canceled, …) and hands the result back unchanged — see Database errors. What it queued is sent right after the response withEdgeRuntime.waitUntil.
3. Report errors you handle#
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) sends the error and waits for it before your function answers. Bound slow calls with AbortSignal.timeout below Supabase's own limit, so your code — not the platform — decides when to give up and the error is sent first.
4. Set the DSN and deploy#
supabase secrets set BELVADOR_DSN="https://<public-key>@ingest.belvador.dev/<project-id>" APP_ENV=productionThen deploy as usual (supabase functions deploy <name>). Locally, supabase functions serve reads the same variables from supabase/functions/.env.
Set it up with an AI agent#
Paste this into Claude Code, Cursor, Lovable or another coding agent. It contains the files above and the rules it must follow:
Belvador errors: Supabase Edge Functions (@sentry/deno)Tested with: @sentry/* 11.9.0Guide: https://belvador.dev/en/docs/sdk/supabase-edgeGoalReport this server's errors to Belvador with the official Sentry SDK and Belvador's scrubbing, exactly as in the files below. Keep any other error SDK out: one Sentry.init per process.RulesCopy the files as they are and keep every Sentry.init option: sendDefaultPii: false, tracesSampleRate: 0, maxBreadcrumbs: 50, beforeSend: scrubEvent, beforeBreadcrumb: scrubBreadcrumb. Read the DSN from BELVADOR_DSN; do not invent other environment variables. Do not change authentication, routing or business logic. Report errors the code already handles with captureException (report() on Supabase Edge Functions) and wrap Supabase or PostgREST results that return an error instead of throwing with captureResult, without changing what the code does with the result.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```VerifyStart the app locally with BELVADOR_DSN set and trigger one test error (a route or a one-off script that throws). Confirm it appears as an issue in the Belvador project. Do not send test errors to production unless the user says so.ReportList the files you changed, the packages added with exact versions, the environment variables the app now needs, the test error you saw arrive, and anything you could not verify.Limits to know#
Only wrapped handlers report errors. An error in a timer, an un-awaited promise or module code outside the handler is not reported: Supabase's runtime has no global hook that reliably runs before the worker stops. Await your work inside the handler.
The platform can stop a function first. Supabase stops a function at its CPU, memory or wall-clock limit (see Supabase's limits); an error still unsent at that moment is lost.
EdgeRuntime.waitUntildoes not extend those limits.No disk: errors are not kept across a restart (
@belvador/spoolneeds a filesystem). If your node is unreachable, the SDK retries for a few seconds and then drops the error.Supabase's own platform logs (Postgres, Auth, Edge Functions) go through a log drain, not this recipe.
Spotted something wrong or missing? Tell us: hello@belvador.dev