Page & Session Correlation
A page load often fans out into several log entries across the stack — a client-side page-view log, a fetch to a Route Handler, a database query on the server. Without a shared identifier, those show up in SimpleLogs as unrelated entries.
@simplelogs/next solves this automatically with two identifiers that get attached to every log and timing entry:
| Identifier | Scope | Storage | Lifetime |
|---|---|---|---|
| Page identifier | One page load | In-memory (module-level) | Regenerated on every full page load/refresh |
| Session identifier | One browser | localStorage | Persists across page loads; expires after a period of inactivity (default 30 min, sliding) |
Both are added to metadata.pageIdentifier and metadata.sessionIdentifier automatically — no code changes required to start seeing them.
How it works on the client
The first client-side log(), start(), end(), or record() call on a page generates a page identifier. It's cached in memory and reused for every subsequent call on that page — a hard reload naturally produces a new one, since the module re-executes.
The session identifier is read from (and refreshed in) localStorage on every call. If more than sessionTimeout ms (default 30 minutes) have passed since the session was last active, a new session identifier is generated; otherwise the existing one is returned and its "last active" timestamp is refreshed, so an actively-used session never expires mid-visit.
'use client';
import { useSimpleLogs } from '@simplelogs/next';
export function CheckoutButton() {
const logger = useSimpleLogs();
return (
<button onClick={() => logger.log({ touchpoint: 'ui/checkout/clicked' })}>
Checkout
</button>
);
}
// The entry sent to SimpleLogs automatically includes:
// metadata: { pageIdentifier: "a1b2c3d4-...", sessionIdentifier: "e5f6g7h8-..." }
This applies to every client call — log, start, end, and record — not just the first one, so the identifiers still show up on an end() entry even if you pass your own metadata there.
Reading the identifiers directly
Both are exported as plain functions, and also available on the object returned by useSimpleLogs():
import { getPageIdentifier, getSessionIdentifier } from '@simplelogs/next';
const pageId = getPageIdentifier(); // string | undefined
const sessionId = getSessionIdentifier(); // string | undefined
'use client';
import { useSimpleLogs } from '@simplelogs/next';
const logger = useSimpleLogs();
logger.getPageIdentifier();
logger.getSessionIdentifier();
Both return undefined when called outside a browser (e.g. accidentally on the server) — they only make sense in a client context.
Propagating to the server automatically
To tie a client-side event to the server-side work it triggers (a Route Handler, a Server Action, a Server Component render), the SDK also patches fetch on the client and reads the correlation headers back out on the server — no manual plumbing required in the common case.
On the client: any same-origin fetch() call automatically gets two extra headers:
x-simplelogs-page-id: a1b2c3d4-...
x-simplelogs-session-id: e5f6g7h8-...
This applies to fetch calls your app makes directly, as well as calls Next.js itself makes on your behalf (like dispatching a Server Action from a hydrated client component). Requests to a different origin are left untouched, and so are the SDK's own outbound calls to the SimpleLogs ingestion endpoint.
On the server: serverLogger.log() / start() / end() read those headers back out via next/headers() and merge them into the entry's metadata — automatically, with no extra code:
import { serverLogger } from '@simplelogs/next/server';
export async function POST(request: Request) {
await serverLogger.start({ touchpoint: 'db/query/orders' });
const orders = await db.query('SELECT ...');
await serverLogger.end({ touchpoint: 'db/query/orders' });
// metadata.pageIdentifier / metadata.sessionIdentifier on this entry
// match the client-side entries from the page that triggered this request.
return Response.json(orders);
}
:::info Requires the App Router
Automatic server-side pickup relies on next/headers(), which is only available in request-scoped contexts: Route Handlers, Server Components, and Server Actions under the App Router. It is not available in the Pages Router or in plain Node scripts — see manual override below.
:::
Does this work in Server Actions?
Yes, in the common case. When a client component invokes a Server Action (a form action prop or an event handler, once the page has hydrated), Next.js dispatches it as a fetch POST to the current route — the same fetch the SDK patches — so the headers ride along, and next/headers() inside the action sees them exactly like it would in a Route Handler.
Two edge cases where it won't apply:
- Native, no-JS form submissions (the browser submits the form directly before hydration, or JS never loads) aren't
fetchcalls at all, so no headers are attached. - Server-to-server invocations — a Server Action called directly during a Server Component's render, with no round trip through the browser — have no client-side page/session identifier to propagate in the first place.
Manual override for non-App-Router usage
If you're on the Pages Router, in an Express/Fastify server, or in a background job — anywhere next/headers() isn't available — pass the identifiers (or the raw incoming request headers) explicitly. This short-circuits the automatic lookup:
// Explicit ids
await serverLogger.log({
touchpoint: 'api/orders/submit',
pageIdentifier: req.headers['x-simplelogs-page-id'],
sessionIdentifier: req.headers['x-simplelogs-session-id'],
});
// Or forward the whole headers object
await serverLogger.log({
touchpoint: 'api/orders/submit',
requestHeaders: req.headers,
});
requestHeaders accepts a Headers instance or a plain header record, and takes precedence over pageIdentifier/sessionIdentifier if both are provided.
Configuration
| Option | Env var | Default | Applies to | Description |
|---|---|---|---|---|
sessionTimeout | SIMPLELOGS_SESSION_TIMEOUT_MS | 1800000 (30 min) | Client only | Inactivity window before a new session identifier is generated |
autoPropagateHeaders | SIMPLELOGS_AUTO_PROPAGATE_HEADERS | true | Client + server | Set to false to disable page/session tagging, fetch header injection, and server-side header pickup entirely |
configureSDK({
sessionTimeout: 60 * 60 * 1000, // 1 hour
autoPropagateHeaders: true,
});
sessionTimeout only affects browser behavior, so set it via configureSDK() or <SimpleLogsProvider config={{...}}> — it has no effect if placed in simplelogs.config.json, since that file is only ever read on the server and the session identifier logic never runs there.
Things to know
- Same-origin only, by default. Correlation headers are only added to
fetchrequests targeting the same origin as the page. This avoids breaking CORS preflight on third-party APIs and avoids leaking internal identifiers off-origin. - No SPA route-level regeneration. The page identifier resets on a full page load/refresh, not on client-side route changes (there's no App Router navigation tracking baked in). If you need a fresh identifier per client-side route change, generate one yourself.
serverLoggercalls are nowasync. Reading the correlation headers requires anawaitbefore the entry is queued. See Server Logging for what this means for your code.