DEV Community

OlafJohansson3168
OlafJohansson3168

Posted on

Simple Error API Design for Lean Node.js SaaS: Exceptions, Traces, and Search

Short answer: choose the error-tracking API that captures exceptions at one clear boundary, preserves a readable stack trace, supports bounded-field search in a dashboard, and can prove redaction, retention, and alert ownership with a test. A small SaaS should reject any design that lets diagnostic reporting delay a customer request or masquerade as the source of truth for business state.

That decision rule is deliberately narrower than “pick the most capable dashboard.” In a payment or ledger backend, an error event is a diagnostic copy. The transaction record, idempotency key, audit trail, and reconciliation process remain authoritative.

How should a small SaaS team design a Node.js error-tracking API?

Start with invariants. Every exception needs one reporting boundary; every event needs an environment and release; and retrying the reporting operation must never retry the business mutation. A useful searchable vocabulary is bounded: service, operation, environment, and release. Put an opaque correlation ID in context so an engineer can connect a stack trace to an internal audit entry. Raw request bodies usually add privacy risk faster than diagnostic value.

Node.js needs more than one failure boundary. Framework error middleware sees request failures, while process-level handlers see failures that escape the request. Installing both without a deduplication rule can turn one defect into two issues and two notifications. I use a generated event ID for diagnostic idempotency, then test handled exceptions, rejected asynchronous work, and termination behavior separately. Exactly-once business processing is a different claim; durable idempotency records and reconciliation establish it, not the count of dashboard entries.

Privacy belongs before transport. Remove authorization headers, cookies, payment tokens, raw customer payloads, and credentials while the event is still in process memory. A retention control cannot retrieve a secret already copied into an alert or export. Compliance review also has to cover the actual event schema, access model, processor terms, residency, deletion mechanism, and retention period. I'm not sure why teams sometimes review the contract but skip the schema; the recurring exposure starts in those fields.

Search is an acceptance test, not a screenshot. Send a sanitized corpus with changing identifiers. Verify that related causes group without merging unrelated failures, locate events by release and operation, and follow one opaque correlation ID into the audit trail. Then trigger exactly one alert and require a named owner to acknowledge it.

No owner, no signal.

Where do capture, audit, and reconciliation stop being interchangeable?

I keep four records separate. Business state says what committed. An audit row says which principal and idempotency key attempted a transition. A diagnostic event explains code behavior. A notification asks a person to inspect it. Later records cannot prove an earlier business outcome, so a tidy error dashboard must never be treated as a ledger.

Consider a provider call that returns 200 while its promised side effect never occurs. There is no thrown exception, the error index stays quiet, and reconciliation discovers the missing state six hours later. That is transport success without business completion. The correct response is a domain assertion such as “authorized but not settled by deadline,” a durable audit row keyed by the operation's idempotency key, and a reconciliation job. Absence of a stack trace is not evidence of success. I keep this distinction visible in runbooks because a green transport metric can otherwise close an incident before the business invariant is checked, especially when a retry changes the provider's response but not the missing ledger transition. The diagnostic event should point to that audit row, not pretend to replace it.

The reporting path has a failure budget of its own. Serialization can fail, a bounded queue can fill, shutdown can end before a buffer flushes, and sampling can discard events during a storm. After ingestion, grouping can hide a new cause, routing can target an unowned channel, or retention can remove evidence before an investigation. Document those losses. Telemetry may be buffered or dropped under pressure, but it must not hold a customer request indefinitely and must never sit inside the database transaction whose failure it reports.

OpenTelemetry treats logs as a telemetry signal and defines concepts for correlating log records with other signals. That improves portability and cross-service context, but it does not supply storage, grouping, retention, deletion, or an issue-ownership workflow. A signal model is a transport and correlation foundation, not proof that triage exists.

How should teams compare a dashboard, logs pipeline, and owned collector?

Compare evidence and ownership, not logos. The architecture that looks smallest at ingestion can become the largest operational commitment after search, deletion, alert routing, and access audits arrive.

Architecture Appropriate when Evidence required before adoption Limitation
Managed issue tracker A small team needs capture, grouping, search, a dashboard, and alert routing without operating storage Pre-send redaction, representative grouping, release mapping, export, deletion, and an end-to-end alert test Adds a data processor and a separate workflow; its feature surface may exceed the team's needs
Existing managed logs platform Structured logs, access, retention, and on-call routing already exist Dependable stack parsing, saved searches, deduplication, regression checks, and named ownership Log storage does not create issue grouping or triage discipline by itself
OpenTelemetry pipeline with owned storage Portability and correlation across signals justify platform work Resource attributes, collector behavior under load, storage queries, capacity limits, and alert tests The team owns upgrades, capacity planning, and recovery
Narrow internal event service Policy requires a controlled ingestion boundary and requirements are stable Authentication, idempotent acceptance, bounded payloads, retention deletion, search latency, and access auditing Grouping, source mapping, triage, and notifications become an internal product

For a small backend without an observability platform, I usually reject logs-only capture because searchable text is only one part of error tracking. The missing work includes normalizing variable messages, grouping occurrences, detecting a regression after a release, suppressing duplicate alerts, assigning ownership, and rehearsing deletion. The catch is real: a managed tracker is not suitable when policy forbids another processor, required controls cannot be met, or an established logs platform already passes the tests. Stick with that platform in those cases. Choose an owned collector when cross-service correlation and portability are explicit requirements and a platform team accepts the maintenance burden.

Cost belongs in the bake-off, though it should not lead the decision. Model ordinary traffic, a failure burst, retention, high-cardinality fields, data transfer, and engineer time for upgrades and on-call work. Put a hard budget on diagnostic traffic so it cannot compete with customer operations. Your mileage may vary: a three-service SaaS and an organization with a staffed telemetry platform are solving different ownership problems even when their event schemas look similar.

What does a portable capture contract look like in code?

The application in the question is Node.js, but I express the contract in Go during backend design reviews because an explicit interface makes authority and retry behavior visible. A Node.js error middleware can implement the same contract. The adapter owns transport; the capture boundary owns redaction and event identity.

package diagnostics

import (
    "context"
    "crypto/rand"
    "encoding/hex"
    "errors"
    "time"
)

type Event struct {
    ID            string
    OccurredAt    time.Time
    Service       string
    Operation     string
    Environment   string
    Release       string
    CorrelationID string
    ExceptionType string
    Message       string
    Stack         string
}

type EventSink interface {
    // Accept is idempotent by Event.ID and never performs a business mutation.
    Accept(context.Context, Event) error
}

func Capture(ctx context.Context, sink EventSink, event Event) error {
    if event.ID == "" {
        var raw [16]byte
        if _, err := rand.Read(raw[:]); err != nil {
            return err
        }
        event.ID = hex.EncodeToString(raw[:])
    }
    if event.Service == "" || event.Release == "" || event.Stack == "" {
        return errors.New("incomplete diagnostic event")
    }

    reportCtx, cancel := context.WithTimeout(ctx, 200*time.Millisecond)
    defer cancel()
    return sink.Accept(reportCtx, event)
}
Enter fullscreen mode Exit fullscreen mode

The event ID deduplicates diagnostic retries; it says nothing about whether a charge or job executed once. Keep the operation's idempotency key in the durable business record, and retain only an opaque correlation ID in the diagnostic event. The adapter should expose counters for accepted, duplicate, rejected, and dropped events. During controlled shutdown it may flush a bounded queue, but request handlers should not wait for a global flush.

One subtle boundary remains: Capture returns a reporting error so the caller can count it, while the outer handler preserves the original application error. Do not replace a ledger failure with a telemetry failure. Record both through separate paths, complete rollback, and let reconciliation determine the business outcome.

I set the reporting timeout to 200 milliseconds in the example. It is a budget, not a promise that every network will answer in that time.

How can a lean team roll out and revisit the decision?

Begin with a staging-only synthetic exception behind a short-lived feature toggle. Give the toggle an owner and removal date. The test should verify redaction, readable stacks, release association, grouping, search, and exactly one notification; repeat a smaller smoke test after changing the runtime, framework, build pipeline, or telemetry library. Feature toggles separate deployment from exposure, but temporary test machinery still needs deletion.

Reject a home-built dashboard unless a controlled ingestion surface is a hard policy requirement and the organization is prepared to operate authentication, payload limits, grouping, source mapping, indexed search, retention deletion, access audits, alert deduplication, ownership, and export as a product. A narrow receiver is not automatically a simple system.

Sampling protects request latency and event budgets during a failure storm, but it makes the diagnostic index incomplete by design. It cannot establish the number of failed transactions or the correctness of a customer balance. Durable domain state, audit records, idempotency keys, and reconciliation answer those questions. Revisit the architecture when service count, data policy, or on-call ownership changes; the acceptance corpus and deletion test should change with it.

References

Top comments (0)