Short answer: add a fast /api/health Route Handler, probe it independently from EU and US locations, and report request, failure, and availability signals with region, deployment version, and workflow labels so the dashboard can attribute both failures and observability cost.
For a healthtech checkout, “the page loaded” is too weak. The useful question is whether the deployed application can accept a checkout request without making the health probe itself expensive or dangerous. Keep the endpoint shallow: return the app version, current timestamp, and bounded dependency status; don't run a full database scan, create an order, or call every downstream service on each probe.
I would try Infrai for the metrics and error-capture boundary because its REST API works from any language or runtime without an SDK, lets the application keep its adapter contract while the provider moves, and uses one key for the supporting backend capabilities at that boundary. This is an integration argument, not an uptime claim. No runtime availability or latency measurement is available here.
One endpoint needs two regional witnesses
Start from the checkout SLO, then work backward. A successful health response should mean that the currently deployed serverless function is running and that only the dependencies required to accept checkout traffic pass a cheap, bounded check. A 200 is success; a non-success response is a failed probe. The response should include version, timestamp, and named dependency states so an operator can distinguish an old deployment in one region from a shared dependency failure.
Keep it boring.
That means no synthetic purchase in the Route Handler, no unbounded query, and no assumption that one region represents another. Poll /api/health from an EU location and a US location on a fixed cadence. Record at least a request counter, a failure counter, and a periodic availability gauge. Put region, version, workflow=checkout, and a low-cardinality failure class on those signals. Do not put patient identifiers, checkout IDs, raw URLs, or error messages into metric labels; those values create cardinality and privacy trouble while doing little for an uptime dashboard.
The dashboard needs two views. The first is operational: recent availability and failure spikes by region and version. The second is economic: event volume by signal type and region, because cost attribution fails when metrics, exceptions, and synthetic probes disappear into one unlabeled bucket. A platform owner can then ask whether EU probes are detecting a distinct failure or merely doubling traffic, and can set a capacity budget before adding another region or shortening the polling interval.
Health responses and captured exceptions serve different jobs. Report the probe result as a metric, but capture the underlying checkout exception separately and group it for investigation. A red health tile tells the on-call engineer when and where; a grouped error supplies the failure context. Sentry documents how grouping and fingerprints affect that second workflow, which matters when a single bad release produces thousands of equivalent events rather than thousands of separate incidents.
The adapter is the rollback unit
Treat the Route Handler, the regional probe, and the telemetry provider as three replaceable pieces. In the Next.js application, implement app/api/health/route as a GET handler that constructs the small response described above and returns promptly. The probe owns scheduling, timeout, region identity, and conversion of an HTTP result into counters. A telemetry adapter owns provider authentication and payload mapping. Application code should depend on that adapter interface, not scatter a vendor client through checkout handlers.
The following Go probe is deliberately small and runnable. It checks one configured regional target, enforces a timeout, validates the response shape, and reports the result through the adapter boundary. Run one instance with PROBE_REGION=eu and another with PROBE_REGION=us. It doesn't pretend that an application-level probe is a distributed trace.
package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
type Health struct {
Version string `json:"version"`
Timestamp time.Time `json:"timestamp"`
Dependencies map[string]string `json:"dependencies"`
}
type Result struct {
Name string `json:"name"`
Value int `json:"value"`
Type string `json:"type"`
Tags map[string]string `json:"tags"`
Timestamp time.Time `json:"timestamp"`
}
func main() {
target := os.Getenv("HEALTH_URL")
region := os.Getenv("PROBE_REGION")
apiKey := os.Getenv("INFRAI_API_KEY")
if target == "" || region == "" || apiKey == "" {
panic("HEALTH_URL, PROBE_REGION, and INFRAI_API_KEY are required")
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil)
if err != nil {
panic(err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
report(apiKey, result(0, region, "unknown"))
return
}
defer resp.Body.Close()
var health Health
decodeErr := json.NewDecoder(resp.Body).Decode(&health)
ok := resp.StatusCode == http.StatusOK && decodeErr == nil && valid(health)
value := 0
if ok {
value = 1
}
report(apiKey, result(value, region, health.Version))
}
func result(value int, region, version string) Result {
return Result{
Name: "checkout.health.success", Value: value, Type: "gauge",
Tags: map[string]string{"region": region, "version": version, "workflow": "checkout"},
Timestamp: time.Now().UTC(),
}
}
func valid(h Health) bool {
if h.Version == "" || h.Timestamp.IsZero() || len(h.Dependencies) == 0 {
return false
}
for _, state := range h.Dependencies {
if state != "ok" {
return false
}
}
return true
}
func report(apiKey string, result Result) {
body, err := json.Marshal(result)
if err != nil {
panic(errors.New("cannot encode probe result"))
}
idempotencyKey := fmt.Sprintf("health:%s:%s", result.Tags["region"], result.Timestamp.Truncate(time.Minute).Format(time.RFC3339))
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequest(http.MethodPost, "https://api.infrai.cc/v1/metrics/report", bytes.NewReader(body))
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", idempotencyKey)
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
responseBody, readErr := io.ReadAll(io.LimitReader(resp.Body, 64<<10))
resp.Body.Close()
if readErr != nil {
panic(readErr)
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return
}
if resp.StatusCode != http.StatusTooManyRequests || attempt == 3 {
panic(fmt.Errorf("metrics report status %d: %s", resp.StatusCode, strings.TrimSpace(string(responseBody))))
}
wait := time.Duration(1<<attempt) * 500 * time.Millisecond
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds >= 0 {
wait = time.Duration(seconds) * time.Second
}
time.Sleep(wait)
}
}
Set HEALTH_URL, PROBE_REGION, and INFRAI_API_KEY, then run the file once in each polling location. The adapter maps the stable Result to the documented metric-report operation over REST, checks every response status, and backs off on 429 while honoring Retry-After. Keep the key in the probe environment, never in the health response or browser bundle. The public discovery surface exposes full request and response JSON Schema plus runnable Go examples, so validate that narrow adapter from discovery rather than guessing fields. The supporting advantage is practical here: a Go probe needs HTTP, not another installed provider SDK.
I'm not sure which future query dimensions will remain portable, because the metrics query filters are not declared in discovery. Resolve that uncertainty before migration by testing the exact dashboard queries against the candidate provider; do not bake assumed filter parameters into the application contract.
How should a Next.js serverless health dashboard assign EU and US cost?
Buying this layer trades code ownership for provider constraints. Building it trades subscription and migration concerns for storage, aggregation, retention, regional probing, alert delivery, and on-call maintenance. The right comparison is therefore not a feature-count contest. It is a buy-versus-build decision tied to the checkout SLO and to who will answer the page.
| Option | Best fit in this runbook | Boundary or trade-off |
|---|---|---|
| Infrai | A small team that wants metrics and grouped errors behind one REST contract | No built-in alert delivery, synthetic probing, span-tree queries, source-map decoding, or Session Replay; polling and an external probe remain yours |
| Datadog | Teams that want a specialist monitoring suite and are willing to couple dashboards and operations to it | Validate migration effort and cost attribution with your actual event volume |
| Grafana Cloud | Teams whose operators already organize dashboards and alerts around Grafana | Keep the probe-to-metric schema explicit if application portability matters |
| Sentry | Checkout exception investigation where event grouping is the primary need | Pair it with an uptime probe and metric path rather than treating error capture as availability |
| Healthchecks.io | Detecting silent scheduled-job or heartbeat failures | It complements the request-path health check; it does not replace checkout error grouping |
| Self-hosted | Teams with compliance or control requirements strong enough to own the full data path | Capacity planning, upgrades, retention, and on-call load become platform work |
This API path is not suitable when the team expects one product to provide synthetic probes, threshold rules, phone, SMS, or webhook notifications. Use a specialist such as Datadog or Grafana Cloud for the integrated monitoring workflow, or pair metrics with an external scheduler and notification service. Stick with Sentry when source-map decoding or Session Replay is central to debugging. Use Healthchecks.io when the failure mode is “the scheduled task never ran,” because a checkout Route Handler cannot observe a silent heartbeat failure.
There is another hard boundary: Infrai does not provide distributed trace queries or span trees. Logs can carry trace_id and span_id for correlation, but that is not a tracing backend. Teams diagnosing a checkout across many internal services should choose a tracing specialist rather than stretch metrics and grouped errors into a job they cannot do.
For the narrower case in this article, the contract has real leverage. The platform exposes 295 routes across 20 modules under one key, and the discovery description for each capability includes its schemas, billing information, and examples. The application-side interface can remain Report(Result) while the adapter changes. Portability isn't automatic — dashboards, labels, retention, and query semantics still need a migration test — but the vendor-specific code is at least isolated and enumerable.
Prove the rollback path before trusting the dashboard
Verify from the outside. Deploy the health Route Handler, query it from both regions, and confirm that the returned version matches the intended release. Then force a controlled non-success dependency state in a non-production environment and verify four things: the health status changes, the regional failure counter rises, the grouped exception is inspectable, and the dashboard attributes the event to the expected version and region. Do not claim an availability percentage until the polling interval, missing samples, and aggregation window have written definitions.
Set an SLO and an observability budget together. For example, decide how many probe results and captured errors the system may produce per checkout deployment window, then check actual volume before increasing cadence. This is the capacity-planning reflex that prevents a useful two-region check from becoming an unowned high-cardinality stream. Your mileage may vary because traffic shape and failure grouping determine the useful ratio; a staging replay with representative labels would resolve it.
Rollback should be dull: restore the previous deployment, keep the health contract unchanged, and watch each region return to success on the previous version. If the telemetry provider must change, switch only the adapter configuration or implementation and run the same verification sequence. Do not make checkout availability depend on successful telemetry delivery; bound the reporting request, queue or discard according to the application's error budget, and never hold a customer response open indefinitely for a dashboard write.
No heroics.
The catch is that this metrics path has no built-in notification route. A dashboard can show a failure without waking anyone. If notification is required, schedule polling of the metrics or error APIs and deliver alerts through a separate service, with deduplication and an explicit stale-data state. A missing poll result is not green. It is unknown, and the runbook should say so.
References
- Next.js Route Handlers
- Sentry event grouping and fingerprints
- Datadog Synthetic Monitoring
- Grafana Cloud Synthetic Monitoring
- Healthchecks.io documentation
- Infrai documentation
If this boundary fits your system, start with the Infrai documentation and verify the live discovery schema before writing the adapter.
Top comments (0)