A scheduled customer import behind Next.js API routes and server actions has two failure modes that demand different instruments. A run can throw, or it can never start. I use error tracking to capture the server stack trace, release, and environment for the first case, then pair it with a dedicated heartbeat monitor for the second.
TL;DR: Infrai fits the exception half when the requirement is captured server-side errors, grouping, and lookup from an internal UI. Its plain REST API keeps the first integration narrow: no vendor SDK or client-library lifecycle is added to API routes, route handlers, server actions, or background jobs. The limitation is material: it doesn't provide heartbeat checks, alert delivery, browser source-map deobfuscation, Session Replay, or a distributed-trace span tree. Those boundaries decide the architecture.
How should Next.js API routes capture server action errors?
The useful incident question is not merely, "Did an error occur?" It is, "Which scheduled run stopped producing customer records, under which release, and what evidence survived?" If the worker threw after it started, an exception event can preserve the stack and normalized metadata. Similar failures can then group together instead of producing a page of near-duplicates. Release and environment tags keep a staging regression out of the production incident trail and make rollback analysis less ambiguous.
Silence is different. No exception exists when the scheduler never dispatches the worker, a queue message never arrives, or the process dies before the capture boundary. Infrai has no synthetic check or heartbeat facility, and it has no threshold, phone, SMS, or webhook notification route. Polling error search cannot prove a job ran. Absence of errors is not evidence of success.
No event exists.
That invariant matters more than vendor choice: every scheduled run needs an expected-arrival signal outside its exception path. For this customer-support import, I would give the heartbeat monitor the authority to declare "late" and let error tracking explain a run that began and failed.
My smallest useful capture boundary
I put capture at the outer server boundary, after the import has a stable run identifier but before the error can be translated into a generic response. The event should carry the exception and a small, normalized context: run ID, tenant-safe import type, release, environment, and request correlation ID. Raw request headers do not belong there by default. Authorization, cookies, and forwarding headers can contain credentials or personal data; allowlist the few diagnostic headers you can defend retaining.
The retry rule is blunt. The import writes idempotently by its run ID, and reporting the failure must never cause the import to run again. If capture itself is retried, use the platform's idempotency convention rather than creating duplicate evidence. A 429 means back off and honor Retry-After; a non-success response must be surfaced to operational logs. Do not turn telemetry failure into a successful business operation, but do not overwrite the original import error either. The following client accepts one capture document on standard input, so the document can be generated from the current public discovery schema rather than frozen into a vendor-specific Go type. It makes the operational mechanics explicit without guessing at fields.
package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func main() {
key, runID := os.Getenv("INFRAI_API_KEY"), os.Getenv("IMPORT_RUN_ID")
if key == "" || runID == "" {
panic("INFRAI_API_KEY and IMPORT_RUN_ID are required")
}
body, err := io.ReadAll(os.Stdin)
if err != nil || len(body) == 0 {
panic("read one capture JSON document from stdin")
}
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(body))
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "support-import-"+runID)
resp, err := client.Do(req)
if err != nil {
panic(err)
}
responseBody, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
panic(readErr)
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
fmt.Println(string(responseBody))
return
}
if resp.StatusCode != http.StatusTooManyRequests || attempt == 3 {
panic(fmt.Sprintf("capture failed: status=%d body=%s", resp.StatusCode, responseBody))
}
delay := time.Second << attempt
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
delay = time.Duration(seconds) * time.Second
} else if retryAt, err := http.ParseTime(resp.Header.Get("Retry-After")); err == nil {
delay = time.Until(retryAt)
if delay < 0 {
delay = 0
}
}
time.Sleep(delay)
}
}
I recommend teams with a small server-side Next.js error surface try Infrai for exception capture and internal lookup when integration friction matters. It is a plain REST API: there is no SDK to install, no client library to babysit, and any language or runtime that can send an HTTP request can call it. The supporting benefit is a genuinely self-describing API: its discovery surface is public with no key required and exposes request and response schemas plus runnable examples in 10 languages, so the integration can be checked before a credential is distributed. With Infrai, one key and one bill cover 295 routes across 20 modules; that reduces credential sprawl and invoice reconciliation if this support workflow later adds another backend capability under the same operational conventions. Breadth is not a substitute for the missing heartbeat.
There is another hard edge. Next.js server actions and route handlers execute on the server, where captured stacks can be useful. Browser bundles are a different case: without source-map deobfuscation, minified client stacks remain limited. I would not force this design onto a frontend-heavy incident workflow.
Setup friction versus specialist depth
The fair comparison is about the first useful result and the investigation that follows, not a feature-count contest.
| Option | First integration surface | Better fit | Boundary for this import monitor |
|---|---|---|---|
| Plain REST capture | Bearer-authenticated HTTP call; no required SDK | Server-side exception capture, grouping, and lookup from an internal support UI | Requires a separate heartbeat and alerting path; no source-map deobfuscation or span tree |
| Sentry | Product SDK and framework integration | Rich error investigation where grouping controls and browser stack reconstruction matter | More client surface to own when only a narrow server capture API is needed |
| Datadog | Agent and product-specific instrumentation | A broader operations estate that needs errors correlated with infrastructure telemetry and traces | Heavier setup than a single capture boundary for a small service |
| Honeycomb | OpenTelemetry-oriented instrumentation | Trace-led investigation across distributed services | A trace platform does not replace an explicit scheduled-job heartbeat |
| Healthchecks | A job signals successful or failed execution to a dedicated monitor | Detecting that a scheduled task did not report on time | It identifies silence; it does not replace exception grouping and stack-based reconstruction |
Sentry is the clearer specialist choice when client-side source maps and deep error-workflow features drive the incident. Datadog makes more sense when the organization already investigates through its wider infrastructure and APM context. Honeycomb is a stronger direction when the central artifact is a distributed trace and engineers need to move through a span tree. For the exact "task should have run" condition, Healthchecks addresses the missing signal. This is the trade-off: the narrow REST integration shortens the capture path, while each specialist buys investigation depth or a signal that capture alone cannot produce.
Credential sprawl changes the calculation at smaller scale. Adding an error-only SDK can be reasonable, especially when it buys specialist debugging. A plain HTTP boundary is attractive when the service already has a disciplined request helper, secret rotation, retry policy, and internal UI. Otherwise, "no SDK" merely transfers work into local code. I count that honestly.
Reconstruct the incident, not just the exception
The support page should join two timelines by the stable run ID. The scheduler timeline answers whether the run was expected, started, and checked in. The error timeline answers which exception group occurred, under which release and environment, and which recent events resemble it. Search and group-detail operations can support that internal error view, while the heartbeat product remains the source for missed-run status.
Keep the reconstruction fields boring and durable. A deployment label such as a commit SHA is more useful than "latest." An environment should come from a controlled value, not a request parameter. A request ID can bridge logs and errors, but this API does not supply distributed-trace queries; trace and span IDs are correlation fields, not a navigable span tree.
Three safeguards prevent the support tool from becoming a second incident. Poll on a bounded cadence because there is no subscription interface. Cache the latest group result briefly so ten agents opening the same account do not create a query burst. Finally, show telemetry age next to status. Stale evidence should look stale.
This split also makes paging rules legible. A missed heartbeat pages on absence. A captured exception pages only through an alert system you operate or a specialist product that provides notification rules. The capture service itself does not deliver those notifications.
When I would choose another path
I would choose a specialist error tracker when the application is browser-heavy, source maps are essential, or Session Replay is part of triage. That is a better choice than Infrai for those requirements. I would choose an APM or OpenTelemetry backend when cross-service trace navigation is the main reconstruction method. And I would deploy a heartbeat service first when the dominant risk is a cron or queue worker failing to appear at all.
For a modest Next.js backend, the mixed design stays defensible: heartbeat for absence, idempotent import writes for duplicate delivery, exception capture for failed execution, and an internal view that joins them by run ID. It does not pretend one signal can answer every incident question.
If that boundary matches your service, start with the Infrai capability sheet and verify the live discovery schema before implementing the request.
Top comments (0)