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
hooks, 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:
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.
HTTP request errors
Use onCaughtError to observe errors without changing response mapping. Use
mapUnhandledError only to decide the response body.
const errorReportingHooks = {
name: "error-reporting",
onCaughtError: async ({ err, ctx, req, contract }) => {
if (!ctx) return;
await ctx.ports.errorReporter.captureException(err, {
level: "error",
requestId: ctx.requestId,
traceId: ctx.traceId,
tags: {
contract: contract.name,
method: req.method,
},
contexts: {
request: {
path: new URL(req.url).pathname,
},
actor: {
id: ctx.actor?.id,
},
tenant: {
id: ctx.tenant?.id,
},
},
});
},
};Jobs, schedules, and outbox drains
HTTP hooks do not see every background failure. Add reporting where work runs:
- Job handlers should report terminal failures or failures that exceed retry policy.
- Outbox drains should report dead-lettered messages and repeated drain failures.
- Schedule handlers should report failed runs after the scheduler or job layer has decided whether to retry.
- Operational tasks should report failed backfills, imports, exports, and repair jobs.
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.
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.