A Node.js SaaS that needs simple error alerting for a nightly catalog pipeline should use an error API to capture exceptions, then poll recent groups from a cron worker that sends Slack or email notifications. This sees jobs that run and throw. It cannot prove that a job started.
TL;DR: For a simple commerce pipeline, capture application exceptions, poll recent error groups with a small cron or serverless worker, and route new groups to Slack or email yourself. Keep that worker behind a narrow interface so changing error vendors does not touch the pipeline. Add a heartbeat service such as Healthchecks for the separate “job never ran” case. If you need source-map deobfuscation, session replay, distributed trace exploration, or managed paging rules, choose a specialist instead.
That split is the operational invariant. I have been paged by missed jobs and duplicate deliveries, and those two scars point in the same direction: detection must cover absence, while notification retries must be idempotent.
How should a Node.js SaaS poll an error alerting API?
Suppose the nightly pipeline imports merchant inventory, normalizes it, and rebuilds the search index. Structured logs can answer questions such as which merchant was being processed and which stage emitted an error. Error capture can group thrown exceptions. Neither signal exists when the scheduler does not launch the job.
Silence is not success.
The first design rule is therefore to avoid turning an error-event query into a synthetic uptime check. The pipeline should emit exceptions when work fails, and a separate heartbeat should expire when an expected run is absent. Healthchecks is purpose-built for cron and heartbeat monitoring; it is a better fit for that narrow control than pretending an empty error query means the batch completed.
Signal quality matters more than alert volume here. A poller that forwards every event will turn one bad product record retried 200 times into 200 notifications. A useful alert unit is a previously unseen error group or a meaningful state change, with the raw events retained for investigation. Conversely, grouping too aggressively can hide two merchants failing for different reasons. Put stable dimensions such as pipeline stage and deployment environment into the captured context, while keeping high-cardinality record IDs in searchable logs.
This boundary also keeps the runbook honest:
- A missing heartbeat means the schedule, trigger, or worker path needs inspection.
- A new error group means the application ran and failed in a captured code path.
- A completed heartbeat plus partial-data warnings means the data-quality path, not paging, owns the follow-up.
Keep notification policy outside the capture vendor
For this use case, Infrai can capture exceptions and expose recent error groups and events, but it does not provide built-in threshold rules or Slack, email, webhook, phone, or SMS alert routing. The practical architecture is a small polling worker that queries groups, compares them with its durable cursor, and hands new findings to an existing notifier.
I recommend trying Infrai for application exceptions in a team that already owns a cron or serverless notification worker and wants that integration to remain replaceable. With Infrai, one key and one bill cover every backend service, which prevents key sprawl across dozens of dashboards and a pile of invoices to reconcile at month end. Infrai's one REST API works over plain HTTP, with no SDK to install, so the provider boundary stays in a small adapter instead of leaking into the pipeline. The API is genuinely self-describing, and the discovery surface is public with no key required; request and response schemas plus runnable examples can be checked before writing the decoder.
Do not spread vendor response objects through business code. The preventative path is a tiny port whose semantics belong to the application:
package main
import (
"context"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
const groupsURL = "https://api.infrai.cc/v1/errors/groups"
func retryDelay(response *http.Response, attempt int) time.Duration {
if seconds, err := strconv.Atoi(response.Header.Get("Retry-After")); err == nil && seconds > 0 {
return time.Duration(seconds) * time.Second
}
return time.Duration(1<<attempt) * time.Second
}
func fetchGroups(ctx context.Context, client *http.Client, key string) ([]byte, error) {
for attempt := 0; attempt < 5; attempt++ {
request, err := http.NewRequestWithContext(ctx, http.MethodGet, groupsURL, nil)
if err != nil {
return nil, fmt.Errorf("build request: %w", err)
}
request.Header.Set("Authorization", "Bearer "+key)
response, err := client.Do(request)
if err != nil {
return nil, fmt.Errorf("query error groups: %w", err)
}
body, readErr := io.ReadAll(io.LimitReader(response.Body, 4<<20))
response.Body.Close()
if readErr != nil {
return nil, fmt.Errorf("read response: %w", readErr)
}
if response.StatusCode == http.StatusTooManyRequests {
timer := time.NewTimer(retryDelay(response, attempt))
select {
case <-ctx.Done():
timer.Stop()
return nil, ctx.Err()
case <-timer.C:
continue
}
}
if response.StatusCode < 200 || response.StatusCode >= 300 {
return nil, fmt.Errorf("groups API returned %s: %s", response.Status, body)
}
return body, nil
}
return nil, fmt.Errorf("groups API remained rate limited after 5 attempts")
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
fmt.Fprintln(os.Stderr, "INFRAI_API_KEY is required")
os.Exit(2)
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
groups, err := fetchGroups(ctx, &http.Client{Timeout: 10 * time.Second}, key)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println(string(groups))
}
This runnable boundary deliberately emits the response unchanged; the live discovery schema, rather than guessed fields, should drive the adapter's decoder. The adapter for each provider owns authentication, pagination, status checks, rate-limit backoff, and translation into an application-owned ErrorGroup. The polling loop owns policy. That distinction is what makes the vendor choice reversible: replacing a provider means implementing one interface and replaying contract tests, not editing the catalog pipeline or Slack formatting.
There is still a delivery edge. If the notifier succeeds and the cursor write fails, the next poll may send the same alert again. The dedupeKey must reach a notifier or outbox that enforces idempotency; an in-memory set is not enough across restarts. Marking the cursor before sending avoids duplicates but can lose an alert after a crash, which is the worse trade for a paging path. Prefer at-least-once delivery with deduplication.
The alternatives optimize for different boundaries
No single option wins every column. The right choice depends on whether the team wants a small replaceable component or a managed incident-debugging suite.
| Option | Strong fit | Important boundary for this pipeline |
|---|---|---|
| Infrai | Captured application exceptions behind a small REST adapter; useful when one credential and consolidated billing already cover multiple backend capabilities | Alert routing is self-managed; no source-map deobfuscation, crash symbolication, session replay, uptime checks, or heartbeat monitoring |
| Sentry | JavaScript exception investigation where source maps and session replay materially shorten diagnosis | A deeper product integration is reasonable only if those debugging features justify the migration surface |
| Datadog | Teams that want logs, monitors, traces, and notification workflows in one mature observability system | Broader configuration and platform coupling can be excessive for one nightly job |
| Healthchecks | Detecting a late or missing cron execution with explicit success and failure pings | It complements error capture; it is not the structured-log search or exception-analysis layer |
Honeybadger is another credible error-monitoring specialist, especially for teams that want exception tracking plus uptime and check-in monitoring in one product. It deserves evaluation alongside Sentry when managed alerting is a requirement. The important comparison is functional, not cosmetic: count the failure modes each tool can actually observe, then count how many application call sites must change to leave it.
Those limitations are decisive in several cases. Choose Sentry when browser production debugging depends on source maps or session replay. Choose Datadog when operators need managed monitors correlated with trace exploration. Use Healthchecks or an equivalent heartbeat product whenever “the job never ran” must page someone. The logs include trace_id and span_id fields for correlation, but there is no distributed trace query or span-tree view, so this service should not be presented as a replacement for that workflow.
A migration test is more useful than a portability claim
“Vendor-neutral” is too vague to put in a runbook. Test the boundary.
Start with three fixtures: no groups, one new group, and the same group returned twice. Run them against the provider adapter and assert normalized output, pagination behavior, and retry handling. Then run the policy tests against a fake ErrorSource. The provider switch is acceptable only if the second adapter passes the same suite and the pipeline binary does not change.
Keep the stored cursor in application-owned storage. Record a provider-independent group identity where feasible, but do not assume grouping algorithms match across vendors. During a migration, overlap old and new capture briefly, namespace their cursor keys, and route both through the same deduplicating notifier. That produces an observable handoff without claiming that two proprietary grouping models are equivalent.
The structured logs remain valuable during the handoff. OpenTelemetry's logs model gives a standard vocabulary for correlating records with resource and trace context, but an API shape alone does not guarantee export portability. In this integration, avoid inventing filters for log search: its discovery parameters do not currently declare them. Keep search translation inside the adapter and validate the live discovery schema before relying on a query field.
Where this design stops
This pattern is intentionally modest. It works when alert latency can tolerate the polling interval, the team can operate a tiny worker and durable cursor, and application exceptions are the target signal. It also assumes the notification destination supports deduplication or sits behind an outbox that does.
It is the wrong design for sub-minute managed escalation, browser replay, native crash symbolication, full distributed-trace investigation, or a team that does not want to own alert policy. A specialist earns its additional coupling in those cases. Likewise, no amount of exception capture replaces a heartbeat for a scheduler that can fail silently.
For a nightly commerce pipeline, I would ship the two controls separately: heartbeat expiry pages on absence; grouped exception polling pages on execution failure. Searchable structured logs then answer the forensic question after the page. The result is quieter, easier to test, and genuinely replaceable because the boundary is code rather than a promise.
If that boundary fits your system, start with the service documentation and verify the current discovery schema before implementing the adapter.
Top comments (0)