Error reporting and alerting
Beignet separates three observability concerns:
- Logging records structured runtime facts.
- Error reporting captures exceptions and failure context for triage.
- Alerting turns symptoms into operator action.
Use ErrorReporterPort when application code needs to report an unexpected
failure without importing a vendor SDK.
Capture errors
Declare errorReporter as an app port and report unexpected failures from use
cases, jobs, schedules, outbox drains, and tasks:
await ctx.ports.errorReporter.captureException(error, {
level: "error",
requestId: ctx.requestId,
traceId: ctx.traceId,
tags: {
feature: "billing",
},
contexts: {
tenant: { id: ctx.tenant.id },
},
});Capture messages for important non-exception signals:
await ctx.ports.errorReporter.captureMessage("Payment provider degraded", {
level: "warning",
tags: { provider: "stripe" },
});Expected business failures such as validation failures, not-found results, denied policies, and known catalog errors usually belong in logs or audit records, not high-priority exception alerts.
Setup with Sentry
Install the Sentry provider:
bun add @beignet/provider-error-reporting-sentry @sentry/nodeRegister it in server/providers.ts. Provider-contributed ports replace
earlier bound ports during startup, so a generated app can keep its local
createNoopErrorReporter() fallback and let this provider replace it when
Sentry is installed.
import { createSentryErrorReportingProvider } from "@beignet/provider-error-reporting-sentry";
export const providers = [
createSentryErrorReportingProvider({
dsn: process.env.SENTRY_DSN,
init: {
environment: process.env.NODE_ENV,
},
}),
];The provider contributes ctx.ports.errorReporter and ctx.ports.sentry as an
escape hatch for advanced Sentry operations. With no dsn or SENTRY_DSN, the
provider still contributes the Beignet port but does not initialize Sentry.
When the app registers a separate OpenTelemetry SDK, configure
init: { skipOpenTelemetrySetup: true } so Sentry remains the error reporter
without installing a second tracing pipeline. See
OpenTelemetry for the combined provider order.
HTTP request errors
Install createErrorReportingHooks(...) in server/index.ts to capture
unexpected route, hook, response-validation, and server failures. The hook
observes caught errors without changing response mapping. Use
mapUnhandledError only to decide the response body.
import {
createErrorReportingHooks,
createIdempotencyHooks,
} from "@beignet/core/server";
import { createNextServer, createNextServerLoader } from "@beignet/next";
import type { AppContext } from "@/app-context";
import { initialPorts } from "@/infra/port-wiring";
import { appContext } from "./context";
import { routes } from "./routes";
export const getServer = createNextServerLoader(async () => {
const { providers } = await import("./providers");
return createNextServer({
ports: initialPorts,
providers,
hooks: [
createErrorReportingHooks<AppContext>(),
createIdempotencyHooks<AppContext>(),
],
context: appContext,
routes,
mapUnhandledError: ({ err, ctx }) => {
ctx?.ports.logger.error("Unhandled API error", {
error: err,
requestId: ctx?.requestId,
});
return {
status: 500,
body: {
code: "INTERNAL_SERVER_ERROR",
message: "Internal server error",
requestId: ctx?.requestId,
},
};
},
});
});By default the hook captures unexpected failures and skips expected application
catalog errors, auth denials, tenant requirements, idempotency conflicts,
entitlement denials, and request validation failures. Pass shouldReport or
reportOptions when an app needs different alerting rules or extra tags.
Reporting preparation, capture, and failure observation are independently
bounded to one second so a reporting backend cannot hold an HTTP response open.
Set timeoutMs to tune the bound. Use timeoutMs: false only when intentionally
allowing reporting to block request completion. Callback, resolver, reporter,
and timeout failures are isolated and may be observed through
onReporterError.
Runtime ownership
Report a logical failure once at the boundary that knows it is terminal:
| Boundary | Reporting owner | Default behavior |
|---|---|---|
| HTTP | createErrorReportingHooks(...) | Reports unexpected caught failures; skips catalog, auth, policy, tenancy, idempotency, entitlement, and validation outcomes. |
| BullMQ jobs | createBullMQJobWorker(...) | Reports terminal, invalid, unknown-job, and worker infrastructure failures. Retry attempts remain instrumentation. |
| Inngest jobs | createInngestJobFunction(...) | Reports through native onFailure after Inngest exhausts retries. |
| Next schedule routes | createScheduleRoute(...) | Reports a failed invocation that the route converts to a 500 response. |
| Next/CLI outbox drains | Drain adapter | Reports dead letters, settlement failures, and drain infrastructure failures; scheduled retries are not incidents. |
| CLI tasks and schedules | CLI runner | Reports terminal failures and performs a bounded flush before context shutdown. |
| Registered listeners | App-owned registration provider | The canonical generated provider reports from registerListeners(...).onError. |
| Direct core primitive calls | Calling app | Use tryReportException(...) at the outer boundary that decides the failure is terminal. |
Do not also report inside a handler when its worker, route, or runner owns the terminal boundary; doing both creates duplicate incidents. Prefer durable failure state for work that must be retried or reconciled. Error reporting tells an operator something went wrong; it does not make the work durable.
What context to send
Attach stable identifiers, not raw payloads:
| Field | Use |
|---|---|
requestId | Correlate logs, devtools, traces, and support tickets |
traceId | Connect nested use case, provider, outbox, and job events |
actorId | Identify the user, service, or anonymous actor |
tenantId | Scope the failure to a tenant or workspace |
contractName | Identify the HTTP boundary |
useCaseName | Identify the application workflow |
jobName | Identify background work |
outboxMessageId | Reconcile durable delivery failures |
resourceType and resourceId | Find the affected record |
Do not send request bodies, raw provider responses, access tokens, cookies, passwords, PHI, payment details, private messages, or full authorization headers unless your reporting vendor and retention policy are approved for that data.
Beignet applies default sensitive-key redaction to structured Sentry metadata,
but it cannot safely rewrite exception messages or infer domain-sensitive
fields. AppError.details is not automatically redacted and is never copied by
the default HTTP hook; treat manual capture of an AppError as an explicit data
export decision.
Use Privacy lifecycle to define which fields may leave app-owned storage and which fields must only appear as stable identifiers.
Testing
createTestPorts(...) includes an in-memory errorReporter port by default.
Use it directly when testing failure paths:
const { ports, errorReporter } = createTestPorts<AppPorts>();
await useCase.run({ ctx: { ports }, input });
expect(errorReporter.reports).toEqual([
expect.objectContaining({
type: "exception",
}),
]);You can also import the memory reporter directly:
import { createMemoryErrorReporter } from "@beignet/core/error-reporting";
const errorReporter = createMemoryErrorReporter();Devtools versus production reporting
Devtools are local diagnostics. When devtools are installed before an error reporting provider, captured exceptions and messages appear under the Errors view with request and trace correlation.
Use devtools locally, structured logs in production, and a production error reporter for exceptions that operators need to triage.
Alerting
Do not alert on every captured exception. Alert on symptoms that require human action:
- elevated 5xx rate
- repeated auth or payment provider failures
- queue or outbox dead-letter growth
- schedule missed-run or failure rate
Start with slow, high-signal alerts and add more only when they lead to useful operator action. Every alert should have an owner, a severity, a runbook link, and enough context to find the affected tenant or resource.
Related pages
- Logging for structured application logs.
- Errors for app error catalogs and client error handling.
- Outbox and Jobs for durable failure semantics.
- Going to production for redaction and sensitive data boundaries.