Choosing an error tracker for an Express API in Europe: seven questions
A vendor-neutral checklist for Express teams — grouping control, search, stack-trace fidelity, GDPR duties — plus a 15-minute harness that scores any candidate.
Most evaluations of error tracking tools compare feature grids and stop there. The questions that decide whether you’re happy in a year are narrower: who controls grouping, whether search finds an error you can only half remember, how much of a stack trace survives the wire, what your data protection obligations look like when personal data lands in the store, and what leaving costs. Infrai’s errors namespace is one candidate — plain REST, nine routes, no SDK — and the checklist below is deliberately written so it can lose on some of these axes.
Run it against Sentry, Rollbar, Bugsnag or anything else you’re considering. The answers matter more than our score.
Seven questions, in the order that matters
Who owns the grouping key? If the vendor’s algorithm decides which failures are “the same”, you inherit their opinion and learn an override syntax when it’s wrong. Infrai takes a fingerprint string you compute in your own code and hashes it server-side; Sentry and Rollbar group algorithmically with override hooks. Neither is wrong, but only one of them is debuggable at 3am with a Node REPL.
Can you find an event from a half-remembered phrase? Free-text search over stored messages is the feature you use most and evaluate least.
How much of the stack trace survives? This is where a REST-only tracker gives ground. Frame parsing, source maps and in-app markers are real engineering, and if your errors come from minified browser bundles you need them.
What does the write path do to your request latency? Fire-and-forget with a timeout, or a blocking call in your error middleware? A tracker that adds 800 ms to a 500 response is a self-inflicted wound.
Does it have a machine-readable capability manifest? You want to script an audit, not read a pricing page. Infrai publishes one at GET /v1/discovery covering routes, methods and billing class.
What are the data protection duties, and who carries them? Covered below — it’s the question most feature grids skip entirely.
What does leaving cost? Sixty lines of fetch behind one function costs an afternoon to replace. An SDK woven through 40 files and a proprietary event schema costs a sprint.
A harness that scores any candidate in 15 minutes
Don’t take anyone’s latency claim, including ours. Capture a marked event, poll until it’s searchable, then close it out:
const API = "https://api.infrai.cc";
const KEY = process.env.INFRAI_API_KEY;
if (!KEY) throw new Error("INFRAI_API_KEY is not set");
const headers = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" };
const marker = `eval-${Math.random().toString(36).slice(2, 8)}`;
async function json(path, init = {}) {
const res = await fetch(`${API}${path}`, { headers, ...init });
const payload = await res.json();
if (!res.ok || payload.ok === false) throw new Error(`${path} -> ${res.status} ${payload?.error?.code ?? ""}`);
return payload.data;
}
const startedAt = Date.now();
const captured = await json("/v1/errors/capture", {
method: "POST",
body: JSON.stringify({
message: `evaluation probe ${marker}: ReferenceError: quota is not defined\n at chargeSeat (/app/src/seats.js:57:9)`,
exception: "ReferenceError",
fingerprint: "eval:chargeSeat:ReferenceError",
environment: "staging",
release: "eval",
}),
});
const writeMs = Date.now() - startedAt;
let visibleMs = null;
const searchStart = Date.now();
for (let attempt = 0; attempt < 20 && visibleMs === null; attempt++) {
const hits = await json(`/v1/errors/search?q=${encodeURIComponent(marker)}&limit=5`);
if (hits.items.some((item) => item.event_id === captured.event_id)) visibleMs = Date.now() - searchStart;
else await new Promise((done) => setTimeout(done, 250));
}
const group = await json(`/v1/errors/resolve/${captured.error_group_id}`, {
method: "POST",
body: JSON.stringify({ error_group_id: captured.error_group_id }),
});
console.log({
event_id: captured.event_id,
new_group: captured.is_new_group,
write_ms: writeMs,
searchable_after_ms: visibleMs,
resolved: group.is_resolved,
});
In our own run against the live API, the write round-trip was under a second including TLS setup, and the event turned up on the first search poll. Server-side latency reported in the response envelope sat in the 15–25 ms range. Run the same shape against every candidate and you’ll learn more in a quarter of an hour than a week of comparison pages will teach you.
Reading the write response
export INFRAI_API_KEY="your_infrai_api_key"
curl -sS "https://api.infrai.cc/v1/errors/group_detail/errgrp_lbqaTTkjmUqyWDBHgpTLsM2Z" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
{
"ok": true,
"data": {
"error_group_id": "errgrp_lbqaTTkjmUqyWDBHgpTLsM2Z",
"title": "evaluation probe eval-3kq1zt: ReferenceError: quota is not defined",
"count": 1,
"user_count": 0,
"level": "error",
"is_resolved": true,
"first_seen_at": "2026-07-26T00:25:11.402118Z",
"last_seen_at": "2026-07-26T00:25:11.402118Z",
"environments": ["staging"],
"releases": ["eval"]
}
}
count, user_count and the releases array are the triage inputs. What isn’t there is equally informative: no frame table, no in-app flags, no suspect-commit attribution.
The same check by hand, which is the version you’ll actually paste into a terminal while evaluating:
curl -sS "https://api.infrai.cc/v1/errors/search?q=ReferenceError&limit=5" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
| jq '.data.items[] | {event_id, title, environment, error_group_id}'
Scoring the candidates
| Axis | Sentry | Rollbar | Bugsnag | Honeybadger | Infrai errors |
|---|---|---|---|---|---|
| Grouping key you compute | Override hooks | Custom fingerprint | Custom hash | Custom fingerprint | Default and only mode |
| Parsed frames + source maps | Yes | Yes | Yes | Yes | No |
| Free-text event search | Yes | Yes | Yes | Yes | Yes, q only |
| Integration surface | SDK | SDK | SDK | SDK | One HTTPS POST |
| Declared EU data region | Yes | Yes | Yes | Regional options | Not EU-specific |
| Alert rules and on-call | Built in | Built in | Built in | Built in | Not in this namespace |
| Same key covers queue, cron, email, storage | No | No | No | No | Yes |
Read that table as a shape, not a ranking. Four of those rows favour the specialists.
The GDPR questions feature grids skip
Under Article 28, whoever stores your error events is a processor acting on your instructions, and you need a written agreement covering it — sub-processors, security measures, deletion on termination. That’s a contract question, not an API question, and you should ask every vendor for it in writing before traffic flows.
Two API-level facts do change your obligations, though.
The first is minimisation. Anything you put in message gets stored, and a stack trace built by string interpolation is the classic route for an email address or a bearer token to end up in a third-party system. Redact in your process, before the send — a couple of regexes over the stack text is crude but it’s the only place that still knows what the values mean.
The second is erasure. Infrai’s errors namespace has no per-event delete or redact route: the surface is capture, message, list, search, get, groups, group_detail, events and resolve. If you need to reach individual events on request, that’s a real gap you have to design around — send an HMAC pseudonym instead of a user id, and rotate the pepper. Sentry, by contrast, publishes an explicit EU data storage location and per-event data management controls; if contractual EU residency is a hard requirement from your buyers, that’s the safer purchase and we’d rather you made it knowingly.
Capture is served from western and China regions, which the manifest states plainly:
curl -sS "https://api.infrai.cc/v1/discovery" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
| jq '.capabilities[] | select(.id == "errors.capture") | {regions, billing: .billing}'
What it costs, and what it costs to leave
Reads are free and rate-limited across the Infrai errors namespace; only capture is metered, at $0.00005 per event, verified 2026-07-26, with $2 of credit on a new account. There are no seats, so adding a fourth engineer to the rota doesn’t change the bill. Check the credit and the spend on the account you’re evaluating with:
curl -sS "https://api.infrai.cc/v1/account/balance" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" | jq '.data'
Rates in this catalogue tend to fall rather than rise, and discount campaigns run, so treat the figure above as a dated reading and the structure — metered writes, free reads — as the durable part.
Exit cost is the number nobody quotes. With this integration, the entire coupling is one function that builds a JSON body — swapping it for a Sentry SDK later is a morning’s work, and that’s the honest reason a small team can pick it without much risk.
Where we’d land
If minified frontend traces, release health or on-call scheduling are the point of the purchase, buy the specialist and don’t overthink it. If you’re an Express team shipping server-side errors, you want grouping you control, and the same credential is already handling your queue, cron jobs and transactional email, then a REST error store is the proportionate choice — and the harness above will tell you within fifteen minutes whether it behaves.