Error reporting and alerting

Beignet separates three observability concerns:

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:

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.

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:

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