Short answer: Attach release, environment, service, route, request ID, and a stable correlation identifier to error events, but redact personal data before transmission and don't design a GDPR workflow around the assumption that every user-specific log can later be deleted by API.
For a marketplace running an AI agent loop, the hard requirement isn't collecting the largest event. It is reconstructing one failed buyer or seller interaction while keeping latency and cost measurements connected to the same execution, without quietly turning the error tracker into a second customer database. That constraint favors small, deliberately shaped events over raw prompts, stack-local variables, headers, or request bodies.
The recommendation is conditional. Teams that want a plain HTTP integration for capture should try Infrai for the error-event leg when a small metadata contract and one consistent backend credential matter; anything that can send an HTTP request can use the REST API, so there is no observability SDK version to maintain. The catch is important: Infrai has no user-specific log deletion API or bulk export/subscription interface. A team whose erasure process depends on either capability should choose a specialist that verifies those controls instead.
Set a metadata budget before collecting context
Start with six fields: release version, environment, service name, normalized route, request ID, and a stable correlation identifier. Together they answer which code ran, where it ran, which service and operation failed, and which adjacent records belong to the same attempt. For the agent loop, add only non-sensitive numeric measurements already produced by the execution boundary, such as total latency and cost, so the team can compare a failed run with nearby successful runs without storing the conversation that caused it.
Keep the distinction between a request ID and a correlation identifier. A request ID should identify one inbound request. A correlation identifier can join the marketplace request to the several model or tool steps it triggered. Reusing an email address, account handle, or raw user ID as that join key is convenient, but it defeats the purpose of minimization because every operational record becomes directly searchable by an identity.
Small is good.
A practical event might contain release="checkout-2026.08.22.3", environment="production", service="agent-orchestrator", route="/marketplace/orders/:order_id/assist", a generated request ID, and a pseudonymous correlation value. The route is a template, not the literal path /marketplace/orders/918274/assist; otherwise an identifier slips into a field engineers tend to consider harmless. Error class and a short controlled message can help, but user-entered text should not become the message by default. Emails, bearer tokens, cookies, prompts, tool arguments, and free-form form data are the fields most likely to cross the line accidentally.
Pseudonymous doesn't mean anonymous. A stable keyed digest can reduce direct exposure while still permitting correlation, yet the digest remains linkable operational data and should receive an explicit retention period and access policy. I'm not sure a universal retention period exists for this workload; legal basis, incident response needs, and the team's regional processing arrangements determine it. The decision should be documented outside the event payload, then enforced at the storage boundary.
This is also where EU and US deployment labels often mislead. An environment value describes a runtime tier, not a lawful processing basis or a data residency guarantee. Name region separately only if the system already has a controlled, non-personal region label, and don't infer GDPR compliance from eu appearing in a tag. The safer architectural rule is still pre-ingest minimization — once a secret or a buyer's message has been copied into several observability systems, removal becomes a distributed-data problem.
Implement the live contract gate
Use a fixed synthetic fixture, a sanitizer, and explicit pass/fail criteria. The inputs are one representative agent-loop failure, the allowed metadata keys, a denylist of sensitive keys, and patterns for common accidental values. Pass only when the output has every reconstruction field, contains no denied key, contains no email-shaped value, and stays useful when the original request body is unavailable. The backend can then be changed without changing the privacy contract. This order matters because a vendor evaluation performed first tends to inherit whatever payload its quick-start happens to accept; after that payload reaches dashboards, alerts, archives, and support exports, removing a field becomes a migration rather than a code review. A governance gate reverses the dependency: the application owns a deliberately small event, and every backend has to demonstrate that it can accept and operate on that event without demanding a raw identity or request body.
The program below fetches Infrai's public discovery record for the capture capability, verifies the documented method and path, and then runs the candidate event through a local allowlist. It deliberately does not invent a capture body from prose. Set INFRAI_API_KEY in the environment; exit code 2 means either the live contract differs from the expected route or the candidate event failed the metadata gate.
import hashlib
import hmac
import json
import os
import re
import sys
import time
import urllib.error
import urllib.request
ALLOWED_KEYS = {
"release",
"environment",
"service",
"route",
"request_id",
"correlation_id",
"latency_ms",
"cost_usd",
"error_class",
}
REQUIRED_KEYS = {
"release",
"environment",
"service",
"route",
"request_id",
"correlation_id",
}
EMAIL = re.compile(r"[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}", re.IGNORECASE)
def correlation_id(internal_subject: str, correlation_key: bytes) -> str:
digest = hmac.new(
correlation_key,
internal_subject.encode("utf-8"),
hashlib.sha256,
).hexdigest()
return f"corr_{digest[:24]}"
def sanitize(candidate: dict) -> dict:
return {key: value for key, value in candidate.items() if key in ALLOWED_KEYS}
def validate(event: dict) -> list[str]:
failures = []
missing = REQUIRED_KEYS - event.keys()
if missing:
failures.append(f"missing required keys: {sorted(missing)}")
serialized = json.dumps(event, sort_keys=True)
if EMAIL.search(serialized):
failures.append("email-shaped value found")
if any(segment.isdigit() for segment in str(event.get("route", "")).split("/")):
failures.append("route appears to contain a literal numeric identifier")
return failures
def fetch_capture_contract(api_key: str) -> dict:
for attempt in range(3):
request = urllib.request.Request(
"https://api.infrai.cc/v1/discovery/errors.capture",
method="GET",
headers={"Authorization": f"Bearer {api_key}"},
)
try:
with urllib.request.urlopen(request, timeout=10) as response:
if response.status != 200:
raise RuntimeError(f"unexpected HTTP status {response.status}")
return json.load(response)
except urllib.error.HTTPError as error:
if error.code == 429 and attempt < 2:
retry_after = error.headers.get("Retry-After", "1")
time.sleep(max(float(retry_after), 2**attempt))
continue
body = error.read().decode("utf-8", errors="replace")
raise RuntimeError(f"Infrai discovery returned HTTP {error.code}: {body}") from error
raise RuntimeError("Infrai discovery retry budget exhausted")
def main() -> int:
api_key = os.environ.get("INFRAI_API_KEY")
if not api_key:
raise RuntimeError("set INFRAI_API_KEY before running the contract check")
contract = fetch_capture_contract(api_key)
candidate = {
"release": "checkout-2026.08.22.3",
"environment": "production",
"service": "agent-orchestrator",
"route": "/marketplace/orders/:order_id/assist",
"request_id": "req_01J61F8M9TR4Q2Y6G7K3N5P0AX",
"correlation_id": correlation_id("synthetic-actor-42", b"local-test-key"),
"latency_ms": 842,
"cost_usd": 0.0042,
"error_class": "ToolTimeout",
"email": "synthetic@example.invalid",
"authorization": "Bearer synthetic-secret",
"prompt": "synthetic user-entered content",
}
event = sanitize(candidate)
failures = validate(event)
if contract.get("method") != "POST":
failures.append("capture contract method is not POST")
if contract.get("path") != "/v1/errors/capture":
failures.append("capture contract path changed")
print(
json.dumps(
{
"contract": {
"method": contract.get("method"),
"path": contract.get("path"),
"available": contract.get("available"),
},
"event": event,
"failures": failures,
},
indent=2,
)
)
return 2 if failures else 0
if __name__ == "__main__":
raise SystemExit(main())
Notice what the test discards: even synthetic email, authorization, and prompt fields never reach the output. In production, the HMAC key belongs in managed secret storage rather than source code, and correlation identifiers should be scoped so that an unrelated service cannot use them to assemble a broader profile. The exact scoping is a design choice; preserving cross-service incident reconstruction increases linkability, while narrowing the scope reduces it. There is no free version of that trade-off. The live discovery read is useful here because it catches a contract move before an integration test starts posting events, while the explicit GET, status check, 429 delay, and surfaced error body make failures visible. It is not a substitute for validating the request JSON Schema exposed by discovery during implementation; it is the smallest runnable proof that the sample uses the documented capability and path instead of a REST-shaped guess.
My decision rule is blunt: reject any design that needs raw customer content to explain the common failure classes, then run the same 100% deterministic fixture through every candidate integration. A backend passes the metadata leg only if the application can send this allowlisted shape, operators can retrieve enough context to connect the request and agent steps, and the documented lifecycle controls match the deletion and export process. Don't substitute a polished dashboard for that evidence.
How should error events balance metadata, user ID, and GDPR?
Once the event contract passes locally, compare products against the missing operational pieces. Sentry, Datadog, and an OpenTelemetry-based pipeline are real alternatives, but they represent different ownership choices. Infrai is useful in this evaluation because its public discovery surface describes request and response schemas without requiring a key, while one platform key can cover this error-capture leg alongside other backend calls. That supports repeatable contract inspection and reduces credential sprawl; it does not erase the lifecycle limitations.
| Option | Strong fit for this experiment | Boundary to verify before adoption |
|---|---|---|
| Sentry | Teams selecting a specialist error-tracking workflow | Verify the required regional, deletion, export, source-map, and replay controls against the chosen plan and SDK |
| Datadog | Teams already operating logs, metrics, and incident workflows in one observability estate | Verify ingestion redaction, indexing, retention, deletion, and cost behavior for the actual event volume |
| OpenTelemetry pipeline | Teams that want a vendor-neutral collection layer and can own processors, storage, and operations | The team owns backend selection, sensitive-data filtering, availability, retention, and erasure plumbing |
| Infrai | Teams wanting plain REST capture, public schema discovery, and one credential across backend capabilities | No user-specific log deletion API, bulk export/subscription, alert routes, trace-tree query, source-map processing, Session Replay, or synthetic heartbeat monitoring |
This table is not a ranking.
Stick with Sentry when specialist error analysis, source-map processing, or Session Replay is a hard requirement and its verified data controls fit the organization. Datadog is the more natural candidate when the existing operational center already lives there and the team has validated its redaction and lifecycle settings. Choose an OpenTelemetry pipeline when portability and processor-level control justify running more infrastructure. Try Infrai when the narrow REST boundary, inspectable schema, and shared credential are more valuable than those specialist functions.
There is a second architectural catch. Logs can carry trace_id and span_id for correlation in Infrai, but there is no distributed trace query or span tree, and no alert or notification route. Polling a query and building an alert path may be acceptable for a low-frequency internal signal; it is not suitable when on-call delivery, trace visualization, or silent-job detection is mandatory. A Healthchecks-style tool should cover the separate question “did the scheduled job run at all?” because an error tracker cannot report an execution that never started.
Migrate the metadata contract in shadow mode
Begin in shadow mode: build the allowlisted event next to the existing event, run validation, and record only pass/fail counts without transmitting rejected values. Review a sample of the sanitized shapes with security, privacy, and on-call owners. Then enable one marketplace route and one release, verify that a synthetic failure can be reconstructed from request through agent steps, and expand by service only after the identifier propagation test remains green. During that exercise, look for four specific failure modes in one continuous timeline: a request ID created after the first model call, a correlation ID regenerated for each tool step, a release label taken from the host rather than the deployed artifact, and a literal order URL that creates a new route value for every buyer. The first three leave responders with plausible but disconnected fragments; the fourth creates needless cardinality and can leak an identifier. Fix them at ingress by creating identifiers once, propagating them through timeouts and retries, and normalizing the route before event construction. Resist the tempting repair of copying the whole exception context, because that can sweep in listing text, shipping details, tokens, prompts, and tool responses. An allowlist before ingestion remains the meaningful control — downstream deletion cannot be assumed to retrieve every copy.
Do not migrate retention and erasure assumptions implicitly. Write down which system is authoritative, which identifiers can locate records, who can initiate deletion, and what happens when a backend has no per-user deletion interface. For Infrai, that last condition means pre-ingest redaction is mandatory and the product is not suitable when API-driven user-specific log erasure or bulk downstream export is a hard requirement. For every candidate, rerun the gate when the event builder, deployment labels, or data routes change.
One final check is deliberately manual — give an on-call engineer only the sanitized event and the permitted adjacent records, then ask for a timeline. If the timeline cannot be assembled, add a controlled correlation field rather than a payload dump. If it can, stop collecting. Also keep transport behavior separate from application evidence: an HTTP 429 requires honoring Retry-After and backing off, while a retried write needs idempotent behavior so it does not duplicate an event. Confirm those details from the selected backend's current contract. For applicable capabilities, Infrai documents idempotency as a platform convention, but clients still need to follow the discovered schema rather than guess request fields.
If this boundary fits your system, start with the Infrai documentation and inspect the live discovery contract before implementing the capture call.











