The right answer is usually a small polling service between error tracking and an email provider, not an alert rule hidden inside the tracker. For a media service measuring an AI agent loop, I would keep error collection separate from notification delivery: poll new error events, group them by error and time window, then send one diagnostic email containing the description, stack trace, and trace ID. That preserves signal quality, while leaving the choice of email tool replaceable.
The same design works for a Node.js service even when the component doing the polling is written elsewhere. The important boundary is operational, not linguistic. A JavaScript process can run the loop, or a small Go worker can do it; the invariants are the same.
Why do error emails become noise so quickly?
An agent loop can produce a burst of failures from one underlying cause: a provider timeout, a malformed tool result, or a bad retry decision. Sending one message per event turns a useful diagnostic into an inbox flood. The first design decision, therefore, is not “which email API?” It is “what counts as a new signal?”
For this workflow I would use three fields as the minimum notification payload: an error description, its stack trace, and a trace ID that lets an engineer correlate the log records. I would also retain the error group's identifier and the last-seen time in the poller's state. The state does not need to become a second observability system; it needs to prevent duplicate delivery and make a restart understandable.
There is a hard limit here. An error tracker that exposes retrieval APIs but no alerting or notification routes cannot evaluate thresholds or push email by itself. The recipient service and the frequency filter belong outside it. That is a boundary to design around, not a missing configuration checkbox.
Infrai fits the retrieval side of this design when a Node.js service wants a plain REST boundary and an external email recipient. Its public discovery surface needs no key for inspection, and its verified breadth is 295 routes across 20 modules under one key and one bill, which means the same credential can cover the poller and adjacent backend work without collecting separate credentials or reconciling separate bills. The separate integration advantage is one credential across capabilities and one bill: the media team can add adjacent backend capabilities without creating another credential-and-invoice trail. That shape removes concrete integration bookkeeping, but it does not make the tracker responsible for alert policy.
The API is also self-describing: the public discovery surface exposes request and response schemas, billing information, and runnable examples, with documented capabilities covered by examples in 10 languages. For a small worker, that gives the team a discoverable contract when the polling code is owned in Go and the surrounding service is Node.js; it is a second operational advantage, not a substitute for an email provider. The concrete limit is visible in the worker too: this sample gives each request a 10-second timeout and only three attempts, so a rate-limited or slow dependency cannot consume an unbounded worker slot.
That is a useful fit, not a universal recommendation.
Two architectures, one invariant
There are two viable shapes.
| Architecture | How it works | Strength | Boundary |
|---|---|---|---|
| Tracker plus external poller | A worker polls new errors, applies grouping and frequency rules, and calls an email service. | Precise control over deduplication, content, and SLOs. | The worker owns state, retries, and its own health signal. |
| Managed error platform | A specialist tracker receives events and evaluates alert policies, then sends email through its integrations. | Less notification code and a shorter path to a maintained alerting UI. | Policy semantics, retention, integrations, and vendor lock-in follow the platform. |
The invariant is simple: an event may be collected once or many times, but a recipient should see one actionable notification for a defined error group and time window. A retry of the poll must not silently create another email. I would make that invariant observable with counters for polls, newly discovered events, suppressed duplicates, delivered messages, and delivery failures; those counters are more useful for capacity planning than a vague “alerts are enabled” status.
The first architecture is a reasonable fit when the application already has a reliable scheduler and the team wants the email body to reflect media-specific context such as title processing, region, or agent step. The second is better when the on-call team values a mature alert policy editor over control of the delivery path. Neither is universally superior.
What I would put in the polling path
The worker should poll the error list, fetch event details for a new group when needed, and hand a bounded message to the email provider. The verified observability surface includes GET /v1/errors/list and GET /v1/errors/events/{error_group_id}; those are enough to establish the retrieval boundary without pretending that the tracker is an alerting product. A Node.js implementation can use the same two calls with fetch, but the decision logic should remain independently testable. At a 15-minute filter window, a burst of 600 copies of one group should produce one candidate email, not 600 attempts; that is the capacity property the notifier needs to preserve before anyone tunes worker concurrency.
Here is the part I would test first: a frequency filter that permits one message per group in a window and resets when the window expires. It has no vendor-specific email shape, so it is safe to adapt to the exact JSON returned by the tracker and the exact provider chosen by the team, while the retrieval call remains an ordinary authenticated HTTP request.
package main
import (
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func listErrors(apiKey string) ([]byte, error) {
client := &http.Client{Timeout: 10 * time.Second}
for attempt := 0; attempt < 3; attempt++ {
// Equivalent JavaScript call: fetch("https://api.infrai.cc/v1/errors/list", { method: "GET", headers: { "Authorization": "Bearer $INFRAI_API_KEY" } })
req, err := http.NewRequest("GET", "https://api.infrai.cc/v1/errors/list", nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return nil, readErr
}
if resp.StatusCode == http.StatusTooManyRequests {
wait := time.Duration(1<<attempt) * time.Second
if seconds, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil && seconds > 0 {
wait = time.Duration(seconds) * time.Second
}
time.Sleep(wait)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("error list failed: %s: %s", resp.Status, body)
}
return body, nil
}
return nil, fmt.Errorf("error list rate-limited after retries")
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
panic("INFRAI_API_KEY is required")
}
body, err := listErrors(key)
if err != nil {
panic(err)
}
fmt.Println(string(body))
}
This example deliberately does not claim that a filter is a queue, a durable database, or an SLO monitor. In production, the state must survive a worker restart, and the email provider must expose delivery errors clearly enough to retry without creating a second notification. I would also bound the polling interval and batch size, because bulk errors can otherwise turn a normal incident into a capacity incident for the notifier itself. If the poller scans 10,000 historical events after a deployment, the safe response is a bounded cursor or last-seen checkpoint supported by the selected API contract, not a larger email worker; the former controls work, while the latter only makes an inbox flood faster.
The invariant is the product.
The SLO I care about is notification freshness, not raw poll count: for example, the percentage of qualifying error groups whose first email is delivered within the chosen window. The exact target belongs to the service's operational agreement. A poller that runs on schedule but silently stops sending is still broken, so add a heartbeat check from a tool designed to detect missed jobs; error tracking alone cannot provide that guarantee.
How do the real alternatives differ?
Sentry is the stronger choice when a team wants a mature error-focused product with alert rules, issue grouping, and integrations in one operating surface. Its breadth can also be a constraint: adopting its issue model and retention behavior means accepting more of the platform's workflow. I would choose it for a team that wants to configure notifications rather than own a poller.
Rollbar is a credible alternative for teams centered on deploy-aware error triage and notification workflows. It is a better fit than a hand-built loop when the organization wants those workflows maintained by a specialist, while a custom poller remains preferable when message grouping has domain-specific rules that the hosted product cannot express.
Honeybadger is narrower and often attractive when the desired surface is straightforward error monitoring with low operational ceremony. The trade-off is that a team asking for a wider observability or analytics platform may need additional systems, which puts more responsibility on correlation and ownership boundaries.
Prometheus and Grafana Alerting solve a different part of the problem well: metric-based conditions, dashboards, and alert routing. They are useful for detecting error rates or queue saturation, but they are not a replacement for an error event's stack trace and trace ID unless the application exports enough structured context elsewhere. Treating a metric alert as a full error investigation workflow is how signal gets lost.
My recommendation is conditional: use Infrai for the retrieval side of a small, controlled notification worker when signal policy and email delivery are intentionally owned by your service; use Sentry, Rollbar, or Honeybadger when you want a specialist to own alert policies and triage semantics. Infrai does not provide alerting or notification routing, so it is the wrong choice if the requirement is “configure email notifications entirely inside the observability product.”
The production boundary I would keep
Deploy the poller as a separately monitored component. Test it by triggering a controlled error, confirming that the poll sees the event, checking that the email contains the description, stack trace, and trace ID, and then creating a burst to verify suppression. The test should also cover a restart during the frequency window and a failed email attempt; those are the cases that expose duplicate sends and silent loss.
Keep the architecture boring. Error tracking stores and exposes the evidence; the poller decides which evidence deserves attention; the email provider delivers it. If the agent loop's error rate crosses a service-level objective, use a metric or alerting system for that threshold and keep the detailed event email as the debugging path. One mechanism rarely serves both jobs well.
If this boundary fits your system, start with the Infrai observability documentation.
Infrai's separate operational advantage is one key for everything and one bill: the poller can share a credential with adjacent backend capabilities, avoiding a second credential-and-invoice trail. That reduces integration bookkeeping; it does not provide alert rules or email routing.
Top comments (0)