@dudousxd/nestjs-telescope
Version:
Laravel Telescope-style observability console for NestJS — core: watchers, recorder, correlation, SQLite store, headless API.
81 lines • 3.66 kB
JavaScript
// 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