Database spans
Trace MongoDB queries automatically with wrapCollection, or record SQL and NoSQL queries as database spans in Node.js.
MongoDB - wrapCollection
Wrap a MongoDB collection once and every operation is traced automatically. Import from @refactorlabs/recount/mongodb:
import { wrapCollection } from '@refactorlabs/recount/mongodb';
const usersCol = wrapCollection(db.collection('users'));Then use it exactly as you would a normal collection:
await recount.startTrace('Process application', async () => {
// db span recorded automatically - filter, collection, duration, found/not
const user = await usersCol.findOne({ accountId: req.accountId });
const results = await usersCol
.find({ status: 'active', plan: 'enterprise' })
.toArray();
});Each span captures the operation name, collection, filter/pipeline, and a result summary (found, count, insertedId, etc.). Document contents are never recorded - only metadata about the query result.
Naming spans
By default the span name is collection.operation (e.g. users.findOne). Wrap the call with recount.named() to give it a meaningful name:
const user = await recount.named('Load account', () =>
usersCol.findOne({ accountId: req.accountId })
);
// → db span named 'Load account' instead of 'users.findOne'Cursor iteration
find() and aggregate() return a wrapped cursor - however you drain it, one span is recorded once iteration finishes, with count set to however many documents were read. .toArray(), for await, .forEach(), and .next() are all supported:
// for await
for await (const user of usersCol.find({ status: 'active' })) {
process(user);
}
// .forEach()
await usersCol.find({ status: 'active' }).forEach((user) => process(user));
// .next()
const cursor = usersCol.find({ status: 'active' });
let user;
while ((user = await cursor.next()) !== null) {
process(user);
}close() (and Symbol.asyncDispose, so using works) finalizes the span too, for cases where you stop draining early. rewind() resets the count so a second drain records its own span. tryNext() is tracked but doesn't end the span on a null result, since tailable/awaitData cursors return null while still open - close() is what ends those.
Redacting filter values
If your filters contain PII, pass a redact option:
const customersCol = wrapCollection(db.collection('customers'), {
redact: ['name', 'billing.accountName', 'billing.accountNumber']
});Redact paths are applied to the span's input (the filter object). See Redaction for syntax.
Supported operations
findOne, find (cursor - see Cursor iteration), insertOne, insertMany, updateOne, updateMany, deleteOne, deleteMany, replaceOne, findOneAndUpdate, findOneAndDelete, aggregate (cursor - see Cursor iteration), countDocuments
Install
mongodb is a peer dependency - use the version already in your project:
npm install @refactorlabs/recount
# mongodb already in your projectManual spans
For SQL databases or when you need full control, use addSpan with type: 'db' directly.
SQL
recount.addSpan('db', 'Fetch users', {
metadata: { table: 'users', queryType: 'sql' },
input: { query: 'SELECT * FROM users WHERE active = $1', params: [true] },
output: { count: 42 },
durationMs: 12
});NoSQL
recount.addSpan('db', 'Fetch users', {
metadata: { collection: 'users', queryType: 'nosql' },
input: {
filter: { active: true },
projection: { name: 1, email: 1 },
sort: { createdAt: -1 }
},
output: { count: 42 },
durationMs: 8
});Metadata fields
| Field | Type | Description |
|---|---|---|
queryType | 'sql' | 'nosql' | Controls how the query is displayed in the dashboard |
table | string | Table name (SQL), shown in the span header |
collection | string | Collection name (NoSQL), shown in the span header |
Output convention
Pass summary stats (counts, affected rows, timings) rather than raw documents.
Types
import type { DbSpanOptions, DbSpanMetadata } from '@refactorlabs/recount';type DbSpanMetadata = {
collection?: string;
table?: string;
queryType?: 'sql' | 'nosql';
};
type DbSpanOptions = {
metadata?: DbSpanMetadata;
input?: JsonObject;
output?: JsonObject;
durationMs?: number;
};