Skip to main content

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:

IdentifierScopeStorageLifetime
Page identifierOne page loadIn-memory (module-level)Regenerated on every full page load/refresh
Session identifierOne browserlocalStoragePersists 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:

app/api/orders/route.ts
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 fetch calls 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​

OptionEnv varDefaultApplies toDescription
sessionTimeoutSIMPLELOGS_SESSION_TIMEOUT_MS1800000 (30 min)Client onlyInactivity window before a new session identifier is generated
autoPropagateHeadersSIMPLELOGS_AUTO_PROPAGATE_HEADERStrueClient + serverSet 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,
});
info

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 fetch requests 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.
  • serverLogger calls are now async. Reading the correlation headers requires an await before the entry is queued. See Server Logging for what this means for your code.