DEV Community

SiegfriedFletcher5869
SiegfriedFletcher5869

Posted on

Error Grouping API Comparison: Search and Resolution for Small B2B SaaS Servers

Short answer: choose the error API whose grouping, search, event-detail, and resolution behaviors you can verify with a small, repeatable probe; the winning constraint is evidence retrieval, not the longest feature list.

For a small B2B SaaS, server-error tracking is an indexing and handoff problem. Before the tracker, an exception is a line in an event stream and an on-call engineer searches raw text. After the tracker, an immutable event keeps the evidence, a fingerprint places related events in a group, search finds the work item, and resolution records a human decision. Logs still matter. The Twelve-Factor App describes them as event streams, so an error tracker should sit beside that stream rather than replace it.

Tiny contract.

How should a small B2B SaaS test server error search and event detail?

Start with six operations: ingest, group, search, inspect, resolve, and reopen. Ask every candidate to show the request and response for each operation, then run the calls against a disposable project. A useful mental diagram is: application -> event envelope -> fingerprint -> group -> operator state. Each arrow needs a documented result and an owner.

Use a comparison sheet like this:

Decision point Minimum useful behavior Acceptance check
Grouping Default rules plus an application-supplied fingerprint Send two events that should merge, then two that must split
Search Time, environment, release, service, and tenant-safe filters Find a known event without relying on its group ID
Event detail Stack, timestamp, release, request context, and redacted fields Confirm the original event remains readable after grouping
Resolution An API-visible state change with actor and time Resolve, send a recurrence, and record the documented result
US/EU path Storage, processing, backup, and support-access boundaries Trace one event through the written data flow
Export Pagination or documented bulk export Rebuild an incident timeline outside the UI

Weight the rows by your operation. Two people on call may value legible detail and fast search over dashboards. A regulated customer base may put residency and deletion first. I'm not sure which constraint will bite first; your mileage may vary. Write the weights down before the trial.

Grouping is a contract, not a screenshot

Grouping is lossy compression. It is useful only if engineers can recover the individual evidence. A fingerprint should describe the actionable cause, not every changing value in the message. Request IDs, timestamps, and generated URLs commonly split one defect into many groups. An over-broad fingerprint does the reverse: unrelated failures collapse together, and one resolution can hide fresh work.

Test both directions across a deploy. Line numbers and generated frames can move, so send equivalent exceptions from two releases. If the service accepts an application fingerprint, keep a small versioned function in your repository and exclude secrets and personal data. The same intent can then be emitted to different backends.

type ErrorContext = {
  service: string;
  operation: string;
  errorClass: string;
  schemaVersion: 1;
};

function fingerprint(context: ErrorContext): string {
  return [
    `v${context.schemaVersion}`,
    context.service,
    context.operation,
    context.errorClass,
  ].join(":");
}

const groupKey = fingerprint({
  service: "billing-worker",
  operation: "create-invoice",
  errorClass: "UpstreamTimeout",
  schemaVersion: 1,
});
Enter fullscreen mode Exit fullscreen mode

Keep the original exception class, stack, release, environment, trace ID, and request ID as event fields. Keep tenant identity pseudonymous. Grouping answers “which work item?” Detail answers “what happened this time?” Search must cover both.

I treat a successful 200 as transport evidence only. The workflow is unproven until a known event is searchable, its detail is readable, and its group state can be changed. A 429 should trigger bounded exponential backoff when the documented contract calls for it; it is not evidence that grouping worked.

A copyable acceptance probe

Save the result of a probe in CI or a test artifact. The paths below are intentionally generic; adapt them to each service's documented contract rather than assuming REST naming. The probe sends a synthetic server error, searches by a unique ID, reads immutable detail, and resolves its group.

type EventRecord = {
  id: string;
  groupId: string;
  probeId: string;
  status: "open" | "resolved";
};

const baseUrl = process.env.ERROR_API_BASE_URL;
if (!baseUrl) throw new Error("ERROR_API_BASE_URL is required");
const token = process.env.ERROR_API_TOKEN;
if (!token) throw new Error("ERROR_API_TOKEN is required");

async function api<T>(path: string, init?: RequestInit): Promise<T> {
  const response = await fetch(`${baseUrl}${path}`, {
    ...init,
    headers: {
      authorization: `Bearer ${token}`,
      "content-type": "application/json",
      ...init?.headers,
    },
  });
  if (!response.ok) throw new Error(`Error API returned ${response.status}`);
  return response.json() as Promise<T>;
}

const probeId = crypto.randomUUID();
const idempotencyKey = `probe-${probeId}`;
await api<{ accepted: boolean }>("/events", {
  method: "POST",
  headers: { "idempotency-key": idempotencyKey },
  body: JSON.stringify({
    probeId,
    environment: "acceptance",
    service: "billing-worker",
    release: "probe-1",
    fingerprint: "v1:billing-worker:create-invoice:SyntheticError",
    errorClass: "SyntheticError",
    message: "Acceptance probe",
  }),
});

const result = await api<{ events: EventRecord[] }>(
  `/events/search?probeId=${encodeURIComponent(probeId)}`,
);
const event = result.events.at(0);
if (!event) throw new Error("Probe event was not searchable");

const detail = await api<EventRecord>(`/events/${event.id}`);
await api<{ status: "resolved" }>(`/groups/${detail.groupId}/resolve`, {
  method: "POST",
  headers: { "idempotency-key": `resolve-${detail.groupId}-${probeId}` },
  body: JSON.stringify({ reason: "acceptance-probe" }),
});
Enter fullscreen mode Exit fullscreen mode

Run it against a disposable project on a schedule. Redact before transmission and assert that forbidden fields never arrive. Measure the interval between accepted ingestion and searchable detail; do not assume those operations are synchronous. If a write returns 429, retry with bounded exponential backoff and reuse the same idempotency key when the documented API supports one. The catch is coverage: this thin probe does not test source maps, framework hooks, mobile crashes, or session replay. Evaluate those separately if they are requirements.

Can logs, alerts, and regional controls replace an error workflow?

“EU available” is not a complete data-flow statement. Draw the path for each tenant: application region, ingestion host, processing region, primary storage, backups, support access, and deletion. Then list which fields cross a boundary and which identifiers appear in alerts or tickets. Separate regional projects can make a residency commitment easier to explain, but they duplicate configuration and add cross-region operational work. One global project simplifies a queue and search, yet may not fit a strict contractual boundary. Stick with regional isolation when the boundary matters more than one consolidated work list.

Use logs, metrics, and traces together. Logs preserve the broad event stream, metrics show rates and service health, and traces connect work across boundaries. An exception workflow adds grouping and ownership state. A practical handoff is: an alert detects an abnormal rate; a trace or request ID locates the path; the event keeps stack and release evidence; the group records triage. Sending every log line into the exception queue creates noise, while sending only exceptions loses context. Link the systems with stable IDs.

Portability depends on what your application owns. Keep the event envelope, fingerprint function, redaction policy, release identifier, and tenant-safe tags in your codebase. Saved searches, dashboards, automation, and historical group IDs are usually backend-specific. Export a sample during evaluation and verify that timestamps, pagination, event-to-group relationships, and resolution history remain understandable outside the product.

Define exit criteria before the trial: merge and split cases pass; a known event is searchable; detail preserves evidence; resolution and recurrence follow written semantics; redaction works; the regional path is acceptable; and export is usable. Select against those results. Skip the leaderboard.

References

Top comments (0)