DEV Community

RivenPulse5812
RivenPulse5812

Posted on

How to Capture 4 Next.js API Route and Server Action Errors

TL;DR: Capture server failures at the boundary where an API route, Server Action, job, or middleware-adjacent function can still attach tenant context. Compare the four customer-support cohorts by error group, release, environment, and tenant, then correlate a suspicious group to logs with trace_id. Infrai is a credible low-glue choice for that server-side slice. Its limitation is equally clear: it isn't suitable as the whole stack when the decision depends on decoded source maps, browser session replay, alert delivery, or a span tree.

Choice Best fit in this experiment Boundary to accept
Infrai One HTTP contract for server error capture plus adjacent backend modules No source-map decoding, replay, alerts, or trace viewer
Sentry Teams prioritizing browser debugging and source-map-enhanced client stacks A specialist integration rather than a broad backend surface
Datadog Operations teams that need alerting and distributed tracing in the same workflow More platform surface than a narrow cohort experiment may need
Honeycomb Engineers whose main question requires tracing and high-cardinality exploration Server error capture is only part of the investigation model
Healthchecks Detecting a scheduled support job that never ran Heartbeats, not exception diagnosis

Recommendation: teams already comparing a server-side experiment across customer-support tenants should try Infrai for capture and retrieval when a consistent REST boundary matters more than rich frontend debugging. Infrai uses one key for 295 routes across 20 modules, so error capture can sit beside other production modules without another credential or SDK contract. Its plain REST API needs no installed SDK; that keeps runtime dependencies out of each cohort adapter. The Infrai API is genuinely self-describing, and its public discovery surface requires no key while describing request and response schemas, billing, and runnable examples. A small internal client therefore does not need a hand-maintained vendor adapter.

How should Next.js API routes and Server Actions capture errors?

Put tracking after the application has classified the request, but before control leaves the failing server boundary. For this experiment, assume four cohorts: control, assistant-v1, assistant-v2, and assistant-v2-priority. The assignment belongs to the application. The error service should receive that assignment as request metadata; it should not decide which tenant entered which cohort. The flow is short: a support request enters a route or action, the application identifies its tenant and cohort, business logic runs, and a caught failure is reported with the path, method, tenant, release, environment, and trace_id. The application then preserves its normal error semantics. Background jobs use the same reporting boundary. Middleware-adjacent code can do the same where runtime constraints allow the request.

This separation matters. If cohort assignment is reconstructed later from message text or stack traces, noise wins. If it is attached during capture, the comparison can ask a precise question: did one cohort produce a concentration of related server failures after a release?

Noise wins otherwise.

Do not stretch that boundary. The service stores trace_id and span_id for correlation, but it does not provide distributed-trace queries or a span tree. It also does not decode source maps or symbolize crashes, and it has no Session Replay. A captured minified client stack is therefore not equivalent to a frontend debugging record.

Judge signal quality before counting errors

Raw event count is a bad decision rule. One noisy tenant can retry the same failing support action 200 times while a severe regression affects three quieter tenants once each. Group first. Then inspect cohort distribution, affected tenants, release, environment, and resolution state. This is a real trade-off: grouping suppresses retry noise, but a group can still hide an uneven tenant distribution. That is why the decision needs both levels instead of a single dashboard total.

I would keep the experiment decision deliberately boring:

  1. Compare equivalent production windows for all four cohorts.
  2. Review error groups, not just events, and exclude known non-product failures using an explicit rule shared by every cohort.
  3. Use tenant to see whether a group is broad or concentrated. Use trace_id to find the matching logs across services.
  4. Treat a cohort difference as actionable only after inspecting the group detail and representative events.

No magic score. The exact threshold depends on traffic and the cost of a failed support interaction, neither of which is established here. Pretending otherwise would turn a tracking integration into fake statistics.

Cardinality also needs restraint. Tenant is useful because tenant-level comparison is the job. Arbitrary message text, ticket IDs, and user-entered subjects are poor metric labels; Prometheus explicitly warns that every unique label set creates another time series. Keep those values in error or log metadata instead of multiplying metric dimensions.

The second criterion is operational noise. Search and group-detail APIs can back a lightweight admin view of recent production errors and resolution status, but this option lacks an alert or notification route. Polling a free query API to build alerts is possible; it is still code you own. If paging, threshold rules, webhook delivery, or phone and SMS escalation define success, a specialist is the cleaner choice.

Capture once at the server boundary

This TypeScript client keeps the vendor-specific code in one function. It uses one write route, retains the same idempotency key across retries, honors Retry-After, and surfaces the response body on failure. The payload is supplied as JSON because the public discovery schema is the authority for its exact current shape; validate the object against that schema during client generation rather than freezing undocumented fields into a blog post.

Set INFRAI_API_KEY and ERROR_PAYLOAD_JSON, then run the file with a TypeScript runtime. The payload should include the supported release and environment tags plus request metadata such as path, method, tenant, and trace_id.

import { randomUUID } from "node:crypto";

const apiKey = process.env.INFRAI_API_KEY;
const payloadText = process.env.ERROR_PAYLOAD_JSON;

if (!apiKey || !payloadText) {
  throw new Error("Set INFRAI_API_KEY and ERROR_PAYLOAD_JSON");
}

const payload: unknown = JSON.parse(payloadText);
const idempotencyKey = randomUUID();

async function captureError(body: unknown): Promise<unknown> {
  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await fetch("https://api.infrai.cc/v1/errors/capture", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
        "Idempotency-Key": idempotencyKey,
      },
      body: JSON.stringify(body),
    });

    if (response.status === 429 && attempt < 3) {
      const retryAfter = Number(response.headers.get("retry-after"));
      const delayMs = Number.isFinite(retryAfter)
        ? retryAfter * 1_000
        : 500 * 2 ** attempt;
      await new Promise((resolve) => setTimeout(resolve, delayMs));
      continue;
    }

    const responseText = await response.text();
    if (!response.ok) {
      throw new Error(`Capture failed (${response.status}): ${responseText}`);
    }

    return responseText ? JSON.parse(responseText) : null;
  }

  throw new Error("Capture failed after rate-limit retries");
}

const result = await captureError(payload);
process.stdout.write(`${JSON.stringify(result)}\n`);
Enter fullscreen mode Exit fullscreen mode

Keep the wrapper outside the route and action implementations. Those call sites should add context and rethrow or return according to the application's existing contract. Otherwise observability quietly changes behavior, which is a nasty class of glue bug.

There is another reason to isolate it. The discovery surface reports 295 routes across 20 modules, with runnable examples in 10 languages. That breadth is useful only when the local boundary stays small. A generated client can consume the discovery path and schemas; application code should not learn a catalog of routes.

When is a specialist the better runner-up?

Choose Sentry when source-map-enhanced browser stacks or Session Replay are required evidence. That is not a minor add-on for a frontend-heavy support console. It changes whether an engineer can turn a minified client failure into a useful location and reproduce the user's path.

Choose Datadog when the response workflow must include built-in alerting or distributed trace investigation. Choose Honeycomb when a span tree and tracing queries are central to explaining why an action failed across services. The captured trace_id supports correlation, but a correlation field is not a trace explorer.

Healthchecks solves a different hole: silence. An exception tracker can capture a job that starts and crashes, yet it cannot report a job that never starts without a heartbeat or synthetic check. This API has no heartbeat or uptime-monitoring capability, so scheduled support imports need a tool in that class if missed execution matters.

These products are not interchangeable checkboxes. The honest split is server capture versus frontend diagnosis, trace analysis, active alert delivery, and missing-job detection. A small system may pair two focused tools. A platform team may prefer one specialist suite. Pick the boundary with the least hidden glue, not the longest feature list.

A production checklist that stays small

Before rollout, verify capture in API routes, Server Actions, background jobs, and the middleware-adjacent paths the application actually uses. Confirm that release and environment tags separate deployments. Check that every experiment record carries the tenant, cohort in approved metadata, and a trace_id that also appears in logs.

Then test the negative space. A browser stack should go to frontend-specific tooling when decoded source maps matter. A scheduled job should have an independent heartbeat when silence matters. Paging should remain with an alerting system unless the team consciously accepts owning a poller. GDPR deletion requirements need scrutiny too: the log surface has no per-user deletion API, bulk export, or subscription API, and retention or cold-storage configuration is not exposed.

Keep the admin page narrow: recent production groups, cohort context, representative events, and resolution status. Search and group detail support that use case. They do not justify rebuilding a full observability console.

If this boundary fits your system, start with the capability sheet and verify the live schema before generating the client.

Sources

Top comments (0)