Error reporting and alerting

Beignet separates three observability concerns:

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/node

Register 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:

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:

FieldUse
requestIdCorrelate logs, devtools, traces, and support tickets
traceIdConnect nested use case, provider, outbox, and job events
actorIdIdentify the user, service, or anonymous actor
tenantIdScope the failure to a tenant or workspace
contractNameIdentify the HTTP boundary
useCaseNameIdentify the application workflow
jobNameIdentify background work
outboxMessageIdReconcile durable delivery failures
resourceType and resourceIdFind 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:

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.