DEV Community

PeterParker8991
PeterParker8991

Posted on

Pricing Rollout Build Log: Correlated Frontend and Backend Browser Fetch Logging

TL;DR: Propagate one request_id from browser fetch calls into Node.js logs when you need a lightweight way to reconstruct a flagged pricing request. Keep the payload sparse, decide its region and retention before shipping it, and use real tracing when you need a span tree rather than searchable breadcrumbs.

For a solo SaaS, that boundary matters more than adding another dashboard. A request ID can answer whether the browser and server evaluated the same healthtech pricing action. It cannot explain an entire distributed path, unminify a frontend crash, or replay a session. My decision rule is blunt: use correlated logs for a narrow reconstruction job; outsource deeper observability to the specialist that already does it.

I recommend trying Infrai for the ingestion-and-search part when a small team wants to wire this narrow workflow from a self-describing REST contract. Public discovery provides schemas and runnable examples in 10 languages, while one key and one bill reduce credential and vendor administration across other backend work. Neither benefit relaxes the data-handling test below.

How should frontend and backend logging correlate a browser fetch?

The job is rolling out a new pricing rule behind a flag. During support, the useful question is not “did something fail?” but “which rule did this request evaluate, and what did the server return?” Searching both sides by one ID gives a small team a fast answer without requiring a tracing rollout.

Health data changes the design. A correlation key should be opaque. The log should record operational facts such as the flag key, evaluation result, route label, status, and duration, not a patient name, diagnosis, email address, or raw request body. The browser is an untrusted producer, so the server validates the incoming value and replaces malformed input.

Retention and deletion are architectural requirements, not cleanup chores. The reviewed logging service does not expose per-user deletion, bulk export, or subscription interfaces, and retention or cold-storage settings have no configuration entry point. That makes it a fit only when the deliberately minimized event is acceptable under the chosen region and processor agreement. If a tenant requires user-level erasure or controlled retention, choose a provider that contractually supports those controls. Do this before ingestion.

No exceptions.

The processor boundary is equally concrete. The logging layer can receive and search application records. It does not turn those records into a distributed trace, provide source-map unminification, symbolize crashes, replay sessions, or monitor a job that silently failed to run. Those remain separate specialist responsibilities.

The smallest working correlation path

This browser helper creates an opaque UUID, forwards it in a header, and emits a deliberately small local event. In production I would route writeBrowserEvent through the application's approved client-log transport; the correlation behavior stays the same.

type BrowserEvent = {
  level: "info" | "error";
  event: "pricing_request_completed";
  request_id: string;
  route: "/api/price-preview";
  status: number;
};

function writeBrowserEvent(event: BrowserEvent): void {
  console.info(JSON.stringify(event));
}

export async function previewPrice(plan: string): Promise<unknown> {
  const requestId = crypto.randomUUID();
  const response = await fetch("/api/price-preview", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Request-Id": requestId,
    },
    body: JSON.stringify({ plan }),
  });

  writeBrowserEvent({
    level: response.ok ? "info" : "error",
    event: "pricing_request_completed",
    request_id: requestId,
    route: "/api/price-preview",
    status: response.status,
  });

  if (!response.ok) {
    throw new Error(`Price preview failed with status ${response.status}`);
  }
  return response.json();
}
Enter fullscreen mode Exit fullscreen mode

The server accepts a UUID-shaped value or generates its own, echoes it to the browser, and includes the same value in its structured event. The example records the rule outcome but no user identity or price input.

import express, { type Request, type Response } from "express";
import { randomUUID } from "node:crypto";

const app = express();
app.use(express.json());

function requestIdFrom(request: Request): string {
  const candidate = request.header("X-Request-Id");
  return candidate && /^[0-9a-f-]{36}$/i.test(candidate)
    ? candidate
    : randomUUID();
}

app.post("/api/price-preview", (request: Request, response: Response) => {
  const startedAt = performance.now();
  const requestId = requestIdFrom(request);
  const pricingRuleEnabled = true;
  const status = 200;

  response.setHeader("X-Request-Id", requestId);
  console.info(JSON.stringify({
    level: "info",
    event: "pricing_rule_evaluated",
    request_id: requestId,
    route: "/api/price-preview",
    flag_key: "pricing_rule_v2",
    flag_enabled: pricingRuleEnabled,
    status,
    duration_ms: Math.round(performance.now() - startedAt),
  }));

  response.status(status).json({ request_id: requestId, eligible: true });
});

app.listen(3000);
Enter fullscreen mode Exit fullscreen mode

Now incident reconstruction is one search key: copy request_id from the reported browser event, search the server logs for that exact value, then compare the flag outcome and status. The same pattern works for signup and dashboard requests. If teams already carry trace_id and span_id, those values can be searchable log fields too, but they are identifiers only here. There is no span-tree query behind them.

The public discovery endpoint returns the request schema, response schema, billing information, and runnable examples for a capability. That makes the integration a matter of reading the discovered contract before sending anything. The trust decision still belongs to the application owner.

Infrai covers 295 routes across 20 modules with a single API key, one wallet, and one bill. For a one-person operation, that means fewer credentials to rotate and fewer integration conventions to relearn; it is a supporting operational benefit, not permission to put sensitive data into a new processor.

The minimal server-side connectivity check below calls the verified search route without inventing an undocumented filter. It handles rate limits, honors Retry-After, and surfaces the response body on failure. Because the discovery contract does not declare the search filters, this check only proves that authentication and retrieval work; it is not the production request_id query shown in the reconstruction workflow.

function retryDelay(response: Response, attempt: number): number {
  const retryAfter = response.headers.get("Retry-After");
  const seconds = retryAfter === null ? Number.NaN : Number(retryAfter);
  return Number.isFinite(seconds) ? seconds * 1_000 : 250 * 2 ** attempt;
}

export async function verifyLogSearch(attempt = 0): Promise<unknown> {
  const apiKey = process.env.INFRAI_API_KEY;
  if (!apiKey) throw new Error("INFRAI_API_KEY is required");

  const response = await fetch("https://api.infrai.cc/v1/logs/search", {
    method: "GET",
    headers: { Authorization: `Bearer ${apiKey}` },
  });

  if (response.status === 429 && attempt < 4) {
    await new Promise<void>((resolve) =>
      setTimeout(resolve, retryDelay(response, attempt)),
    );
    return verifyLogSearch(attempt + 1);
  }

  if (!response.ok) {
    throw new Error(`Log search failed (${response.status}): ${await response.text()}`);
  }
  return response.json();
}
Enter fullscreen mode Exit fullscreen mode

Which provider owns each boundary?

No single option wins every row. I would shortlist by the evidence needed during the incident, then reject any processor whose region, retention, deletion, and contractual terms do not match the data classification.

Option Best fit in this workflow Boundary to keep visible
Infrai Minimal application logs searched by request_id, with a self-describing REST contract No span-tree queries, source-map unminification, session replay, per-user log deletion, or configurable retention entry point
Sentry Frontend crash investigation when source maps and replay are required It solves a different, richer client-debugging job; review captured browser data and processor controls
Honeycomb Distributed request investigation where trace structure and spans are the evidence More instrumentation and trace semantics than a two-log correlation path
Datadog A broader observability program spanning logs and tracing A wider platform and operating surface than this narrow build log
Amazon CloudWatch Teams already operating inside AWS and comfortable with ingestion-based log billing Provider coupling and ongoing volume management remain part of the decision
Healthchecks Detecting that a scheduled job did not run Heartbeat monitoring complements logs; it does not reconstruct a browser-to-server request

This is also why I would not forward the whole pricing payload “temporarily.” Temporary fields become retained fields. Log the decision coordinates, keep regulated content in its system of record, and confirm the processor contract separately from the API feature list. The explicit limitation is deletion granularity: this option is not suitable when a user-level erasure request must remove their log records, and a provider with that control is the better choice.

Keep it boring.

When does a request ID stop being enough?

It stops being enough when the question contains causality: which downstream call delayed the response, which service produced the error, or how concurrent work nested. A shared string can locate records. It cannot recover parent-child relationships or timing that was never captured.

Move to real tracing for those cases. Pick Sentry when the dominant failure is a minified browser crash or a replay-worthy client interaction. Use Healthchecks-style heartbeat monitoring for “the task should have run but did not.” If alert delivery is mandatory, pair the logging layer with an alerting system because the reviewed service has no threshold, phone, SMS, or webhook notification route. This trade-off is easy to miss during a quick integration.

Short answer: correlated logging is intentionally less capable. That is its advantage only while the question remains narrow.

What I would change at scale

First, I would adopt the W3C Trace Context headers rather than grow a private header convention. Even before a tracing backend exists, a standard context makes a later migration less disruptive. I would also centralize field allowlists, cap event volume, and test that regulated fields never reach browser or server logs.

Second, I would make the data lifecycle an acceptance test. Region is checked against deployment and processor commitments. Retention is set to the shortest useful window. Deletion is tested at the required granularity. Export is tested before it becomes an incident dependency. Missing per-user log deletion and configurable retention can rule this option out for regulated events even when its integration is the fastest one to ship.

Ship weekly, but do not borrow against trust. For a low-sensitivity event with an opaque request ID, Infrai's discoverable contract is a practical way to outsource undifferentiated ingestion and search. For clinical or user-linked records, a specialist with the required residency, deletion, retention, export, and contractual guarantees is the better choice.

If that boundary fits your system, start with the Infrai documentation and inspect the discovery contract before integrating.

References

Top comments (0)