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.
Two host-specific wrinkles and one debugging tool: tell Telescope how to read your authenticated user, register request capture correctly when your app uses a global prefix, and drop ad-hoc values into the dashboard from anywhere with telescopeDump.
Record the authenticated user — resolveUser
By default each captured request entry reads request.user (the Passport/guard convention) for its user field. If your app attaches the user somewhere else, point resolveUser at it:
TelescopeModule.forRoot({
// Read the user from wherever your guard puts it. Return null/undefined for
// anonymous. The resolved value is redacted by the Recorder like any content.
resolveUser: (request) => {
const session = (request as { session?: { user?: unknown } }).session;
return session?.user ?? null;
},
});How it works
resolveUser(request)receives the raw platform request (Express or Fastify). Return the user object, ornull/undefinedfor an anonymous request.- It never crashes capture — a hook that throws is treated as "no user" (
null), not an error. - The result is redacted by the recorder like any other content, so a
password/tokenfield on the user object is masked automatically. Don't pre-strip it.
resolveUser only sets the user field on the request entry. It does not gate the dashboard — that's dashboardAuth. The two are unrelated.
Find everything a user did
Once a request carries a user, the built-in userTagger tags that request entry user:<id> automatically — no configuration. It reads content.user, preferring id, then _id (Mongo), then email, so whatever your resolveUser returns is enough to identify the actor.
Because tags are indexed and filterable end-to-end, this turns into a one-click pivot in the dashboard: every user-tagged entry shows a user:<id> chip in the entries table, and the entry detail view offers "View all activity for this user". Clicking either lands you on the entries list pre-filtered to that tag (#/entries?tag=user:<id>) — every request that user made, across types, in one view.
The entries pages also carry a dedicated User combobox over the user: tag namespace these tags create: it lists the ids userTagger emitted (with per-user entry counts), and selecting one renders a User: <id> chip instead of a raw tag. The "View all activity for this user" pivot feeds that same control, so a user:<id> arriving via ?tag= shows up as a User chip. See the dashboard tour.
// The userTagger is in BUILTIN_TAGGERS — nothing to wire up. A request whose
// resolved user is { id: 42 } is tagged "user:42"; the dashboard does the rest.Capture under a global prefix — telescopeRequestCapture
If your app calls app.setGlobalPrefix('api'), NestJS scopes module middleware to the prefixed route table — so Telescope's auto-registered capture middleware only ever sees /, and request entries stop appearing. The fix is two steps: turn off the auto-registration and register capture globally in bootstrap.
// app.module.ts — opt out of the module-level middleware
import { TelescopeModule } from '@dudousxd/nestjs-telescope';
TelescopeModule.forRoot({
registerRequestMiddleware: false, // we register it globally in main.ts instead
});// main.ts — register the capture before listen()
import { NestFactory } from '@nestjs/core';
import {
TelescopeService,
telescopeRequestCapture,
} from '@dudousxd/nestjs-telescope';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api');
// Global capture sees EVERY route, prefix or not.
app.use(telescopeRequestCapture(app.get(TelescopeService)));
await app.listen(3000);
}
bootstrap();How it works
telescopeRequestCapture(service)returns a framework-agnostic(req, res, next)handler — the same capture logic the module would have registered, but mounted globally viaapp.use(...)so the global prefix can't scope it away.- It still skips Telescope's own routes (the dashboard, its API, its assets) to avoid self-capturing the dashboard's polling — so you can mount it unconditionally.
- Order doesn't matter much, but registering it before other global middleware means the request batch is open for the whole downstream execution, so everything a request causes correlates to it.
Only one capture path
Set registerRequestMiddleware: false whenever you call telescopeRequestCapture yourself — leaving the default true would register capture twice and open two batches per request. It's one or the other.
Ad-hoc debugging — telescopeDump
telescopeDump(value, label?) is console.log for the dashboard: it drops a value into the Dumps tab, correlated to the active request batch — so you can inspect exactly what a handler saw, in context, without a debugger.
import { telescopeDump } from '@dudousxd/nestjs-telescope';
@Injectable()
export class PricingService {
quote(input: QuoteInput): Quote {
const candidates = this.buildCandidates(input);
telescopeDump(candidates, 'pricing candidates'); // shows up in Dumps, in this request's batch
return this.pickBest(candidates);
}
}How it works
- No dependency injection at the call site.
telescopeDumpis a free function — import and call it anywhere, including in shared code. - It's a no-op until Telescope is wired (and after shutdown), so importing it in a library that's sometimes used outside a Telescope-enabled app never crashes.
- The dump correlates to the active batch. Called inside a request handler or job, the dump appears alongside that request's queries, cache ops, and exceptions — the same correlation view as everything else.
Dumps are a dev tool — they capture whatever you hand them (redacted like any content). Leave a few in hot paths during an investigation, then remove them; they're not meant to ship as permanent instrumentation (that's what watchers are for).
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.
Archiving exceptions to S3
Export exception entries to Amazon S3 before the pruner deletes them, using the archive.sink hook and @aws-sdk/client-s3 — host-side code; Telescope itself stays dependency-free.