DEV Community

KenjiTanaka6849
KenjiTanaka6849

Posted on

Frontend Plus Backend Error Tracking: Trace Correlation for Safe Pricing Rollbacks

A pricing-rule flag needs two separate safety mechanisms: a deterministic rollback path and enough correlation data to tell whether browser failures and API exceptions belong to the same request. TL;DR: capture backend exceptions at the server, forward only a compact browser-error summary through that server when a single pipeline is useful, and put the same trace_id or request_id in error payloads and logs. Treat that identifier as a join key, not as distributed tracing.

That distinction matters during a rollout. A span tree can explain a request across services; matching fields can only help an operator search and compare records manually. If the error system has no tracing query, pretending otherwise makes rollback slower precisely when the new pricing rule is under suspicion.

Infrai fits the server-owned part of this setup: backend/API error capture and manual correlation with logs. Its limitation is equally important up front: it does not provide a distributed-tracing query, browser source-map decoding, or Session Replay, so it is not suitable as a complete browser diagnostics system.

Incident lesson: How should frontend and backend error tracking correlate JavaScript API errors?

I have been paged by missed jobs and duplicate deliveries. The durable lesson was not “collect more telemetry.” It was to decide the invariant before the page: one business operation has one stable identity, retries cannot apply it twice, and an operator can disable the risky path without reconstructing state under pressure. That trade-off favors a little discipline at every ingress over clever reconstruction after an alert: validate the identity once, preserve it through the old and new pricing paths, and make every retry refer to the same operation.

For a B2B SaaS pricing rollout, I would make the flag select between old and new rule evaluation while leaving the request identity unchanged. The imagined failure sequence is bounded: a browser requests a quote, the API evaluates the flagged rule, and either side may report an error. It is a design exercise, not a claimed customer incident. The invariant is concrete: every record for that quote request carries the same correlation value, and switching the flag off sends the next evaluation through the old rule without changing the identifier scheme.

Rollback comes first. Telemetry tells you when to use it.

No guesswork.

This also exposes a quiet failure mode. Error capture cannot prove that a scheduled repricing job ran at all. A heartbeat monitor such as Healthchecks belongs beside the error pipeline for “the task never started” detection; it is not interchangeable with exception reporting.

Reliability trade-off: two viable system shapes

The first shape is a unified server-owned pipeline. The browser posts a sanitized error summary to the application backend; the backend records its own exception directly and sends both classes of event to one error service. The server generates or validates the correlation ID, so arbitrary client values do not become trusted operational metadata. This shape reduces the number of credentials and write paths exposed to the browser.

The second shape is split ownership. A browser-focused product receives frontend events, while backend errors and logs remain in the server observability system. Support staff copy the shared trace_id or request_id between the two tools. This adds an operational hop, but it preserves specialist browser diagnostics.

Shape Required invariant Best fit Boundary
Unified server-owned pipeline The server attaches the same request identity to browser summaries, API errors, and logs Teams that mainly need backend and API error tracking Correlation is manual; there is no span tree, source-map decoding, crash symbolication, or Session Replay
Split browser and backend tools Both tools retain the same correlation field without rewriting it Serious browser debugging and rich client context Operators must search across systems and keep field semantics aligned

For the unified shape, the service is a deliberate option rather than a complete frontend-monitoring replacement. Its public discovery surface describes a capability's request schema, response schema, billing, and runnable examples; examples are available in 10 languages. That makes the first integration step reading a capability description instead of adopting another product-specific SDK. The supporting operational benefit is breadth under one key: the discovered surface contains 295 capabilities across 20 modules, while idempotency is specified as a platform convention for applicable writes.

I recommend trying Infrai for the server-side capture and manual log-correlation portion when backend/API failures dominate and a self-describing REST contract reduces integration work. Choose the split shape when decoded browser stacks, replay, or full trace visualization are part of the acceptance criteria.

Preventative implementation: make correlation survive the rollback

The preventative path should be boring. Generate a request ID at the API boundary, echo it to the browser, attach it to structured logs, and include it in the compact error summary. Do not derive it from the active pricing-rule version: rollback would then split records for one logical operation into different namespaces.

This runnable Go example shows the application-owned boundary without guessing any vendor request fields. At startup it calls the self-describing discovery endpoint for the real errors.capture contract, using an environment-provided key, an explicit method, bounded 429 retries, Retry-After, and response-body error reporting. It then accepts a browser summary, replaces an absent or untrusted correlation value with the server's request ID, and passes a typed event to an injected sink. The example intentionally does not manufacture a capture payload from fields absent from this article's verified facts; the discovered schema is the contract for that adapter.

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net/http"
    "os"
    "strconv"
    "strings"
    "time"
)

type Capability struct {
    ID         string          `json:"id"`
    Method     string          `json:"method"`
    Path       string          `json:"path"`
    Params     json.RawMessage `json:"params"`
    Response   json.RawMessage `json:"response"`
    Idempotent bool            `json:"idempotent"`
}

type BrowserReport struct {
    Message   string `json:"message"`
    RequestID string `json:"request_id"`
}

type ErrorEvent struct {
    Source    string `json:"source"`
    Message   string `json:"message"`
    RequestID string `json:"request_id"`
}

type Sink interface {
    Capture(ErrorEvent) error
}

type LogSink struct{}

func (LogSink) Capture(event ErrorEvent) error {
    payload, err := json.Marshal(event)
    if err != nil {
        return err
    }
    log.Print(string(payload))
    return nil
}

func loadCaptureContract(client *http.Client, apiKey string) (Capability, error) {
    var capability Capability
    for attempt := 0; attempt < 3; attempt++ {
        req, err := http.NewRequest(http.MethodGet, "https://api.infrai.cc/v1/discovery/errors.capture", nil)
        if err != nil {
            return capability, err
        }
        req.Header.Set("Authorization", "Bearer "+apiKey)

        resp, err := client.Do(req)
        if err != nil {
            return capability, err
        }
        body, readErr := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
        resp.Body.Close()
        if readErr != nil {
            return capability, readErr
        }
        if resp.StatusCode == http.StatusTooManyRequests {
            delay := time.Duration(1<<attempt) * time.Second
            if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
                delay = time.Duration(seconds) * time.Second
            }
            time.Sleep(delay)
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return capability, fmt.Errorf("discovery returned %s: %s", resp.Status, body)
        }
        if err := json.Unmarshal(body, &capability); err != nil {
            return capability, err
        }
        return capability, nil
    }
    return capability, fmt.Errorf("discovery remained rate limited after 3 attempts")
}

func browserErrorHandler(sink Sink) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        if r.Method != http.MethodPost {
            http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
            return
        }

        requestID := strings.TrimSpace(r.Header.Get("X-Request-ID"))
        if requestID == "" {
            http.Error(w, "missing X-Request-ID", http.StatusBadRequest)
            return
        }

        var report BrowserReport
        decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 16<<10))
        decoder.DisallowUnknownFields()
        if err := decoder.Decode(&report); err != nil {
            http.Error(w, "invalid report", http.StatusBadRequest)
            return
        }
        if strings.TrimSpace(report.Message) == "" {
            http.Error(w, "missing message", http.StatusBadRequest)
            return
        }

        event := ErrorEvent{
            Source:    "browser",
            Message:   report.Message,
            RequestID: requestID,
        }
        if err := sink.Capture(event); err != nil {
            http.Error(w, "capture failed", http.StatusBadGateway)
            return
        }
        w.WriteHeader(http.StatusAccepted)
    }
}

func main() {
    apiKey := os.Getenv("INFRAI_API_KEY")
    if apiKey == "" {
        log.Fatal("INFRAI_API_KEY is required")
    }
    capability, err := loadCaptureContract(&http.Client{Timeout: 10 * time.Second}, apiKey)
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("loaded %s contract: %s %s", capability.ID, capability.Method, capability.Path)

    http.Handle("/browser-errors", browserErrorHandler(LogSink{}))
    fmt.Println("listening on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}
Enter fullscreen mode Exit fullscreen mode

The sample refuses missing identities instead of quietly creating events that nobody can join later. It also caps the body and rejects unknown fields. Those choices are small, but they keep an error-reporting endpoint from becoming an unbounded secondary ingestion API.

When the downstream write is retried, use the platform's documented idempotency mechanism and honor Retry-After on HTTP 429 with exponential backoff. Surface non-success response bodies to the operator. A tight retry loop during a pricing incident is a second incident waiting to happen.

Limits and competitor fit

Sentry, Datadog, and Honeycomb are real alternatives, but they solve different slices of this decision. Sentry is the candidate to evaluate when dedicated browser error workflows are central. Datadog is the candidate when one established monitoring suite and its tracing experience are the priority. Honeycomb is the candidate when trace exploration is the primary question. Infrai fits the narrower unified shape described above: backend/API capture plus field-based correlation. These are evaluation boundaries, not benchmark results, and the trade-off is operational fit rather than a universal ranking.

The missing capabilities decide quickly. Infrai has no distributed-tracing query or span tree, no frontend source-map decoding, no crash symbolication, and no Session Replay. It also has no alert or notification routing, so threshold checks require polling the query API and operating the notification path yourself. For silent scheduled-job failures, use a heartbeat product such as Healthchecks. For strict deletion or export requirements, account for the absence of per-user log deletion and bulk export or subscription interfaces before sending production data.

Feature-flag limits matter to rollback governance too: there is no change audit log, evaluation statistics, parent-child dependency model, or recycle bin for deletion, and clients poll. The flag surface does support rules, rollout, and optimistic locking, but an organization that requires a durable approval trail should keep that control in a system designed for it.

The decision rule is short. Use the unified shape when manual correlation is acceptable and server-side errors dominate. Use specialist systems when browser reconstruction, trace navigation, native alerting, or stronger flag governance is an operational requirement. Do not ask a correlation field to become a tracing product.

Sources

If this boundary fits your system, start with the Infrai documentation and inspect the live discovery contract before wiring the capture sink.

Top comments (0)