Error reporting and alerting
Beignet separates three observability concerns:
- Logging records structured runtime facts.
- Error reporting sends exceptions and failure context to an external system such as Sentry, Datadog, Honeycomb, Axiom, or a hosted log pipeline.
- Alerting turns symptoms into operator action.
Beignet does not ship a first-party error-reporting provider yet. Use an app-owned port so your use cases and hooks do not depend on a vendor SDK directly.
App-owned error reporter port
Define the port in your app:
// ports/error-reporter.ts
export interface ErrorReporterPort {
captureException(
error: unknown,
context?: Record<string, unknown>,
): Promise<void> | void;
captureMessage(
message: string,
context?: Record<string, unknown>,
): Promise<void> | void;
}
Add it to AppPorts, then implement the adapter in infra/:
// infra/error-reporting/noop-error-reporter.ts
import type { ErrorReporterPort } from "@/ports/error-reporter";
export function createNoopErrorReporter(): ErrorReporterPort {
return {
captureException: () => {},
captureMessage: () => {},
};
}
Production adapters can wrap a vendor SDK while tests use the no-op or a memory
reporter. Keep vendor SDK imports in infra/, provider packages, or worker
entrypoints; do not import them from use cases, domain code, contracts, or
feature routes.
HTTP request errors
Use onCaughtError to observe errors without changing the response mapping.
Use mapUnhandledError only to decide the response body.
const errorReportingHooks = {
name: "error-reporting",
onCaughtError: async ({ err, ctx, req, contract }) => {
await ctx?.ports.errorReporter.captureException(err, {
requestId: ctx.requestId,
contractName: contract.name,
method: req.method,
path: new URL(req.url).pathname,
actorId: ctx.actor?.id,
tenantId: ctx.tenant?.id,
});
},
};
export const server = await createNextServer({
ports: appPorts,
hooks: [errorReportingHooks],
createContext,
mapUnhandledError: ({ ctx }) => ({
status: 500,
body: {
code: "INTERNAL_SERVER_ERROR",
message: "Internal server error",
requestId: ctx?.requestId,
},
}),
});
Report unexpected exceptions and infrastructure failures. Expected business errors 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.
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 commands 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.
Use Beignet's shared redaction helpers before sending structured metadata:
import { redactHeaders, redactValue } from "@beignet/core/ports";
ctx.ports.errorReporter.captureException(error, {
headers: redactHeaders(request.headers),
provider: redactValue(providerMetadata),
});
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
- upload completion failures
- database migration or connection failures
- mail delivery failure rate
- webhook/idempotency conflict spikes
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.
Devtools versus production reporting
Devtools are local diagnostics. They are useful for reproducing failures and checking request correlation, but they are not a production alerting system.
Use devtools locally, structured logs in production, and a production error reporter for exceptions that operators need to triage.
Testing
Use a memory reporter in tests:
export function createMemoryErrorReporter(): ErrorReporterPort & {
exceptions: Array<{ error: unknown; context?: Record<string, unknown> }>;
} {
const exceptions: Array<{
error: unknown;
context?: Record<string, unknown>;
}> = [];
return {
exceptions,
captureException: (error, context) => {
exceptions.push({ error, context });
},
captureMessage: () => {},
};
}
Assert that terminal failure paths report enough context, and assert that expected business failures do not create noisy production alerts.
Related pages
- Logging for structured application logs.
- Errors for app error catalogs and client error handling.
- Devtools for local request and provider timelines.
- Outbox and Jobs for durable failure semantics.
- Audit and activity logging for durable business activity records.
- Production security for redaction and sensitive data boundaries.
- Privacy lifecycle for retention, export, deletion, anonymization, and what not to log.