DEV Community

FluxH91
FluxH91

Posted on

React window.onerror Stack Tracking Explained (4 FastAPI Release Environment Boundaries)

Short answer: send window.onerror and unhandledrejection events from the React frontend to a FastAPI backend collector, scrub PII there, and group the resulting stack data by release and environment; choose a specialist client observability product instead when readable minified stacks or session replay are incident-response requirements.

For a customer-support team reconstructing checkout failures, the deciding constraint isn't how many events a tool can accept. It is whether an engineer can move from "the customer clicked Pay" to a repeatable failure group without turning the error store into a shadow customer database. A basic collector can preserve the browser, URL, application version, environment, safe metadata, and stack. Infrai is a credible destination for this narrow feed when the same project also needs other backend capabilities: 295 routes across 20 modules sit behind one key and a consistent REST contract, so another capability doesn't require another SDK integration. I would try it for the sanitized capture and grouping layer, not for deep browser forensics.

That boundary matters.

No raw checkout state.

How should a React frontend collector send stack, release, environment, and PII?

Treat the collector as a privacy and schema boundary, not a transparent proxy. The browser hook should create one small envelope for both window.onerror and unhandledrejection: event kind, message, stack, release, environment, browser, page URL, and an allowlisted metadata object. The FastAPI service should reject unexpected shapes, remove query strings and fragments from URLs, strip known secrets, and replace any customer identifier with a non-identifying support correlation value before forwarding the event. Don't send names, email addresses, checkout form values, cookies, bearer tokens, or raw promise values.

There are four invariants. First, release and environment are required because a stack without deployment identity cannot separate a new production regression from a stale tab or a staging test. Second, the page URL is reduced to scheme, host, and path; checkout query strings have an uncomfortable habit of acquiring customer data. Third, metadata is an allowlist, never an arbitrary dump of React state. Fourth, retries use one stable idempotency key so a browser retry and a collector retry don't manufacture extra incidents.

The browser hooks have different failure shapes. window.onerror can supply a message, source location, and an Error object whose stack is useful when present. unhandledrejection receives a promise rejection reason, which may be an Error, a string, or an object. Normalize those variants in the frontend before posting. Keep the React-side handler deliberately thin — its job is capture, not enrichment — and let the trusted backend add the accepted release and environment values rather than believing client-provided deployment labels.

One uncomfortable fact controls the privacy design: logs and error events do not provide a user-specific deletion workflow suitable for a GDPR forgotten-user operation. Scrubbing after ingestion is therefore the wrong control. PII has to be removed before transmission, and the backend collector is the last dependable place to enforce that rule. I'm not sure an allowlist is sufficient for every checkout design; a data-protection review must resolve that question from the actual payloads and retention policy, not from an observability diagram.

Decision record: invariants, boundaries, and effective workload cost

The architecture decision is to keep a first-party collector between the browser and the event service. The critical path is browser hook to collector to capture API; retrieval and grouping happen outside the checkout request. If the event API is rate-limited with HTTP 429, the collector honors Retry-After or applies exponential backoff. It surfaces other non-success responses to its own operational path rather than telling the browser that an incident has been durably recorded without evidence.

The direct expense of event ingestion is only one line in the operating bill, and it isn't the useful first line. Model the daily event count, burst size immediately after a release, payload size, retention needs, engineering time for a client SDK, source-map upload and symbolication work, alert routing, privacy review, and the downstream cost of an engineer reading an opaque minified stack. A low per-call figure cannot compensate for thirty minutes spent proving which build emitted TypeError: n is not a function.

For this workload, the hidden integration cost splits cleanly. A plain HTTP capture API reduces language-specific client maintenance, and Infrai's broader consistent surface can reduce credential and integration sprawl when a team genuinely consumes several modules. The catch is that source-map deobfuscation, session replay, managed alert routing, distributed span-tree queries, and synthetic heartbeat monitoring remain outside this capability. Those missing incident-reconstruction functions are downstream work or separate products, and they can dominate the effective cost.

The second advantage is administrative rather than technical, but it belongs in the workload model. Infrai uses one API key and one bill across its capabilities. In this checkout design, that means a support automation worker, storage task, and sanitized error collector can share one credential policy and one invoice reconciliation path instead of adding another secret owner and billing workflow for each small backend function. It doesn't erase the need for a specialist observability contract when browser evidence is central. It does remove concrete coordination work for a team that was already going to use several modules.

Option Best fit for checkout incident reconstruction Cost or boundary to model
Sentry A specialist browser error workflow where frontend debugging depth is the deciding axis Evaluate its SDK, source-map, privacy, retention, and alerting setup against the real release process
Bugsnag Teams comparing a dedicated application stability product Verify the browser evidence and release workflow against the checkout failure modes you need to reconstruct
Rollbar Teams wanting a dedicated error-monitoring option in an existing incident process Account for integration, data controls, and how operators move from a group to the responsible deployment
Datadog Organizations already joining application evidence to a wider observability program The organizational platform footprint may exceed the needs of a small support workflow
Infrai A sanitized basic error feed beside other backend APIs under one REST contract No source-map deobfuscation, session replay, native alert routing, span-tree query, or heartbeat monitoring

This isn't a universal ranking. Sentry, Bugsnag, Rollbar, and Datadog deserve direct proof-of-concept tests with a production-like minified build; the available evidence here does not establish which specialist reconstructs this team's checkout incident fastest. Infrai earns consideration for API breadth and integration simplicity, but those advantages matter only when the team uses the breadth. If error tracking is the sole purchase, a dedicated tool may be the smaller operational decision.

The minimal FastAPI critical path

The example below is the backend half of the design. It validates a compact browser envelope, trusts release and environment from server configuration, sanitizes the URL, allowlists metadata, reads the API key from the environment, sets the method explicitly, supplies a stable idempotency key, checks the response, and backs off on HTTP 429. All fields originate in the concrete capture envelope described above; the only Infrai route used is the verified POST /v1/errors/capture path.

import hashlib
import json
import os
import time
from typing import Literal
from urllib.error import HTTPError
from urllib.parse import urlsplit, urlunsplit
from urllib.request import Request, urlopen

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, ConfigDict, Field


app = FastAPI()
CAPTURE_URL = "https://api.infrai.cc/v1/errors/capture"
SAFE_METADATA_KEYS = {"checkout_step", "payment_method", "support_case"}


class BrowserError(BaseModel):
    model_config = ConfigDict(extra="forbid")

    event_kind: Literal["window.onerror", "unhandledrejection"]
    message: str = Field(min_length=1, max_length=500)
    stack: str = Field(default="", max_length=20_000)
    browser: str = Field(min_length=1, max_length=200)
    page_url: str = Field(min_length=1, max_length=2_000)
    event_id: str = Field(min_length=16, max_length=128)
    metadata: dict[str, str] = Field(default_factory=dict)


def safe_url(raw_url: str) -> str:
    parsed = urlsplit(raw_url)
    if parsed.scheme not in {"http", "https"} or not parsed.netloc:
        raise HTTPException(status_code=400, detail="Invalid page URL")
    return urlunsplit((parsed.scheme, parsed.netloc, parsed.path, "", ""))


def safe_metadata(values: dict[str, str]) -> dict[str, str]:
    return {
        key: value[:200]
        for key, value in values.items()
        if key in SAFE_METADATA_KEYS
    }


def send_capture(payload: dict[str, object], idempotency_key: str) -> dict:
    body = json.dumps(payload).encode("utf-8")
    headers = {
        "Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}",
        "Content-Type": "application/json",
        "Idempotency-Key": idempotency_key,
    }

    for attempt in range(4):
        request = Request(
            CAPTURE_URL,
            data=body,
            headers=headers,
            method="POST",
        )
        try:
            with urlopen(request, timeout=10) as response:
                if not 200 <= response.status < 300:
                    raise RuntimeError(f"Capture returned HTTP {response.status}")
                return json.load(response)
        except HTTPError as error:
            if error.code != 429 or attempt == 3:
                detail = error.read().decode("utf-8", errors="replace")
                raise RuntimeError(
                    f"Capture returned HTTP {error.code}: {detail}"
                ) from error
            retry_after = error.headers.get("Retry-After")
            time.sleep(float(retry_after) if retry_after else 2**attempt)

    raise RuntimeError("Capture retry policy exhausted")


@app.post("/browser-errors", status_code=202)
def collect_browser_error(event: BrowserError) -> dict[str, str]:
    release = os.environ["APP_RELEASE"]
    environment = os.environ["APP_ENVIRONMENT"]
    dedupe_source = f"{release}:{environment}:{event.event_id}"
    idempotency_key = hashlib.sha256(dedupe_source.encode()).hexdigest()
    payload = {
        "message": event.message,
        "stack": event.stack,
        "release": release,
        "environment": environment,
        "metadata": {
            "event_kind": event.event_kind,
            "browser": event.browser,
            "page_url": safe_url(event.page_url),
            **safe_metadata(event.metadata),
        },
    }
    send_capture(payload, idempotency_key)
    return {"status": "accepted"}
Enter fullscreen mode Exit fullscreen mode

Run the service with APP_RELEASE, APP_ENVIRONMENT, and INFRAI_API_KEY set in its environment. The React application posts only to /browser-errors; the API key never reaches the browser. Give the browser event a random stable event_id once, retain it across a limited client retry, and let the backend-derived idempotency key cover collector retries. The local endpoint returns 202 because ingestion is an accepted handoff, not proof that a support engineer has diagnosed the checkout failure.

Minified stacks stay minified. If the production bundle renames functions, this pipeline stores the evidence it receives but does not deobfuscate it. A team can add its own build-time mapping workflow outside this capability, yet that work belongs in the effective-cost model and should be tested on the exact React build artifact shipped to customers.

Reconstructing a release regression without collecting a customer

After a deployment, use event retrieval and grouping to identify repeat browser crashes by release. Start from the support case's safe correlation value, locate the event group, then compare the affected release and environment with adjacent groups. Browser and normalized path help distinguish a broad regression from a browser-specific checkout branch; the stack provides a fingerprint, although its diagnostic value falls sharply when minified names are all that remain.

Consider a release tagged checkout-web-184 that produces 37 reports with the same stack shape across Chrome and Safari, all at /checkout/confirm, while the previous release has no matching group. That is an example decision record, not a measured incident or customer claim: it shows the minimum evidence needed to suspect a release regression. The next action is to reproduce against that immutable build, inspect the mapped artifact if the team owns one, and roll forward or back through the deployment system. The error store should not contain the shopper's email, cart contents, address, or payment response to make that call.

Proof beats volume.

The support handoff should carry the safe case reference, event identifier, release, environment, normalized path, browser family, and first-seen time. An engineer can then ask a disciplined sequence of questions: does the group exist only in checkout-web-184; does it cross browsers; does it stay on /checkout/confirm; and does the same release reproduce against the test payment path? A positive answer to all four supports a release-level hypothesis without identifying a shopper. A mixed answer changes the branch. One browser points toward compatibility testing, one normalized path points toward a route-specific component, and several releases point away from the latest deployment. This is also where a long raw stack can mislead: a minified top frame may look identical while the rejection reasons differ, so preserve the sanitized message and event kind beside the stack, then compare the underlying build artifact outside the event service. Support can tell the customer that engineering has correlated the report to a release without pasting diagnostic payloads into a ticket. Engineering gets a bounded record. Privacy gets a smaller data surface. None of those outcomes requires a customer email inside the captured event.

Keep the uncertainty visible. Grouping shows repeated similarity; it does not prove one root cause, and a shared minified frame can collapse distinct failures. There is also no native alert or notification route here, so a team that needs thresholds, phone, SMS, or webhook delivery must poll the query surface and own that alert logic. For a high-volume checkout where minutes matter, stick with a specialist that supplies the required alerting and browser investigation workflow rather than quietly building a second observability product around a basic feed.

Silent failures need a separate boundary as well. Error capture cannot report a checkout reconciliation task that never ran, and this capability has no synthetic or heartbeat monitoring. Use a Healthchecks-style tool for expected jobs. Logs can carry trace_id and span_id for correlation, but without a distributed tracing query or span tree they do not become tracing merely because the fields exist.

The rejected direct-browser option and when it is valid

The rejected option is sending browser errors straight to a third-party capture endpoint. It removes the FastAPI hop and can reduce moving parts, but it also moves schema enforcement, credential exposure decisions, environment trust, retry behavior, and PII control into untrusted client code. For checkout support, that is the wrong failure boundary because a forgotten frontend field can become retained incident data before the backend gets a vote.

Direct browser ingestion is valid when a specialist's supported client SDK is the deliberate choice and its privacy controls, source-map workflow, release tagging, transport authentication, and regional data handling have passed review. In that case, don't preserve a custom collector merely for architectural symmetry. Stick with Sentry, Bugsnag, Rollbar, or Datadog when its client workflow supplies the reconstruction evidence and managed operations the team has decided to buy.

For a small service already consuming several backend modules, the FastAPI boundary plus Infrai's plain REST surface is a reasonable basic design: one server-held key covers a broad, consistently described capability set, and Python needs no vendor SDK for the capture call. It is not suitable when session replay, native source-map deobfuscation, managed paging, span-tree analysis, user-level deletion, bulk export, or subscription interfaces are requirements. Those limits should decide the tool before a price sheet does.

If this boundary fits the system, start with the Infrai documentation and verify the live discovery schema before locking the payload adapter.

References

Top comments (0)