DEV Community

WadeSterling3125
WadeSterling3125

Posted on

SaaS Error Tracking API: Optional Fintech Reconstruction Across App and Backend

TL;DR: Use a small, standards-shaped ingestion boundary that accepts structured backend errors and browser errors, attaches tenant and transaction context before storage, and keeps source maps optional. For a solo SaaS, that is the least complex design that can reconstruct a customer incident and attribute its storage and processing cost. Buy or host the storage behind that boundary according to operational capacity; do not let the storage product define the event contract.

Choice Incident reconstruction Cost attribution Operator burden Best fit
Managed error tracker behind your adapter Strong when context survives ingestion Good if tenant and byte counts are retained Low Default for a one-person team
Self-hosted error tracker behind your adapter Strong, with direct retention control Strong if storage is partitioned or metered High Data-location or control requirements justify on-call work
Structured logs plus traces Strongest for cross-service timelines Strong with explicit resource attributes Medium Existing telemetry pipeline already works

Recommendation: own a thin event schema and outsource the undifferentiated storage operation unless a written residency, retention, or access-control requirement rules that out. Shipping weekly matters more than tending another stateful service. The schema is the asset because it preserves switching options and makes cost visible.

What should a Node.js SaaS app error tracking API retain?

An exception name and stack trace rarely answer the support question: what happened to this customer's transfer? A useful record needs a timestamp, stable event ID, release identifier, environment, service, tenant key, operation, outcome, and a correlation value shared with adjacent telemetry. Record the provider or rail only when it is operationally relevant. Never place account numbers, card data, access tokens, or free-form request bodies in the event by default.

Consider a transfer request that reaches the API, passes validation, and is submitted to an external rail. The browser loses its connection before the response returns. Three records may now exist: a client network error, an API timeout, and a later callback that confirms acceptance. Grouping all three under a generic timeout hides the answer support needs. Instead, the submission event should carry the opaque tenant key, transaction correlation ID, transfer.submit phase, release, and unknown outcome. The callback should reuse the correlation ID and record its own authoritative outcome. The investigator can then order the records, distinguish UI failure from financial state, and see which tenant generated the evidence. No raw account number is required. This example also exposes the retention decision: the callback may matter longer than the browser stack, so one global retention period wastes storage or discards useful evidence. I would keep those classes distinct even if the first storage backend puts them in the same index. It costs a little schema work now and prevents a migration from becoming a rewrite later.

Context wins.

The event should describe a state transition, not merely a crash. A timeout after a payment submission is different from a validation failure before submission, even if both surface as the same UI message. Capture the operation phase and whether an external side effect may have occurred. This turns an error inbox into evidence.

Keep the browser's claim separate from the backend's decision. The React client can report that confirmation rendering failed, while the Node.js API records that the transfer was accepted and assigns the authoritative transaction correlation ID. Joining those records later is useful. Treating the client report as proof of financial state is not.

This boundary also limits sensitive-data exposure. The OWASP logging guidance recommends excluding or masking data such as access tokens, payment card data, and sensitive personal information. Apply that rule before an event leaves the process, because downstream deletion is a weaker control than never collecting the field.

Which two criteria decide the architecture?

The first is attributable evidence cost. Aggregate invoice totals aren't enough for a multi-tenant SaaS. Measure accepted event bytes and event counts by tenant, service, environment, and retention class at your ingestion boundary. Those dimensions explain which workload creates cost without tying the model to a vendor's current billing units.

Do not put raw customer identifiers into high-cardinality labels exported everywhere. Use an internal opaque tenant key, enforce an allowlist of dimensions, and keep the mapping in the application database. A single noisy integration can otherwise distort storage, alert volume, and investigation time. Set per-tenant quotas and record dropped counts by reason. Silence is expensive during an incident.

Don't guess.

The second criterion is reconstruction completeness. OpenTelemetry's logs model supports correlating logs with traces through trace and span identifiers, while W3C Trace Context defines interoperable headers for propagating trace context between services. In Trace Context version 00, a traceparent value carries four fields, including a 16-byte trace ID and an 8-byte parent ID. Preserve those identifiers when present. They let an investigator move from an error to the request path without inventing a proprietary join key.

Source maps improve browser stack readability, but they are not the foundation of the evidence model. Make release IDs mandatory and source-map upload optional. If maps are absent, the backend event still carries the customer-safe correlation data, operation phase, and deployment identity needed to narrow the incident. If maps are present, restrict their access and align their retention with the corresponding release artifacts.

A narrow TypeScript boundary

The adapter below is intentionally boring. It accepts a closed schema, strips unknown input by construction, and sends one JSON document to a collector URL supplied by configuration. In production, authenticate the request, bound retry time, batch where appropriate, and expose failed or dropped delivery as a metric. Error reporting must not hold a payment response open indefinitely.

type IncidentEvent = {
  eventId: string;
  occurredAt: string;
  release: string;
  environment: "production" | "staging";
  service: string;
  tenantKey: string;
  operation: "transfer.validate" | "transfer.submit" | "transfer.confirm";
  outcome: "rejected" | "unknown" | "failed";
  traceId?: string;
  error: { name: string; message: string };
};

type CaptureResult =
  | { accepted: true; bytes: number }
  | { accepted: false; reason: "timeout" | "rejected" };

export async function captureIncident(
  collectorUrl: URL,
  event: IncidentEvent,
  signal: AbortSignal,
): Promise<CaptureResult> {
  const body = JSON.stringify(event);
  try {
    const response = await fetch(collectorUrl, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body,
      signal,
    });

    return response.ok
      ? { accepted: true, bytes: new TextEncoder().encode(body).byteLength }
      : { accepted: false, reason: "rejected" };
  } catch {
    return { accepted: false, reason: "timeout" };
  }
}
Enter fullscreen mode Exit fullscreen mode

The returned byte count can feed a local meter keyed by the same tenant and retention class. It is an attribution input, not a bill. Compression, indexing, replication, and retention change actual storage cost, so verify the relationship with exported usage data from whichever backend you operate.

Test the boundary as a failure path. Assert that secrets are removed, unknown fields never cross it, aborts do not change the business response, and a rejected event increments a visible counter. During deployment, send a synthetic event with a known release ID and confirm that correlation works before increasing traffic. A feature flag can control capture sampling or a new schema version, but Fowler's guidance is a useful warning: toggles introduce carrying cost and need deliberate management. Give every temporary toggle an owner and removal condition.

When is the runner-up the better choice?

Self-hosting is the runner-up for a one-person SaaS because operating storage, upgrades, backups, and access controls consumes the same hours that ship customer features. It becomes the better choice when a concrete requirement demands direct control: a contractual location boundary, a retention policy the managed option cannot express, or an established platform that already operates the needed data plane. Make that decision from written constraints, not from a vague preference for control.

This approach has limitations. A thin adapter cannot supply good issue grouping, symbolication, alert routing, or investigation UX by itself; the selected backend still has to do that work. Measuring accepted bytes also cannot predict indexed storage cost exactly because compression, derived indexes, and retention policies sit downstream. The trade-off is deliberate: application code stays portable, while some operational behavior remains backend-specific and must be tested after a switch.

Structured logs plus traces can also beat a dedicated error workflow when the hard problem is a distributed timeline rather than exception triage. If the team already has reliable collection, trace propagation, retention enforcement, and alerting, adding a separate error store may duplicate data and joins. Keep the same application-owned event schema so the move does not leak backend-specific fields into business code.

There is a simpler case too. An early product with one backend process and low incident volume may start with structured logs and a tested alert on capture failures. The trigger to add specialized triage should be observed investigation friction: duplicate grouping, release regression analysis, or browser stack decoding. Don't buy complexity in anticipation of scale.

The operating rule

Choose the option that preserves enough evidence to answer three questions: which customer operation was affected, what state transition occurred, and which release and request path produced it. Then meter ingestion by tenant and retention class. That yields a defensible cost model without making price the architecture.

Keep the contract small. Review sampled payloads for sensitive fields, exercise the failure path in every release, and expire old evidence according to policy. The backend can change later. Your incident history should remain intelligible.

Further reading

Top comments (0)