Server errors and performance
Capture your server's errors with a cleaned stack and time every route, from Express, Fastify, Next.js or a Sentry SDK, with no request data sent.
Your server's errors and its request performance, next to the browser's. Server errors land on the Problems page marked Server, grouped with a cleaned stack. Each route's latency, throughput and error rate show under Speed, Server. Both come from @littlefriend/node with your secret server key, and errors can also come from a Sentry SDK you already run.
Install
Create a server key in the dashboard under Settings, Keys, or with littlefriend keys create --kind server, and keep it in your server's environment. Then create one client for the whole process.
import { LittleFriend } from '@littlefriend/node';
const key = process.env.LF_SERVER_KEY;
export const lf = key
? new LittleFriend({ key, release: process.env.GIT_COMMIT, environment: process.env.NODE_ENV })
: null;release and environment go with every error, so the Problems page can say which release an error first appeared in. Both are optional.
Add it to your framework
Each adapter times every request and captures every error answered with a 5xx. None of them can throw into your app, and a collector that is down never slows a request.
import express from 'express';
import { lf } from './lib/little-friend';
const app = express();
if (lf) app.use(lf.express());
app.get('/orders/:id', async (req, res) => {
res.json(await loadOrder(req.params.id));
});
// After your routes, before your own error handlers. It calls next(err).
if (lf) app.use(lf.expressErrorHandler());import Fastify from 'fastify';
import { lf } from './lib/little-friend';
const app = Fastify();
if (lf) await app.register(lf.fastify());import { lf } from '@/lib/little-friend';
async function handler(request: Request): Promise<Response> {
return Response.json(await loadOrders());
}
export const GET = lf ? lf.wrap(handler, { route: '/api/orders' }) : handler;Pass the route to wrap when you can. Without one, it builds the template from the handler's params, so /api/users/42 counts as /api/users/:id, and otherwise counts the request under /*. It never keeps the path.
Anywhere else, such as a queue worker or a cron job, capture an error yourself. The call never throws.
import { lf } from './lib/little-friend';
try {
await syncInventory();
} catch (err) {
lf?.captureException(err, { route: '/jobs/sync-inventory' });
throw err;
}To also capture errors that would crash the process, uncaught exceptions and unhandled rejections, opt in once at startup. The error is sent within two seconds, then Node exits with code 1 as it would have. If your app has its own uncaughtException handler, that handler still decides.
import { lf } from './lib/little-friend';
// Once, at startup. A crash is sent, then the process exits with code 1 as before.
lf?.captureProcessErrors();What an error sends
- The error's type and message.
- Up to 30 stack frames: function, file, line and column, with files relative to your app's folder.
- The release and environment you set, and the route the request matched.
Never sent: the request body, headers, query string, cookies, IP address, the signed-in user or any variable. Little Friend cleans the message and every file path again before anything is stored, with the rules browser errors use: a home folder's user name becomes [user], ids and tokens in paths become :id, and emails, card numbers, secrets and other personal data in the message are replaced. An error with a 4xx status or statusCode is the client's mistake and is not captured.
Errors group by type, message with numbers left out, and the first frames in your own code, so Order 41 not found and Order 42 not found thrown in the same place are one group, and a redeploy that moves a line keeps it. A server error never shares a group with a browser error.
Link an error to a visit
In journey mode, a server error can link to the visit that caused it, and to its session replay. Send the page's session id in an x-lf-session header on your own API calls, and the adapters read it. In aggregate mode there is no session to send, and none is kept.
import { getSession } from '@littlefriend/tracker';
const session = getSession();
await fetch('/api/orders', {
method: 'POST',
headers: session ? { 'x-lf-session': session } : {},
body: JSON.stringify(order),
});Request performance
The adapters time each request from when your framework receives it to when the response finishes, and count it under the route template it matched, such as /orders/:id, never the path a visitor asked for. Each process adds requests up in memory, per hour, route, method and status class, and sends the totals every 30 seconds (you can choose 10 to 60). Requests no route matched count under /*, and so do new routes once a process has 1,000 of them in an hour.
- Requests and throughput, requests a minute over the range.
- Error rate: requests answered with a 5xx, out of all requests.
- p50, p95 and p99 latency, read from histograms, so any range and route mix is exact to within one histogram step.
The Speed page shows them under Server, per route and method. The same report is GET /api/projects/:id/reports/server, and the MCP server's get_server_performance tool.
Already on Sentry
A Sentry SDK can send its errors to Little Friend too. Point it at a DSN with your server key as the key. The number at the end can be any number: the key names the project.
import * as Sentry from '@sentry/node';
Sentry.init({
dsn: 'https://lfs_YOUR_SERVER_KEY@in.littlefriend.io/1',
release: process.env.GIT_COMMIT,
environment: process.env.NODE_ENV,
});Little Friend reads errors only: the exception's type, message and frames, the release, the environment and the transaction name as the route when it is a route template, such as GET /orders/:id. Breadcrumbs, user, request data, tags and contexts are never read. Messages below error level, performance data, sessions, attachments and replays are accepted and dropped, so the SDK never retries them. For request performance, add the Little Friend middleware. The DSN looks like https://lfs_YOUR_SERVER_KEY@in.littlefriend.io/1.
This DSN holds your secret server key, so use it only in server-side Sentry configs. Never put it in code that ships to a browser or an app. Little Friend refuses it from a browser or app Sentry SDK.
In Next.js, set it in sentry.server.config.ts and sentry.edge.config.ts (or the server branch of instrumentation.ts), read from a server-only variable. Never set it in sentry.client.config.ts or instrumentation-client.ts, and never in a NEXT_PUBLIC_ variable. The browser keeps its own Sentry DSN.
// sentry.server.config.ts: runs on the server only. LF_SERVER_KEY is a server-only variable.
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: `https://${process.env.LF_SERVER_KEY}@in.littlefriend.io/1`,
});Retention and deletion
Error groups and request totals are kept as long as the project's other aggregates. Each server error is also kept as a raw event, as long as raw events. In journey mode it belongs to its visit, and deleting a visit deletes the server errors linked to it. Deleting the project deletes all of it.