Skip to main content

Client Logging

Client-side logging runs in the browser. The client logger batches entries and sends them using navigator.sendBeacon (or fetch with keepalive) so logging does not block user interactions.

Setup

The simplest way to set up the client logger is via the Provider pattern. With the provider in place, use useSimpleLogs() in any client component to get the logger.

If you're not using the provider, initialize the SDK once near the top of your client entry point:

import { SimpleLogs } from '@simplelogs/next';

SimpleLogs.init({
clientKey: process.env.NEXT_PUBLIC_SIMPLELOGS_CLIENT_KEY!,
});
warning

Use NEXT_PUBLIC_ prefix for env vars exposed to the browser. The client key is safe to expose to the browser because it is restricted by your origin allowlist. Never expose your server key.


useSimpleLogs() hook

Returns the client logger singleton. Config is read from the nearest <SimpleLogsProvider> in the React tree.

'use client';

import { useSimpleLogs } from '@simplelogs/next';

export function CheckoutButton() {
const logger = useSimpleLogs();

return (
<button
onClick={() => logger.log({
touchpoint: 'ui/checkout/button-clicked',
message: 'Checkout button clicked',
level: 'info',
})}
>
Checkout
</button>
);
}

logger.log(options)

Logs a discrete client-side event.

logger.log({
touchpoint: 'ui/search/query-submitted',
message: 'Search performed',
level: 'info',
metadata: { query: 'running shoes', resultsCount: 42 },
});

LogOptions

FieldTypeRequiredDefaultDescription
touchpointstringNoTouchpoint name
keystringNoCorrelation key
messagestringNoFree-form message
level"info" | "warn" | "error" | "debug"No"info"Severity level
metadataRecord<string, unknown>NoArbitrary structured data

logger.start(options) and logger.end(options)

Same semantics as the server logger — measure the duration of an operation:

logger.start({ touchpoint: 'ui/modal/open', key: 'checkout-modal' });

// ... user interacts with modal ...

logger.end({ key: 'checkout-modal', metadata: { completed: true } });

See Server Logging → start / end for the full matching rules, StartOptions, and EndOptions reference — they are identical on the client.


logger.record(options)

Records a timing entry with a pre-computed start time, end time, and duration. Use this when you already know both timestamps (e.g., from a PerformanceObserver).

const start = performance.now();
await doWork();
const end = performance.now();

logger.record({
touchpoint: 'ui/animation/frame-render',
startTime: Date.now() - (end - start),
endTime: Date.now(),
duration: end - start,
metadata: { frames: 60 },
});

RecordOptions

FieldTypeRequiredDescription
startTimenumberYesUnix timestamp (ms)
endTimenumberYesUnix timestamp (ms)
durationnumberYesDuration in ms
touchpointstringNoTouchpoint name
keystringNoCorrelation key
metadataRecord<string, unknown>NoStructured data

flushClient()

Forces an immediate flush of the client queue. Called automatically on beforeunload if there are pending entries.

import { flushClient } from '@simplelogs/next';

// Flush before navigating away
await flushClient();
router.push('/next-page');

How client entries are delivered

The client queue flushes entries in one of two ways depending on fireAndForget:

ModeTransportRetry on failureBlocks unload
fireAndForget: true (default)navigator.sendBeaconNoNo
fireAndForget: falsefetch with keepaliveYes (re-queued)No

Beacon is appropriate for most logging scenarios — it's designed to outlive page unloads and doesn't slow down navigation. Use fetch mode when you need delivery guarantees (e.g., critical error reporting).


The module-level SimpleLogs export

If you're not using the React hook (e.g., in a utility function outside of a component), you can use the module-level SimpleLogs singleton:

import { SimpleLogs } from '@simplelogs/next';

export function trackEvent(name: string, data: Record<string, unknown>) {
SimpleLogs.log({
touchpoint: name,
metadata: data,
});
}

This is the same object returned by useSimpleLogs() — they share the same queue.