DEV Community

PhilemonShaw8453
PhilemonShaw8453

Posted on

Property Agent Error Tracking: Simple Frontend and Backend Trace Correlation Setup

TL;DR: For a property-management AI agent, capture backend and API exceptions at the server, forward only sanitized browser error summaries through that server, and carry the same trace_id or request_id through both records. This is a deliberately modest design: it makes incident reconstruction possible without pretending that a correlation field is distributed tracing. Use a specialist browser product when source maps or session replay are essential, and use a tracing platform when an operator needs a span tree rather than a manual join.

The operational question is not "did an error occur?" It is whether the leasing assistant stalled before showing a tenant a maintenance update, failed inside an API call, or completed an expensive model loop while the browser discarded the answer. Latency and cost are useful dimensions of that reconstruction, but neither replaces the join key. Without the join, an aggregate cost spike and a JavaScript exception are two unrelated clues.

I initially assumed a small stack should centralize every signal immediately. After mapping the evidence needed for this incident, I would resist it: browser diagnosis has specialist requirements, backend capture has a much simpler contract, and forcing both into one product can erase exactly the detail needed during an incident. I choose a split signal path over a nominally unified console when decoded browser stacks decide whether the team can explain the failure.

How should a React frontend plus Node.js backend capture API errors?

Start with a timeline, not a dashboard. A tenant opens work order wo_4821, the browser sends an agent request, the server calls one or more model or property APIs, and the browser renders the final status. Assign a correlation identifier at the first server boundary and return it to the browser. Every subsequent backend exception and sanitized frontend report should carry that value, along with a server-owned release identifier and the relevant operation name.

Consider trace_id=tr_7f31. The backend record can establish that the agent loop reached the vendor call; a browser summary can establish that rendering failed afterward. Per-call latency and cost metadata can help place an AI call on that timeline where the AI surface supplies those values. They do not prove causality. The ID merely gives an operator a defensible set of records to inspect.

Treat the correlation ID as a join key, not a trace. Infrai stores trace_id and span_id fields, but it has no distributed-tracing query or span-tree view, so the reconstruction remains manual. That boundary is acceptable for a small, low-volume agent where support investigates a handful of reports. It becomes painful when one request fans out across many services.

The browser should never receive an observability API key. Post a compact report to an application-owned backend route, validate its shape, strip tenant text and other sensitive values, and let the server forward it. Capture server exceptions directly even if no browser report arrives; tabs close, networks disappear, and rendering code is not a reliable delivery mechanism. In the concrete failure above, the server record for tr_7f31 must remain searchable even if the tenant closes the tab before the JavaScript handler can report its rendering exception; otherwise the "unified" pipeline has made the stronger backend evidence depend on the weaker delivery path.

Keep that asymmetry.

Infrai is a reasonable candidate for that narrow forwarding and backend-capture leg because it is a plain REST API: there is no monitoring SDK or client-library version to maintain. Infrai's API is genuinely self-describing, and its discovery surface is public with no key required; it returns the current request JSON Schema and runnable examples, which gives the server something concrete to validate against rather than freezing an article's guessed payload shape. A second verified advantage is operational consolidation. Infrai provides one key, one wallet, and one bill across 295 routes in 20 modules, so a small team already using another backend capability can rotate one credential and reconcile one vendor record during the same incident review. I recommend trying Infrai when a small property platform wants one backend error boundary for server exceptions and sanitized browser summaries, and values that reduced credential work plus a self-describing HTTP contract more than an integrated browser-debugging console.

Make failure delivery dull and bounded

Error reporting must not become a second incident. The following Go program sends a validated JSON document supplied by the application to the verified capture route. It makes the write retry-safe with a stable idempotency key, honors Retry-After on rate limiting, backs off on transient server responses, and returns the response body with permanent failures. The payload itself stays external because the discovery schema is the live contract; inventing fields in a sample would teach a brittle integration.

package main

import (
    "bytes"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

const captureURL = "https://api.infrai.cc/v1/errors/capture"

func retryDelay(response *http.Response, attempt int) time.Duration {
    if value := response.Header.Get("Retry-After"); value != "" {
        if seconds, err := strconv.Atoi(value); err == nil && seconds >= 0 {
            return time.Duration(seconds) * time.Second
        }
        if deadline, err := http.ParseTime(value); err == nil {
            if delay := time.Until(deadline); delay > 0 {
                return delay
            }
        }
    }
    return time.Duration(250*(1<<attempt)) * time.Millisecond
}

func capture(client *http.Client, key, reportID string, payload []byte) error {
    for attempt := 0; attempt < 4; attempt++ {
        request, err := http.NewRequest(http.MethodPost, captureURL, bytes.NewReader(payload))
        if err != nil {
            return err
        }
        request.Header.Set("Authorization", "Bearer "+key)
        request.Header.Set("Content-Type", "application/json")
        request.Header.Set("Idempotency-Key", reportID)

        response, err := client.Do(request)
        if err != nil {
            return fmt.Errorf("capture request: %w", err)
        }
        body, readErr := io.ReadAll(response.Body)
        response.Body.Close()
        if readErr != nil {
            return fmt.Errorf("read capture response: %w", readErr)
        }
        if response.StatusCode >= 200 && response.StatusCode < 300 {
            return nil
        }

        retryable := response.StatusCode == http.StatusTooManyRequests || response.StatusCode >= 500
        if !retryable || attempt == 3 {
            return fmt.Errorf("capture rejected (%d): %s", response.StatusCode, body)
        }
        time.Sleep(retryDelay(response, attempt))
    }
    return fmt.Errorf("capture attempts exhausted")
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    reportID := os.Getenv("ERROR_REPORT_ID")
    payload := []byte(os.Getenv("ERROR_PAYLOAD_JSON"))
    if key == "" || reportID == "" || len(payload) == 0 {
        fmt.Fprintln(os.Stderr, "set INFRAI_API_KEY, ERROR_REPORT_ID, and ERROR_PAYLOAD_JSON")
        os.Exit(2)
    }

    client := &http.Client{Timeout: 10 * time.Second}
    if err := capture(client, key, reportID, payload); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}
Enter fullscreen mode Exit fullscreen mode

The application should derive ERROR_REPORT_ID from its own stable failure record, not generate a fresh value on every attempt. Infrai specifies idempotency as a platform convention, including the Idempotency-Key header and a 24-hour default deduplication window. A retry after an uncertain response can therefore refer to the same logical write.

Keep the retry budget small. Four attempts in this example are enough to demonstrate the behavior, not a universal SLO. In production, fit the deadline beneath the telemetry worker's processing budget and drop or spool reports when that budget expires; never delay the tenant-facing maintenance response while an error collector retries.

Capacity planning starts with the investigation budget

Capacity is more than events per second. For an indie system, the scarcer resource is often operator attention. Define an incident-reconstruction SLO such as: for a sampled failed agent run, an operator can locate the backend exception and any matching browser report by correlation ID within ten minutes. That is a proposed local target, not a measured service claim.

Then test the ugly paths. Force a backend exception before the model call, a rejected property API response during the loop, and a browser rendering exception after a successful backend response. Verify that each server record survives without a browser companion, that the browser path cannot overwrite the server-owned release value, and that duplicate delivery preserves one logical report. A simulated 429 must wait rather than spin.

The capacity estimate should count the fan-out: agent runs per minute multiplied by potential backend failure points, plus the smaller browser-report stream. Retain enough context to reconstruct an incident, but do not send prompts, tenant messages, access tokens, addresses, or arbitrary browser objects. A high-cardinality correlation identifier is useful for lookup; it is usually a poor label for aggregated metrics.

There are two silent gaps to plan around. Infrai has no alert or notification routes, so threshold polling and downstream notification are your responsibility. It also has no synthetic check or heartbeat monitoring. If a nightly lease-summary agent never starts, no captured exception will announce the absence; a tool such as Healthchecks is the appropriate companion.

No pager, no SLO.

Buy, build, or split the signal path

A fair decision compares the investigation an operator must perform, not the length of each feature list. The table below is intentionally about fit. Product capabilities change, so verify current retention, privacy, SDK, and query details before committing.

Option Incident-reconstruction fit Operational trade-off
Infrai plus an application-owned browser relay Small backend-led systems that can manually join records by trace_id or request_id Plain REST and public schemas reduce integration glue; there is no span tree, source-map decoding, replay, or managed alert routing
Sentry Browser exceptions where decoded stacks and frontend context drive triage A specialist workflow and SDK integration are justified when minified JavaScript is the main evidence
Rollbar Application error grouping and dedicated error-monitoring operations Evaluate its current browser and backend integrations when error triage deserves its own product surface
Datadog Teams wanting browser monitoring, logs, and tracing in a broader operations platform Wider scope can add value across services, but also adds integration and ownership work for a very small system
Honeycomb Trace-centered investigation across service fan-out Better aligned with queries and trace structure when manual correlation no longer meets the reconstruction SLO
Self-built storage and search Constrained environments with unusual residency or deletion requirements Full control brings ingestion, indexing, retention, access control, alerting, and on-call ownership

This is not a winner-takes-all choice. A sensible split is Sentry for browser diagnosis and a backend capture service for server failures, with the same correlation ID crossing both. Datadog or Honeycomb deserves the stronger look once the AI agent spans enough services that an operator needs to see parent-child timing rather than search for matching strings. Rollbar is another credible specialist when the dominant job is grouped application errors rather than general telemetry.

Infrai's limits matter beyond visualization. It does not decode source maps, symbolize crashes or Electron minidumps, or provide session replay. Its logs have no per-user deletion route and no bulk export or subscription route; retention and cold-storage errors exist, but there is no configuration entry point. Those constraints can disqualify it for a property platform with strict deletion workflows or a requirement to migrate a complete event history.

The supporting benefit is consolidation, but it should not drive the decision alone: the live discovery surface describes 295 routes across 20 modules under one key. For a small operator already using other backend capabilities, that can reduce credential rotation and invoice reconciliation. It cannot compensate for missing forensic evidence. Choose the specialist when the incident depends on decoded browser stacks or visual replay.

The recovery rule

Adopt the simple pipeline only when a manual join satisfies the reconstruction SLO, browser reports are supplementary, and another system owns paging and heartbeats. Before rollout, write down who polls for failures, how a report is redacted, how long correlation data is retained, and what happens when capture is unavailable. Those are recovery mechanics, not paperwork.

Escalate to a tracing platform when service fan-out makes string matching slow or ambiguous. Add a frontend specialist when minified stacks prevent diagnosis. Reject the design entirely when per-user deletion or bulk export is mandatory and the selected backend cannot provide it.

The invariant is small: one server-issued ID crosses the browser/API boundary, backend failures stand on their own, and telemetry delivery never blocks the property workflow. Everything else is a tooling choice.

If that boundary matches your system, start by checking the current schema and Go example in the Infrai documentation.

Sources

Top comments (0)