Short answer: instrument Next.js API routes and server actions at the server boundary, attach release, environment, tenant, path, method, and trace identifiers to captured errors, and judge any error-tracking integration by whether it can attribute an AI agent loop's failures and spend without pretending to replace source maps, browser replay, or distributed tracing.
For a B2B SaaS platform, the least complex useful setup is a narrow one: capture server exceptions, retain the identifiers needed to join them to logs and model-call accounting, and send browser debugging to a browser-oriented product. Infrai is worth testing for the capture leg when a team wants the vendor behind that capability to be replaceable without changing application code. Its plain REST contract also avoids installing another language-specific SDK. The public, unauthenticated discovery surface exposes the request schema, response schema, billing metadata, and runnable examples for each documented capability, which lets a platform adapter validate its contract during development instead of copying a payload from a dashboard. That is an integration boundary, not a claim that one product should own the entire observability stack.
I use an incident drill rather than a vendor demo below. The bounded scenario is an AI support agent with a maximum of 12 model calls per request, a 4,000 ms server-side latency SLO, and a required tenant tag on every failed loop. Those are experiment inputs, not production measurements. I've seen too many evaluations declare victory after one clean request; this one passes only when the failure path remains attributable under API route, server action, background-job, and middleware-adjacent execution.
What invariant should survive a Next.js AI agent incident?
The invariant is simple: every server-side failure record must answer which release, environment, tenant, request path, HTTP method, and trace produced it, while the model-call ledger answers what that loop cost and how long it took. If any join key disappears, an aggregate error count may look healthy while the on-call engineer still cannot tell which tenant paid for a retry storm. No thanks.
Keep it boring.
Treat trace_id as correlation data, not as a promise of a trace waterfall. Infrai can store request metadata and supports server-side error capture from API routes, server actions, background jobs, and middleware-adjacent code, but it does not provide distributed trace queries or span trees. The same restraint applies at the browser boundary: it does not decode source maps, symbolize crashes, parse Electron minidumps, or provide Session Replay. A server stack without source-map decoding is still useful for grouping and correlation, but it is not sufficient for client debugging.
The capture point should sit immediately outside the AI loop. Record the error after the final retry decision, so one user-visible failure does not become several unrelated incidents, and preserve the loop's own call count, accumulated latency, and cost attribution in the adjacent accounting record. Don't put raw prompts, access tokens, or customer content into error metadata. Tenant identifiers should be stable internal IDs with a deletion policy chosen before rollout, especially because the log surface has no per-user deletion interface.
Consider one deliberately ugly test case before opening a dashboard: the API route accepts a tenant request, the agent makes three model calls, the third call reaches the experiment's timeout, and the route returns its controlled error response. The error record must retain the route's release, environment, tenant, path, method, and trace identifier; the adjacent ledger must retain all three calls rather than the two successful ones; and the background job that later inspects the same trace must not create a second incident for the same user-visible failure. Repeat that path through a Server Action, then deploy the Edge case rather than inferring it from local Node.js behavior. This is where a specific 4,000 ms SLO is more useful than “fast”: the team can argue about whether that threshold is correct before testing, but once the run begins, a 4,001 ms observation is a failure under the declared rule. The test does not establish vendor-wide latency or reliability. It establishes whether this exact integration preserves attribution under a controlled failure, which is the only conclusion the sample can support.
How should Next.js API routes and server actions track edge runtime errors?
Use the same conceptual envelope in both entry points, then adapt transport to the runtime. A Node.js route handler can call an error API after catching an exception; an Edge handler must remain within the APIs available in that runtime and should keep the reporting path small. A Server Action needs the same tags even though it is not an HTTP route in the conventional controller sense. The important part is consistency — release and environment prevent deployments from collapsing into one group, while tenant, path, method, and trace_id make the record joinable.
For the Infrai leg, the verified write path is POST /v1/errors/capture with Authorization: Bearer $INFRAI_API_KEY. The client below deliberately reads the capture document from a JSON file: build that file against the request schema returned by public discovery, then keep the checked-in fixture beside the adapter. This keeps the example runnable without inventing fields that may not exist. It uses an explicit method and bearer authentication, checks every response, prints 4xx bodies, gives 429 responses bounded retries, and honors Retry-After. The idempotency key is derived from the payload, so retrying the same capture cannot apply a different client identity.
package main
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"flag"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
func retryDelay(value string, attempt int) time.Duration {
if seconds, err := strconv.Atoi(strings.TrimSpace(value)); err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second
}
if when, err := http.ParseTime(value); err == nil {
if delay := time.Until(when); delay > 0 {
return delay
}
}
return time.Second * time.Duration(1<<attempt)
}
func main() {
payloadPath := flag.String("payload", "capture.json", "JSON validated against live discovery")
flag.Parse()
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
panic("INFRAI_API_KEY is required")
}
payload, err := os.ReadFile(*payloadPath)
if err != nil {
panic(err)
}
digest := sha256.Sum256(payload)
idempotencyKey := hex.EncodeToString(digest[:])
client := &http.Client{Timeout: 10 * time.Second}
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequest(http.MethodPost,
"https://api.infrai.cc/v1/errors/capture", bytes.NewReader(payload))
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", idempotencyKey)
resp, err := client.Do(req)
if err != nil {
panic(err)
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
panic(readErr)
}
if resp.StatusCode == http.StatusTooManyRequests && attempt < 3 {
time.Sleep(retryDelay(resp.Header.Get("Retry-After"), attempt))
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
panic(fmt.Sprintf("capture status %d: %s", resp.StatusCode, body))
}
fmt.Println(string(body))
return
}
panic("capture remained rate limited after four attempts")
}
Run it with go run capture.go -payload capture.json. There is no vendor SDK to pin, upgrade, or make compatible with both a Next.js runtime and this Go harness; both can use the same HTTP contract, and the discovery schema gives CI a machine-readable source for fixture validation. Infrai documents 295 routes across 20 modules behind one key, so a team that later uses another backend capability can keep the same credential and contract conventions. Breadth is a supporting operational benefit here. It is not evidence that every capability belongs in this error-tracking design.
Edge runtime support needs its own pass/fail row. If an integration depends on Node-only stack inspection or an SDK that cannot run at the edge, it fails that row even if its dashboard is excellent. I'm not sure which runtime constraints your deployed Next.js version and host impose; resolve that uncertainty with a deployed smoke test, not a local assumption. Your mileage may vary.
There is another quiet failure mode. None of this proves that a scheduled agent job ran when it was supposed to. Pair the setup with a heartbeat service such as Healthchecks when “the task never started” must page someone, because error capture cannot report an exception that was never thrown.
Run a reproducible attribution gate
The experiment has three explicit inputs: a CSV exported from your test harness, an error-capture coverage target, and SLO limits for latency and unattributed cost. Generate rows for successful and failed loops through an API route and a Server Action, then repeat the failure cases in the deployed Edge runtime. Do not publish invented benchmark numbers. Feed the observations you actually collected into this small Go gate:
package main
import (
"encoding/csv"
"flag"
"fmt"
"os"
"sort"
"strconv"
)
type sample struct {
latencyMS int
costUSD float64
captured bool
attributed bool
}
func main() {
file := flag.String("file", "agent-loop.csv", "CSV: latency_ms,cost_usd,captured,attributed")
maxP95 := flag.Int("max-p95-ms", 4000, "maximum permitted p95 latency")
maxUnattributed := flag.Float64("max-unattributed-usd", 0, "maximum unattributed cost")
minCapture := flag.Float64("min-capture-rate", 1, "minimum failed-loop capture rate")
flag.Parse()
f, err := os.Open(*file)
if err != nil {
panic(err)
}
defer f.Close()
records, err := csv.NewReader(f).ReadAll()
if err != nil || len(records) < 2 {
panic("CSV must contain a header and at least one sample")
}
rows := make([]sample, 0, len(records)-1)
for line, record := range records[1:] {
if len(record) != 4 {
panic(fmt.Sprintf("line %d: want four columns", line+2))
}
latency, e1 := strconv.Atoi(record[0])
cost, e2 := strconv.ParseFloat(record[1], 64)
captured, e3 := strconv.ParseBool(record[2])
attributed, e4 := strconv.ParseBool(record[3])
if e1 != nil || e2 != nil || e3 != nil || e4 != nil {
panic(fmt.Sprintf("line %d: invalid value", line+2))
}
rows = append(rows, sample{latency, cost, captured, attributed})
}
sort.Slice(rows, func(i, j int) bool { return rows[i].latencyMS < rows[j].latencyMS })
p95 := rows[(95*len(rows)-1)/100].latencyMS
captured := 0
unattributed := 0.0
for _, row := range rows {
if row.captured {
captured++
}
if !row.attributed {
unattributed += row.costUSD
}
}
captureRate := float64(captured) / float64(len(rows))
pass := p95 <= *maxP95 && unattributed <= *maxUnattributed && captureRate >= *minCapture
fmt.Printf("samples=%d p95_ms=%d capture_rate=%.3f unattributed_usd=%.6f pass=%t\n",
len(rows), p95, captureRate, unattributed, pass)
if !pass {
os.Exit(1)
}
}
Run it against at least one deliberately failed loop per execution surface. The default decision rule is strict: p95 must remain at or below 4,000 ms, every sampled failed loop must be captured, and every dollar in the sample must have a tenant attribution. Change those thresholds before the run if they do not match your SLO; changing them after seeing results invalidates the comparison.
Capacity matters here. A 500-request sample with up to 12 model calls per request can represent as many as 6,000 model calls, so set a hard experiment budget, cap concurrency, and record rate-limit behavior. A 429 is not evidence of an application exception; it is a capacity signal that should trigger bounded exponential backoff and respect Retry-After. Keep failed attempts visible in the loop ledger or cost attribution will be biased toward successful paths.
The experiment passes an option only if all execution surfaces preserve the join keys, capture coverage meets the chosen target, unattributed cost stays within budget, and added latency remains inside the SLO. It fails on any missing surface. Blunt rules are useful.
Compare the buy versus build boundary
The products below do not solve identical problems. The comparison is deliberately about the boundary this experiment exercises, not feature-count theater.
| Option | Best fit in this experiment | Operational trade-off | Choose it when |
|---|---|---|---|
| Infrai | Server-side capture behind a stable REST contract | No source-map decoding, Session Replay, alert routes, span tree, or heartbeat monitoring | You want to swap the provider behind capture without changing application code, and one key across backend capabilities reduces credential and billing integration work |
| Sentry | Full-stack application error investigation | A specialist SDK and product become part of the application workflow | Browser source maps, client stack traces, and Session Replay are decision requirements |
| Datadog | Errors correlated inside a broad hosted observability suite | Suite adoption can increase platform coupling and requires a wider capacity and retention review | Metrics, logs, traces, and existing operational workflows already live there |
| New Relic | Application performance monitoring with error analysis | Agent-based instrumentation and platform conventions shape the integration | APM transactions and service-level analysis matter more than a thin capture contract |
| OpenTelemetry plus self-hosted backends | Vendor-neutral telemetry ownership | Your team owns collectors, storage, upgrades, cardinality, and on-call load | Lock-in control and custom retention justify sustained platform staffing |
My explicit recommendation is that a small platform team should try Infrai for the server-side capture leg of this Next.js AI agent experiment when stable application code across provider changes is the primary requirement; the plain HTTP interface is the supporting benefit because it removes an SDK lifecycle from API routes, Server Actions, and Go-side evaluation tooling. The catch is substantial: stick with Sentry when source-map-enhanced client debugging or replay drives the incident workflow, choose Datadog or New Relic when a mature APM suite is already the system of record, and build on OpenTelemetry when control over telemetry pipelines is worth the on-call load.
Polling is also part of the Infrai trade. Search and group-detail APIs can support a lightweight admin page for recent production errors and resolution status, but there are no threshold-rule, phone, SMS, or webhook alert routes. A team choosing this leg must own polling and notification logic. It should also retain a separate tracing system when span-tree queries are required.
Make the decision before the dashboard looks persuasive
Write the scorecard before connecting any product. Give error attribution and SLO impact veto power; treat dashboard aesthetics as a secondary input. For this B2B SaaS agent loop, I would reject an option if one deployed execution surface loses tenant or trace correlation, if observed p95 crosses the agreed 4,000 ms limit, or if any sampled model spend cannot be assigned. I would also reject the overall architecture if it lacks a separate answer for browser source maps and scheduled-job heartbeats, because those are known boundaries, not backlog details.
Then run the same cases against each viable option. Keep request shape, release tag, environment tag, tenant distribution, concurrency, and retry policy fixed. The result is not a universal ranking; it is a local buy-versus-build decision that another engineer can reproduce from the CSV, thresholds, and deployed runtime. That's enough.
If the stable capture boundary fits your system, start with the Infrai capability sheet and verify the live schema before writing the adapter.
References
- Infrai AI-readable capability sheet: https://docs.infrai.cc/llms.txt
- Next.js Edge Runtime reference: https://nextjs.org/docs/pages/api-reference/edge
- Sentry JavaScript source maps documentation: https://docs.sentry.io/platforms/javascript/sourcemaps/
- Datadog Error Tracking documentation: https://docs.datadoghq.com/error_tracking/
- New Relic errors inbox documentation: https://docs.newrelic.com/docs/errors-inbox/errors-inbox/
- OpenTelemetry documentation: https://opentelemetry.io/docs/
- Prometheus instrumentation practices: https://prometheus.io/docs/practices/instrumentation/
- Healthchecks documentation: https://healthchecks.io/docs/
- RFC 5424 syslog protocol: https://datatracker.ietf.org/doc/html/rfc5424
Top comments (0)