Tenant-Aware Error Capture in NestJS: HTTP Filters, Cron Jobs, Queue Workers A developer detailed a tenant-aware error capture setup for NestJS backends, covering HTTP filters, cron jobs, and queue workers. The approach stamps tenant and experiment cohort onto events before they leave the Node.js process, enabling reliable comparison between variant and control groups. The article compares error tracking tools including Sentry, OpenTelemetry, Axiom, PostHog, and Infrai, advising selection based on on-call needs. An error-tracking setup for a NestJS backend has one hard problem, and it isn't which vendor you sign up with: HTTP exceptions arrive by the thousand, cron jobs and queue workers arrive by the handful, and if both land in the same bucket the noisy one wins every chart you draw. Use one capture layer with three entry points — a global exception filter for HTTP, a wrapper around each scheduled job, an explicit catch inside every queue worker — and stamp the tenant plus its experiment cohort onto the event before it leaves the Node.js process. That's the whole shape of it. The diagram in words: three producers a request, a tick, a message feed one capture function, which writes one event schema carrying tenant id , cohort and surface , which feeds one comparison you can actually trust. Everything below is about keeping that pipe honest when a support SaaS is running an experiment — say, AI-drafted first replies for half the tenants — and someone asks whether the variant cohort is breaking more often than the control. | Option | How the app talks to it | Cohort tagging | The catch | |---|---|---|---| | Sentry | official NestJS SDK, filter and interceptor helpers | tags, releases, scopes per event | you track SDK versions against your Nest and Node.js versions | | OpenTelemetry + Grafana Loki | OTel SDK, span events plus structured logs | resource and span attributes | you assemble collector, storage and dashboards yourself | | Axiom | ingest API, or a pino/winston transport | any structured field you send | log-shaped, so exception grouping is a query you write | | PostHog | product analytics SDK with exception capture | person and group properties | cohort math is strong, stack-level triage is thinner | | Infrai | one plain REST call, no SDK to install | whatever tags you put on the event | no heartbeat monitoring, so pair it with Healthchecks-style pings | Sentry is the default for a reason. If your team's daily question is "what changed in this release and which line threw," the SDK, the breadcrumbs and the release tracking do more than a REST call ever will. The trade-off is that you now own a client library inside every runtime — the Nest app, the worker, the Lambda you forgot about. OpenTelemetry plus Loki or Tempo makes sense when errors are one signal among several and you already run the collector. It is the most portable option on this list. It is also the most assembly required, and exception triage is not what it optimizes for. Axiom fits teams who already think in log lines and want cheap high-cardinality search over them. PostHog is worth a look precisely because of the cohort angle: it already knows what a group is, so "variant versus control" is a native question rather than a tag you invent. Deep stack triage is where it thins out. Infrai takes the opposite bet from Sentry: error capture is one plain REST API, so there's no SDK to install and no client version to pin — the same fetch call works from a Nest controller, a cron tick, and a five-year-old worker image. One key covers Infrai's error capture alongside its logs and metrics, so the cohort join stays inside a single query surface instead of three billing relationships. Its discovery endpoint is public and needs no key, which means you can read the request schema for the capture route before you write a line of code. Pick by what your on-call actually does at 3am. Line-level triage, stack frames, release diffing: Sentry. Multi-signal correlation you already operate: OpenTelemetry. One backend contract across HTTP, cron and queue with no SDK matrix to babysit: a REST capture API. Three surfaces, three capture policies. Same event schema. For HTTP, capture on the response boundary, not on every throw. A NotFoundException or a validation error is your API doing its job; a 5xx is an incident. If you report both at the same severity, one tenant with a broken integration will out-shout the entire experiment. Record the route pattern not the raw URL — it carries ticket IDs , the status class, the tenant, the cohort and the request ID. For a cron job, the unit is one scheduled execution, not one thrown value. Record the schedule name, intended run time, attempt number and a stable run ID. Otherwise a job that retried twice looks like three separate defects on Tuesday morning. For a queue worker, the unit is one delivery attempt. Standard queues are at-least-once, so the same message can be processed twice while the system is perfectly healthy. Carry the message ID, the attempt count and an idempotency key, then let the consumer's side effects be idempotent — and report a duplicate delivery as context on the event rather than as two independent business errors. This is the part people skip, and it's the part that quietly ruins cohort comparison: if the variant arm retries more because its work is slower, a naive count shows the variant "erroring more" when it is in fact the same failure counted more times. One module, one function, no per-surface special cases beyond the tags. Explicit method, bearer key from the environment, an idempotency key so a retried capture doesn't double-post, a bounded 429 backoff that honours Retry-After , and a real status check on the way out. js // capture.ts const API ORIGIN = process.env.ERRORS API ORIGIN .replace /\/+$/, "" ; const API KEY = process.env.INFRAI API KEY ; // ifr ... , never inline const CAPTURE PATH = "/v1/errors/capture"; export type Surface = "http" | "cron" | "queue"; export interface CaptureInput { surface: Surface; error: unknown; tenantId: string; cohort: "control" | "variant"; eventKey: string; // stable per attempt context?: Record