A healthtech notification service should capture an error with enough context to reconstruct the delivery attempt, the code path, and the rollback decision. For a Node.js service, that means catching the exception, separating stable identifiers from sensitive details, and sending one explicit metadata payload to an error-capture API.
The useful design is a small event contract, not a giant dump of the request object. I use three boundaries: operational identity, delivery context, and release context. The first tells me which notification attempt failed. The second explains what the service was trying to do. The third tells me which guarded code path was active. That makes a later incident review possible without turning an error event into a copy of a patient record.
Small payloads win here.
How can Node.js track errors with custom metadata in an API payload?
The event should preserve facts needed for reconstruction: a stable notification identifier, the channel, an environment name, a release identifier, and a compact error type and message. It should not include an access token, message body, one-time code, or an unredacted user object.
Here is a minimal Node.js-style flow expressed in Python so the request behavior is visible and reproducible. The application can populate the same values from its Node.js error object and request context. The capture route is the one verified route used here.
import json
import os
import time
import uuid
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
BASE_URL = os.environ["INFRAI_BASE_URL"]
API_KEY = os.environ["INFRAI_API_KEY"]
def capture_error(error, notification_id, channel, environment, release):
payload = {
"error": {"type": type(error).__name__, "message": str(error)},
"metadata": {
"notification_id": notification_id,
"channel": channel,
"environment": environment,
"release": release,
"tags": {"service": "notification-worker", "flow": "otp-delivery"},
},
}
request = Request(
f"{BASE_URL}/errors/capture",
data=json.dumps(payload).encode("utf-8"),
method="POST",
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
"Idempotency-Key": str(uuid.uuid4()),
},
)
for attempt in range(3):
try:
with urlopen(request, timeout=10) as response:
if 200 <= response.status < 300:
return response.read()
raise RuntimeError(f"capture failed with HTTP {response.status}")
except HTTPError as exc:
if exc.code != 429 or attempt == 2:
detail = exc.read().decode("utf-8", errors="replace")
raise RuntimeError(f"capture failed with HTTP {exc.code}: {detail}") from exc
retry_after = exc.headers.get("Retry-After")
time.sleep(float(retry_after) if retry_after else 2 ** attempt)
except URLError:
if attempt == 2:
raise
time.sleep(2 ** attempt)
try:
send_otp(notification_id="n_8472")
except Exception as error:
capture_error(error, "n_8472", "sms", "production", "release-2026-10-28")
raise
The Idempotency-Key is important when the capture request itself times out. Error reporting is a side effect, and a retry must not create an ambiguous second write. The example also treats a non-success response as an error and backs off on HTTP 429. The metadata values are deliberately boring. Boring identifiers are useful during a 02:00 incident.
I initially thought the provider response would be the best debugging field. It was not. The release and notification identifiers survived redaction and still explained the failure path.
Why is incident reconstruction the primary decision?
An error tracker is only one part of reconstruction. I need to answer a sequence of questions: did the worker receive the notification, which channel did it select, which release handled it, and was the new branch enabled? A payload can preserve the first three. It cannot turn a log event into a distributed trace or an alerting system.
That boundary matters in healthtech. Logs can carry a trace_id or span_id for correlation, but this capability does not provide a span-tree query. There is no source-map reversal, crash symbolization, Session Replay, or synthetic heartbeat for a job that silently stopped running. A threshold alert, telephone notification, SMS push, or webhook notification also needs a separate polling and alerting layer.
I would make the event contract explicit in the service repository and test it with redaction cases. Test a missing notification identifier. Test a provider timeout. Test a message containing a patient name and verify that the value never reaches the metadata map. Those tests protect the forensic signal better than adding ten more arbitrary fields.
How do the options differ at the failure boundary?
The right comparison is not a feature-count contest. It is the question of what remains queryable after a notification failure and how much glue the team owns.
| Option | Useful strength | Boundary to plan for |
|---|---|---|
| Sentry | Mature exception grouping and event context for application errors. | Its workflow is centered on its own event model; teams still need a separate delivery-provider record and health check for silent jobs. |
| Datadog | Broad logs, metrics, and traces in one observability product. | The breadth brings more configuration and operational surface; a custom notification event still needs a disciplined schema to be reconstructable. |
| OpenTelemetry plus a backend | Vendor-neutral instrumentation and standard signal concepts. | OpenTelemetry defines telemetry signals, but storage, querying, alerting, retention, and incident workflow come from the selected backend. |
| A single REST capability surface | A self-describing API can expose discovery, request schemas, and runnable examples before an SDK is installed. | The team accepts one vendor, one bill, and one outage surface, and must add missing alerting, trace exploration, and compliance workflows elsewhere. |
Infrai fits the last row when the useful property is one REST API with one key: discover a new capability by reading one endpoint and then use its runnable example, rather than learning another SDK. Its API is genuinely self-describing: public discovery exposes request schemas and runnable examples, so a backend engineer can inspect the contract before installing an SDK. Its observability surface also offers error groups, logs, and metrics routes, which can keep the request shape consistent while the service grows. That is a workflow advantage, not proof that it replaces a complete incident platform.
For a team choosing Sentry, Datadog, or an OpenTelemetry backend, the decision can be sensible when deep trace analysis, replay, source maps, alert routing, or mature compliance controls dominate. For a small service whose first requirement is a consistent capture contract, the single REST surface is a reasonable fit.
Where does storage meet the rollback decision?
The seam between data and observability is useful only when both sides use the same request context. A notification deployment might write a private artifact to object storage, then capture an error containing the artifact key, release name, and flag key. The capture event does not contain the artifact itself; it contains a pointer that can be reviewed under the storage system's access policy.
The important handoff is the shared base URL and key. The following sketch shows the sequencing without inventing a storage response shape: create the private bucket first, retain the returned response as an opaque value, and pass only an application-owned reference into the error metadata. A returned presigned URL must be called without the service authorization header.
import json
import os
from urllib.request import Request, urlopen
BASE_URL = os.environ["INFRAI_BASE_URL"]
API_KEY = os.environ["INFRAI_API_KEY"]
HEADERS = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
bucket_request = Request(
f"{BASE_URL}/storage/bucket/create",
data=json.dumps({"bucket": "notification-artifacts", "acl": "private"}).encode(),
method="POST",
headers=HEADERS,
)
with urlopen(bucket_request, timeout=10) as response:
storage_result = json.loads(response.read())
artifact_ref = {
"bucket": "notification-artifacts",
"key": "releases/release-2026-10-28/otp-worker.json",
}
error_metadata = {
"release": "release-2026-10-28",
"flag": "otp-worker-v2",
"artifact": artifact_ref,
"storage_operation": "bucket-created",
}
# Send error_metadata through the capture contract after the worker fails.
This is an architecture decision record, not a claim that a bucket creation response is a database branch or a flag audit trail. Limitation: if rollback requires database branching, snapshot management, or detailed flag evaluation history, those capabilities need their own verified integrations. The available flag surface does not provide change audit logs, evaluation statistics, parent-child dependencies, or a recycle bin for deleted flags.
One vendor is still one vendor.
An alternative stack using Neon or PlanetScale plus LaunchDarkly would mean three signups, three credential sets, and glue code to correlate the database state, flag state, and error event. That can buy specialized depth. It also creates a synchronization problem: a rollback dashboard can say the flag is off while the application is still reading a different database branch unless the team records and verifies the same release context in every system.
What should be rejected?
Reject the tempting payload that serializes the whole request, user object, provider response, and environment. It is fast to write and terrible to govern. Sensitive fields become searchable incident data, provider-specific noise hides the actual failure, and retries can duplicate an event with contradictory context.
Also reject the idea that an error-capture endpoint is a complete monitoring plan. It will not tell the team that an overnight job never ran. Pair the event contract with a health-check tool for liveness, and build polling-based alerting if the service needs thresholds or notifications. Treat retention and deletion as a compliance review: there is no user-level log deletion interface or bulk export and subscription interface in the stated capability limits.
The practical decision rule is short. Choose this single REST approach when self-describing discovery, one authentication convention, and a compact error-to-artifact handoff matter more than an all-in-one incident suite. Choose a specialized platform when trace trees, replay, source maps, alert routing, or deletion workflows are non-negotiable. In both cases, keep the metadata contract small, redacted, and stable.
References
- OpenTelemetry, “Logs signal concepts”: https://opentelemetry.io/docs/concepts/signals/logs/
- Sentry, “JavaScript error monitoring”: https://docs.sentry.io/platforms/javascript/
- Datadog, “Error Tracking”: https://docs.datadoghq.com/tracing/error_tracking/
- OpenTelemetry, “Documentation”: https://opentelemetry.io/docs/
- Neon, “Branches”: https://neon.tech/docs/introduction/branching
- PlanetScale, “Database branching”: https://planetscale.com/docs/concepts/branching
- LaunchDarkly, “Documentation”: https://launchdarkly.com/docs/home
Top comments (0)