UNPKG

@dudousxd/nestjs-telescope

Version:

Laravel Telescope-style observability console for NestJS — core: watchers, recorder, correlation, SQLite store, headless API.

81 lines 3.66 kB
// packages/core/src/tagging/tagger.ts import { EntryType } from '../entry/entry.js'; /** Default threshold (ms) above which an entry is tagged 'slow'. */ export const SLOW_THRESHOLD_MS = 1000; /** Tags request entries with their HTTP status (`status:NNN`). Type-gated to * request entries so a non-request content type that happens to carry a * `statusCode` field is never mistaken for an HTTP response. */ export const statusTagger = (entry) => { if (entry.type !== EntryType.Request) return []; const status = entry.content.statusCode; return typeof status === 'number' ? [`status:${status}`] : []; }; export const slowTagger = (entry) => entry.durationMs !== null && entry.durationMs >= SLOW_THRESHOLD_MS ? ['slow'] : []; /** True for a non-null object value, narrowing to a string-keyed record so its * properties can be read without `as`. */ function isRecord(value) { return typeof value === 'object' && value !== null; } /** A usable identity token (`string`/`number`) coerced to its string form, or * null. Empty strings and non-finite numbers are treated as "no identity". */ function identityToken(value) { if (typeof value === 'string') return value.length > 0 ? value : null; if (typeof value === 'number') return Number.isFinite(value) ? String(value) : null; return null; } /** * The `user:<id>` tag for a raw user value, or `null` when no usable identity is * present. Prefers `id`, then `_id`, then `email` — so Passport (`id`), Mongo * (`_id`), and email-keyed identities all pivot to the SAME `user:` namespace * the dashboard's User filter reads. Shared by `userTagger` (request entries) * and the client-error controller so a browser-reported error tags identically * to a server request. Cheap property reads only; never throws. */ export function userIdentityTag(user) { if (!isRecord(user)) return null; const id = identityToken(user.id) ?? identityToken(user._id) ?? identityToken(user.email); return id === null ? null : `user:${id}`; } /** Tags request entries with the authenticated user's identity (`user:<id>`). * Reads `content.user` via {@link userIdentityTag} (preferring `id`/`_id`/ * `email`). Type-gated to request entries and built from cheap property reads * only (hot path); any non-object content or missing user yields no tag and * never throws. */ export const userTagger = (entry) => { if (entry.type !== EntryType.Request) return []; if (!isRecord(entry.content)) return []; const tag = userIdentityTag(entry.content.user); return tag === null ? [] : [tag]; }; export const BUILTIN_TAGGERS = [statusTagger, slowTagger, userTagger]; /** Returns the entry's existing tags plus all tagger outputs, de-duplicated, order-preserving. */ export function runTaggers(entry, taggers) { // Fast path: with no taggers there is nothing to merge, so skip the Set + // result array + closure allocation. entry.tags can still contain dups // (the recorder merges context+input tags without de-duping), so preserve // the de-dup contract — but a 0/1-element list cannot contain dups. if (taggers.length === 0) return entry.tags.length <= 1 ? entry.tags : [...new Set(entry.tags)]; const seen = new Set(); const result = []; const add = (tag) => { if (!seen.has(tag)) { seen.add(tag); result.push(tag); } }; for (const tag of entry.tags) add(tag); for (const tagger of taggers) { for (const tag of tagger(entry)) add(tag); } return result; } //# sourceMappingURL=tagger.js.map