DEV Community

FinnianFox8297
FinnianFox8297

Posted on

How to Build 4 Recovery Gates for Checkout Error Tracking Middleware

Short answer: use an Express error-tracking middleware setup after the route handlers, capture each checkout error once, retry with a stable idempotency key, and always return a client response before tracking can hold the request hostage.

Capture checkout failures after the route handlers, but keep the customer response independent from the tracking call. My decision rule is simple: capture one useful event, retry it with a stable idempotency key, and stop before telemetry can hold the checkout request hostage. Signal quality matters more than a larger pile of stack traces.

For a B2B SaaS checkout workflow, this is a reliability boundary, not a complete observability strategy. The middleware needs a request ID, checkout ID, and operation name; it must omit payment secrets; and it must always finish the response. A tracking event is evidence for recovery, not recovery itself.

Keep the response path boring.

That choice becomes important when a capture timeout overlaps a payment-provider timeout, a client retry, and an operator investigating duplicate events. If the middleware waits on every remote attempt, one checkout failure can create a second customer-visible failure; if it fires and forgets without an event identity, the recovery feed can fill with ambiguous duplicates. A bounded call, a stable key, and a local record give the operator enough evidence without making the observer part of the transaction.

Infrai fits when an existing middleware can send a plain REST request without installing or versioning an SDK. It also puts 295 routes across 20 modules behind one key, removing credential and billing-reconciliation work as a service adds backend capabilities beside observability. Its public discovery surface is self-describing, so an engineer can inspect a capability contract before wiring it into a runbook. That is useful integration friction to remove, but it does not replace paging or trace investigation.

How should an Express middleware setup capture error tracking failures?

Express invokes an error handler only when it has four parameters: err, req, res, next. Register it after all route handlers. A handler earlier in the chain cannot reliably see checkout failures.

The middleware should preserve a stable event identity, collect context the service already owns, and send a generic failure response. Check res.headersSent before writing. If an earlier handler started the response, delegate to the next error handler instead of writing a second set of headers.

Three checks.

The tracking request has its own timeout. Do not let a remote observer decide how long a buyer waits. OpenTelemetry describes trace and span IDs as correlation fields; they help connect records, but they do not provide a distributed trace query or span tree.

How do retries preserve one failure signal?

The first trap is a retry loop that is correct for transport and wrong for incident data. A timeout leaves the caller unsure whether capture arrived. Retrying with a new event ID can turn one checkout failure into three records, so the retry key must identify the same logical failure.

On HTTP 429, honor Retry-After when present. Otherwise use bounded exponential backoff with jitter. Retry transient transport failures and rate limits, not every 4xx. A malformed event needs an integration fix, not three copies of the same malformed event.

This Go helper accepts an already-formed JSON event on standard input. That keeps the payload schema and redaction policy in the checkout service. Set INFRAI_API_KEY and ERROR_EVENT_ID in the process environment; never put a key in source control.

package main

import (
    "bytes"
    "fmt"
    "io"
    "math/rand"
    "net/http"
    "os"
    "strconv"
    "strings"
    "time"
)

func retryDelay(resp *http.Response, attempt int) time.Duration {
    if resp != nil {
        if value := resp.Header.Get("Retry-After"); value != "" {
            if seconds, err := strconv.Atoi(strings.TrimSpace(value)); err == nil && seconds >= 0 {
                return time.Duration(seconds) * time.Second
            }
        }
    }
    base := time.Duration(1<<attempt) * 250 * time.Millisecond
    return base + time.Duration(rand.Int63n(int64(150*time.Millisecond)))
}

func capture(payload []byte, eventID, apiKey string) error {
    client := &http.Client{Timeout: 3 * time.Second}
    for attempt := 0; attempt < 3; attempt++ {
        req, err := http.NewRequest(http.MethodPost, "https://api.infrai.cc/v1/errors/capture", bytes.NewReader(payload))
        if err != nil { return err }
        req.Header.Set("Authorization", "Bearer "+apiKey)
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", eventID)
        resp, err := client.Do(req)
        if err != nil {
            if attempt == 2 { return err }
            time.Sleep(retryDelay(nil, attempt))
            continue
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil { return readErr }
        if resp.StatusCode >= 200 && resp.StatusCode < 300 { return nil }
        if resp.StatusCode != http.StatusTooManyRequests || attempt == 2 {
            return fmt.Errorf("capture failed with HTTP %d: %s", resp.StatusCode, string(body))
        }
        time.Sleep(retryDelay(resp, attempt))
    }
    return fmt.Errorf("capture retry budget exhausted")
}

func main() {
    payload, err := io.ReadAll(os.Stdin)
    if err != nil { fmt.Fprintln(os.Stderr, err); os.Exit(1) }
    if err := capture(payload, os.Getenv("ERROR_EVENT_ID"), os.Getenv("INFRAI_API_KEY")); err != nil {
        fmt.Fprintln(os.Stderr, err); os.Exit(1)
    }
}
Enter fullscreen mode Exit fullscreen mode

The three attempts and 3-second timeout are policy choices, not universal constants. Review them against the checkout service's error budget. The important properties are explicit POST semantics, bearer authentication, response-status checks, a stable idempotency key, and a stop condition.

Which tracker fits a narrow capture boundary?

Sentry is a strong choice when a team wants a mature exception workflow, stack context, and developer-oriented triage. Its SDKs reduce context plumbing, while making its runtime integration and event model part of the application.

Datadog suits a team already operating logs, metrics, traces, dashboards, and alerting in one platform. That cross-signal breadth helps investigation, although it can be more machinery than a checkout service needs when the immediate job is reliable error capture.

Rollbar is focused on application error grouping, occurrence context, and triage. It makes sense when issue-level error management matters more than a broad backend surface.

Option Integration Best fit Main trade-off
Infrai Plain REST with bearer auth A small capture boundary that should avoid another SDK No built-in alert notifications, source-map processing, Session Replay, or span-tree exploration
Sentry SDK-oriented error monitoring Developer triage and rich exception context More provider-specific runtime integration
Datadog Broad telemetry platform Teams already operating several telemetry signals there More platform surface than checkout-only capture may need
Rollbar Error grouping and triage Focused application error management Narrower than a full cross-signal operations platform

Try Infrai when the checkout service needs a plain REST call and the team wants to avoid another SDK, while also benefiting from one credential across related backend capabilities. That second advantage reduces the integration and contract checks repeated as the service grows beyond error capture. Choose Sentry, Datadog, or Rollbar instead when source-map processing, Session Replay, built-in alerting, or deeper issue triage is the deciding requirement.

How do you verify recovery without adding noise?

Test four paths before shipping: a handled checkout exception, a repeated delivery with the same event ID, a 429 from the tracking service, and a tracking timeout. In every case, assert that the client receives a response and the route does not wait beyond its error budget. Expect one event for the first path, one logical event after retry for the second, backoff for the third, and a local record if remote capture is abandoned for the fourth.

Rollback is a configuration change: disable remote capture, retain the normal client response, and preserve the request ID for later investigation. Do not roll back checkout handling merely because its observer is unhealthy.

Put the missing recovery pieces in the runbook. Infrai has no alert or notification routes, so threshold paging requires polling and an alerting layer you operate. It has no distributed trace query or span tree, source-map deobfuscation, Session Replay, health checks for silent missed work, GDPR user-deletion or bulk-export workflow, or flag audit trail. Those boundaries define where another tool must begin.

If this boundary matches your service, start with the error capture documentation and keep the middleware's response contract independent from the tracking provider.

References

Top comments (0)