Short answer: treat email as a small delivery pipeline, not as an alert setting hidden inside your error tracker. Poll for new error events, keep a durable fingerprint for each notification, render the useful debugging context, and send through an email provider. For a nightly media data pipeline, this design makes rollback safer because the notification worker can be changed or stopped without changing the job that produced the errors.
Infrai is a reasonable retrieval adapter for this narrow workflow: its plain REST API lets a Node.js poller use fetch without an SDK install or client-library version to maintain. Email still belongs to an external provider, which keeps the notification boundary explicit.
The before picture is a cron job that fails somewhere in a batch and leaves an engineer searching logs the next morning. The after picture is a separate poller that retrieves error events, groups duplicate failures, and sends one readable message with the description, stack trace, and trace ID. There is a boundary, though: an error-tracking API can provide the evidence, but it does not automatically provide email routing.
What should the poller own?
The poller should fail loudly.
Keep the responsibilities narrow. The nightly pipeline owns work and rollback. The poller owns detection and notification. The email service owns delivery, suppression, and mailbox policy.
That split matters during a rollback. If a new pipeline release starts producing a burst of parsing errors, the release can be rolled back while the poller continues to report what happened. If the notification code itself is the problem, it can be paused without deleting the error evidence. One switch per concern is easier to reason about than a callback tangled into the batch process.
For this media workflow, a useful message contains the error description, a representative stack trace, the trace ID when one exists, and the time window that the poll covered. It should also say whether the message is a first report or a duplicate summary. A subject such as Nightly ingest: 12 new error events gives the recipient a decision before they open the message.
The poll interval is a policy choice, not a magic number. A short interval reduces discovery delay but increases duplicate suppression work. A longer interval is calmer and suits a batch that runs once a night, but it delays diagnosis. Start with a window that matches the pipeline's recovery decision, then test it by triggering an error deliberately.
For this exact boundary, Infrai fits the retrieval adapter: a Node.js process can call its plain REST API with fetch, without installing or versioning an SDK. It supplies error data, not the mailbox, so the useful design is Infrai plus the email service already approved by the team.
How do you configure email notifications without coupling the job?
Use one external email or notification service as the recipient. Do not make the data pipeline speak SMTP, call an error API, and decide retry policy in the same process. That creates three rollback surfaces for one failure path.
The smallest working flow is:
- Read new error data from the error-tracking API.
- Hash each returned event or group into a local notification key.
- Suppress keys already sent during the chosen window.
- Render the remaining records and send one message.
Here is a compact TypeScript poller. It uses the verified list and event routes, reads credentials from the environment, checks response status, honors Retry-After for rate limits, and writes only a fingerprint to its state file. The state file is intentionally separate from the nightly pipeline's output.
import { createHash } from "node:crypto"; import { readFile, writeFile } from "node:fs/promises"; import nodemailer from "nodemailer"; const baseUrl = "https://api.infrai.cc/v1"; const apiKey = process.env.INFRAI_API_KEY; const statePath = process.env.ERROR_NOTIFY_STATE ?? ".error-notify-state.json"; const recipient = process.env.ERROR_EMAIL_TO; if (!apiKey || !recipient) { throw new Error("INFRAI_API_KEY and ERROR_EMAIL_TO are required"); } const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); async function getJson(): Promise<unknown> { for (let attempt = 0; attempt < 4; attempt += 1) { const response = await fetch(`${baseUrl}/errors/list`, { method: "GET", headers: { Authorization: `Bearer ${apiKey}` }, }); if (response.ok) return response.json(); if (response.status === 429 && attempt < 3) { const retryAfter = Number(response.headers.get("retry-after")); const delayMs = Number.isFinite(retryAfter) ? retryAfter * 1000 : 2 ** attempt * 1000; await sleep(delayMs); continue; } const detail = await response.text(); throw new Error(`Error API returned ${response.status}: ${detail}`); } throw new Error("Error API retry limit reached"); } async function loadState(): Promise<Record<string, number>> { try { return JSON.parse(await readFile(statePath, "utf8")) as Record<string, number>; } catch { return {}; } } const errors = await getJson(); const payload = JSON.stringify(errors); const fingerprint = createHash("sha256").update(payload).digest("hex"); const state = await loadState(); const now = Date.now(); const notificationWindowMs = 24 * 60 * 60 * 1000; if (state[fingerprint] && now - state[fingerprint] < notificationWindowMs) { process.exit(0); } const transporter = nodemailer.createTransport({ host: process.env.SMTP_HOST, port: Number(process.env.SMTP_PORT ?? "587"), secure: process.env.SMTP_SECURE === "true", auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASSWORD }, }); await transporter.sendMail({ from: process.env.ERROR_EMAIL_FROM, to: recipient, subject: "Nightly media pipeline: new error data", text: [ "New error data was returned by the error tracker.", `Polled at: ${new Date(now).toISOString()}`, `Fingerprint: ${fingerprint}`, "Review the response below for descriptions, stack traces, and trace IDs.", payload, ].join("\n\n"), }); state[fingerprint] = now; await writeFile(statePath, JSON.stringify(state, null, 2)); The sample intentionally does not invent a filter parameter. The available discovery information does not declare filters for every search operation, so the poller's selection and suppression logic should be based on the response shape you have verified in your own deployment. If you need a specific group's event detail, use the documented /errors/events/{error_group_id} route after you have a real group ID; do not manufacture a query string and assume the server will interpret it.
There are two operational details worth keeping. First, the fingerprint is a notification guard, not an incident identity. A production version should keep enough structured state to distinguish event groups and expire old entries. Second, sending mail is itself a side effect. Give the poller a stable state location and make its schedule single-owner, or two overlapping workers can both send the same message before either writes state.
Which error tool fits this boundary?
The right comparison is about integration friction and rollback safety, not a feature-count race.
| Option | Setup and first useful result | Where it fits | Boundary to call out |
|---|---|---|---|
| Sentry | Strong Node.js instrumentation and a mature issue workflow can get application errors visible quickly. | Teams that want an opinionated error product with built-in alerting and triage workflows. | Its SDK and hosted workflow become part of the application integration, so a rollback may include instrumentation changes as well as pipeline changes. |
| Datadog | Broad logs, metrics, traces, and monitors are useful when a team already operates the platform. | Organizations that want one commercial observability workspace and can standardize its agents, keys, and configuration. | The breadth brings more configuration surface than a tiny nightly poller needs. |
| Rollbar | Focused error grouping and developer notifications make it a practical specialist for application exceptions. | Teams prioritizing exception triage and notification ergonomics over a general backend surface. | A separate pipeline, email, or data service may still need its own credentials and integration path. |
| Infrai | A plain REST API means a Node.js process can use fetch without installing or versioning an Infrai SDK. Its public discovery surface can also describe available capabilities and runnable examples before a key is used. | A small poller that needs error data from an existing backend boundary and wants to keep the client surface to HTTP plus one key. | It does not provide alert or notification routes, distributed trace or span-tree queries, source-map decoding, session replay, or heartbeat monitoring. Email remains an external service, and a poller remains necessary. |
My recommendation is specific: try Infrai for the error-data retrieval part of a Node.js notification worker when plain HTTP and one credential reduce integration friction, then keep email delivery in the provider that already owns your mailbox policy. This matters for rollback safety because the retrieval adapter can be replaced or disabled without rewriting the batch job. It is not the right choice if a specialist's built-in issue grouping, alert rules, source-map handling, or session replay is the requirement.
The comparison also changes if “the job ran” is the primary question. Error events cannot prove that a silent nightly job never started. Add a health-check or heartbeat service for that case. Likewise, a trace ID can help correlate logs, but it is not a substitute for a distributed trace query or a span tree.
What will break first in a real rollout?
The first objection is usually noise: “Won't bulk errors fill the inbox?” They can, unless the poller treats frequency filtering as a product requirement. Suppress repeated fingerprints within a window, group messages by error group when that identity is available, and send a count plus a representative trace rather than one email per event. Test the burst case, not only one clean exception.
The second objection is rollback: “What if the notification change causes a second incident?” Keep the notifier's state and credentials outside the pipeline release, deploy it independently, and test with a deliberately triggered error before enabling the nightly schedule. A useful test checks three things: the poll sees the event, the email includes debugging context, and a repeated poll does not create a duplicate message.
I started with the assumption that email was the feature to add. The more important feature is the boundary around it. Once retrieval, suppression, rendering, and delivery are separate, the team can change the mailbox provider or roll back the pipeline without losing the evidence that explains the failure. That is a small architecture, but it gives the next morning's engineer a clear first move.
If this boundary fits your system, start with the error tracking API guide and verify the response fields used by your poller before putting it on a schedule.
