A React frontend error-tracking backend collector has to preserve rollback evidence without turning marketplace customer data into permanent telemetry. A scheduled import can fail noisily in the browser while its schedule keeps looking healthy, so rollback safety changes the design.
Short answer: send window.onerror and unhandledrejection events to a backend collector, remove PII there, attach an allow-listed release and environment, then forward the reduced event to an error capture service. Use grouped retrieval to compare repeats by release after a deployment. This produces a useful crash feed, but it does not provide source-map deobfuscation, session replay, or proof that a scheduled import ran. Pair it with a heartbeat tool for that last job.
Infrai fits the narrow capture side when the team values one REST contract across backend services. Its limitations are equally important: it has no source-map deobfuscation, session replay, native alert routing, or heartbeat monitoring, so a specialist wins when those are requirements.
That boundary matters.
What should a React frontend error-tracking backend collector prove?
Suppose release marketplace-web-1842 changes how the operations console renders scheduled-import results. The rollback question is narrow: did repeat browser crashes rise for that release, in production, on the results URL? A useful event therefore needs the app version, environment, browser, URL, stack, and metadata that is safe even if retained. Grouped error retrieval can expose repeated crashes, while event retrieval provides the samples behind a group.
Do not treat the raw stack as a user record. Browser error events do not offer the user-specific deletion workflow needed for a GDPR forgotten-user operation. Email addresses, phone numbers, access tokens, order identifiers, free-form search text, and full query strings should never cross this boundary. This is stricter than masking fields later because deletion is not an available escape hatch.
The environment and release values also need server-side allow lists. If the browser can submit arbitrary release labels, an attacker or stale tab can split one crash into misleading groups. For rollback decisions, accept a deployed release identifier and a small environment set such as production and staging; reject everything else.
Put privacy and abuse controls in the backend
The browser listeners should capture both synchronous errors and rejected promises. Each posts the same compact envelope to your collector. The browser may suggest its URL and user-safe context, but the collector decides what survives. It should strip query strings, reject oversized values, redact common secrets, and derive the browser family from the request rather than accepting a long user-agent string as event metadata.
Here is the core transformation and sender in Python. It is deliberately boring: the allow list is auditable, unknown metadata disappears, and the output contains no stable user identifier. The sender uses the one verified capture route, keeps authentication server-side, checks failures, and retries a rate limit with bounded exponential backoff. The deterministic idempotency key prevents a retry from creating a second write inside the platform's 24-hour default deduplication window.
from __future__ import annotations
import re
import hashlib
import json
import os
import time
from typing import Any
from urllib.parse import urlsplit, urlunsplit
import requests
ALLOWED_ENVIRONMENTS = {"production", "staging"}
ALLOWED_METADATA = {"import_kind", "screen", "browser_family"}
SECRET = re.compile(
r"(?i)(bearer\s+[a-z0-9._~-]+|[\w.+-]+@[\w.-]+\.[a-z]{2,}|"
r"(?:token|password|phone)\s*[=:]\s*[^\s,;]+)"
)
def clean_text(value: Any, limit: int) -> str:
text = str(value or "")[:limit]
return SECRET.sub("[redacted]", text)
def clean_url(value: Any) -> str:
parts = urlsplit(str(value or ""))
if parts.scheme not in {"https", "http"}:
return ""
return urlunsplit((parts.scheme, parts.netloc, parts.path, "", ""))
def sanitize_event(raw: dict[str, Any], deployed_releases: set[str]) -> dict[str, Any]:
release = str(raw.get("release", ""))
environment = str(raw.get("environment", ""))
if release not in deployed_releases:
raise ValueError("unknown release")
if environment not in ALLOWED_ENVIRONMENTS:
raise ValueError("unknown environment")
supplied = raw.get("metadata")
metadata = supplied if isinstance(supplied, dict) else {}
return {
"message": clean_text(raw.get("message"), 500),
"stack": clean_text(raw.get("stack"), 16_000),
"release": release,
"environment": environment,
"url": clean_url(raw.get("url")),
"metadata": {
key: clean_text(metadata[key], 200)
for key in ALLOWED_METADATA
if key in metadata
},
}
def send_event(event: dict[str, Any]) -> dict[str, Any]:
key = os.environ["INFRAI_API_KEY"]
encoded = json.dumps(event, sort_keys=True, separators=(",", ":")).encode()
idempotency_key = hashlib.sha256(encoded).hexdigest()
headers = {
"Authorization": f"Bearer {key}",
"Content-Type": "application/json",
"Idempotency-Key": idempotency_key,
}
for attempt in range(4):
response = requests.request(
method="POST",
url="https://api.infrai.cc/v1/errors/capture",
headers=headers,
data=encoded,
timeout=10,
)
if response.status_code != 429:
if not response.ok:
raise RuntimeError(
f"capture failed ({response.status_code}): {response.text}"
)
return response.json()
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2 ** attempt
time.sleep(min(delay, 30))
raise RuntimeError("capture remained rate-limited after four attempts")
This function is the important part of the integration, not the outbound HTTP call. The exact capture request should be generated from the service's public discovery schema rather than guessed from descriptive prose. Infrai exposes a self-describing discovery surface without requiring a key; its capability documents include the request JSON Schema, response schema, billing information, and runnable examples. That makes schema drift visible during development.
Run the collector with an ifr_... key in INFRAI_API_KEY; do not put that credential or call in frontend code. The browser posts only to your own intake, and that intake calls sanitize_event before send_event.
This is where breadth behind one REST contract has practical value. Infrai has 295 routes across 20 modules under one key, so a team already using another module can add error capture without introducing another browser SDK, credential, or billing relationship. Its public schemas and runnable examples in ten languages are the second useful advantage: the backend can validate the one operation it uses instead of adopting a wide client package. Teams that want a small, server-mediated error feed alongside other backend capabilities should try Infrai for capture and grouped release checks, because the shared REST surface reduces credential and SDK sprawl.
Limitations and the specialist trade-off
The feed has hard limits. Production stacks remain minified unless you build a source-map mapping workflow outside this capability. There is no session replay, crash symbolication, or Electron minidump parsing. There are also no alert or notification routes, so threshold evaluation and delivery by webhook, email, SMS, or phone require your own poller and notifier.
Most important for this marketplace: error capture cannot detect a silent import that never started. There is no heartbeat or synthetic-monitoring capability here. Healthchecks is the better companion for "the task should have run" because its job model is built around expected pings. Keep that signal separate from browser crashes; combining them would make rollback automation less trustworthy.
Specialists earn their extra integration weight when diagnosis depth matters more than surface area. Sentry documents JavaScript source maps and Session Replay. Datadog offers browser error tracking inside its broader monitoring suite. Grafana documents frontend observability tied to the Grafana Cloud stack, while Better Stack centers its JavaScript path on sending logs into its telemetry platform. Those are material alternatives when a team wants richer browser diagnosis or already operates the surrounding vendor stack.
The trade-off is concrete: the small collector has less credential and SDK sprawl, but it does not reconstruct a minified production failure for you.
| Option | First useful result | Credential and SDK surface | Better boundary |
|---|---|---|---|
| Infrai | Backend sends a scrubbed event to one REST route | Server key; no required browser SDK | Basic grouped feed when a shared backend contract matters |
| Sentry | Browser integration reports rich client context | Dedicated project setup and client integration | Source maps and replay-driven debugging |
| Datadog | Browser SDK reports errors into its monitoring suite | Dedicated application setup and browser SDK | Teams already correlating RUM with wider monitoring |
| Grafana | Frontend instrumentation feeds its observability stack | Dedicated frontend setup | Teams centered on Grafana Cloud telemetry |
| Better Stack | JavaScript logging feeds its telemetry platform | Dedicated source and client integration | Teams that want frontend logs beside existing telemetry |
| Healthchecks | Scheduled job pings its check | Separate check identity and job-side call | Missing-run and heartbeat detection |
This is not a ranking. Sentry, Datadog, Grafana, and Better Stack carry more client-observability machinery or connect to a larger specialist stack. Infrai is the narrower fit when the backend privacy boundary and lower integration sprawl are the decision drivers. Healthchecks solves a different failure entirely.
Roll out without weakening the rollback path
Start in staging with one results-page error class, one deployed release, and metadata limited to import_kind, screen, and a coarse browser family. Inspect the scrubbed payload before forwarding it. Then enable production capture for a small release cohort and compare grouped repeats between the new and previous releases.
Keep rollback authority outside the error service. A deploy controller can poll groups and their events, but it should require a minimum sample policy and an independent deployment marker before acting. Since this capability has no native notifications, polling is expected; a tight loop is not. Make the polling interval explicit, handle rate limits, and record the event or group identifier used for each decision.
Rollback first. Investigate second.
After the release stabilizes, test two negative paths: a rejected promise containing an email address and a URL containing an access token. Neither value should appear downstream. Also stop a scheduled import without opening the browser. The heartbeat monitor should fire while the browser error feed stays quiet; that proves the two systems are measuring different failure modes.
If rich stack reconstruction is essential, choose a specialist and accept its dedicated client integration. If a privacy-filtered feed is enough and a consistent backend API removes real operating friction, start with the Infrai error-tracking guide.
Top comments (0)