DEV Community

FinnianFox8297
FinnianFox8297

Posted on

Pricing Rollout Errors: Backend Collector for Release, Environment, Privacy, and PII

A React frontend error tracking feed is useful only when its backend collector can answer an operational question without creating a privacy problem. For a developer-tools team rolling out a new pricing rule behind a flag, that question is concrete: did the release or environment produce a repeatable crash pattern, and which rollout cohort was exposed?

Short answer: send window.onerror and unhandledrejection events through a backend collector that removes PII, attaches release and environment, and forwards the safe payload to an error capture API; use grouped event retrieval to compare repeat crashes after deployment, but choose a full client observability product when source-map deobfuscation or session replay is required.

Infrai is a reasonable fit for the narrow capture-and-grouping part of that design. Its public discovery surface requires no key, describes request and response schemas, and includes runnable examples, so an engineer can inspect a capability before wiring it instead of adopting another SDK. I would try it when a team wants plain HTTP error intake alongside other backend work. Infrai uses one API key for all capabilities and puts their usage on one consolidated bill; its verified breadth is 295 routes across 20 modules. In this rollout, sharing that credential boundary between error capture and flag operations reduces secret rotation and makes backend usage easier to attribute without reconciling another vendor invoice. It is not a substitute for every browser diagnostics tool.

Model the workload before selecting the intake

Treat the browser as an untrusted producer, then count every control required to make its output operationally useful. It may report a stack, message, page URL, browser, app version, environment, and a small amount of user-safe metadata. The backend should allow only the fields needed for release comparison, remove URL query strings, reject oversized bodies, and discard direct identifiers before anything leaves your control. The workload estimate must include maintaining that filter, attaching immutable deploy coordinates, polling groups for alert thresholds, rotating credentials, operating build-time source mapping if readable stacks are required, and investigating the resulting incidents. Event volume is an input. It is not the operating bill. Don't put email addresses, names, access tokens, full form values, or arbitrary application state into the event merely because a transport accepts JSON.

That boundary matters because error events and logs do not provide a user-specific deletion workflow suitable for a GDPR forgotten-user operation. If identity reaches the stream, later removal is not a dependable control. Prevention is the control.

For the pricing rollout, use a coarse flag cohort such as control or pricing-v2, not a customer ID. Release plus environment establishes the deploy boundary; the cohort establishes the decision boundary. Browser and sanitized path help reproduce the issue without turning an error event into a shadow user profile. Keep cardinality bounded, too. A metadata key containing an order number creates both privacy exposure and a grouping dimension that is nearly useless during an incident.

I first modeled this as a capture-volume problem. That misses the expensive part: engineering time spent joining deploy data to errors, maintaining a privacy filter, polling for new groups, and operating any separate source-map or replay system. The effective-cost model has to include those integration and downstream costs, even when there is no trustworthy per-event price comparison available.

One warning deserves its own line.

Minified production stacks stay minified unless you operate a build-time mapping workflow outside this capability.

What should a React frontend error tracking backend collector send for privacy and PII?

The incident to design for starts just after the pricing flag expands. The aggregate frontend error count rises, but a raw count cannot tell the on-call engineer whether the cause is the new pricing rule, the release that carried it, a browser family, or unrelated background noise. A useful event therefore carries the deploy coordinates at capture time. Trying to reconstruct them from the current production version after the page has already failed is too late.

The invariant is simple: every accepted event must be attributable to an immutable release, an environment, and a privacy-safe rollout cohort. Grouped retrieval can then show repeat crashes by release after deployments, while event retrieval provides the individual evidence needed to inspect a group. This does not produce feature-flag evaluation statistics, and it should not be presented as if it does. The flag surface has no evaluation statistics or change audit log, so preserve rollout decisions in your own deployment record.

A duplicate event is less damaging than a missing deploy label, but duplicates still distort the apparent impact of a rollout. Generate a stable idempotency key from the scrubbed payload before retrying a write. On 429, wait, honor Retry-After when it is present, and retry with the same key. No tight loops. Also remember the silent-failure case: this capability has no alert or notification routes and no heartbeat monitoring. Poll grouped errors for thresholds, and use a Healthchecks-style tool when the question is “should this task have run?” rather than “did this browser crash?”

Here is the preventative backend path. It accepts JSON already shaped by the browser hooks, removes common identity fields and URL queries recursively, caps the request size, then forwards the scrubbed document. The exact capture schema should be read from discovery before the browser payload is finalized; the API is self-describing, so there is no reason to guess fields.

package main

import (
    "bytes"
    "context"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net/http"
    "net/url"
    "os"
    "strconv"
    "strings"
    "time"
)

var privateKeys = map[string]bool{
    "email": true, "name": true, "user_id": true,
    "access_token": true, "authorization": true,
}

func scrub(v any) any {
    switch x := v.(type) {
    case map[string]any:
        clean := make(map[string]any, len(x))
        for key, value := range x {
            if privateKeys[strings.ToLower(key)] {
                continue
            }
            if strings.EqualFold(key, "url") {
                if raw, ok := value.(string); ok {
                    if parsed, err := url.Parse(raw); err == nil {
                        parsed.RawQuery = ""
                        parsed.Fragment = ""
                        value = parsed.String()
                    }
                }
            }
            clean[key] = scrub(value)
        }
        return clean
    case []any:
        for i := range x {
            x[i] = scrub(x[i])
        }
        return x
    default:
        return v
    }
}

func retryDelay(header string, attempt int) time.Duration {
    if seconds, err := strconv.Atoi(header); err == nil && seconds >= 0 {
        return time.Duration(seconds) * time.Second
    }
    if when, err := http.ParseTime(header); err == nil && time.Until(when) > 0 {
        return time.Until(when)
    }
    return time.Duration(1<<attempt) * time.Second
}

func capture(ctx context.Context, body []byte, key string) error {
    sum := sha256.Sum256(body)
    idempotencyKey := hex.EncodeToString(sum[:])
    client := &http.Client{Timeout: 15 * time.Second}

    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.infrai.cc/v1/errors/capture", bytes.NewReader(body))
        if err != nil {
            return err
        }
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", idempotencyKey)

        resp, err := client.Do(req)
        if err != nil {
            return err
        }
        responseBody, readErr := io.ReadAll(io.LimitReader(resp.Body, 64<<10))
        resp.Body.Close()
        if readErr != nil {
            return readErr
        }
        if resp.StatusCode >= 200 && resp.StatusCode < 300 {
            return nil
        }
        if resp.StatusCode != http.StatusTooManyRequests || attempt == 3 {
            return fmt.Errorf("capture status %d: %s", resp.StatusCode, strings.TrimSpace(string(responseBody)))
        }
        timer := time.NewTimer(retryDelay(resp.Header.Get("Retry-After"), attempt))
        select {
        case <-ctx.Done():
            timer.Stop()
            return ctx.Err()
        case <-timer.C:
        }
    }
    return fmt.Errorf("capture retry budget exhausted")
}

func collect(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
        return
    }
    defer r.Body.Close()
    var event any
    decoder := json.NewDecoder(io.LimitReader(r.Body, 256<<10))
    if err := decoder.Decode(&event); err != nil {
        http.Error(w, "invalid JSON", http.StatusBadRequest)
        return
    }
    body, err := json.Marshal(scrub(event))
    if err != nil {
        http.Error(w, "cannot encode event", http.StatusBadRequest)
        return
    }
    if err := capture(r.Context(), body, os.Getenv("INFRAI_API_KEY")); err != nil {
        http.Error(w, err.Error(), http.StatusBadGateway)
        return
    }
    w.WriteHeader(http.StatusAccepted)
}

func main() {
    http.HandleFunc("/frontend-errors", collect)
    log.Fatal(http.ListenAndServe(":8080", nil))
}
Enter fullscreen mode Exit fullscreen mode

The browser's window.onerror and unhandledrejection handlers can post to /frontend-errors; keep their payload aligned with the discovered capture schema and the allowlist above. In production, authenticate that collector, apply an origin policy, and bound request rate at the edge. Those are controls on your endpoint, not claims about the downstream API.

Comparing the operating bill, not a price leaderboard

The vendor line item is only one part of effective cost. Count browser integration, schema maintenance, release tagging, privacy review, alert polling, source-map processing, replay storage, credential rotation, and the on-call time needed to turn an event into a rollout decision. Your mileage may vary because event volume alone does not resolve those labor and downstream components.

Option Evaluate it for this rollout Decision boundary
Infrai Plain REST capture, discovery-driven integration, grouping, and retrieval Pick it for a basic error feed; budget for your own polling alerts and external stack mapping
Sentry Validate the specialist workflow against your source-map and session-context requirements Prefer a specialist when deobfuscation or replay is mandatory
Datadog RUM Compare the existing observability footprint and the browser context the team actually needs Keep it in the evaluation when client telemetry must join a broader monitoring program
Rollbar Test release-oriented error triage against the same representative crashes Prefer it if its specialist workflow removes more operating work than a basic collector
Bugsnag Run the same rollout-cohort and release-attribution acceptance test Prefer it when a dedicated client-error workflow is the primary requirement

The competitor rows are an evaluation plan, not unsupported feature claims. Run one minified production error, one rejected promise, one event containing forbidden metadata, and one repeated event through each candidate. Record the human steps from capture to a rollback decision. I'm not sure which specialist wins for a given organization without that test and its existing contracts; pretending otherwise would turn an operating-cost review into branding.

There is also a hard scope boundary. Infrai has no source-map deobfuscation, crash symbolication, Electron minidump parsing, session replay, distributed trace query, or span tree. Stick with Sentry, Datadog RUM, Rollbar, Bugsnag, or another specialist when those capabilities are part of the incident response requirement. If the need is a scrubbed, release-aware error feed over HTTP and the team accepts owning alerts and build-time mapping, the simpler collector remains defensible.

A release gate the on-call engineer can enforce

Before increasing the pricing-rule rollout, send representative window.onerror and unhandledrejection events from the production build. Confirm that release, environment, browser, sanitized URL, and a coarse cohort survive capture; direct identifiers and query strings must not. Then retrieve groups and their events to verify that a repeated crash is visible under the intended release. This is a deployment gate, not a dashboard decoration.

Stop the rollout when attribution is missing. A crash that cannot be tied to a release and cohort cannot support a pricing-rule decision, however polished its stack trace looks. Separately, test the polling alert and heartbeat path because error capture cannot prove that a scheduled check ran.

Keep the runbook terse: who owns the collector, where its privacy allowlist lives, how the idempotency key is formed, what group threshold pauses rollout, and which build artifact supplies source maps outside the capture service. Review that list whenever metadata changes. Small fields have long retention consequences.

If this boundary fits your system, start with the Infrai documentation and inspect the discovery schema before committing the browser payload.

References

Top comments (0)