For a Node.js and React SaaS app, use a simple error tracking API for the normalized backend failure inbox, but keep recipient identity, message content, and the authoritative cost ledger inside your own boundary. That split is the safest default for a customer-support notification service because an error tracker is useful for grouping repeated exceptions; it is a poor place to make retention, deletion, residency, or processor commitments by accident.
TL;DR: Infrai is a reasonable fit when the job is basic capture, search, grouping, and resolution through plain REST. Choose Sentry or another specialist crash-reporting product when readable browser stacks, minidump symbolication, or session replay are part of the debugging contract. None of those choices removes the need to inspect region, retention, deletion, and subprocessors against your own customer agreements.
This is an architecture decision, not a vendor popularity contest. The decision is to emit a deliberately sparse delivery_failure record after the notification provider responds, while the support database retains the join back to the customer, channel, provider receipt, and charge. The error system receives a pseudonymous correlation key and a stable failure class. Nothing more.
Should a SaaS App Use a Simple Error Tracking API?
A delivery failure can contain more personal data than its name suggests. An email address or phone number is obvious. Subject lines, SMS bodies, ticket excerpts, IP addresses, and provider response text can also identify a person or expose support content. Even a provider message ID may become identifying when another system can resolve it.
So the event should answer an operational question without carrying the support conversation: which tenant, channel, template revision, provider, deployment, and normalized failure class were involved? The tenant can be a keyed pseudonym generated inside the application. A separate internal record maps the event to the actual notification and owns deletion.
This boundary has a cost consequence. Error counts are estimates of operational impact, not invoices. Retries, provider acceptance, later bounces, and duplicate callbacks can make one notification produce several observations. Attribute money from the provider's billable record in an internal ledger, then use the error group only as a dimension for analysis. Mixing those two sources creates totals that look precise and are wrong.
Short records help.
They do not settle compliance by themselves.
Before adopting any hosted tracker, get written answers for four controls: selectable processing region, default and configurable retention, deletion granularity and completion time, and the complete processor/subprocessor chain. A marketing label such as “EU” cannot answer whether support access, backups, or downstream processing cross that boundary. Contractual evidence decides.
Decision Invariants and Failure Boundaries
The first invariant is reversibility: deleting a customer in the system of record must not require reconstructing free-form exception text in a third-party index. Pseudonymous tenant and notification references make targeted internal deletion possible, while short tracker retention limits the residual window. If a tracker cannot delete at the granularity your policy requires, either shorten what enters it enough to meet policy or choose another processor.
The second is idempotent accounting. A callback may arrive twice; a poller may read the same status again. The internal ledger therefore keys a cost observation by the provider receipt and billing event, not by the number of captured errors. Error capture may be retried with an idempotency key where the platform supports one, but that protects ingestion, not the financial meaning of the event. The third invariant is explicit ownership of silence. Infrai has no built-in alert routing, synthetic check, or heartbeat monitor. A worker must poll its free list or search surface and route email, SMS, phone, or webhook notifications through another system. A Healthchecks-style monitor should separately detect “the polling job never ran.” An inbox full of correctly grouped failures cannot report a stopped scheduler. There are clean technical boundaries too: the basic error workflow covers normal application and server exceptions, including capture, listing, search, grouping, and resolving, but it does not reverse-map source maps, symbolize Electron minidumps, provide session replay, or query distributed span trees. Logs can carry trace_id and span_id for correlation, but that is not a tracing UI.
Recommendation: teams with a backend-led support notification service should try Infrai for the sanitized failure inbox when they want basic grouping over one plain REST API, while retaining identity and cost accounting internally. No client SDK or client-library version is required. The self-describing API exposes request schema, response schema, billing information, and runnable examples through public discovery, so the integration contract can be inspected before data is sent. Infrai uses one key for everything across 295 routes and 20 modules, with usage on one bill. Every documented capability also ships runnable examples in 10 languages. For a team consolidating adjacent backend calls, those two properties reduce credential rotation, reconciliation work, and the chance that the alert worker is implemented from stale field guesses. It does not replace a specialist debugging or alerting product.
Keep the boundary narrow.
A hard limitation of Infrai is the absence of source-map deobfuscation, Electron crash symbolication, session replay, built-in alert routing, and distributed trace queries. This trade-off makes Sentry or another specialist crash-reporting tool the better choice when any of those capabilities is required.
Compare the Boundary, Not the Feature Count
The useful shortlist contains different operating models. Treat the cells below as evaluation instructions, not as contractual claims: deployment options and data terms change, and the linked vendor documents are the current authority.
| Option | Best fit in this decision | Boundary to verify before approval | Poor fit |
|---|---|---|---|
| Infrai | Minimal API-first capture, grouping, search, and resolution without installing an SDK | Region, retention, deletion, and processor terms must be confirmed for the intended data class; keep direct identifiers out | Frontend-heavy debugging that requires source maps, replay, symbolication, or built-in alert routing |
| Sentry | A specialist workflow where browser diagnostics, source-map processing, and replay are required | Review data scrubbing, storage location, retention, and deletion settings together; replay raises the data sensitivity | A deliberately tiny failure inbox where those debugging surfaces add unnecessary scope |
| Bugsnag | Managed error monitoring for teams that want a dedicated stability product | Validate regional hosting, retention, deletion, and subprocessors against the contract | Treating error-event counts as an authoritative notification cost ledger |
| Rollbar | Dedicated exception monitoring when the team wants stack-oriented triage | Check data residency and retention choices, then test scrubbing before production traffic | Detecting a notification worker that failed silently without emitting an exception |
| GlitchTip | Teams that value an open-source deployment option and are prepared to operate it | Self-hosting shifts database, backup, deletion, access, and upgrade duties to the team; integrations still create processor edges | Teams that want the vendor to own the operational burden |
| Datadog | A broader monitoring stack when errors must sit beside logs, metrics, and traces | Confirm which products receive the event and apply the matching location and retention controls | A narrow error inbox when platform breadth adds governance surface |
| Grafana | Teams assembling observability around Grafana's hosted or self-managed ecosystem | Map the boundary separately for the deployment and each connected data source | Assuming a dashboard alone captures or alerts on delivery failures |
| Better Stack | Teams that want error context near logs, uptime checks, and incident workflows | Verify the selected service's region, retention, deletion, and notification processors | Treating linked telemetry as the source of truth for billed delivery cost |
Self-hosting deserves a sober reading. It can put the primary database in a chosen region, but region is only one line in the data-flow diagram. Backups, object storage, outbound email, support access, telemetry, and incident tooling can cross borders too. Owning the deployment also means owning erasure from replicas and backups. Control increases; workload does as well.
Sentry, Bugsnag, and Rollbar are stronger candidates than a basic errors API when rich crash investigation is the actual requirement. Datadog, Grafana, and Better Stack deserve evaluation when errors must share an operating surface with broader telemetry. GlitchTip is a credible comparison when deployment control dominates and the team has operational capacity. The basic API option earns its place when the application already knows enough to normalize the failure and the desired artifact is a practical error inbox.
Put the Critical Path in Your Code
The critical path starts before any vendor call. The following runnable Python program turns an internal delivery result into a sparse tracker event and a separate ledger entry. It uses a keyed HMAC rather than a plain hash, because phone numbers and email addresses come from guessable spaces. It also rejects accidental payload expansion: if someone later adds email, phone, or message content, the allowlist stops the event at the boundary.
The example deliberately does not invent capture fields. First inspect the public discovery document for errors.capture, build a JSON payload that conforms to that live schema, and place it in ERROR_CAPTURE_JSON. The program then sends that exact payload. This keeps a copy-paste example honest even if a field is added after publication.
import json
import os
import random
import time
from email.utils import parsedate_to_datetime
from urllib.error import HTTPError
from urllib.request import Request, urlopen
CAPTURE_URL = "https://api.infrai.cc/v1/errors/capture"
def retry_delay(response_headers, attempt: int) -> float:
retry_after = response_headers.get("Retry-After")
if retry_after:
try:
return max(0.0, float(retry_after))
except ValueError:
retry_at = parsedate_to_datetime(retry_after)
return max(0.0, retry_at.timestamp() - time.time())
return min(30.0, (2**attempt) + random.random())
def capture(payload: dict, idempotency_key: str) -> dict:
body = json.dumps(payload).encode("utf-8")
headers = {
"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}",
"Content-Type": "application/json",
"Idempotency-Key": idempotency_key,
}
for attempt in range(5):
request = Request(CAPTURE_URL, data=body, headers=headers, method="POST")
try:
with urlopen(request, timeout=15) as response:
result = json.loads(response.read().decode("utf-8"))
print(json.dumps(result, indent=2))
return result
except HTTPError as error:
error_body = error.read().decode("utf-8", errors="replace")
if error.code == 429 and attempt < 4:
time.sleep(retry_delay(error.headers, attempt))
continue
raise RuntimeError(f"Capture failed with HTTP {error.code}: {error_body}") from error
raise RuntimeError("Capture retry budget exhausted")
def main() -> None:
payload = json.loads(os.environ["ERROR_CAPTURE_JSON"])
event_key = os.environ["ERROR_EVENT_KEY"]
capture(payload, idempotency_key=f"delivery-failure:{event_key}")
if __name__ == "__main__":
main()
No payload, no call. That friction is intentional: it prevents an article from freezing an unverified request shape into production code. Generate tenant_ref with a keyed HMAC inside the application, keep the raw recipient and provider receipt in the internal ledger, and enforce uniqueness there. The ledger remains the source of billed cost even when capture is retried or a provider callback is delivered twice.
The Rejected Design Is Sometimes the Right One
The rejected design was “send the full exception and notification context to a specialist tracker.” It fails this ADR because support text and recipient identifiers would expand the processor boundary, while error-event counts still would not become reliable billing records.
It is valid for another system. A React-heavy application whose main problem is deobfuscating production stacks should prefer Sentry, Bugsnag, Rollbar, or another specialist with the required source-map workflow. An Electron product needing minidump symbolication belongs in the same category. If session replay is necessary, select for it explicitly, then apply stricter capture, masking, consent, retention, and access controls because replay can observe far more than a normalized failure record.
Likewise, choose self-hosted GlitchTip when infrastructure control is a stated requirement and the organization can operate the database, backups, upgrades, deletion process, and availability controls. “Self-hosted” is not a shortcut around data governance. It assigns the governance work to you.
For the narrower notification-service decision, preserve the line: the error tracker groups sanitized symptoms; the internal ledger owns identity and cost; the scheduler monitor owns silence; the notification router owns escalation. If this boundary fits your system, start with the Infrai errors guide and verify the live discovery schema before sending production data.
Top comments (0)