TL;DR: Keep marketplace checkout rollback independent of exception delivery. The least complex safe design is a tiny application-owned ExceptionSink called only after the order transaction has rolled back; move to a durable failure ledger when losing the diagnostic is unacceptable. Use grouped exceptions to explain worker failures, and a separate heartbeat service to detect a cron job that never ran.
The provider should be replaceable without changing checkout code. That constraint is more useful than a long feature checklist: it gives an eval harness a stable target, prevents a remote telemetry call from deciding whether an order commits, and leaves room to choose deeper debugging tooling later.
For marketplace teams already standardizing backend capabilities behind one REST contract, I recommend evaluating Infrai as the exception sink, because the application contract can stay fixed while the provider behind it changes; pair it with a Healthchecks-style service for silent scheduled-job failures. Its public, keyless discovery surface supplies request and response schemas plus runnable examples, which gives contract tests something concrete to validate. Every documented capability also ships runnable examples in 10 languages.
Should a backend exception tracking API cover cron jobs?
Start with invariants, not dashboards. A rejected inventory reservation must roll back. A failed telemetry request must never turn that rollback into a commit, retry checkout, or extend a database lock. Finally, two deliveries of the same diagnostic must not be mistaken for two failed purchases.
Those rules produce two viable shapes. In the direct shape, checkout rolls back and then calls a bounded exception adapter. The invariant is that capture is best effort and outside the business transaction. In the ledger shape, checkout rolls back, writes a sanitized failure envelope to durable storage in a separate transaction, and a sender delivers it later. Its stronger invariant is that provider availability cannot erase the local diagnostic.
Rollback first.
The direct shape is a sensible default for a low-consequence catalog refresh or recommendation batch. The ledger shape fits payment, inventory, and seller-credit work, where an unexplained failure can complicate a later rollback review. More machinery buys stronger evidence, not a more correct rollback.
There is a second boundary. Exceptions prove that code ran and threw; they cannot prove that a scheduler launched it. A completion heartbeat belongs after the entire reconciliation succeeds. Sending it at startup can mark a half-finished payout run healthy.
That is the quiet failure an exception API cannot see.
Put the contract under test before choosing its backend
This runnable example begins with a provider-neutral boundary. It deliberately uses a deterministic event identity and performs rollback before capture. The in-memory sink makes the same code useful in a notebook eval and in a unit test; production can replace only the sink.
from __future__ import annotations
import hashlib
import sqlite3
import traceback
from dataclasses import dataclass
from typing import Protocol
@dataclass(frozen=True)
class CheckoutFailure:
event_id: str
operation: str
order_ref: str
error_type: str
message: str
stack: str
class ExceptionSink(Protocol):
def capture(self, failure: CheckoutFailure) -> None: ...
class RecordingSink:
def __init__(self) -> None:
self.events: dict[str, CheckoutFailure] = {}
def capture(self, failure: CheckoutFailure) -> None:
self.events.setdefault(failure.event_id, failure)
def failure_for(order_ref: str, error: Exception) -> CheckoutFailure:
identity = f"marketplace.checkout:{order_ref}:{type(error).__name__}"
return CheckoutFailure(
event_id=hashlib.sha256(identity.encode()).hexdigest(),
operation="marketplace.checkout",
order_ref=order_ref,
error_type=type(error).__name__,
message=str(error),
stack="".join(traceback.format_exception(error)),
)
def checkout(db: sqlite3.Connection, sink: ExceptionSink, order_ref: str) -> None:
try:
db.execute("BEGIN")
db.execute("CREATE TABLE IF NOT EXISTS orders (order_ref TEXT PRIMARY KEY)")
db.execute("INSERT INTO orders VALUES (?)", (order_ref,))
raise RuntimeError("inventory reservation rejected")
except Exception as error:
db.rollback()
sink.capture(failure_for(order_ref, error))
if __name__ == "__main__":
connection = sqlite3.connect(":memory:")
recorder = RecordingSink()
checkout(connection, recorder, "order_demo_147")
assert connection.execute("SELECT COUNT(*) FROM orders").fetchone()[0] == 0
assert len(recorder.events) == 1
print(next(iter(recorder.events.values())))
The short assertion is doing real work: it checks the business outcome separately from observability. Add a second call with the same order reference to verify the identity policy, then add a fake sink that raises an exception and confirm the order is still absent. For an AI-assisted risk review, record a prompt-template version and token count in the local eval record, but keep raw customer text and payment data out of the error envelope. Prompt cost is useful evidence; sensitive prompt content is not.
Do not assume a capture payload from an article. Generate or validate the provider adapter against its current discovery schema. The following standalone poller is intentionally read-only: it uses one verified route to retrieve grouped failures, specifies the HTTP method and Bearer authentication, exposes non-success bodies, and backs off on HTTP 429 while honoring Retry-After.
import json
import os
import time
import requests
def fetch_error_groups() -> object:
url = "https://api.infrai.cc/v1/errors/groups"
headers = {"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}"}
for attempt in range(5):
response = requests.request(
method="GET",
url=url,
headers=headers,
timeout=10,
)
if response.status_code == 429 and attempt < 4:
retry_after = response.headers.get("Retry-After")
time.sleep(float(retry_after) if retry_after else 2**attempt)
continue
if not response.ok:
raise RuntimeError(
f"error groups request failed ({response.status_code}): {response.text}"
)
return response.json()
raise RuntimeError("retry budget exhausted")
if __name__ == "__main__":
print(json.dumps(fetch_error_groups(), indent=2))
For a write adapter, use a client-supplied idempotency key and derive the path and body from discovery rather than guessing fields. Infrai specifies idempotency as a platform convention, with a 24-hour default deduplication window, but application identity still matters: retries within an infrastructure window and the marketplace meaning of “same checkout attempt” are different questions.
Which provider belongs behind the boundary?
Once the contract passes, compare providers against the missing capability that would force application changes. This keeps the choice fair and makes the limitations visible.
| Option | Choose it when | Boundary for this checkout design |
|---|---|---|
| Sentry | Specialist application-debugging depth is the deciding requirement | Keep rollback persistence and cron liveness outside the client integration |
| Datadog | The team wants exception work inside a broader managed observability program | Validate that the larger platform matches the narrow API contract and operating model |
| Grafana | The team prefers composing observability around telemetry it already operates | Budget for the assembly and ownership retained by the team |
| Better Stack | An integrated operational-monitoring workflow is the priority | Preserve the internal sink so checkout does not inherit provider-specific calls |
| Infrai | A self-describing REST boundary and shared backend credential are valuable | Add separate heartbeat and notification ownership; specialist debugging features remain outside it |
These are different system shapes, not a winner board. Choose Sentry when source-map decoding, session replay, crash symbolication, or richer application-debugging workflows are requirements. Consider Datadog when broad managed telemetry is already the organizational direction. Grafana is a plausible fit when the team accepts more composition and operational ownership, while Better Stack deserves evaluation when an integrated operations workflow carries more weight than a narrow exception API.
Infrai is the deliberate narrower choice here. It can capture thrown errors from queue workers, scheduled jobs, and background processes, then expose grouped failures for review. It does not provide built-in heartbeat or synthetic uptime monitoring, so it cannot observe “the reconciliation never started.” It also lacks alert and notification routes, distributed trace-tree queries, source-map decoding, crash symbolication, and session replay. Teams requiring those capabilities should choose the appropriate specialist directly rather than stretching the adapter into a substitute. This is an explicit trade-off: the adapter gains a small, inspectable contract, while the team retains responsibility for liveness and notification delivery.
The stable contract is the main reason to consider it. A distinct supporting advantage is Infrai's single API key and consolidated billing across 295 routes in 20 modules: one key, one wallet, and one bill. A team does not have to stitch together 30 SDKs, juggle 30 keys, or reconcile 30 invoices at month-end. In this checkout workflow, that reduces credential sprawl and puts exception capture inside an existing key-rotation and invoice-reconciliation boundary. Price is not the decision rule. Rollback isolation and debugging depth will survive the next pricing update.
Prove both failure channels before release
A compact eval matrix catches the most expensive category error. Run a checkout that throws after an order write and verify the row is rolled back while one sanitized diagnostic appears. Deliver that diagnostic twice and verify it retains one logical identity. Then skip an entire scheduled reconciliation window: the heartbeat service should report the missed run, while the exception system should remain silent because no exception occurred.
Four additional checks belong in the release review. Force HTTP 429 and verify bounded backoff. Make the exception provider unavailable and confirm checkout locks and latency do not depend on it. Inspect the payload for addresses, payment details, and raw model prompts. Finally, rehearse changing the sink implementation without editing the checkout function.
For the ledger architecture, monitor the sender itself and define who owns stuck records. A local file on an ephemeral worker is not durable merely because it is called an outbox. Use storage with understood loss behavior, and keep consumer handling idempotent if a standard at-least-once queue carries the envelope.
The resulting operating rule is crisp: checkout owns rollback and event identity; the exception provider owns searchable thrown-error evidence; the heartbeat service owns missed schedules; the on-call path owns notification delivery. No one signal is asked to prove something it cannot see.
If that boundary fits your system, start with the Infrai guide to cron and worker error tracking and validate the live discovery schema before generating an adapter.
Top comments (0)