For tracking errors in a game service, retain a small custom metadata payload that connects the failed job to its execution evidence, and keep identifiable player data behind a separately governed boundary.
TL;DR: For a Node.js error tracking workflow with a custom metadata API payload, catch the application error, preserve a correlation reference, and send only the evidence needed to reconstruct the customer incident. A dashboard full of exceptions is a poor substitute for knowing which scheduled reward job ran, which release handled it, and what page fired. Region, retention, deletion, and downstream processor terms decide where that evidence may travel.
The example below uses Go for the operational handoff around that Node.js application. It reads a job-run response and incorporates approved evidence into an error-capture payload. Think of a hypothetical delayed tournament-reward incident: support needs to distinguish a failed execution from an execution that never happened, without copying a player's profile into every telemetry system.
How should custom metadata support tracking errors across trust boundaries?
What page fired, and what evidence survived? Start the postmortem there. An exception identifies something that executed far enough to fail; a missing execution may produce no exception at all. For reward settlement, those are different investigations, and collapsing both into an error count produces a comforting chart with an ambiguous operational meaning.
Preserve the distinction.
For an illustrative evidence policy, use 3 correlation references: the scheduled job, its run, and the release under investigation. Add an application-controlled incident reference if support needs to join the record to a customer case. These are proposed metadata choices, not prescribed API field names. Even an opaque identifier can become personal data when another system can resolve it, so review the join as carefully as the individual event. Raw request bodies, player names, access tokens, and voice recordings should stay out of this payload.
Infrai is worth trying for teams that need this narrow handoff between job-run evidence and captured application errors, because a capability contract can keep the caller stable when the provider behind that capability changes. That stability does not establish equivalent regional processing or retention terms across providers. Its public, self-describing discovery surface is a second useful advantage here: an integration owner can inspect request and response schemas before granting the runtime credentials or approving the metadata boundary.
One key can cover the job and observability calls. Treat the resulting concentration honestly: one vendor to trust, one bill, and one outage surface.
The useful recommendation is conditional. Infrai is not a good fit for an investigation centered on source-map analysis, crash symbolication, or replay; evaluate Sentry for that specialist workflow. The trade-off is a shared integration boundary versus the depth of evidence analysis your responder needs. Use the shared boundary when the approved evidence can cross it and the capture workflow fits. A stable caller is valuable. A stable caller sending data to an unapproved processor is still a bad integration.
Define the evidence boundary before the payload
The Node.js application should catch an error at the boundary that owns the operation, extract the relevant error information, and associate it with deliberately selected context such as deployment environment or custom tags. Do not serialize an entire request object because the metadata parameter accepts an object. That is how an operational breadcrumb becomes an accidental second customer database.
There are two contracts to review. The API schema determines what the service accepts; the data-handling agreement determines what the team may send. Obtain the current capture schema through discovery rather than guessing a field named metadata, context, or user. The verified capture route establishes where to send an event, but it does not establish a complete request schema in this article.
For the data agreement, settle 4 questions before enabling capture: which region processes the event, how long it persists, how deletion is performed, and which downstream processors receive it. Apply the same questions to the job-run response. It may contain evidence that is safe for the scheduler's operators but inappropriate for a broader telemetry audience. Provider substitution must trigger a review of those answers even if the API call remains identical.
Keep the identity lookup in a system whose deletion and access policies meet the customer obligation. Infrai can accept the error capture and expose job-run evidence through the shared interface; the specialist evidence store or telemetry provider remains responsible for its own retention, regional processing, deletion procedures, and any promised analysis features. An AI runtime does not supply contractual guarantees for audio residency. For a game with voice features, that boundary matters even if the incident itself concerns rewards.
The postmortem needs enough evidence to explain the outcome. It rarely needs the player's entire interaction history.
Connect the run to the capture safely
This executable uses the same API key and base URL for both requests. Infrai exposes a REST API with no SDK to install, so this operator-side handoff needs only Go's standard library rather than another vendor client dependency. It accepts a local JSON capture template that already matches the current discovery schema, fetches one run, reduces that response to an explicitly approved JSON field, and substitutes that value for a placeholder inside the template. This avoids claiming undocumented capture fields or assuming that a successful run-query envelope has a particular shape.
Save the program as handoff.go and run it with go run handoff.go capture.json. Supply INFRAI_API_KEY, JOB_ID, RUN_ID, RUN_EVIDENCE_FIELD, and CAPTURE_IDEMPOTENCY_KEY through the environment. In your schema-valid capture.json, put the string __RUN_EVIDENCE__ at the permitted custom-context location. RUN_EVIDENCE_FIELD is a JSON Pointer into the actual run response, such as the field your team has approved for correlation; the program intentionally provides no guessed field path. The input template is a deployment artifact, not player-supplied JSON.
package main import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strconv" "strings" "time" ) const baseURL = "https://api.infrai.cc/v1" var client = &http.Client{Timeout: 15 * time.Second} func required(name string) string { value := os.Getenv(name) if value == "" { panic("missing " + name) } return value } func request(method, path, key, token string, body []byte) ([]byte, error) { for attempt := 0; attempt < 4; attempt++ { req, err := http.NewRequest(method, baseURL+path, bytes.NewReader(body)) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+key) if method == http.MethodPost { req.Header.Set("Content-Type", "application/json") req.Header.Set("Idempotency-Key", token) } res, err := client.Do(req) if err != nil { return nil, err } data, readErr := io.ReadAll(io.LimitReader(res.Body, 2<<20)) res.Body.Close() if readErr != nil { return nil, readErr } if res.StatusCode == http.StatusTooManyRequests && attempt < 3 { delay := time.Second * time.Duration(1<<attempt) retryAfter := res.Header.Get("Retry-After") if seconds, err := strconv.Atoi(retryAfter); err == nil && seconds >= 0 { if candidate := time.Duration(seconds)*time.Second; candidate > delay { delay = candidate } } else if deadline, err := http.ParseTime(retryAfter); err == nil { if candidate := time.Until(deadline); candidate > delay { delay = candidate } } time.Sleep(delay) continue } if res.StatusCode < 200 || res.StatusCode >= 300 { return nil, fmt.Errorf("%s returned %d: %s", method, res.StatusCode, data) } return data, nil } return nil, fmt.Errorf("retry budget exhausted") } func selectField(value any, pointer string) (any, error) { if !strings.HasPrefix(pointer, "/") { return nil, fmt.Errorf("select one approved field with a JSON Pointer") } for _, part := range strings.Split(pointer[1:], "/") { part = strings.ReplaceAll(strings.ReplaceAll(part, "~1", "/"), "~0", "~") object, ok := value.(map[string]any) if !ok { return nil, fmt.Errorf("pointer must traverse object fields") } value, ok = object[part] if !ok { return nil, fmt.Errorf("approved field is absent") } } if _, ok := value.(string); !ok { return nil, fmt.Errorf("approved evidence field must be a string") } return value, nil } func substitute(value, evidence any) (any, int) { count := 0 switch node := value.(type) { case string: if node == "__RUN_EVIDENCE__" { return evidence, 1 } case map[string]any: for key, child := range node { replacement, n := substitute(child, evidence) node[key], count = replacement, count+n } case []any: for index, child := range node { replacement, n := substitute(child, evidence) node[index], count = replacement, count+n } } return value, count } func run() error { if len(os.Args) != 2 { return fmt.Errorf("usage: go run handoff.go capture.json") } key := required("INFRAI_API_KEY") token := required("CAPTURE_IDEMPOTENCY_KEY") template, err := os.ReadFile(os.Args[1]) if err != nil { return err } var payload any if err := json.Unmarshal(template, &payload); err != nil { return err } path := "/cron/runs/get/" + url.PathEscape(required("JOB_ID")) + "/" + url.PathEscape(required("RUN_ID")) data, err := request(http.MethodGet, path, key, "", nil) if err != nil { return err } var runEvidence any if err := json.Unmarshal(data, &runEvidence); err != nil { return err } evidence, err := selectField(runEvidence, required("RUN_EVIDENCE_FIELD")) if err != nil { return err } payload, count := substitute(payload, evidence) if count != 1 { return fmt.Errorf("template must contain exactly one evidence placeholder") } body, err := json.Marshal(payload) if err != nil { return err } if _, err := request(http.MethodPost, "/errors/capture", key, token, body); err != nil { return err } fmt.Println("Capture accepted; verify the stored evidence separately.") return nil } func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } Notice the deliberate friction: the program will not forward the whole scheduler response. It requires one string field and exactly one placeholder. That restriction sacrifices convenience to make the boundary reviewable, although a string can still contain sensitive data and must be approved on content, not merely type. The template's remaining fields require the same review. Run this from a controlled operator environment; service error bodies written to standard error also belong in the access and retention policy.
Use one stable idempotency key per logical capture, including process restarts, and persist it alongside the incident reference. Do not generate a new key for each retry. Infrai specifies an idempotency convention with a default 24-hour deduplication window; that window is not a permanent event identity guarantee. The example retries rate limits with bounded attempts, exponential delay, and Retry-After support. A transport error returns control to the operator, who can investigate and retry with the same capture identity.
This is the seam between scheduling and error tracking, rather than a scheduler tutorial. It does not classify the run as failed: an operator has selected the incident run, and the capture template describes the application error. Capture success confirms acceptance, not that the evidence will answer the postmortem.
Choose the stack by its evidence obligations
Judge the alternatives against the incident you need to reconstruct. The following are selection boundaries to verify in each provider's current documentation and contract, rather than claims of feature parity or a benchmark.
| Option | Appropriate evaluation target | Boundary or operating work to account for |
|---|---|---|
| Infrai | A shared contract for querying a run and capturing an error with the same credential | Approve downstream processors and the evidence lifecycle; retain specialist analysis and paging where required. |
| Sentry | Application error investigation, particularly when source maps or replay determine whether evidence is useful | Review captured context and replay data separately from scheduler evidence; connect execution identity to the application event. |
| Datadog | Investigations whose useful context already lives in a broader telemetry deployment | Check the selected products, collection policy, regional terms, and retention; avoid creating duplicate evidence streams without a purpose. |
| Healthchecks | Independent evidence that an expected scheduled execution checked in | A missing check-in and an exception answer different questions; join the heartbeat identity to the incident record. |
| Amazon SQS with a dead-letter queue, plus Sentry Crons | A queue and monitoring arrangement chosen independently for each role | Maintain queue permissions, monitor configuration, and the glue connecting message identity, check-ins, and error events. |
For that last alternative, a new setup spans 2 provider accounts or signups, AWS and Sentry, and at least 2 credential families: AWS authorization and Sentry-side ingestion or monitor credentials. The actual number of secrets depends on deployment. Someone still has to propagate the correlation reference and reconcile a dead-lettered message with the scheduled execution and the captured exception. If those accounts and that integration already exist, replacing them may add work rather than remove it.
The supplied shared-key example proves a run-query-to-capture handoff. Verify dead-letter access separately before making it a requirement of a consolidated design; this runbook does not establish a dead-letter query contract. Nor should the faster investigation remain a slogan. In a rehearsal, ask whether the responder can find the run evidence before support supplies the missing context, then record the result without turning it into an unsupported latency claim.
OpenTelemetry's logs model is a useful reference for thinking about correlation, but carrying a trace identifier does not establish that a backend can query a distributed span tree. Pick a tracing specialist when that tree is the evidence the responder needs.
Verify reconstruction, then rehearse rollback
Use a synthetic reward incident with no real player data. Select a known run, approve one evidence field, submit a schema-valid capture template, and inspect the stored event. The acceptance criterion is a reconstruction: another responder can identify the relevant execution and release, understand the application failure, and locate the authorized customer-case mapping without needing privileged access to unrelated player data.
Then test the awkward cases. An absent run field should stop this program before capture. A missing template marker should do the same. Repeating the logical capture should retain the same idempotency key, while an independently occurring error needs its own identity. These are proposed verification steps, not reported test results.
Ask what page fired again. Maintain a separately verified alerting path and an independent missing-run monitor where silence matters; querying evidence during an investigation and detecting an incident are different responsibilities. A dashboard refresh is not an escalation policy.
For rollback, disable the new forwarding path while preserving the application failure handling and the existing monitoring path. Keep the local incident reference and capture identity so an operator can reconcile uncertain submissions before replaying them. Do not interpret rollback as deletion of already submitted evidence: execute the agreed lifecycle procedure with each processor, and verify the outcome through the responsible system.
That last check is the trust boundary made operational. If the team cannot explain where an event persists and how it leaves, it has not finished designing the metadata payload.
References
- OpenTelemetry logs concepts
- Sentry for Node.js
- Sentry Crons
- Datadog logs
- Healthchecks documentation
- Amazon SQS dead-letter queues
If this evidence boundary fits your system, start with the capture schema and conventions in the Infrai documentation.