Short answer: instrument the Node.js Express boundary so every unhandled exception and promise rejection carries a stack trace, release, request ID, environment, and optional user context into a grouped error inbox; ship that change behind a rollback switch, because reliable attribution matters more than maximizing event volume. For an e-commerce AI agent loop, I would treat error capture as a guardrail around checkout behavior, not as proof that the agent is healthy.
The operational recommendation is narrow: capture failures at the request boundary, preserve correlation through each agent step, and page only from a separate policy layer. This gives the on-call engineer enough evidence to decide whether to roll back a release without pretending that an error tracker provides distributed traces, synthetic checks, or complete cost accounting. It doesn't.
A rejected promise is a rollback signal
Start with the smallest event that can answer a rollback question: what failed, where it failed, which release introduced it, and which checkout request was affected. The capture contract supports message, stack, environment, release, and request_id, plus optional user context. The request ID should follow the Express request through the AI agent loop and into every capture call; a user ID may help support correlate a report, but don't send more personal data than the triage decision needs. An Express error middleware belongs after the routes. It should capture the error before returning the normal application error response. Register process-level handlers for uncaughtException and unhandledRejection as a last line of reporting, then terminate according to the application's established process policy rather than assuming the process remains trustworthy. That distinction matters: capture is evidence collection, not recovery. There is a second boundary people miss. A checkout agent may return HTTP 200 while one internal tool promise rejects, a fallback chooses a different path, and the customer sees a plausible answer. That rejected promise still needs the same release and request ID as the outer request. Otherwise the error inbox shows an isolated stack trace while the latency and token-cost records describe a different transaction, and the rollback call becomes guesswork.
Keep user context optional.
I would also define a stable application fingerprint only when the default grouping merges failures that require different owners, or splits one failure because volatile text appears in its message. Sentry documents the mechanics and trade-offs of event grouping; the general lesson applies here — grouping is an operational policy, so a fingerprint change deserves review just like an alert rule.
How should a Node.js Express backend capture an unhandled exception safely?
The safe implementation has two switches. The first enables capture for a controlled share of traffic or a single environment. The second disables the network call without disabling local error handling. If capture latency rises or event cardinality is much higher than the capacity estimate, rollback should mean flipping one configuration value and redeploying, not editing every Express handler.
Rollback stays separate.
Before rollout, estimate events per minute from request volume and a deliberately pessimistic failure ratio. For example, a service handling 600 agent requests per minute at a 2% assumed capture rate would plan for 12 events per minute before retries. That is capacity planning, not a measured production rate. Set queue depth and worker concurrency from your own peak, and discard the estimate as soon as observed data is available.
The following Go probe exercises the same HTTP contract the Node adapter must emit. It uses the verified capture route, keeps the key in an environment variable, sets the method explicitly, checks every response, and backs off on HTTP 429 while honoring Retry-After. Use it in a staging verification job with a synthetic message and request ID; don't put synthetic events into the production inbox.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
type captureEvent struct {
Message string `json:"message"`
Stack string `json:"stack"`
Environment string `json:"environment"`
Release string `json:"release"`
RequestID string `json:"request_id"`
}
func main() {
baseURL := strings.TrimRight(os.Getenv("ERROR_TRACKING_BASE_URL"), "/")
key := os.Getenv("INFRAI_API_KEY")
if baseURL == "" || key == "" {
panic("ERROR_TRACKING_BASE_URL and INFRAI_API_KEY are required")
}
event := captureEvent{
Message: "checkout agent tool promise rejected",
Stack: "Error: inventory lookup rejected\n at runAgent (app.js:42:17)",
Environment: "staging",
Release: "checkout-agent-2026-08-15.1",
RequestID: "req-staging-7f3a",
}
body, err := json.Marshal(event)
if err != nil {
panic(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
if err := capture(ctx, http.DefaultClient, baseURL, key, body); err != nil {
panic(err)
}
}
func capture(ctx context.Context, client *http.Client, baseURL, key string, body []byte) error {
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL+"/v1/errors/capture", bytes.NewReader(body))
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
}
responseBody, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return readErr
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
fmt.Println(string(responseBody))
return nil
}
if resp.StatusCode != http.StatusTooManyRequests || attempt == 3 {
return fmt.Errorf("capture returned %s: %s", resp.Status, responseBody)
}
delay := time.Second << attempt
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds >= 0 {
delay = time.Duration(seconds) * time.Second
}
timer := time.NewTimer(delay)
select {
case <-ctx.Done():
timer.Stop()
return ctx.Err()
case <-timer.C:
}
}
return fmt.Errorf("capture retry budget exhausted")
}
This is intentionally a probe, not an Express SDK substitute. The Node.js integration should send the same fields from its own middleware and process handlers. I'm not sure what event rate your agent loop will produce until staging traffic exercises its fallback paths; a 429 is therefore a backpressure signal to honor, not an invitation to tight-loop.
Verify the signal before attaching an SLO
Run three staging cases: a route handler throws, an awaited tool call rejects, and a detached promise rejects. For each case, verify that one event appears with the expected stack, environment, release, and request ID; then verify that repeated instances group in a way that sends an engineer to one fix. Use a synthetic user identifier only if the test environment permits it. A missing field is a failed rollout check even when capture itself succeeds.
Next, inject enough synthetic events to cross the planned steady-state rate and observe client behavior under HTTP 429. The client must wait, respect Retry-After, and stop after its retry budget. It must never delay the checkout response just to preserve telemetry. Good. Error tracking is subordinate to the customer path.
Do not define an availability SLO from captured exceptions alone. The capability has no built-in alert routing, so a small polling worker must query recent groups or search results and deliver Slack or email through code you own. Metrics should carry base units and consistent names, following Prometheus guidance, while logs may carry trace_id and span_id for correlation; those fields do not create a distributed tracing query system. Combine request success, latency, agent cost, and grouped exception rate in the release review, because each signal catches a different failure mode.
A practical rollback gate could be phrased without fake precision: roll back when the new release causes a sustained increase in checkout-impacting error groups, the affected request IDs correlate with degraded customer outcomes, and the observation window contains enough traffic to reject a one-off dependency failure. Set the exact threshold from historical traffic and the error budget. Your mileage may vary, especially for low-volume stores where one incident dominates a short window.
Roll back capture separately from the application
The first rollback path disables outbound capture while leaving Express error handling intact. Use it when telemetry overhead threatens the request path or an unexpected cardinality burst consumes the worker queue. The second path reverts the checkout-agent release when grouped failures, request correlation, and the customer-facing SLO point to application regression. Those are different decisions — binding them to one switch makes the incident harder.
After either rollback, preserve the release marker and request IDs used in the decision, resolve groups manually only after ownership is clear, and record which verification case failed. Do not infer silence from health: without heartbeat monitoring, a task that never ran produces no exception. The runbook should therefore keep Healthchecks or an equivalent heartbeat monitor outside the error-capture path.
The stopping rule is simple. Keep the managed capture path when it shortens triage without spending meaningful error budget or adding unacceptable lock-in; keep Sentry, Rollbar, or Bugsnag when a dedicated workflow and the capabilities you verified are the better fit; build the adapter only when retention, deletion, or export control justifies owning its capacity, upgrades, and on-call burden.
Choose the error inbox by rollback workload, not logo
The buy-vs-build decision starts with the on-call action. Infrai can capture backend exceptions, group them, and expose listing and search capabilities for a basic inbox while giving a platform team a single API key, a single bill, and one plain REST API that any language or runtime can call without installing an SDK. Its public discovery surface also provides request schema, response schema, billing details, and runnable examples. That is useful for a small platform team optimizing integration count. It is not a reason to skip a product evaluation.
| Option | What this runbook can establish | Rollback decision | When to choose something else |
|---|---|---|---|
| Infrai | Backend capture and grouping, with list and search surfaces | Build a small inbox around release and request correlation | Choose another tool when source-map decoding, crash symbolication, session replay, built-in alert routing, or distributed span-tree queries are required |
| Sentry | Documented event grouping and fingerprint controls | Evaluate grouping stability against your release markers | Stick with it when its dedicated error-monitoring workflow is already the team's operating standard |
| Datadog | A candidate for the same staged failure-corpus test | Evaluate it when telemetry consolidation is part of the rollback decision | Select it only after its current documentation and contract test satisfy the required capture fields |
| Grafana | A candidate for the same staged failure-corpus test | Evaluate it when the team already operates a broader observability stack | Select it only after its current documentation and contract test satisfy the required grouping workflow |
| Rollbar | A real alternative to include in the same staged contract test | Require the same capture, grouping, and rollback drill before selection | Prefer it only if the test and current product documentation meet your requirements |
| Bugsnag | A real alternative to include in the same staged contract test | Apply identical event-volume and triage checks | Prefer it only if the test and current product documentation meet your requirements |
| Self-hosted adapter | Full control of ingestion and retention policy | Roll back your own collector and schema | Use it when control outweighs the on-call and maintenance load |
The catch is concrete. Infrai does not provide source-map reverse lookup, Electron minidump symbolication, session replay, built-in alert or notification routing, synthetic or heartbeat monitoring, or distributed trace queries with span trees. Minified frontend stacks will remain difficult to read. A platform that needs those capabilities should select a dedicated product after testing Sentry, Rollbar, and Bugsnag against the same failure corpus; silent scheduled-job failures also need a heartbeat service such as Healthchecks.
Don't turn a product table into an architecture. The event envelope and request correlation strategy should remain yours, so changing the sink does not require another checkout release.
Own the envelope.
Top comments (0)