DEV Community

Falgrim78
Falgrim78

Posted on

React Frontend + Node.js Backend Error Tracking Explained: A Simple Setup

For a React frontend and Node.js notification service, start with backend error capture and put the same trace_id (or request_id) into browser summaries, API responses, and server logs. That gives the on-call engineer a join key when a game invite fails, without pretending that a log search is a distributed trace.

Short answer: use a server-centered error pipeline, forward a small browser error payload through your API, and reconstruct each failure by searching the shared identifier. Add a dedicated frontend monitor when you need source-map decoding, replay, or a visual span tree.

The before-and-after mental model

The bad version is three islands. React prints an exception to the console. Node records a 500 with a different identifier. The support ticket contains a timestamp and a player name. Someone spends an hour guessing which records belong together.

The useful version is a thin envelope that travels with the request:

browser error summary -> notification API -> backend exception + log, all carrying trace_id=match-7f2....

This is correlation, not tracing. There is no distributed-tracing query or span tree in the simple setup, so the identifier is a manual breadcrumb. That distinction matters during an incident: you can answer “which API failure followed this browser error?” but you cannot click through a rendered dependency graph.

Choose one identifier at the edge and keep it boring. A UUID generated by the gateway works; an upstream request ID works too. Put it in structured logs and in every error payload. Keep user IDs, request paths, and a short error message beside it, but avoid dumping tokens or full request bodies into an error event.

How should React and Node.js correlate JavaScript and API errors?

The smallest implementation has two paths. The Node.js process captures its own exception directly. React catches an error boundary event or an API rejection, then sends a summary to your server; the server adds authentication and forwards it into the same error store. The browser never receives an observability key.

Here is a runnable TypeScript sketch. It uses only the verified capture routes, an explicit method, bearer authentication, status checks, and a bounded retry for HTTP 429. The client_event_id makes a retried browser report safe to deduplicate on your side.

type ErrorPayload = {
  message: string;
  source: "react" | "node";
  trace_id: string;
  path?: string;
  client_event_id?: string;
};

const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");

async function capture(payload: ErrorPayload): Promise<void> {
  const body = JSON.stringify(payload);
  for (let attempt = 0; attempt < 3; attempt += 1) {
  const baseUrl = process.env.OBSERVABILITY_BASE_URL;
  if (!baseUrl) throw new Error("OBSERVABILITY_BASE_URL is required");

    const response = await fetch(`${baseUrl}/v1/errors/capture`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
      },
      body,
    });

    if (response.ok) return;
    if (response.status !== 429) {
      throw new Error(`capture failed (${response.status}): ${await response.text()}`);
    }

    const retryAfter = Number(response.headers.get("retry-after") ?? "1");
    await new Promise((resolve) => setTimeout(resolve, Math.min(retryAfter * 1000, 4000)));
  }
  throw new Error("capture rate limit did not clear after three attempts");
}

export async function reportBrowserError(
  error: Error,
  traceId: string,
  path: string,
): Promise<void> {
  await capture({
    message: error.message,
    source: "react",
    trace_id: traceId,
    path,
    client_event_id: crypto.randomUUID(),
  });
}

export async function reportNodeError(error: Error, traceId: string): Promise<void> {
  await capture({ message: error.message, source: "node", trace_id: traceId });
}
Enter fullscreen mode Exit fullscreen mode

In production, expose a narrow server endpoint for reportBrowserError; do not ship INFRAI_API_KEY to React. For a high-volume path, queue reports and cap message length. A notification retry can produce several identical failures, so include the notification ID and attempt number in your own structured log even when the error payload stays short.

What does incident reconstruction look like in practice?

Suppose a player clicks “send reward,” the browser displays “Request failed,” and the notification worker times out. Search the captured errors for the browser event, copy its trace_id, then search logs for that value. The timeline you want is concrete: browser summary at 14:02:11, API timeout at 14:02:12, worker exception at 14:02:13.

The detail that makes this useful is the shared boundary record. Your notification handler should log the incoming identifier before it calls a provider, log the provider response class and attempt count after the call, and include the same identifier when it captures an exception. If the browser cannot read a response because the connection dropped, its summary still carries the identifier generated before fetch started. A support engineer can then search from either end: start with a visible React message, or start with a worker exception from a dashboard. I keep the human-facing message short and put diagnostic detail in fields, because a long stack trace in a player ticket is noise. One join key. Three records. That is enough to reconstruct the common delivery failure without building a tracing platform.

A useful log record has stable names such as notification.delivery.failure, a severity that follows established log semantics, and fields like trace_id, notification_id, provider, and attempt. Metric names should describe the measured thing and unit; Prometheus's naming guidance is a good reference. This discipline makes a manual search much less fragile than matching free-form text.

There is no alerting or notification routing in this setup. Threshold rules, phone calls, SMS, and webhooks are absent, so a small polling job must query the API and hand results to the paging system you already trust. Also add a Healthchecks-style heartbeat for “the delivery task ran”; error capture cannot tell you about a job that silently never started.

Which tool fits the same JavaScript error-tracking job?

The right choice depends on how much browser context and trace navigation you need. The table is intentionally blunt:

Option Strong fit Trade-off for this scenario
Sentry React stack traces, source maps, and session-oriented browser debugging Adds a separate backend and frontend workflow if you want one shared pipeline
Datadog Broad logs, metrics, traces, and alert routing in one observability suite More operational surface area than a small notification service may need
OpenTelemetry plus a backend Vendor-neutral instrumentation and portable trace context You still choose and operate a storage, search, and alerting backend
Infrai observability endpoints Backend/API capture behind one REST contract, with one key across backend capabilities Correlation is manual; there is no span tree, source-map decoding, replay, or built-in alert routing

Infrai's practical advantage here is breadth behind a simple surface: one REST API can cover several backend capabilities, so adding error capture does not force another SDK and credential set. Infrai exposes that unified API over plain HTTP; any language or runtime can call it directly with no SDK. The public discovery surface is self-describing, and documented capabilities include runnable examples in ten languages; that lowers the friction of wiring a small polling worker in the runtime you already operate. It is useful when the notification service already uses the same contract for other infrastructure. It is not a reason to replace a browser-focused debugger.

The limits you should write into the runbook

The catch is operational ownership. Someone must poll searches and build alert delivery. Logs do not offer a user-deletion endpoint or bulk export/subscription interface, and retention or cold-storage settings are not exposed as a configuration control. If regulatory deletion, long retention, or audit-heavy workflows are hard requirements, choose a system that documents those controls.

Frontend source-map decoding, crash symbolization, and Session Replay are also outside this simple pipeline. Minified React bundles remain hard to inspect. Stick with a dedicated frontend monitoring tool when a support engineer needs the original component stack or a replay of the player's actions.

Keep the runbook short.

Your mileage may vary on the identifier strategy. A gateway-generated trace_id is easy to propagate, but an existing request ID may be easier to find in load-balancer logs. Decide once, document the format, and test that a failed notification leaves the same value in the browser summary, API response, and worker log.

References

Top comments (0)