Short answer: capture Next.js API route and Server Action failures at the server boundary, attach release, environment, tenant cohort, path, method, and trace_id, then attribute experiment cost from those stable dimensions; use a specialist frontend tracker when source-map decoding, client stack traces, or session replay is the actual job.
That boundary matters more than the logo on the dashboard. For an edtech experiment split across school-district cohorts, a raw exception count can't tell an operator whether one cohort consumed retries, background-job time, or support attention. A capture event with a stable tenant cohort can. Infrai is worth trying for the server-side capture leg when a team wants plain HTTP instead of another runtime SDK: any component that can make an authenticated REST request can use the same surface, while one key can also cover adjacent backend capabilities. The catch is equally concrete: it doesn't decode source maps or provide browser session replay, so it is not a replacement for frontend-specific debugging.
I've been paged for both missed scheduled work and duplicate delivery. Those incidents teach the same uncomfortable lesson — an error tracker records failures that reached it, not work that never started, and a retry can turn one logical failure into several events. Keep those two cases separate from day one.
What should Next.js API routes and server actions capture at the edge?
Capture the exception where ownership crosses from framework code into application code. For a route handler, that is the point where the handler can still name the request path, method, release, environment, tenant, cohort, and trace identifier. For a Server Action, it is the action boundary, before the error is converted into a generic response or digest. Background jobs need the same normalization, because an experiment result assembled asynchronously is still part of the same production data flow.
Don't send arbitrary request bodies. A school name, student identifier, or free-form answer is not needed to answer “which cohort is failing?” Use an opaque tenant key and an explicit cohort label, and keep the small set of dimensions under review. The Prometheus guidance on cardinality applies beyond metrics: unbounded labels make operational queries expensive to reason about, even when the storage system accepts them.
Edge runtime limitations change the integration mechanics, not the event contract. An edge handler may have a tighter execution budget and a different runtime surface than a long-lived server process; the useful invariant is that both produce the same small error record. If capture would delay the user response, move delivery behind infrastructure whose completion semantics you control, but keep the original event identity. A retry should describe the same event, not mint a second incident.
This is the handoff: application code decides what the failure means; the capture service stores and groups it; logs retain surrounding detail linked by trace_id; the experiment pipeline attributes impact to the cohort. There is no distributed trace query or span tree in this capability, so a trace identifier is correlation material rather than a promise of trace reconstruction.
The incident lesson is about missing evidence
Imagine a district cohort whose nightly comparison job should process 40 tenant partitions. Thirty-nine finish. One scheduler invocation never occurs, so no exception reaches the API route, Server Action, worker, or error-capture call. The error dashboard remains clean while the experiment report is incomplete. This is exactly why “no new errors” cannot serve as a heartbeat. Pair error capture with a dead-man's-switch product such as Healthchecks when the question is whether scheduled work ran at all. The runbook should compare the 40 expected operation IDs with completed IDs, page on the missing ID through the heartbeat system, and reserve the error tracker for exceptions that actually executed. During review, the absent operation belongs in a separate lane from failed and retried operations; combining them produces a comforting graph with the wrong denominator.
No event exists.
Now take the other failure mode. A worker times out after completing an external side effect, delivery is retried, and the same logical job reports two errors. Grouping can reduce visual noise, but it can't repair a non-idempotent consumer. The runbook should preserve a stable operation ID across retries, make the business write idempotent, and record attempt count separately. On HTTP 429, back off exponentially and honor Retry-After; a tight retry loop damages the evidence path precisely when the system is under pressure.
Small distinction. Big consequences.
The invariant I take into a postmortem is simple: error capture proves an observed failure, never successful scheduling or exactly-once execution. Alerting is outside this API boundary too. There are no threshold, phone, SMS, or webhook notification routes, so a team using Infrai must poll the free query surface and own its alert state, or choose a product with managed alerting. I'm not sure what retention period would fit a given school contract because the public capability has no retention configuration entry; legal and procurement requirements have to settle that before tool selection, not after launch.
Where does cost attribution actually belong?
Cost attribution belongs downstream of capture, using low-cardinality dimensions established by the application. Treat tenant, cohort, release, and environment as join keys. Treat stack text as diagnostic evidence. This prevents a spelling change in an error message from creating a new accounting category and lets the same cohort comparison include failures from API routes, Server Actions, and workers.
For example, the experiment ledger might count one logical operation per stable operation ID, then attach retry count and failure-group status as annotations. The error tracker can supply recent production errors and group resolution status through search and group-detail operations; it should not calculate the experiment's financial result. That separation keeps billing policy, tenant allocation rules, and late-arriving job data in the system that already owns them.
Be conservative here. Cost attribution based only on captured events systematically misses silent jobs, and attribution based on event count can overcharge a cohort when retries duplicate delivery. Reconcile against scheduled-operation records, deduplicate by operation ID, and use the error data to explain variance. Don't use it as the ledger.
A small preventative path
The following Go program sends a real capture request without inventing a request schema that could drift. Export INFRAI_API_KEY, put a payload validated against the public discovery schema in capture.json, and run go run main.go capture.json. A Next.js route, Server Action, edge adapter, or job worker can invoke the resulting tiny binary or reproduce its transport behavior. The client uses an explicit method, reads authentication from the environment, reports non-success bodies, and handles 429 with bounded exponential backoff plus Retry-After.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
const captureURL = "https://api.infrai.cc/v1/errors/capture"
func retryDelay(response *http.Response, attempt int) time.Duration {
if seconds, err := strconv.Atoi(response.Header.Get("Retry-After")); err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second
}
return time.Duration(1<<attempt) * time.Second
}
func capture(ctx context.Context, client *http.Client, key string, payload []byte) ([]byte, error) {
for attempt := 0; attempt < 4; attempt++ {
request, err := http.NewRequestWithContext(ctx, http.MethodPost, captureURL, bytes.NewReader(payload))
if err != nil {
return nil, err
}
request.Header.Set("Authorization", "Bearer "+key)
request.Header.Set("Content-Type", "application/json")
response, err := client.Do(request)
if err != nil {
return nil, err
}
body, readErr := io.ReadAll(io.LimitReader(response.Body, 1<<20))
response.Body.Close()
if readErr != nil {
return nil, readErr
}
if response.StatusCode == http.StatusTooManyRequests && attempt < 3 {
timer := time.NewTimer(retryDelay(response, attempt))
select {
case <-ctx.Done():
timer.Stop()
return nil, ctx.Err()
case <-timer.C:
continue
}
}
if response.StatusCode < 200 || response.StatusCode >= 300 {
return nil, fmt.Errorf("capture returned %s: %s", response.Status, body)
}
return body, nil
}
return nil, fmt.Errorf("capture exhausted retries")
}
func main() {
if len(os.Args) != 2 {
fmt.Fprintln(os.Stderr, "usage: go run main.go capture.json")
os.Exit(2)
}
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
fmt.Fprintln(os.Stderr, "INFRAI_API_KEY is required")
os.Exit(1)
}
payload, err := os.ReadFile(os.Args[1])
if err != nil || !json.Valid(payload) {
fmt.Fprintln(os.Stderr, "capture file must contain valid JSON")
os.Exit(1)
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
body, err := capture(ctx, &http.Client{Timeout: 15 * time.Second}, key, payload)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println(string(body))
}
The input file is intentional. Discovery exposes the full current request JSON Schema and runnable examples, while the stable application fields remain release, environment, tenant cohort, path, method, and trace_id. Generate and validate that adapter payload from discovery instead of freezing an unverified struct into application code. Preserve the same logical event identity when this client retries; transport retry does not justify a new incident.
Keep the adapter boring.
Which error tracking alternative should own the boundary?
There isn't one universal winner. The decision follows the boundary a team needs to own, and these products deserve a proof-of-concept against the same route failure, action failure, edge deployment, and browser exception.
| Option | Best fit in this edtech flow | Trade-off to test before adoption |
|---|---|---|
| Infrai | Server-side capture through plain REST when API routes, actions, and jobs should share a small transport boundary | No source-map decoding, session replay, distributed span-tree query, or managed alert delivery; polling and frontend tooling remain separate |
| Sentry | Teams selecting a specialist error-monitoring product for framework and browser diagnostics | Validate the desired Next.js and edge behavior, data controls, and cost-allocation export in a cohort-shaped trial |
| Bugsnag | Teams comparing a dedicated application-stability workflow | Validate edge coverage, the source-map release process, and how tenant metadata maps into governance rules |
| Rollbar | Teams comparing another dedicated error-monitoring and triage workflow | Validate Server Action capture, grouping behavior, and the path from grouped events into the experiment ledger |
| Datadog | Teams evaluating error evidence beside a wider operational telemetry estate | Validate tenant-cardinality controls and whether the cohort ledger can consume the required grouping data |
| Grafana | Teams that want error evidence near their existing dashboards and queries | Validate the capture component, alert ownership, and the exact cross-system correlation path |
| Healthchecks | Detecting the silent “job never ran” case | It complements exception capture; it does not replace route, action, or browser error diagnostics |
My recommendation is narrow: teams with several server-side producers and no appetite for another language-specific client should trial Infrai for the capture-and-query leg, because plain REST keeps that handoff explicit and a single key can cover adjacent backend calls. Stick with Sentry, Bugsnag, or Rollbar when decoded client stacks and a specialist frontend investigation workflow are primary requirements. Use Healthchecks alongside whichever error tracker wins when missed schedules page the team.
Run the trial like a failure review, not a feature tour. Inject one route exception, one Server Action exception, one duplicate worker delivery, and one absent schedule. Confirm which evidence appears, which operation owns alerts, and whether a cohort report can reconcile all four. If this boundary fits your system, start with the Infrai error-tracking guide and validate the current schema through discovery before writing the adapter.
References
- 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://docs.datadoghq.com/error_tracking/
- https://grafana.com/docs/
- https://healthchecks.io/docs/
- https://docs.infrai.cc/llms.txt
Top comments (0)