Instrument server-side exceptions before attempting richer browser diagnostics in a production Next.js application. The deciding constraint is signal quality: API routes, route handlers, and server actions sit close to authoritative tenant and experiment context, while browser reports often add noise from extensions, stale assets, and interrupted sessions.
TL;DR: catch an error at the server boundary, attach a stable release, environment, route, tenant cohort, and experiment identifier, capture it once, then rethrow or return the response your application contract requires. Start with one narrow dashboard for open production groups. Keep silent-job detection and paging separate from exception ingestion.
For a media experiment comparing tenant cohorts, this ordering answers the useful question first: did treatment traffic fail differently from control traffic? It does not pretend that every exception proves the experiment caused the failure.
What signal are we trying to preserve?
The event needs enough context to compare cohorts without turning every customer, URL, or content item into a new dimension. A practical normalized record contains the exception class and message, a scrubbed stack, the deployment release, production or another controlled environment value, the server boundary, and bounded experiment labels such as experiment=homepage-ranking and cohort=control|treatment.
Keep tenant identity available for investigation, but do not group by it. If 8,000 media tenants hit the same null dereference, the operator needs one group with affected-tenant context, not 8,000 groups. The same warning applies to metrics: Prometheus explicitly cautions against labels with unbounded cardinality. Error grouping is not identical to a Prometheus time series, but the operational failure is familiar: user IDs, request IDs, and raw URLs can overwhelm the dimensions meant for comparison.
Three labels deserve special discipline:
-
releasemust come from the deployed artifact, not the local clock. -
environmentmust use a small allowlist soprod,production, andProductiondo not fragment one incident. -
cohortmust record the assignment already made by the experiment system. Error handling must never assign or recalculate it.
That last rule matters. If the capture path changes assignment, the measurement system has become part of the treatment.
How should Next.js server boundaries capture exceptions?
Put a small adapter around each boundary rather than scattering vendor calls through business logic. In an API route or route handler, the adapter catches an otherwise unhandled exception, normalizes known context, submits one event, and then preserves the endpoint's established error response. In a server action, it does the same before rethrowing an unexpected error; expected validation failures should remain typed application results rather than error-tracker noise.
Do not capture at three layers. A repository function, server action, and global handler recording the same exception will inflate the apparent treatment failure rate. Pick the outermost layer that still knows the experiment context, mark the exception as captured for the lifetime of the request, and let inner code return or throw normally.
Small beats clever here.
The following Go program is a runnable capture client without a fabricated payload schema. Export INFRAI_BASE_URL, INFRAI_API_KEY, and ERROR_EVENT_JSON; the last value must be JSON already validated against the current public discovery schema for errors.capture. In production, the Next.js boundary would produce that validated JSON after normalization. A stable ERROR_EVENT_ID becomes the idempotency key, so a 429 retry cannot create a second write within the platform's documented 24-hour default deduplication window.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func retryDelay(h http.Header, attempt int) time.Duration {
if seconds, err := strconv.Atoi(h.Get("Retry-After")); err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second
}
return time.Duration(1<<attempt) * time.Second
}
func main() {
baseURL := os.Getenv("INFRAI_BASE_URL")
key := os.Getenv("INFRAI_API_KEY")
payload := []byte(os.Getenv("ERROR_EVENT_JSON"))
eventID := os.Getenv("ERROR_EVENT_ID")
if baseURL == "" || key == "" || eventID == "" || !json.Valid(payload) {
panic("base URL, API key, event ID, and valid event JSON are required")
}
client := &http.Client{Timeout: 5 * time.Second}
for attempt := 0; attempt < 3; attempt++ {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
baseURL+"/v1/errors/capture", bytes.NewReader(payload))
if err != nil {
cancel()
panic(err)
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", eventID)
resp, err := client.Do(req)
cancel()
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 < 2 {
time.Sleep(retryDelay(resp.Header, attempt))
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
panic(fmt.Sprintf("capture failed: status=%d body=%s", resp.StatusCode, body))
}
fmt.Println(string(body))
return
}
}
The capture client belongs behind this adapter. It should use a short timeout and surface delivery failures to ordinary application logs, but telemetry failure must not replace the original exception. Scrub authorization headers, cookies, request bodies, and media metadata before transmission. Treat the tenant identifier as potentially sensitive even when it is technically just a UUID.
Retries require care. Retrying a failed capture can duplicate an event unless the selected service documents deduplication for that write. Use a stable event identifier when supported; otherwise prefer one bounded retry with an explicit duplicate-risk annotation over an unbounded loop. Never delay the user response for a long retry chain.
Which error tracker fits this boundary?
The products overlap, but they optimize for different operating models. Choose on debugging depth, workflow integration, and the number of control planes the team is prepared to own. Price is too volatile and too secondary to decide this architecture.
| Option | Strong fit | Boundary to account for |
|---|---|---|
| Sentry | Teams that need mature Next.js integration, source maps, release association, and browser Session Replay | It introduces a dedicated error-monitoring SDK and control plane; configure sampling and data scrubbing deliberately |
| Datadog Error Tracking | Teams already correlating application errors with Datadog logs, traces, and infrastructure telemetry | The value is greatest inside the wider Datadog platform, which can be more system than a focused error-only deployment needs |
| Honeybadger | Teams wanting focused exception monitoring plus uptime and check-in workflows | Verify that its grouping, frontend diagnostics, and existing integrations match the team's deeper debugging requirements |
| Infrai | Teams standardizing many backend services behind one REST API, one key, and one bill; its public discovery describes 295 capabilities and provides request schemas and runnable examples | Use it first for server capture and searchable groups; source-map decoding, Session Replay, minidump symbolication, native alert delivery, trace-tree queries, and heartbeat monitoring are outside this fit, so those needs require separate tools |
Rollbar is another credible exception-focused choice, particularly where its framework SDK and occurrence-based investigation model already match the deployment workflow. Adding a fifth trial rarely improves the decision, though. Shortlist two products, send the same deliberately generated server exception through both, and compare the resulting group, release context, scrubbed payload, and time-to-triage.
The limitations are decisive. This narrower option is not a fit for a team that needs decoded browser source maps, Session Replay, Electron minidump symbolication, built-in notifications, a queryable span tree, or heartbeat checks in the same product; Sentry is the stronger shortlist for frontend debugging, while Datadog is the stronger shortlist when trace and infrastructure correlation drive triage. Its log surface also lacks a per-user deletion route and bulk export or subscription interface, which makes it unsuitable when those controls are mandatory. Retention and cold-storage behavior should not be assumed configurable without a documented configuration surface.
The one-key model can reduce credential sprawl across backend services and leave one invoice to reconcile at month end. That is an operational advantage, not a substitute for source maps. A frontend-heavy media application whose primary question is “which minified client frame broke?” should favor Sentry-style tooling; a backend-first cohort comparison can accept a narrower capture surface.
How do we verify cohort signal without creating noise?
Deploy the adapter to staging first and generate one controlled exception through each boundary: an API route, a route handler, and a server action. Confirm that all three carry the same release and environment vocabulary, secrets are absent, and repeated occurrences form the expected groups. Then verify that an expected validation rejection creates no error event.
In production, begin with observation rather than automation. Query open error groups by environment for an internal dashboard, and compare control and treatment using both error counts and cohort traffic volume. Raw counts lie when allocation is 90/10. The minimum defensible view is an error rate per cohort over the same time window, with a minimum sample rule defined by the experiment owner.
Do not put tenant_id into a metric label to get that view. Store bounded cohort labels on the error event, retain the tenant as drill-down context, and aggregate after retrieval. For scheduled aggregation, record the query window and release so a rerun produces the same report. Idempotency applies to analysis jobs too.
A query is not a page.
No native notification route means polling can power an internal dashboard or a deliberately built alert evaluator, while a dedicated alerting system owns escalation policy, retries, and phone or webhook delivery. Likewise, exception capture cannot detect a comparison job that never started. Use a dead-man's-switch service such as Healthchecks for the scheduled cohort job, because silence emits no exception.
Trace identifiers can correlate a captured error with separately stored logs, but identifiers alone do not produce a distributed span tree. If the investigation requires parent-child latency analysis, retain an OpenTelemetry-compatible tracing backend alongside the error tracker.
Verification and rollback runbook
Promote gradually. Start with internal tenants or a small deterministic slice, then inspect grouping and event volume before expanding. The release tag must make the old and new instrumentation distinguishable; without it, rollback evidence is muddy.
The go/no-go checks are concrete: the application response remains unchanged, one request produces at most one captured event, sensitive fields are removed, cohort assignment agrees with the experiment platform, and capture timeout does not consume the route's latency budget. Also stop if the treatment appears worse only in raw counts but not after normalization by requests.
Rollback should be boring. Disable capture through a server-side configuration switch, redeploy the last known configuration, and leave the application error path intact. Do not delete the evidence collected before rollback. Annotate the affected release and exclude known synthetic verification events from experiment analysis.
Finally, test absence. Pause the scheduled comparison in staging and confirm that the heartbeat monitor reports the missed run. That check closes the gap exception tracking cannot see.
Top comments (0)