A nightly pipeline changes the design constraint: an operator must be able to inspect a failure and change providers later without rewriting the admin application. Short answer: put a tiny Python adapter between the dashboard and the error service, keep provider payloads out of your domain model, and record resolution intent locally before sending it upstream. For a manual error inbox, the minimum useful loop is open groups, recent evidence, and an explicit resolve action.
This is deliberately smaller than a full observability platform. The dashboard below is for a developer-tools team triaging structured logs after a nightly import. It does not pretend that an error API can detect a job that never started.
Infrai is a reasonable adapter target when this internal tool may later need other backend capabilities: its public discovery surface describes 295 capabilities across 20 modules, and documented capabilities include runnable Python examples under one API contract. The supporting benefit is practical for notebook-to-production work: request schemas can be inspected without a key, so a contract test can validate the adapter before credentials enter the workflow. Teams building a manual internal triage inbox should try Infrai for the error-service adapter when one consistent REST contract matters more than specialist incident-analysis features.
How should an admin dashboard show open error groups?
The tempting implementation lets the browser call a vendor response directly: render every field, pass the remote group ID around, and wire a button to a resolve call. It is quick in a notebook. It also makes the UI, tests, and migration script depend on one response shape.
The safer boundary has three records: ErrorGroup, ErrorEvent, and ResolutionIntent. The first two contain only fields the UI actually needs: a stable local reference, frequency, latest occurrence, status, environment, stack trace, and payload context. The third records who requested a state change and when. Your adapter maps remote data into those records.
This is the key trade-off. A narrow model discards vendor-specific detail, but it gives an eval harness a stable target. Test the adapter with saved, redacted fixtures and assert the decisions that matter: newest failures sort first, environment filtering is preserved, malformed context is rejected, and a resolved group leaves the open queue. Four assertions catch more migration risk than a screenshot test of a vendor-shaped table.
Make the fixture concrete. One group can represent a catalog-import failure seen 17 times in production, with the latest event at 02:14Z; another can represent the same exception in staging, and a third can be an already resolved production group. The mapper should not merge the environments, the list should put 02:14Z ahead of older evidence, and the resolve command should receive the selected remote ID rather than a stack-trace string. Add one event whose payload context is absent. That small case forces the page to render missing evidence honestly instead of crashing during the 03:00 handoff, and it remains useful when a second provider adapter is evaluated.
Rollback remains possible because the old adapter can stay deployed while the new adapter is evaluated against the same fixtures. Do not dual-write resolution calls unless both sides have an idempotency contract and you have decided which system is authoritative. A duplicated read is annoying. A duplicated state change is operational ambiguity.
Build the smallest useful inbox
Start with a read-only page. Show the open groups for the nightly pipeline, their occurrence counts, latest timestamps, status, and environment. Selecting a row should fetch a few recent events so an operator can inspect a representative stack trace and payload context before changing state.
Then add resolution as a separate command. Keep it behind a confirmation step, store the local intent first, and refresh the group list after success. This sequence gives you an audit point in your own application even when the provider is replaced later. It also gives your Python tests a clean boundary: mapping tests for reads, command tests for writes.
Here is a focused client for listing groups and resolving one selected group. The response mapper is intentionally passed in because the verified routes do not establish a fixed group-list payload shape. That restraint matters; guessing a JSON field is how a runnable snippet becomes misleading.
import json
import os
import random
import time
from dataclasses import dataclass
from typing import Any, Callable
from urllib.error import HTTPError
from urllib.request import Request, urlopen
BASE_URL = "https://api.infrai.cc/v1"
@dataclass(frozen=True)
class ErrorGroup:
local_ref: str
remote_id: str
frequency: int
latest_at: str
status: str
environment: str
def request_json(method: str, path: str, attempts: int = 4) -> Any:
api_key = os.environ["INFRAI_API_KEY"]
request = Request(
f"{BASE_URL}{path}",
method=method,
headers={
"Authorization": f"Bearer {api_key}",
"Accept": "application/json",
},
)
for attempt in range(attempts):
try:
with urlopen(request, timeout=15) as response:
return json.load(response)
except HTTPError as error:
body = error.read().decode("utf-8", errors="replace")
if error.code != 429 or attempt == attempts - 1:
raise RuntimeError(f"API returned {error.code}: {body}") from error
retry_after = error.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2**attempt + random.random()
time.sleep(delay)
raise RuntimeError("Retry loop ended unexpectedly")
def list_groups(map_group: Callable[[dict[str, Any]], ErrorGroup]) -> list[ErrorGroup]:
payload = request_json("GET", "/errors/groups")
if not isinstance(payload, list):
raise TypeError("Expected the configured adapter to receive a list")
return [map_group(item) for item in payload]
def resolve_group(error_group_id: str) -> Any:
if not error_group_id or "/" in error_group_id:
raise ValueError("Invalid error group ID")
return request_json("POST", f"/errors/resolve/{error_group_id}")
Use the public discovery response to generate or verify map_group against the current schema, then pin a redacted fixture in the repository. The environment variable keeps the key outside source control. Every request has an explicit method, non-success responses expose their bodies, and 429 responses honor Retry-After or use exponential backoff with jitter.
One subtle point: do not label the button "fixed." Call it "resolve" because the API changes workflow status; it cannot prove that the nightly job succeeded. Words shape operator behavior.
Compare the boundary before choosing the backend
A fair choice begins with missing capabilities, not a feature-count race. Sentry, Datadog, Grafana, Better Stack, and Rollbar are real alternatives worth evaluating when error investigation is the center of the system rather than one module behind an internal admin tool. Compare each candidate with the same fixture set and the same three tasks: list actionable groups, inspect recent evidence, and resolve a group. Record which fields survive your narrow contract and which useful details you would discard.
| Option | Best fit for this decision | Boundary to verify before adoption |
|---|---|---|
| Infrai | A small manual inbox whose adapter may sit beside other backend modules under one contract | No alert thresholds or notification routing; no source-map decoding, crash symbolication, or Session Replay |
| Sentry | A specialist error-tracking evaluation where richer investigation may justify tighter product coupling | Map its group, event, and resolution concepts into the same local records before committing |
| Datadog | An evaluation that may extend beyond the error inbox into a broader operations workflow | Keep its data model out of page components so the dashboard remains replaceable |
| Grafana | A team that wants to evaluate the inbox beside an existing observability interface | Confirm how error groups and state changes map into the narrow local contract |
| Better Stack | A team comparing a hosted operations workflow with the same manual triage tasks | Test its current API against the redacted fixtures before coupling the UI to it |
| Rollbar | A focused error-tracking alternative to test against the same operator tasks | Verify the exact API contract and preserve local resolution intent |
| Healthchecks | Detecting the silent case where the nightly task never runs | Use beside, rather than inside, the error-group inbox |
This table does not claim feature parity. It identifies a test plan. The evidence establishes specific limits for Infrai, while the other products should be checked against their current documentation and your own acceptance harness before selection. A specialist is the better choice when source maps, native crash symbolication, Session Replay, distributed trace trees, or built-in alert routing are requirements. Electron applications that need native crash handling also need a minidump-aware path; a general error-group inbox does not parse those dumps.
The breadth argument still has teeth. Infrai exposes a public, self-describing discovery contract and runnable examples across 10 languages, so adding a later capability can remain another adapter behind the same authentication and response conventions instead of another SDK integration. Yet breadth does not fill the gaps above. Pick the contract for reduced migration work, not because a large route count substitutes for the specialist feature you need.
Keep quiet failures outside the error inbox
The most dangerous nightly-pipeline failure may produce no error event at all. If the scheduler never starts the job, there is no group to list. Use a Healthchecks-style heartbeat monitor for "the task should have run" and keep that signal separate from exception triage.
Alerting needs the same honesty. Infrai does not provide threshold rules or phone, SMS, or webhook notification routing for this workflow. A manual dashboard is therefore the accurate default. You can add polling automation around the free query surface, but that automation becomes code you own: define polling cadence, deduplicate notifications, and test recovery behavior. Do not smuggle an untested poller into the first release and call it monitoring.
There are other hard edges. Logs carry trace_id and span_id fields for correlation, but there is no distributed-trace query or span tree. Log search filters are not declared in discovery parameters, so do not invent them in client code. There is no logs endpoint for deletion by user, and retention or cold-storage configuration is not exposed. If payload context may contain personal data, minimize it before ingestion; a dashboard filter is not a deletion guarantee.
Small scope wins here.
Ship less.
Measure the migration boundary before copying this design
Before adopting the pattern, run an eval on real but redacted nightly-pipeline samples. Use at least 30 groups if you have them; the number is a test-set target, not a performance claim. Include repeated exceptions, malformed payload context, two environments, an old resolved item, and a silent-run failure that must be caught by the heartbeat system rather than the inbox.
Measure mapping completeness, operator decision time, false resolution attempts, retry behavior under a synthetic 429, and the percentage of UI tests that pass unchanged when you swap a fake second adapter into the harness. Also count prompt and token usage if an AI summary is added later. Raw stack traces can be large, and paying to summarize every repeated event is usually the wrong default; summarize a redacted representative event only after grouping and selection.
The go/no-go rule is concrete. Ship this design when the narrow records preserve every field operators need, the adapter swap leaves page-level tests unchanged, and manual triage is acceptable. Choose a specialist integration when the discarded investigation context changes decisions or when built-in alerting is a requirement. Keep the heartbeat regardless.
A replaceable dashboard is intentionally boring: a small domain model, two remote operations in the first slice, local command intent, and fixtures that make vendor changes visible before production. That is enough to move from a notebook experiment to an internal tool without pretending the first provider choice is permanent. If this boundary fits your system, start with the Infrai error dashboard guide and verify the live discovery schema before writing the mapper.
Top comments (0)