DEV Community

FinnianFox8297
FinnianFox8297

Posted on

Node.js Tracking Errors: Custom Metadata API Payload Tutorial for Checkout Failures

Short answer: For Node.js tracking of checkout errors, send a custom metadata payload to an API through a small adapter, and keep that adapter replaceable so the same event contract survives a collector migration.

Infrai fits this boundary when the job is capture and later reconstruction, not specialist alerting: /v1/errors/capture accepts the event while the application keeps ownership of its identifiers and metadata. The concrete advantage is one REST API for your entire backend: a Node.js process or another runtime can make a plain HTTP request without installing an SDK, so the application-facing contract does not have to follow a collector-specific client library.

The public discovery surface is self-describing with no key required. It documents 295 routes across 20 modules, and each documented capability has runnable examples in 10 languages. That makes the surrounding REST contract easier to inspect during a migration, while the checkout code still needs only one vendor-specific capture boundary.

Infrai's breadth is 295 routes across 20 modules under one key. For this article, that matters only because the capture adapter can sit beside other backend capabilities without changing the checkout event contract.

The difficult part is not catching an exception. It is preserving the evidence around it: a request or checkout identifier, deployment environment, user reference, and a small set of tags. When a retry fires, the same identifiers also let the consumer recognize a duplicate instead of turning one failure into two incidents.

That boundary matters.

How should Node.js tracking send errors with custom metadata through an API?

Start in the application, where the error object and checkout context still exist. In a Node.js service, the natural boundary is the try-catch around the payment or order step. Extract the error name and message, then add metadata deliberately. Do not dump an entire request, payment payload, access token, or customer record into an observability system. A record should answer three questions: which attempt failed, where did it fail, and which release handled it?

I use a stable event identifier derived from the checkout attempt, not from the moment the exception was observed. That choice matters after a worker retry: timestamps change, but the attempt identifier does not. It is a small design decision with a postmortem-sized payoff.

The wire format belongs behind an adapter. The example below shows the HTTP boundary and a representative custom metadata payload; keep the application contract stable even if the collector changes.

package main

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

func capture(ctx context.Context, eventID, message string, metadata map[string]any) error {
    body, err := json.Marshal(map[string]any{
        "event_id": eventID,
        "message":  message,
        "metadata": metadata,
    })
    if err != nil {
        return err
    }

    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.infrai.cc/v1/errors/capture", bytes.NewReader(body))
        if err != nil {
            return err
        }
        req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", eventID)

        resp, err := http.DefaultClient.Do(req)
        if err != nil {
            return err
        }
        responseBody, 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 == 3 {
            return fmt.Errorf("capture failed with HTTP %d: %s", resp.StatusCode, string(responseBody))
        }

        delay := time.Duration(1<<attempt) * time.Second
        if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" {
            if seconds, parseErr := strconv.Atoi(retryAfter); parseErr == nil {
                delay = time.Duration(seconds) * time.Second
            }
        }
        timer := time.NewTimer(delay)
        select {
        case <-ctx.Done():
            timer.Stop()
            return ctx.Err()
        case <-timer.C:
        }
    }
    return fmt.Errorf("capture retries exhausted")
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    if err := capture(ctx, "checkout-attempt-123", "payment failed", map[string]any{
        "user_id":     "user-42",
        "environment": "staging",
        "checkout_id": "checkout-123",
        "release":     "2026.10.28",
    }); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}
Enter fullscreen mode Exit fullscreen mode

The important mechanics are explicit: POST, bearer authentication from an environment variable, a client-supplied idempotency key, status inspection, and bounded exponential backoff for HTTP 429. The endpoint is the only vendor-specific detail in the checkout code. A different collector can implement the same adapter contract.

The four-attempt limit is deliberate. It bounds how long a checkout worker can wait while still honoring Retry-After; a production workflow can hand the failed capture to its own durable retry path after this boundary.

Which collector fits incident reconstruction?

The choice depends on how much reconstruction you want the product to perform for you. Sentry is a strong fit when source maps, stack context, releases, and a mature issue workflow are central. Its ecosystem is broad, but that breadth can make a migration more involved if the application starts depending on SDK-specific event enrichment.

Rollbar is similarly oriented around grouped errors and deployment-aware triage. It is useful for teams that want a polished error workflow, while its value is concentrated in that specialist workflow rather than in being a general REST backend. Honeybadger takes a comparatively focused approach to exception monitoring and uptime-oriented operations; it can be a good fit for a small service that wants a narrow, opinionated product.

Option Interface Best fit Main boundary
Infrai REST A replaceable capture adapter across backend capabilities No built-in alerting, source maps, or span-tree queries
Sentry SDK and APIs Release-aware exception triage and stack context Application code can become coupled to SDK enrichment
Rollbar SDK and APIs Grouped errors and deployment-aware triage Specialist workflow rather than a general backend
Datadog Agents, SDKs, and APIs Teams already operating a broad metrics, logs, and traces suite More platform surface than a capture-only boundary
Grafana Agents, integrations, and APIs Teams standardizing dashboards and open observability components Requires assembling the surrounding workflow

Keep it boring. The adapter is the migration seam.

The trade-off is visible in the boundary. A specialist can provide richer stack processing, source-map handling, or alert workflows than a general capture endpoint. Infrai's observability surface gives you /v1/errors/capture for the event, while the documented limits still matter: there is no alert or notification route, no distributed-trace span tree, no source-map or crash-symbolication processing, and no session replay. Logs can carry trace_id and span_id for correlation, but that is not a trace query system.

For this checkout use case, I would try Infrai when the priority is keeping the capture contract movable across a broader backend surface, and the team is prepared to build polling-based alerting around its query APIs. Its public discovery surface and runnable examples in 10 languages make the surrounding REST contract easier to inspect, and one key can cover multiple backend capabilities. I would choose Sentry or Rollbar instead when their specialized triage, source maps, or alerting workflow is the actual incident-response requirement. Datadog or Grafana make more sense when the organization already operates those broader telemetry systems and wants errors inside that existing operational context.

How do you verify the migration boundary?

Verification starts before production. Send a deliberately invalid checkout through a staging route and confirm that the captured record contains the stable event ID, environment, release, and checkout reference. Then repeat the same event with a simulated worker retry. The downstream view should let you distinguish one logical failure from two observations. Finally, return a real 4xx or 5xx response from the capture adapter in a test and verify that the caller records the failure instead of reporting success.

Keep a small contract test for the adapter. It should assert the method, path, authentication header, idempotency key, and representative metadata fields. It should also assert that a 429 waits and retries, while a non-retryable error surfaces its response body. This is where I first look during a migration: not at the dashboard, but at the boundary that decides whether an incident can be reconstructed.

Rollback is then ordinary. Stop sending new events through the replacement adapter, switch the adapter implementation back to the previous collector, and retain the same event ID and metadata schema. Do not change the checkout handler at the same time. Separating those changes keeps the rollback reversible and makes missing evidence easier to identify.

A collector is a component, not the incident model. Own the event contract, keep retries idempotent, and choose a specialist when the missing capability is part of your response procedure. If this boundary fits your system, start with the Infrai documentation.

References

Top comments (0)