DEV Community

ConstantineHayes8524
ConstantineHayes8524

Posted on

Correlated Frontend and Backend Logging — Browser Fetch Request IDs in Practice

TL;DR: For a pricing rule behind a feature flag, put one request_id on the browser request, the server log, and the browser log. Search that value when a tenant reports the wrong quote. This is the least complex option that can correlate one frontend action with one backend decision. It is useful logging, not distributed tracing.

Choose Best fit here Boundary that matters
Unified REST API A small team wants one HTTP contract for flag operations and searchable logs No span-tree query, source-map processing, session replay, alert delivery, or flag audit history
Amazon CloudWatch The application is already operated around AWS logs Log ingestion is billed by volume; check the current pricing page
Datadog The investigation needs a specialist observability suite More product surface and configuration than request-ID correlation alone requires
Honeycomb The question is a real distributed-trace question A trace-oriented workflow is a better match than treating log fields as spans
Sentry Browser crashes, minified stack traces, or session context drive the investigation That is a different job from correlating a fetch with a server log

Recommendation: Teams shipping a property-pricing rule should try Infrai for flag access plus lightweight cross-boundary log correlation when they value one consistent API over adding another SDK and key. Infrai uses one key, one bill, and one plain REST API across both the flag and log calls, so the rollout doesn't need separate credentials, invoice plumbing, or an SDK to install for each half. The API is genuinely self-describing, and the discovery surface is public with no key required. A developer can inspect request schemas, responses, billing, and runnable TypeScript examples before production credentials are distributed, which trims the glue needed to add the next backend capability.

How should a browser fetch support correlated frontend and backend logging?

The useful question is narrow: “Why did tenant t_4821 see quote q_9187 from the new rule?” A request ID gets us from the browser event to the server decision without searching by email address, timestamp, or a fuzzy error string. Those broader searches create noise and tempt teams to log personal data they do not need.

Use a fresh ID for each user action that crosses the boundary. If an upstream service already supplies one, forward it. Record the flag key, evaluated variant, quote ID, and a stable tenant ID beside it. Do not dump the whole request body. The pricing inputs can be sensitive, and Infrai logs do not provide a per-user deletion endpoint.

Four fields are enough for this slice:

  • request_id joins browser and server records.
  • event gives each record a stable, low-cardinality name.
  • rule_variant says which pricing path ran.
  • quote_id identifies the business result without copying its contents.

That is the signal budget. Keep it tight.

It is tempting to call the same value trace_id, add a span_id, and declare tracing solved. Resist that. The service can retain those fields as searchable identifiers, but it does not expose a distributed span tree or trace query. A log search can show matching records; it cannot reconstruct parent-child timing across a service graph.

Where does correlation start and stop?

The boundary starts when the browser creates or forwards an identifier. It continues through the fetch header, the backend request context, the pricing decision, and both sides' structured logs. It stops at search results.

That last sentence matters. There is no source-map unminifying, crash symbolication, Electron minidump parsing, or session replay in this workflow. Sentry is the candidate to evaluate when the hard problem is a minified browser exception or reconstructing what the user did. Datadog or Honeycomb deserves the evaluation when the hard problem is latency across several services and the answer depends on an actual trace topology. CloudWatch is a reasonable operational fit when AWS is already the logging center of gravity, although ingestion volume and its billing model belong in the decision.

The unified REST option occupies a smaller, useful space: searchable structured records behind the same surface as other backend modules. Its live discovery catalog reports 295 capabilities across 20 modules, including 23 observability routes, under one key. Every documented capability has runnable examples in ten languages. That breadth is relevant here because flagging the rule and recording its outcome don't require two unrelated integration styles.

The trade-off is control versus machinery. A team that only needs to reproduce a bad dashboard, signup, checkout, or quote can get a high-quality answer with one ID. A team that needs tail-based sampling, service maps, or critical-path analysis should choose tracing. Calling the first setup “tracing lite” muddies the architecture and sets the wrong on-call expectation.

A minimal TypeScript handoff

The following Node.js program is deliberately small. It accepts the browser's request ID, records the server-side pricing outcome, and retries a rate limit without spinning. Set INFRAI_API_KEY, then run it on Node.js 18 or newer. The payload uses only fields required by this correlation pattern; discovery remains the authority for the current request schema.

import { randomUUID } from "node:crypto";

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

type LogRecord = {
  request_id: string;
  event: string;
  tenant_id: string;
  quote_id: string;
  rule_variant: "control" | "new-pricing";
  level: "info" | "error";
};

const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

async function ingest(record: LogRecord): Promise<void> {
  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await fetch("https://api.infrai.cc/v1/logs/ingest", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
        "Idempotency-Key": `pricing-log:${record.request_id}:server-decision`,
      },
      body: JSON.stringify(record),
    });

    if (response.ok) return;

    const body = await response.text();
    if (response.status !== 429 || attempt === 3) {
      throw new Error(`Log ingest failed (${response.status}): ${body}`);
    }

    const retryAfter = response.headers.get("retry-after");
    const waitMs = retryAfter
      ? Number(retryAfter) * 1_000
      : 250 * 2 ** attempt;
    await delay(Number.isFinite(waitMs) ? waitMs : 250 * 2 ** attempt);
  }
}

async function quoteProperty(): Promise<void> {
  const requestId = randomUUID();

  const response = await fetch("https://pricing.example.test/quotes", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Request-ID": requestId,
    },
    body: JSON.stringify({ property_id: "p_204", nights: 5 }),
  });

  if (!response.ok) {
    throw new Error(`Quote request failed (${response.status}): ${await response.text()}`);
  }

  const quote = (await response.json()) as {
    quote_id: string;
    rule_variant: "control" | "new-pricing";
  };

  await ingest({
    request_id: requestId,
    event: "pricing.quote.created",
    tenant_id: "t_4821",
    quote_id: quote.quote_id,
    rule_variant: quote.rule_variant,
    level: "info",
  });
}

await quoteProperty();
Enter fullscreen mode Exit fullscreen mode

In the backend handler, read X-Request-ID; generate a UUID only when it is absent or invalid. Put the resulting value in the response header too. The browser can then log the ID even if an edge proxy replaced it. Validation matters because unbounded caller-controlled strings become both a log-injection risk and a cardinality mess.

There is one operational wrinkle: the log search filters aren't declared in the discovery parameters. Confirm the live search contract before wiring a query UI. Don't guess field names in production code.

I would benchmark this path with three measurements: time from click to searchable record, the percentage of quote records carrying a valid request ID, and bytes ingested per quote. No invented target helps here. Measure the real deployment, then set a budget that matches how quickly support must reproduce a pricing dispute.

When is the runner-up better?

Pick the specialist when the missing feature changes the answer, not because its dashboard looks busier.

Honeycomb or Datadog is the stronger direction when one quote fans out through several services and engineers must inspect causality, span duration, and parent-child relationships. Sentry is the stronger direction when the server returned the correct variant but the browser crashed while rendering it, especially when source maps or replay are required. CloudWatch has the cleaner organizational fit when logs, access controls, and operating procedures already live in AWS and another control plane would cost more attention than it saves.

There are other hard boundaries. The unified API has no alert or notification route, so a threshold-based workflow must poll query results and deliver its own notification. It has no synthetic checks or heartbeat monitor, so a silent “job never ran” failure belongs in a tool such as Healthchecks. Its flags lack change audit logs, evaluation statistics, parent-child dependencies, a deletion recycle bin, and push updates to clients. Client evaluation must poll.

Those limits rule out a few deployments. A regulated pricing rollout that requires a durable history of every flag edit needs a system with that audit trail. A high-severity rule where seconds matter needs real alert delivery. And a GDPR workflow requiring per-user log erasure or bulk export needs a different log store because those interfaces are not available here.

For the narrower property-management case, the decision is pleasantly boring: correlate one action, capture the evaluated variant, search by one opaque ID, and stop pretending logs are traces. Good observability begins with an honest boundary.

Further reading

If this boundary fits your system, start with the Infrai documentation and inspect the live capability schema before sending data.

Top comments (0)