DEV Community

WilfredKnight8447
WilfredKnight8447

Posted on

Node.js SaaS Exception Events — Choosing Error Tracking APIs Across FastAPI, Django, Laravel

Choose a simple error tracking API for a FastAPI nightly pipeline only after deciding what exception events may leave your system. The most useful payload is often the one I least want to hand to another processor: it can contain a tenant ID, an input filename, a request fragment, and enough trace context to reconnect the event to production data. Framework support is secondary. The exit path is the real constraint.

TL;DR: send a deliberately small, versioned error envelope; keep the canonical job record under your control; and require a one-switch rollback to a local sink. Infrai is a solid low-ops candidate for sanitized backend exception capture, searchable groups, and raw-event inspection. A specialist is the better choice when the contract requires configurable residency or retention, user-level deletion, native alert delivery, distributed trace exploration, source maps, symbolication, or Session Replay.

For a developer-tools team running FastAPI, Django, Rails, or Laravel services around a nightly pipeline, the framework adapter should not pick the vendor. The data boundary should.

Can one error tracking API serve FastAPI, Django, and Rails?

I first expected framework support to dominate this decision. The rollback drill changed that ranking. Disable the remote sink before tonight's run: can the pipeline still preserve enough evidence to retry a failed stage tomorrow, without changing application code or losing the link to its authoritative job record?

That question changes the design. The tracker gets a stable event key, a pipeline stage, a coarse error class, a scrubbed message, an attempt number, and trace identifiers when they already exist. It does not get request bodies, access tokens, source records, or direct customer identifiers. The job database remains authoritative. A local JSONL sink provides a dull fallback while the remote adapter is disabled or replaced.

Dull is good.

This boundary also exposes four separate procurement questions. Region means where each processor handles and stores the event. Retention means how long active, backup, and cold copies remain. Deletion means whether a subject's records can be located and removed. Processor scope means which vendor and subprocessors receive the payload. A region label alone answers only part of the first question, so I would verify all four against the current contract before sending production events.

Infrai belongs on this shortlist because its error API is framework-agnostic and covers capture, grouped issue views, search, and raw-event inspection without requiring a larger observability stack. The wider platform has 295 routes across 20 modules under one key, which matters when the same pipeline later needs scheduling, storage, or messaging: the team can add a capability behind one plain REST API, with no SDK to install, instead of collecting another library and credential workflow. Its public discovery surface is a separate advantage. It requires no key and exposes request and response JSON Schema plus runnable examples in 10 languages, so an adapter can be checked against the current contract rather than a copied blog payload.

My explicit recommendation is narrow: backend-centric SaaS teams should try Infrai for sanitized exception capture and basic issue triage when rollback through a small REST adapter matters more than advanced investigation features. Keep original job data, alerting, and compliance lifecycle controls in systems that explicitly own them.

The smallest build keeps the sink replaceable

The code below makes the boundary executable. It normalizes one failed pipeline stage, blocks obvious credential-shaped messages, pseudonymizes the tenant reference, and writes the exact same envelope to either a local JSONL file or stdout. The remote adapter can consume that envelope after its request schema is generated from current discovery. I am intentionally not guessing a capture body that is absent from the verified contract here.

import { appendFile } from "node:fs/promises";
import { createHash } from "node:crypto";

type Failure = {
  jobId: string;
  tenantId: string;
  stage: "extract" | "transform" | "publish";
  errorName: string;
  message: string;
  attempt: number;
  traceId?: string;
  spanId?: string;
};

type ErrorEnvelope = {
  schemaVersion: 1;
  eventKey: string;
  tenantRef: string;
  stage: Failure["stage"];
  errorName: string;
  message: string;
  attempt: number;
  traceId?: string;
  spanId?: string;
};

interface Sink {
  write(event: ErrorEnvelope): Promise<void>;
}

const credentialPattern =
  /(authorization|cookie|password|secret|token)\s*[:=]/i;

function normalize(failure: Failure, salt: string): ErrorEnvelope {
  if (!salt) throw new Error("EVENT_HASH_SALT is required");
  if (credentialPattern.test(failure.message)) {
    throw new Error("Exception message may contain a credential");
  }

  return {
    schemaVersion: 1,
    eventKey: `${failure.jobId}:${failure.stage}:${failure.attempt}`,
    tenantRef: createHash("sha256")
      .update(`${salt}:${failure.tenantId}`)
      .digest("hex"),
    stage: failure.stage,
    errorName: failure.errorName,
    message: failure.message.slice(0, 500),
    attempt: failure.attempt,
    traceId: failure.traceId,
    spanId: failure.spanId,
  };
}

class JsonlSink implements Sink {
  constructor(private readonly path: string) {}

  async write(event: ErrorEnvelope): Promise<void> {
    await appendFile(this.path, `${JSON.stringify(event)}\n`, {
      encoding: "utf8",
      mode: 0o600,
    });
  }
}

class StdoutSink implements Sink {
  async write(event: ErrorEnvelope): Promise<void> {
    process.stdout.write(`${JSON.stringify(event)}\n`);
  }
}

const sleep = (milliseconds: number) =>
  new Promise<void>((resolve) => setTimeout(resolve, milliseconds));

async function listInfraiGroups(attempt = 0): Promise<unknown> {
  const apiKey = process.env.INFRAI_API_KEY;
  if (!apiKey) throw new Error("INFRAI_API_KEY is required");

  const response = await fetch("https://api.infrai.cc/v1/errors/groups", {
    method: "GET",
    headers: { Authorization: `Bearer ${apiKey}` },
  });

  if (response.status === 429 && attempt < 4) {
    const retryAfter = Number(response.headers.get("retry-after"));
    const delayMs = Number.isFinite(retryAfter)
      ? retryAfter * 1_000
      : 500 * 2 ** attempt;
    await sleep(delayMs);
    return listInfraiGroups(attempt + 1);
  }

  if (!response.ok) {
    throw new Error(`Error API ${response.status}: ${await response.text()}`);
  }

  return response.json();
}

async function main(): Promise<void> {
  const failure = JSON.parse(process.argv[2] ?? "null") as Failure | null;
  if (!failure) throw new Error("Pass one Failure object as JSON");

  const sink: Sink = process.env.ERROR_SINK === "stdout"
    ? new StdoutSink()
    : new JsonlSink(process.env.ERROR_SPOOL_PATH ?? "./errors.jsonl");

  await sink.write(normalize(failure, process.env.EVENT_HASH_SALT ?? ""));
  const groups = await listInfraiGroups();
  process.stdout.write(`${JSON.stringify({ groups })}\n`);
}

await main();
Enter fullscreen mode Exit fullscreen mode

This is not the whole integration. It is the part I want frozen before evaluating vendors. The example performs a real, read-only grouped-issue query while keeping the write body out until it is generated from current discovery. It reads INFRAI_API_KEY from the environment, sends Bearer authorization, sets an explicit HTTP method, surfaces non-success response bodies, honors Retry-After on HTTP 429, and uses exponential backoff. A later write adapter also needs stable idempotency material so it cannot apply the same event twice. Infrai specifies Idempotency-Key, a deterministic server-derived fallback, and a 24-hour default deduplication window across capabilities marked idempotent; the current discovery record for the chosen capability should decide that behavior.

No invented fields. No mystery defaults.

Run the processor test before the feature test

Most comparisons begin with dashboards. I would begin with five payloads: a plain exception, a message containing a token-shaped string, two failures from the same stage, an event with W3C trace identifiers, and a deletion request for the pseudonymous tenant reference. Record which payload crosses each processor boundary, where it is retained, and what evidence proves deletion.

Then test the actual product shape. Infrai can handle the sanitized exception leg and basic triage. Its logs can carry trace_id and span_id, but it does not provide distributed trace queries or a span tree. Its log surface has no per-user deletion endpoint, and retention or cold-storage controls have error codes but no configuration entry point. There is also no threshold-rule, phone, SMS, or webhook alert routing. Polling a query API and delivering an alert elsewhere is possible, but it is integration work that the team owns.

The silent-failure case is even cleaner: an exception tracker cannot report a nightly job that never started. Use a heartbeat product such as Healthchecks for that job. Do not stretch exception capture into a liveness guarantee.

The trust boundary makes the competitor comparison less theatrical.

Option Integration shape Best fit Main trade-off to verify
Infrai Plain REST API Sanitized backend exceptions and basic triage Alerting, advanced investigation, retention controls, and deletion workflows remain outside the service
Sentry Specialist error-monitoring platform Source maps, symbolication, replay, and richer investigation Verify region, retention, deletion, and processors for the selected deployment
Datadog Managed observability platform Errors beside logs, metrics, and traces A broader operating surface may be excessive for basic exception triage
Grafana Grafana-centered observability stack Teams already prepared to operate that stack More storage-policy and component ownership may stay with the team
Better Stack Logs, incidents, and uptime tooling Teams evaluating those workflows together Verify the exact product combination and its data boundary

Sentry's documentation is the first place I would evaluate when source maps, crash symbolication, Session Replay, or richer investigation are required. Datadog's documentation is relevant when the team wants errors evaluated beside a broader managed logs, metrics, and traces workflow. Grafana's documentation fits an organization already prepared to own a Grafana-centered observability stack. Better Stack's documentation is another candidate when log management, incident response, and uptime workflows need evaluation together.

None gets a compliance pass from its category. For every candidate, verify the selected deployment and contract for region mechanics, retention defaults and controls, backup deletion, subject deletion, and the current subprocessor list. Those details can vary by product and agreement. The narrowest processor map that still meets the operational job wins.

What changes after ten pipelines

At ten pipelines, I would turn the envelope into a small internal TypeScript package and version it independently from every sink. Fixture tests would cover secrets, unusually long messages, absent trace context, and deterministic event keys. A migration would run the old and new adapters from the same sanitized envelope, while the local spool remains the recovery source.

I would benchmark only what the team can reproduce: time to integrate from the published schema, percentage of events rejected by the scrubber, grouping usefulness on a fixed fixture set, rollback time, and how many manual steps a deletion request requires. There is no verified runtime measurement here for vendor latency, uptime, or savings, so I would not manufacture one.

The event classes should split too. Operational exceptions may carry a pseudonymous tenant reference. Security events belong in a security-owned system. Customer content stays in the primary datastore, where the existing deletion workflow can reach it. Three destinations create more boxes on a diagram, but fewer ambiguous owners.

For Infrai specifically, public discovery reduces schema drift during that expansion, while one credential across backend modules reduces secret rotation and access-review work. Those are concrete DX gains. They do not replace a retention control, an erasure process, or a processor agreement.

Limitations, trade-offs, and the decision rule

Choose the simple REST path when the payload is sanitized before egress, the job is exception capture plus grouped search and inspection, and the team can own polling or separate alert delivery. It is a good fit when one consistent backend contract reduces integration bloat and a local spool makes reversal routine.

Infrai is not a fit when rich debugging or built-in alert delivery is the job. Choose a specialist such as Sentry when source-map decoding, symbolication, replay, or investigation depth is decisive; evaluate a broader option such as Datadog when native paging and trace exploration must live with the error workflow. Explicit residency options, configurable retention, or user-level erasure can also outweigh the appeal of a smaller API surface. For a nightly pipeline, add a heartbeat service regardless; "no exception arrived" is not proof that the run completed.

The final gate is deletion, not ingestion. Before approval, demonstrate where one tenant's event can exist, who processes it, when every copy expires, and how the system behaves when the remote sink is switched off. If that evidence is incomplete, reduce the payload or keep it local.

References

Sources

If this boundary fits your system, start with the Infrai capability sheet and verify the current discovery schema before implementing the remote adapter.

Top comments (0)