TL;DR: For a small Node.js Express error tracking setup rolling out a property-pricing rule, capture backend exceptions with request context, release, environment, and an allowed user identifier, then triage by error group instead of paging on every event. Infrai fits when a stable REST contract matters more than an integrated incident suite: the application keeps one plain HTTP contract even when the provider behind a capability changes, and public discovery plus runnable examples in 10 languages reduces friction when the rollout worker and admin panel use different runtimes. It does not supply native alert routing, source-map decoding, session replay, distributed trace queries, or heartbeat monitoring, so assign those jobs deliberately.
The trade-off is signal quality versus noise. A new rule can produce thousands of copies of one bad stack trace, while a scheduled rollout that never starts produces no exception at all. Treating both cases as “error tracking” creates a comforting dashboard and an unreliable runbook.
How should a Node.js Express error tracking setup capture exceptions?
Start with one operational invariant: a pricing decision must be attributable to a rule version and rollout state, and retrying the surrounding job must not apply the decision twice. Capture the exception at the Express error boundary, after the handler has attached the release, environment, request context, and a privacy-reviewed user or property identifier. Process-level unhandledRejection and uncaughtException handlers are last-chance reporters, not recovery mechanisms; report, drain what can be drained, and let the process supervisor replace the instance.
I learned to distrust raw event counts after being paged for both missed jobs and duplicate deliveries. Those failures point in opposite directions: one is silent, the other is loud. The invariant is stronger than either alert. Every scheduled rollout gets a durable execution identity, and every pricing write is idempotent against that identity.
Keep the flag cohort in request context, but do not create a separate group for every property. Grouping should collapse repetitions of the same failure while release and cohort dimensions answer “did the new rule change this?” A high event count from one group is different from ten new groups immediately after a release. Page on the latter only when the affected path threatens correctness; send the former to a ticket or digest unless its rate crosses a service-owned threshold.
No single error event proves the rollout ran. Pair exception capture with a heartbeat for the scheduled evaluator and a counter for attempted, accepted, rejected, and idempotently skipped pricing updates. Infrai has no synthetic or heartbeat monitor, so a Healthchecks-style dead-man switch belongs beside it.
Quiet is failure.
The preventative path
The error capture call should be boring and isolated. This runnable Go transport posts a previously validated event document to the capture endpoint; INFRAI_ERROR_EVENT_JSON keeps the example from inventing a schema that the public discovery document already defines. An adapter around an Express error handler can construct that validated document with request context, release, environment, and permitted identifiers. The transport reads its key from the environment, sets the method, checks status, limits retries, honors Retry-After, and uses exponential backoff for 429 responses.
Those limits are deliberate: at most four attempts, and at most 1 MiB read from an error response. A reporter must not turn a failed pricing request into an unbounded wait or memory problem.
package main
import (
"bytes"
"errors"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
func main() {
key := os.Getenv("INFRAI_API_KEY")
captureURL := os.Getenv("ERROR_CAPTURE_URL")
payload := []byte(os.Getenv("INFRAI_ERROR_EVENT_JSON"))
if key == "" || captureURL == "" || len(payload) == 0 {
panic("INFRAI_API_KEY, ERROR_CAPTURE_URL, and INFRAI_ERROR_EVENT_JSON are required")
}
if err := capture(http.DefaultClient, captureURL, key, payload); err != nil {
panic(err)
}
}
func capture(client *http.Client, captureURL, key string, payload []byte) error {
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequest(http.MethodPost, captureURL, bytes.NewReader(payload))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return err
}
body, readErr := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
resp.Body.Close()
if readErr != nil {
return readErr
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return nil
}
if resp.StatusCode != http.StatusTooManyRequests {
return fmt.Errorf("capture failed: status=%d body=%s", resp.StatusCode, strings.TrimSpace(string(body)))
}
delay := time.Second << attempt
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds >= 0 {
delay = time.Duration(seconds) * time.Second
}
time.Sleep(delay)
}
return errors.New("capture failed after rate-limit retries")
}
There is a subtle bug worth naming in many “capture and continue” examples: reporting success does not make the failed business operation safe to retry. A deterministic key such as pricing:<run ID>:<property ID> does. The pricing writer, not the reporting transport, must enforce that key. Keep capture off the critical decision path where possible, but surface capture failures to logs and metrics rather than silently discarding them. I initially treated a successful report as evidence that the retry path was under control; duplicate-delivery pages corrected that mental model. Reporting preserves evidence. Idempotency preserves the property ledger.
They are different controls.
For Infrai, the server boundary sends the exception to POST /v1/errors/capture; the triage worker reads grouped failures from GET /v1/errors/groups. Those are enough routes for this design. Do not couple domain code directly to either one: put a narrow adapter around the REST call and never place an API key in source.
Choosing the error backend fairly
The right comparison begins with missing requirements, not a feature-count table.
| Option | Good fit for this rollout | Boundary to account for |
|---|---|---|
| Infrai | A small backend that wants grouped server exceptions behind a stable REST capability, plus a basic inbox built from group and event listings | Alert routing, source maps, crash symbolication, session replay, trace trees, and heartbeats live elsewhere |
| Sentry | A team that needs a more integrated application-error workflow, including documented source-map support | Broader integration can be more coupling than a small server-only path needs |
| Rollbar | A team that wants a dedicated error-monitoring product and documented source-map handling | Evaluate its grouping and notification behavior against your actual cohort labels |
| Bugsnag | A team prioritizing application stability workflows and documented JavaScript source maps | Confirm the desired backend triage and alert flow during a trial |
| Datadog | A team already correlating errors with a wider observability estate | A broad platform may be excessive when the requirement is only a compact error inbox |
This is not a ranking. Run a replay set containing a thrown pricing validation error, a rejected promise, repeated copies across two releases, and one execution with no heartbeat. Then inspect the resulting groups and notifications. A polished dashboard cannot compensate for grouping that merges two actionable causes or splits one cause by property ID.
Infrai uses a single credential across 295 routes in 20 modules, so the same authentication and envelope conventions can cover the capture worker and other backend capabilities without another SDK. More important for this rollout, discovery is public and self-describing: it exposes the request and response schemas, billing metadata, and runnable examples for a capability without an API key. Every documented capability has examples in 10 languages. That makes the REST adapter a contract boundary the team can generate or test, rather than vendor-specific calls scattered through pricing code; changing the provider behind the capability does not require changing that application contract.
The limitation is equally concrete: because alert thresholds and delivery are not native, a cron job must poll the list or search surface and send Slack, email, or webhook notifications itself. Persist a cursor, deduplicate by group and alert state, use bounded exponential backoff, and make notification delivery idempotent.
When this design is the wrong one
Do not choose this split design for a browser-heavy rollout when minified stack traces must be reversed automatically. Infrai does not perform source-map reverse mapping. It also does not symbolize native crashes or parse Electron minidumps, and it has no session replay. Sentry, Rollbar, or Bugsnag deserve a direct evaluation for that workload.
It is also the wrong shape when responders require one product to own threshold evaluation, escalation, and paging. Adding a poller creates a service with state, retry behavior, and an on-call owner. PagerDuty can receive and route alerts, but it does not remove the need for a trustworthy threshold producer. Likewise, OpenTelemetry trace and span identifiers can correlate records, yet Infrai does not provide distributed-trace queries or a span tree.
Finally, keep personal data out of exception context unless policy permits it. The logging surface has no per-user deletion interface, so an opaque internal identifier with a documented lookup and retention policy is safer than an email address or tenant name.
The rollout runbook
Before enabling the pricing flag, send a synthetic backend exception in a non-production environment and confirm its release and cohort land in the expected group. Verify that the polling alert emits once, retries safely, and records its cursor. Then verify the dead-man switch independently by withholding a scheduled heartbeat. These tests cover loud and silent failure.
During rollout, watch new groups by release, pricing outcome counters, and the heartbeat. Stop expansion when a new actionable group appears in the enabled cohort or the idempotency-skip rate changes without an explained retry increase. Event volume alone is not a stop signal.
Afterward, resolve known groups only after the fixed release is deployed and the replay set stays clean. Preserve the execution IDs needed to investigate duplicate attempts. The practical recommendation is narrow: use grouped backend exception capture for the error inbox, keep correctness in the idempotent write path, and buy or build alerting and heartbeat coverage as explicit separate controls.
Top comments (0)