DEV Community

EvanShepherd8274
EvanShepherd8274

Posted on

Marketplace SaaS Error Alerting API Poll Events Explained Through Cost Attribution

For a Node.js SaaS, choose an error alerting API design that can poll persisted events when reconstructing a customer incident matters more than immediate notification. Use direct webhooks only when every qualifying failure can be acted on without joining it to marketplace context. The deciding factor is evidence: can an operator query the failed attempt, tenant, operation, retry history, and notification outcome after the Slack or email alert has scrolled away?

TL;DR: for a small marketplace, record structured failure events once, retain them under a deliberate policy, and let a scheduled worker query new events before sending Slack or email through generic adapters. Attribute storage, query, and delivery activity to a tenant or workload. This costs more engineering time than calling a webhook inside a catch block, but it preserves the evidence needed to explain a customer-visible failure.

Choice Incident evidence Cost attribution Operational burden Best fit
Notify inside the request Weak unless separately persisted Usually coarse Low at first Low-risk, low-volume jobs
Persist, then poll Strong and replayable Natural per tenant or workload Moderate Marketplace incident reconstruction
Persist, then push Strong if delivery is durable Natural per tenant or workload Highest Tight alert-latency requirements

Recommendation: start with persist-then-poll. It separates evidence capture from notification delivery, gives the alert worker a retry boundary, and keeps the first version understandable enough to operate while shipping weekly.

What evidence will answer the customer tomorrow?

An alert is a prompt to investigate. It is not the investigation record. A Slack message saying listing import failed cannot establish which merchant was affected, which attempt ran, whether a retry succeeded, or whether the same failure crossed multiple operations. Email has the same limitation. Both are delivery surfaces, not source-of-truth storage.

The durable event should carry the smallest useful reconstruction envelope: an event identifier, occurrence time, tenant identifier, operation identifier, failure class, severity, and attributes that are safe to retain. For a marketplace, that might describe a seller catalog sync or payout calculation. Do not copy arbitrary request bodies into the event. Credentials and customer data create a much more expensive retention problem than the alerting feature was meant to solve.

OpenTelemetry's logs data model is a useful neutral reference because it defines fields such as Timestamp, ObservedTimestamp, SeverityText, Body, Resource, and Attributes. It also explains how logs can carry trace context. That common shape makes later export or correlation possible without making a notification channel the owner of the evidence.

Time fields deserve care. The occurrence time says when the failure happened; the observed time says when the collector or application observed it. A delayed worker can make those differ. Keep both when the distinction exists, because an incident timeline built only from ingestion order can be wrong.

Keep the payload bounded. Store stable identifiers and a sanitized error summary, then retain domain records in their proper system. The event points to the marketplace operation; it should not become a shadow database.

Evidence first.

Cost attribution is a schema decision

For a one-person SaaS, total spend is less actionable than spend per unit of business activity. An event model needs an attribution key from day one. tenantId is often enough for a first pass, while workload separates catalog imports from checkout or payout jobs. With those two dimensions, storage growth and alert volume can be grouped around work that produces revenue or support load.

Do not put high-cardinality dimensions into every metric label merely because they are available. Preserve identifiers in the event record, then aggregate intentionally for dashboards. The event query path can answer a narrow incident question; metrics can answer fleet-level rate questions. They have different jobs.

Retention also needs tiers. Recent failures may remain quickly queryable while older evidence moves to cheaper storage or expires according to the business's support and compliance needs. No universal number is defensible here. Pick the window from the longest realistic interval between a customer-visible action and a support investigation, then test retrieval before trusting it.

This is where the revenue-per-hour lens helps. Instrument the few marketplace workflows whose failure can create refunds, delayed fulfillment, or manual support first. Shipping exhaustive telemetry for internal housekeeping jobs can wait. The undifferentiated delivery plumbing belongs behind a small interface so it can be outsourced or replaced without rewriting evidence capture.

Can a Node.js SaaS poll an error alerting API safely?

Yes, but a timestamp-only cursor can miss or duplicate work. Two events can share a timestamp, clocks can disagree, and a worker can crash after sending a notification but before saving its checkpoint. A useful poll order is a stable pair such as (occurredAt, id), with the query expressed as strictly after the last committed pair. This is an explicit trade: the two-field cursor makes every query slightly more involved, but it removes ambiguity at equal timestamps and gives replay a deterministic order. I would accept that small schema cost before adding another delivery channel because it protects the evidence path rather than decorating the notification path.

Delivery must assume duplicates. Use the event ID as an idempotency key, record delivery state, and commit the cursor only after the chosen handling policy completes. If the process dies between the external send and the local commit, the event may be delivered again. The receiving adapter or delivery ledger should make that retry harmless. Exactly-once claims at this boundary usually hide the real failure window.

Silence lies.

A poller that returns no failures may mean the marketplace is healthy, or it may mean collection stopped. Emit a heartbeat or track the age of the newest observed event. Alert on stale ingestion separately from application failures. Otherwise, the quietest dashboard can represent the worst outage.

A cron schedule is adequate when notification latency can be measured in minutes and overlapping runs are prevented. If the worker can run longer than its interval, use a lease or another single-run mechanism. Bound each query, continue through pages, and place poison events in a reviewable dead-letter state after a finite retry policy. One malformed record should not hold the cursor forever.

A small TypeScript boundary

The implementation needs fewer concepts than most alerting SDKs suggest. Keep persistence, querying, checkpointing, and notification as separate interfaces. The following worker deliberately omits database and channel-specific clients; those are adapters, not the architecture.

type Cursor = { occurredAt: string; id: string };

type FailureEvent = {
  id: string;
  occurredAt: string;
  observedAt: string;
  tenantId: string;
  workload: "catalog-sync" | "checkout" | "payout";
  severity: "error" | "fatal";
  operationId: string;
  summary: string;
};

interface EventStore {
  after(cursor: Cursor | null, limit: number): Promise<FailureEvent[]>;
}

interface Checkpoints {
  load(consumer: string): Promise<Cursor | null>;
  commit(consumer: string, cursor: Cursor): Promise<void>;
}

interface Notifier {
  send(event: FailureEvent, idempotencyKey: string): Promise<void>;
}

async function pollFailures(
  store: EventStore,
  checkpoints: Checkpoints,
  notifier: Notifier,
): Promise<number> {
  const consumer = "marketplace-incident-alerts";
  const cursor = await checkpoints.load(consumer);
  const events = await store.after(cursor, 100);

  for (const event of events) {
    await notifier.send(event, event.id);
    await checkpoints.commit(consumer, {
      occurredAt: event.occurredAt,
      id: event.id,
    });
  }

  return events.length;
}
Enter fullscreen mode Exit fullscreen mode

The query must sort ascending by the same two fields used by the cursor. The store should enforce unique event IDs. Validate timestamps and required attribution fields at ingestion, before a malformed event reaches this loop. Those constraints are part of the system, even though the storage adapter is intentionally absent from the example.

The boundary stays small on purpose.

Test three crash boundaries: before send, after send, and after checkpoint commit. Then verify that no event disappears and repeated delivery remains safe. Also test two events with the same occurrence time, a page containing exactly 100 events, and an event whose notifier rejects permanently. These cases are more valuable than a happy-path test that proves an HTTP call was made. They expose the awkward parts early, while the worker is still one loop and the cost of changing its contract is measured in an hour rather than a release cycle.

When is push the better runner-up?

Use durable push when minutes of polling delay would materially increase customer harm, or when event volume makes frequent empty queries wasteful. The same evidence store should remain authoritative. A queue or webhook dispatcher then wakes the consumer, while replay still comes from persisted events.

Push adds ownership: retry schedules, dead-letter handling, signature verification for webhooks, backpressure, and delivery observability. That can be the correct trade for payment authorization or inventory reservation failures. It is harder to justify for a weekly back-office import whose operator will respond during working hours.

Direct notification from the failing request is the smaller runner-up. It fits low-risk tasks where losing an occasional message would not block reconstruction because another authoritative business record already exists. Keep the network call off the critical response path, cap retries, and treat a failed notification as its own observable outcome.

Keep it boring.

The practical choice is based on recovery evidence and response time, not a feature checklist. Persist-then-poll is a sound baseline for marketplace failures because it makes customer incident reconstruction testable and makes event cost attributable. Move to durable push when measured response requirements demand it. Keep notification channels replaceable either way.

References

Top comments (0)