feat(observability): report silent catalog UX degradations to Sentry

The catalog failures that hurt UX — a drill/parts fetch that fails into an empty
"couldn't load" panel, or a decoded vehicle whose category tree comes back empty
("model var ama parça yok") — all return HTTP 200 with a degraded body. Nothing
throws, so the global Sentry exception filter never sees them and they go
unnoticed (serkan's complaint was exactly this class). Report them explicitly.

- new common/catalog-degradation.ts: reportCatalogDegradation(kind, ctx),
  fingerprinted by kind+source+brand so each failure mode collapses into one
  countable Sentry issue (e.g. "drill-load-error · pl24/Ford — N events, M users").
- categories.service: capture on getCategoryWithParts loadError and on an empty
  getCategoryTree, Redis-deduped to <=1 event/hour per category/vehicle so a
  broken catalog can't flood the stream; telemetry never throws into the request.

tsc + biome clean, categories suite 10/10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 17:41:51 +03:00
parent 5903151692
commit e8e9771759
3 changed files with 128 additions and 0 deletions

View File

@@ -0,0 +1,58 @@
import * as Sentry from "@sentry/nestjs";
/**
* A *silent* catalog UX failure: the request succeeds (HTTP 200) but the user
* gets a degraded result — an empty parts panel / retryable load error, or an
* empty category tree on a vehicle that decoded fine. Because nothing throws,
* the global exception filter never sees these, so they go unnoticed — exactly
* how serkan's "şase girdim, model var ama parça yok" sat invisible. We report
* them to Sentry explicitly so they surface and get triaged like real errors.
*/
export type CatalogDegradationKind =
| "empty-tree" // vehicle decoded but the category tree came back empty
| "drill-load-error"; // a category drill/parts fetch failed → empty/retry panel, not parts
export interface CatalogDegradationContext {
vehicleId?: string | null;
vin?: string | null;
brand?: string | null;
model?: string | null;
source?: string | null;
categoryId?: string | null;
categoryName?: string | null;
linkPath?: string | null;
}
/**
* Send a catalog degradation to Sentry as a warning. Fingerprinted by
* kind + source + brand so every instance of one failure mode collapses into a
* single, countable Sentry issue ("drill-load-error · pl24/Ford — 1.2k events,
* 80 users") rather than thousands of unique events. Call sites must dedup
* (e.g. a short Redis TTL per vehicle/category) before invoking this, and must
* never let it throw into the request path.
*/
export function reportCatalogDegradation(
kind: CatalogDegradationKind,
ctx: CatalogDegradationContext,
): void {
const source = ctx.source ?? "unknown";
const brand = ctx.brand ?? "unknown";
Sentry.captureMessage(`catalog degraded: ${kind} (${source}/${brand})`, {
level: "warning",
tags: {
catalog_degradation: kind,
catalog_source: source,
catalog_brand: brand,
},
// Group by failure mode, not by individual vehicle/category.
fingerprint: ["catalog-degradation", kind, source, brand.toLowerCase()],
extra: {
vehicleId: ctx.vehicleId ?? null,
vin: ctx.vin ?? null,
model: ctx.model ?? null,
categoryId: ctx.categoryId ?? null,
categoryName: ctx.categoryName ?? null,
linkPath: ctx.linkPath ?? null,
},
});
}