Timeline and queries
What each request did, on a timeline: database queries, outgoing calls, console lines and your own measures, with slow queries and N+1 loops flagged.
Every request gets a timeline of what happened while it was handled: database queries, outgoing HTTP calls, console lines, exceptions and your own measures. The request list shows the counters (queries, slow queries, N+1 statements, outgoing calls), so "this 940 ms request ran 31 queries and called Stripe twice" is one look away.
What is recorded#
| What | Libraries |
|---|---|
| PostgreSQL | pg 8 (also pg-pool, pg-cursor, pg-query-stream) |
| MySQL / MariaDB | mysql2 3, callbacks, mysql2/promise and streams |
| MongoDB | the mongodb driver 5 and 6, so Mongoose 7 and 8 |
| Prisma | Prisma 7 (driver adapters): the adapter's pg or mysql2 queries, labeled with the Prisma call that ran them. Prisma 5 and 6: one span per call, such as User.findMany({ where: { id: ? } }) (with a driver adapter too: Prisma 6 runs it out of reach of the request) |
| ORMs and query builders | Knex, Sequelize, TypeORM, Drizzle (node-postgres), Kysely: they run on pg or mysql2, so their queries are recorded with nothing to set up |
| Outgoing HTTP | node:http / node:https (and clients built on them: axios, got, node-fetch...) and fetch |
| Logs | console.log, info, warn, error, debug and trace |
Drivers are found in your project's node_modules and instrumented when PeekInside starts: there is nothing to add to your code. Each query is recorded with its text, duration, rows and error, and the values sent with it.
Statements, slow queries and N+1#
Queries are grouped into statements (the same SQL with different values is one statement), and each statement shows where your code ran it (file and line), how often, how long it took in total and at worst. Two patterns are flagged:
- Slow queries: queries that took
slowQueryMs(default 100 ms) or longer. - N+1: a statement that ran
nPlusOneThreshold(default 5) times or more in one request, the classic loop that runs one query per row. Executions that repeat an earlier one exactly (same statement, same values) are counted as duplicates.
PeekInside.attach(app, { slowQueryMs: 250, nPlusOneThreshold: 10 });
Masking#
Values are masked when they are captured, so masked values never leave your process:
- A value bound to a column whose name looks sensitive (password, secret, token, api key, session, authorization, cookie, credential, private key, CVV, SSN, card number) is replaced by
[masked]. The column is read from the statement:password = $1,insert into users (email, password) values (?, ?),set password = ?, quoted and table-prefixed names included. String literals compared to such columns are masked in the statement text too. - For MongoDB and Prisma, any key with such a name is masked, at any depth.
- Strings that look like secrets are masked whatever their name: JWTs and password hashes (bcrypt, argon2, scrypt).
- Long strings are cut at 256 characters, binary data and large objects are described (
<binary 32 bytes>,<json 3.2 KB>). - Outgoing URLs lose their credentials, and sensitive query parameters are masked.
Set queryParams: false to keep no values at all.
Measuring your own code#
const pdf = await peekInside.measure('render invoice', () => renderPdf(order));
// or, without the instance at hand:
const rates = await PeekInside.measure('load exchange rates', loadRates);
measure(name, fn) runs fn (sync or async) and adds a span with its duration to the request's timeline. It returns what fn returns (for an async fn, a promise that settles the same way), and when fn throws or rejects, the error is noted on the span and passed on unchanged. Outside a request it just runs fn.
PeekInside.requestId() returns the id of the request the calling code runs for (or null), the same id the dashboard shows: put it in your own logs to go from a log line to the request.
logger.info({ requestId: PeekInside.requestId() }, 'Charging card');
Bundled apps and monorepos#
Automatic instrumentation finds drivers from your project folder. When it can't (an app bundled with esbuild or webpack, a monorepo package with its own copy of a driver), pass them to instrument():
import pg from 'pg';
import mongoose from 'mongoose';
import { PrismaClient } from '@prisma/client';
PeekInside.attach(app).instrument(pg, mongoose, PrismaClient);
It takes pg, mysql2, mysql2/promise, mongodb, mongoose, a PrismaClient class or instance, and the @prisma/client module; anything else is ignored.
Limits#
- Only what runs while a request is handled is recorded, until its response is sent: startup work and timers that weren't started by a request have no timeline.
- Per request, at most 500 query spans, 100 outgoing calls, 200 log lines, 100 measures and 10 exceptions are listed, and 200 distinct statements; the counters count everything. Values are kept for the first 20 executions of each statement. The timeline of one request stays under 512 KB.
- Outgoing calls are seen through the diagnostics channels Node.js publishes for its HTTP client and for
fetch; on a Node.js version that doesn't publish them, those calls aren't recorded. - With the MongoDB driver 5 (callbacks), and for Prisma's raw queries with the query engine, the line that ran a statement can't be found and isn't shown.
- Recording costs about 8 µs per query on a local PostgreSQL (about 40 µs per query without PeekInside); outside requests, the instrumentation does nothing measurable.