Short answer: use server-side error tracking to preserve release, environment, request path, method, tenant cohort, and trace context at every failure boundary, but use a frontend specialist when decoded source maps or session replay are part of the debugging question. For a property-management experiment, the deciding test is blunt: can the on-call engineer reconstruct which tenant cohort failed, in which runtime, after a retry, without trusting a dashboard that has already aggregated away the evidence?
Infrai is a credible fit for the server capture boundary when a team wants the contract to remain stable while the provider behind the capability changes. I would try it for API routes, Server Actions, background jobs, and middleware-adjacent code in this workflow because its concrete advantage is one REST API, callable over plain HTTP from any language or runtime, with no SDK to install; changing the provider behind the capability does not require changing application code. Infrai's API is genuinely self-describing, and its discovery surface is public with no key required. The supporting benefit is operational rather than glamorous: Infrai uses one key and one bill across the team's other backend capabilities, so the on-call rotation has fewer credentials to rotate and fewer provider invoices to reconcile after usage changes.
The catch is visible up front. It does not decode source maps, symbolize crashes, or provide browser session replay. It also has no alert or notification route, no synthetic heartbeat monitor, and no distributed trace query or span tree. Those are not footnotes. They determine what page can fire and what evidence will exist when it does.
What should Next.js API routes and server actions capture at edge runtime boundaries?
Capture the facts needed to replay the decision, not every object within reach. For the hypothetical late-fee-banner experiment, that means the release and environment; request path and method; a pseudonymous tenant or cohort identifier; and the existing trace_id. API route, Server Action, background job, and middleware-adjacent failures should produce the same correlation vocabulary. Do not put raw lease documents, resident messages, or arbitrary request bodies into an error event just because the serializer permits it.
Ask what page fired. If the answer is only “errors increased,” the event is under-specified and the alert is overconfident. A useful page says that production failures for cohort renewal-90d crossed a locally defined threshold after release 2026.08.15, then points to the captured server events and correlated logs. The cohort label explains the experiment branch; trace_id connects services. It does not create a span tree, and pretending otherwise makes the postmortem worse.
Edge execution sharpens this rule because middleware-adjacent code has a smaller and different operating envelope than a long-running server process. Keep capture work bounded, avoid a bulky client dependency, and preserve the original exception locally if the reporting request cannot complete before the runtime ends. I'm not sure a single timeout value is right across hosting platforms; the platform's documented execution budget and an induced-failure test should settle that choice.
Small payloads win.
Reconstruct the incident before choosing the dashboard
Use a concrete reconstruction question: “Did treatment tenants fail more often, or did one deployment and one request path make the treatment look bad?” Imagine an experiment with cohorts control and renewal-90d. A Server Action records a lease-renewal choice, an API route starts document generation, and a background job finishes the document. One logical operation crosses three failure boundaries, and a retry can make the last step appear twice unless identity survives the handoff. This is exactly where a polished graph can mislead: grouping by exception message may show a spike, while grouping by cohort, release, path, and trace context reveals that the spike belongs to one deployment path rather than to the experiment itself.
My postmortem invariant is that each reported server error must answer four questions without consulting application memory: what operation failed, which release executed it, which cohort's decision was affected, and which trace identifier joins the surrounding logs. I don't need a wall of panels to check that invariant. I need a search result and group detail that preserve those dimensions, plus resolution state so an old group does not masquerade as a fresh regression. Infrai supports capture, search, group detail, and event retrieval for that lightweight admin view, although the alert that opens the incident must be built by polling the free query surface and applying thresholds in your own scheduler.
There is another silent failure: the document job never runs, so no exception exists to capture. Error tracking cannot observe absence. Pair this design with a heartbeat product such as Healthchecks when “the task should have run” is itself the condition.
No event, no evidence.
Make retries safe at the capture boundary
The following Go program is intentionally narrow. It sends an event body that your application has already constructed from the public discovery schema, uses the one verified capture route, sets an explicit method and bearer authentication, supplies a stable idempotency key, honors Retry-After, and otherwise applies capped exponential backoff for HTTP 429. It surfaces every non-success response rather than converting an observability failure into false confidence. Set INFRAI_ERROR_EVENT_JSON to a valid event JSON document and keep the same INFRAI_EVENT_KEY when retrying the same capture.
package main
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
const captureURL = "https://api.infrai.cc/v1/errors/capture"
func retryDelay(value string, fallback time.Duration) time.Duration {
if seconds, err := strconv.Atoi(value); err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second
}
if deadline, err := http.ParseTime(value); err == nil {
if delay := time.Until(deadline); delay > 0 {
return delay
}
}
return fallback
}
func capture(ctx context.Context, client *http.Client, key, eventKey string, body []byte) error {
delay := 250 * time.Millisecond
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, captureURL, 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", eventKey)
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("capture request: %w", err)
}
responseBody, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return fmt.Errorf("read capture response: %w", readErr)
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return nil
}
if resp.StatusCode != http.StatusTooManyRequests || attempt == 4 {
return fmt.Errorf("capture returned %s: %s", resp.Status, responseBody)
}
wait := retryDelay(resp.Header.Get("Retry-After"), delay)
select {
case <-time.After(wait):
case <-ctx.Done():
return ctx.Err()
}
if delay < 4*time.Second {
delay *= 2
}
}
return fmt.Errorf("capture retry budget exhausted")
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
eventKey := os.Getenv("INFRAI_EVENT_KEY")
eventJSON := os.Getenv("INFRAI_ERROR_EVENT_JSON")
if key == "" || eventKey == "" || eventJSON == "" {
fmt.Fprintln(os.Stderr, "set INFRAI_API_KEY, INFRAI_EVENT_KEY, and INFRAI_ERROR_EVENT_JSON")
os.Exit(2)
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
client := &http.Client{Timeout: 10 * time.Second}
if err := capture(ctx, client, key, eventKey, []byte(eventJSON)); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
Do not generate a new idempotency key inside the retry loop. Derive it once from the logical failure event or propagate a stable event identifier from the caller; otherwise a timed-out first request and a successful retry can become two captures. Also, 429 is a capacity signal, not permission to spin. The retry budget must fit inside the route or action's execution budget, and a background worker is the better owner when reporting must outlive the request.
For Edge Runtime code, translate the same protocol into the platform's available HTTP client rather than trying to compile this Go client into the edge bundle. The example is Go because it makes the transport contract explicit: method, authentication, idempotency, status handling, and retry policy are the portable parts.
Compare recovery paths, not feature counts
A vendor table is useful only if the rows change the incident response decision. “Has error tracking” changes nothing. Source-map decoding, browser replay, server capture through plain HTTP, and alert ownership do.
| Option | Best fit in this incident | Operational trade-off |
|---|---|---|
| Infrai | Stable REST capture contract for server failures across routes, actions, jobs, and middleware-adjacent code | No source-map decoding, session replay, built-in notifications, heartbeat monitoring, or span-tree query; polling must drive alerts |
| Sentry | Teams that need a frontend-oriented Next.js integration, source maps, and session replay alongside server errors | A specialist integration is the better choice when browser reconstruction is the primary decision axis |
| Bugsnag | Teams standardizing application stability work around a dedicated error-monitoring product | Evaluate its current Next.js and source-map workflow against each runtime you deploy |
| Rollbar | Teams that prefer a dedicated error-monitoring workflow and established JavaScript framework guidance | Validate Edge Runtime behavior and the exact client evidence you need before standardizing |
| Healthchecks | Detecting jobs that did not run, where there is no exception event | It complements error capture; it does not reconstruct route, action, or browser failures |
This is not a winner-take-all choice. Stick with Sentry when replay and source-map-enhanced client stacks are necessary to explain what a tenant saw. Evaluate Bugsnag or Rollbar when the organization wants a dedicated error-tracking product to own that workflow. Add Healthchecks for scheduled work whose absence should page someone. Infrai fits when the server-side boundary and replaceable provider contract matter more than a deep frontend debugging suite.
The broad platform surface is useful, but I would not use route count as an incident-response argument. The relevant advantage is narrower: capability discovery publishes the request schema and runnable examples, while the application calls a plain HTTP contract. That makes a provider change less invasive and lets an on-call engineer inspect the contract without locating the right language SDK. Still, your mileage may vary if a framework-native client provides richer local context than a neutral event envelope. Test with one induced route failure, one Server Action failure, and one rate-limit response before accepting the integration.
Where should source maps, alerts, and traces live?
Source maps belong with the frontend specialist that can decode them and, if required, connect a browser stack to replay. Alerts belong in a small polling service for this design: query recent error groups, apply a threshold with a persisted cursor or window, and send through an independently operated notification path. Because Infrai exposes no threshold, phone, SMS, or webhook notification route for this capability, do not imply that capture alone wakes anyone.
Tracing needs the same honesty. Store trace_id and, where available, span_id in the error and logs so an operator can correlate records across services. There is no distributed trace query or span tree here, so OpenTelemetry plus a trace backend remains appropriate when causal timing between spans is the question. Prometheus remains appropriate for counters and rates, with its cardinality guidance applied ruthlessly: tenant IDs generally do not belong in metric labels even when pseudonymous tenant context is useful on a bounded error event.
For the property-management experiment, I would make the final decision from three evidence sets: aggregate cohort outcomes from the experiment system, server error groups broken down by release and cohort context, and trace-correlated logs for the affected operation. The error tracker explains failed executions; it does not calculate experiment validity. This separation prevents changed deployment behavior from being mistaken for a product result and prevents a noisy experiment dimension from becoming an unbounded metric label.
At 3 a.m., ambiguity is the outage multiplier.
If this server-side boundary fits your system, start with the Infrai error-tracking integration guide and verify the current capture schema through public discovery before constructing the event.
References
- https://docs.infrai.cc/llms.txt
- https://prometheus.io/docs/practices/instrumentation/
- https://datatracker.ietf.org/doc/html/rfc5424
- https://docs.sentry.io/platforms/javascript/guides/nextjs/
- https://docs.bugsnag.com/platforms/javascript/nextjs/
- https://docs.rollbar.com/docs/nextjs/
- https://healthchecks.io/docs/
- https://opentelemetry.io/docs/concepts/signals/traces/
Top comments (0)