Custom tags & redaction
Tag entries by tenant from a captured header, mask extra fields with redact.keys/paths and a custom mask, drop noisy entries with filter, and sample high-volume types.
Four small hooks shape what gets captured and how it's labeled: taggers add searchable labels, redact masks sensitive leaves, filter drops entries you never want, and sampling keeps a fraction of high-volume types. All are forRoot options; this recipe wires each against its real signature.
Custom taggers — tag by tenant
A Tagger is (entry: Entry) => string[]. It runs after capture, so it reads the entry's content and returns extra tags (de-duplicated and merged with whatever's already there). To tag a request with the tenant from a header:
import { type Tagger, EntryType } from '@dudousxd/nestjs-telescope';
const tenantTagger: Tagger = (entry) => {
// Type-gate so a non-request entry that happens to carry `headers` is never
// mistaken for an HTTP request.
if (entry.type !== EntryType.Request) return [];
const headers = (entry.content as { headers?: Record<string, string> }).headers;
const tenant = headers?.['x-tenant-id'];
return typeof tenant === 'string' ? [`tenant:${tenant}`] : [];
};
TelescopeModule.forRoot({
taggers: [tenantTagger],
});Now every request carries a tenant:acme tag, and the dashboard can filter the whole capture to one tenant. Your taggers run in addition to the built-ins (status:NNN, slow) — passing taggers doesn't replace them.
How it works
- A tagger sees the finished
Entry, not a request object — so it reads fromentry.content. The request entry's content is{ method, uri, headers, payload, user, ip, statusCode }. - Return
[]for entries you don't tag. Always type-gate (entry.type !== EntryType.Request) before reaching intocontent. - Tags are merged and de-duplicated, order-preserving — you can't accidentally double-add a tag the built-ins already set.
Taggers read content after redaction has run, so don't rely on a value a redact key would have masked. Tag on a header like x-tenant-id (not secret), not on authorization.
Extra redaction — keys, paths, and a custom mask
Capture always redacts a built-in set of sensitive keys (authorization, cookie, password, token, ...). Add your own with redact:
TelescopeModule.forRoot({
redact: {
// Extra key names masked at ANY depth (case-insensitive), merged with
// the built-in DEFAULT_REDACT_KEYS.
keys: ['ssn', 'creditCard', 'x-internal-token'],
// Exact dot-paths from the root of the captured value — masks a field
// REGARDLESS of its key name. Use when a non-sensitive key holds sensitive
// data only in one place.
paths: ['payload.billing.iban', 'user.email'],
// Replacement string. Defaults to '[REDACTED]'.
mask: '***',
},
});How it works
keysmatch a field name anywhere in the tree —ssnmasksbody.ssnandnested.deep.ssnalike. They're merged with the built-in list, never replace it.pathsmatch the full traversal location from the root of the captured content —payload.billing.ibanmasks only that exact spot, leaving anibanelsewhere untouched. Use this to mask an innocuously-named field that's only sensitive in one context.- Redaction is a deep clone — it never mutates the captured value, and it's cycle-safe (a genuine cycle becomes
[Circular]).
Drop noisy entries — filter
filter is a predicate run per entry: return true to keep, false to drop. Use it to silence health-check spam or a chatty internal route before it ever hits storage:
import { type Entry, EntryType } from '@dudousxd/nestjs-telescope';
TelescopeModule.forRoot({
filter: (entry: Entry): boolean => {
if (entry.type === EntryType.Request) {
const uri = (entry.content as { uri?: string }).uri ?? '';
// Drop k8s probes and the metrics scrape — they'd bury real traffic.
if (uri.startsWith('/healthz') || uri.startsWith('/metrics')) return false;
}
return true; // keep everything else
},
});filter runs on the finished entry, so you can branch on type, tags, durationMs, or content. It's the blunt instrument — for "keep some, drop some" of a high-volume type, reach for sampling instead.
Sample high-volume types
sampling is a per-type keep rate (0–1). A bare number applies to every type; an object sets per-type rates with a default fallback:
TelescopeModule.forRoot({
// Keep all exceptions and requests, but only 10% of queries and 5% of cache ops.
sampling: {
query: 0.1,
cache: 0.05,
default: 1, // everything else captured in full
},
});
// Or a single rate for everything:
TelescopeModule.forRoot({ sampling: 0.25 });How it works
- A bare number is normalized to
{ default: <rate> }— it applies to every type without a specific override. - Sampling is the right tool for volume (keep a representative slice of queries);
filteris for noise (drop probes entirely). Reach for whichever matches intent. - Keep exceptions and requests at
1— they're low-volume and high-signal; you almost never want to sample them out.
Level-aware tail sampling — keep the errors
A per-type rate can also be an object { rate, keepErrors }. With keepErrors: true, an entry that is an error is always kept — the rate only applies to the rest. For log, "error" means level warn / error / fatal, so you can sample chatty info/debug logs hard and never drop a warning:
TelescopeModule.forRoot({
sampling: {
// Keep 10% of logs, but every warn/error/fatal line survives the cut.
log: { rate: 0.1, keepErrors: true },
},
});This is tail sampling: the keep decision reads the finished entry's level/tags, so the signal you actually care about is never the thing that gets thrown away.
Dashboard login & sessions
Practical dashboardAuth — login mode validating against your own user table with bcrypt, session mode bridging an existing JWT, and role-gating queue mutations via request.telescopeSession.
Request context for your routes
Capture the authenticated user on each request with resolveUser, keep capture working under setGlobalPrefix via telescopeRequestCapture, and drop ad-hoc debug dumps with telescopeDump.