@dudousxd/nestjs-telescope
Version:
Laravel Telescope-style observability console for NestJS — core: watchers, recorder, correlation, SQLite store, headless API.
65 lines • 3.1 kB
JavaScript
// packages/core/src/alerts/new-exception-tracker.ts
/**
* Default cap on tracked error families. The map is the ONLY state the
* `new-exception` rule keeps, and it must stay bounded so a host that throws a
* long tail of unique error families (e.g. message-embedded ids that defeat the
* family hash) can never grow it without limit. At the cap we evict the OLDEST
* inserted family — the one least likely to still be "recently seen" — which is
* the correct bias for a recency-window dedup.
*/
const DEFAULT_MAX_FAMILIES = 10_000;
/**
* Bounded, in-memory "have we seen this error family recently?" tracker backing
* the `new-exception` rule. Per-replica by design: it lives in process memory, so
* in a multi-replica deployment the SAME family may be reported as new once per
* pod (documented caveat — acceptable for v1; a cross-pod store would defeat the
* "cheap map lookup on the hot flush path" goal).
*
* Eviction uses a `Map`'s insertion-order iteration: the first key is the oldest
* INSERTED entry. We intentionally do NOT re-order on update (that would make it
* an LRU keyed on last-seen and complicate the cheap path); the window check
* already discards stale last-seen times, so insertion-order eviction is a sound,
* O(1) cap with no per-touch bookkeeping.
*/
export class NewExceptionTracker {
seen = new Map();
maxFamilies;
constructor(maxFamilies = DEFAULT_MAX_FAMILIES) {
this.maxFamilies = maxFamilies;
}
/**
* Record an observation of `familyHash` at `nowMs` and report whether this is a
* NEW family for the `windowMs` window — i.e. it was either never seen or last
* seen longer ago than the window. Always updates the stored last-seen time so
* repeat occurrences refresh the window. Returns `true` only on a genuine
* first/expired occurrence (the signal that should fire the alert).
*/
observe(familyHash, nowMs, windowMs) {
const lastSeen = this.seen.get(familyHash);
const isNew = lastSeen === undefined || nowMs - lastSeen >= windowMs;
// Delete-then-set so a refreshed family moves to the END of insertion order,
// keeping genuinely-oldest families at the front for eviction. (This is the
// one place re-ordering helps: it stops a still-active family being evicted
// ahead of a truly idle one.)
this.seen.delete(familyHash);
this.seen.set(familyHash, nowMs);
this.evictIfNeeded();
return isNew;
}
/** Number of tracked families (test/observability seam). */
get size() {
return this.seen.size;
}
/** Evict the oldest entries until the map is within its cap. */
evictIfNeeded() {
while (this.seen.size > this.maxFamilies) {
// `Map` iterates in insertion order; the first key is the oldest.
const oldest = this.seen.keys().next().value;
if (oldest === undefined)
break;
this.seen.delete(oldest);
}
}
}
export { DEFAULT_MAX_FAMILIES };
//# sourceMappingURL=new-exception-tracker.js.map