Browser Logging #
kuzzle-logger/browser is a logger for frontend applications, with the same API as the Node logger. It prints entries to the devtools console and sends them, in batches, to the application's Kuzzle backend. The backend then writes them with its own logger, so browser logs end up in the same place as backend logs (stdout, Loki, Elasticsearch...).
Browser Kuzzle backend Transports
KuzzleLogger ── batches ──▶ browser-logs:push ──▶ app.log.child('browser') ──▶ Loki, ...The browser never holds credentials for the log storage, and it cannot set labels: the backend validates, sanitizes and enriches every entry. See Ingesting browser logs in Kuzzle for the backend side.
The browser entry point is an ES module (dist/esm), meant to be used with a bundler such as Vite or webpack. It has no Node dependency.
Installation #
npm install kuzzle-loggerCreating the logger #
import { KuzzleLogger, createKuzzleSender } from 'kuzzle-logger/browser';
import { Kuzzle, WebSocket } from 'kuzzle-sdk';
const sdk = new Kuzzle(new WebSocket('api.example.com', { port: 443, ssl: true }));
await sdk.connect();
export const logger = new KuzzleLogger({
console: import.meta.env.DEV ? true : 'warn',
level: 'info',
namespace: 'dashboard',
sender: createKuzzleSender(sdk, {
app: { name: 'dashboard', version: '1.2.0' },
}),
});| Option | Default | Description |
|---|---|---|
level | info | Minimum level of the logged entries: trace, debug, info, warn, error, fatal or silent. It can be changed at runtime with logger.level = 'debug'. |
console | true | Prints entries to the devtools console: true for every logged entry, false for none, or a level (entries at or above it). Only entries at or above level are printed. |
namespace | – | Base namespace of the entries. |
getMergingObject | – | Called on each log: its result is merged into the entry context (e.g. the current route). If it throws or does not return an object, the entry is logged without it. |
sender | – | Where entries are sent. Without a sender, the logger only prints to the console. |
Logging #
The API is the same as the Node logger:
logger.info('Dashboard loaded');
logger.debug({ assetId, duration }, 'Asset %s loaded', assetId);
try {
await loadMap();
} catch (error) {
logger.error(error, 'Failed to load the map');
// or: logger.error({ err: error, mapId }, 'Failed to load the map');
}
const mapLogger = logger.child('map'); // namespace: "dashboard:map"
mapLogger.warn('No asset to display');- Namespaces may only contain letters, digits,
:,_and-, up to 64 characters in total (with the parent namespaces): other characters are replaced with_(MapView.vuebecomesMapView_vue) and longer namespaces are truncated. Keep them static, e.g.map, notmap-${mapId}. contextand errors (with theircausechain) are kept up to 5 levels deep: deeper objects and arrays are replaced with[Object]and[Array].- Unlike
console.log, extra arguments are only used for%s/%d/%oplaceholders in the message:logger.info('Loaded', data)dropsdata. Writelogger.info({ data }, 'Loaded'). - A child logger follows the level of its parent, until its own level is set (
child.level = 'debug'). namespaceis a reserved key: anamespacefield of the logged object (or ofgetMergingObject) sets the entry namespace and is not kept incontext. When the logger has a namespace, the logged field is dropped. Use another name, e.g.logger.info({ tenantNamespace }, 'Loaded').
Errors are serialized with their name, message, stack, cause and custom properties. Each entry is sent with this shape (payload v1):
{
"level": "error",
"time": 1759312800000,
"namespace": "dashboard:map",
"msg": "Failed to load the map",
"context": { "mapId": "site-42" },
"err": {
"name": "TypeError",
"message": "x is undefined",
"stack": "TypeError: x is undefined\n at ..."
}
}Logging never throws, even when getMergingObject or the sender fails.
Senders #
Senders buffer entries and send them in batches, one at a time:
- when a batch is full (
maxBatchSizeentries ormaxBatchBytesbytes), orflushIntervalafter the first entry; - immediately for
errorandfatalentries; - when the page is hidden or closed (
pagehide,visibilitychange), withfetchkeepalive.
When sending fails #
- A failed batch is retried with an exponential backoff (
maxRetries,retryDelay), then dropped. The backend is told how many entries were dropped. - A rejected batch (4xx response, except 408 and 429) is dropped without retry.
- After a dropped batch,
errorandfatalentries wait forflushIntervalinstead of being sent immediately, until a batch goes through again. An application that logs its failed requests aterrorlevel therefore does not start a request loop. - Senders never log, and entries logged while a batch is in flight go in the next batch: a failing backend cannot cause a logging loop.
- When the page closes, every pending batch is sent at once, within the browser keepalive limit (64 KB in total). A batch still in flight is sent again, since the page may be gone before it completes: the backend can receive it twice.
Kuzzle SDK sender #
createKuzzleSender(sdk, options?) sends batches with an existing SDK instance (sdk.query), so requests are authenticated as the logged-in user, over WebSocket or HTTP.
| Option | Default | Description |
|---|---|---|
controller | browser-logs | Controller the ingestion controller is registered under. |
action | push | Action name. |
keepaliveUrl | – | HTTP route of the action, e.g. https://api.example.com/_/browser-logs/_push. When set, batches sent while the page is hidden use fetch with keepalive and the SDK token, since a WebSocket request may not survive the page unload. |
HTTP sender #
createHttpSender(options?) posts batches with fetch. Use it when the frontend has no SDK instance.
| Option | Default | Description |
|---|---|---|
url | /_/browser-logs/_push | Endpoint receiving the batches (the Kuzzle HTTP route, on the same origin). |
headers | – | Extra headers, or a function returning them for each request (e.g. () => ({ Authorization: `Bearer ${getToken()}` })). Without an Authorization header, the request is anonymous. |
Batching options #
Both senders accept these options:
| Option | Default | Description |
|---|---|---|
app | – | { name, version } of the frontend, sent with each batch. Informational. |
flushInterval | 5000 | Delay before sending a batch that is not full, in milliseconds. |
maxBatchBytes | 60000 | Maximum size of a batch (JSON, in bytes). Keep it below the backend limits.maxPayloadSize (65536 by default). A larger entry is reduced to its level, time, namespace, message and error (truncated), with context: { truncated: true }. |
maxBatchSize | 20 | Maximum number of entries per batch. Keep it below the backend limits.maxEntries (100 by default). |
maxBufferSize | 500 | Maximum number of entries kept in memory. The oldest entries are dropped first, and the backend logs how many were dropped. |
maxRetries | 3 | Retries of a failed batch before it is dropped. |
retryDelay | 1000 | First retry delay, in milliseconds. It doubles at each attempt, up to 60 seconds. |
await logger.flush() sends the pending entries now, even during a retry backoff. It resolves once the request is answered: if it fails, the entries stay buffered and are retried later. sender.close() stops the timers and removes the page listeners, without sending the pending entries (call flush() first).
Capturing uncaught errors #
Errors should be logged where they are handled. Two opt-in helpers also capture the errors nobody handled.
Global errors #
captureGlobalErrors(logger, options?) logs window error events (uncaught exceptions) and unhandledrejection events (rejected promises nobody awaited) at the error level. It returns a function that removes the listeners.
import { captureGlobalErrors } from 'kuzzle-logger/browser';
const stop = captureGlobalErrors(logger.child('global'));- Each entry has an
eventcontext field (errororunhandledrejection), and the errorlocation(file,line,column) for uncaught exceptions. A rejected value that is not an error is logged in thereasonfield. - Identical errors received within
dedupeIntervalmilliseconds (5000 by default) are logged once, so an error thrown in a loop or a timer does not flood the backend. - The events are not cancelled: the browser still prints them to the console, and the logger does not print them a second time.
- Errors are captured once per target: while a capture is active (e.g. after a hot module reload, or when both a platform and the application call it), later calls add no listener and return the same function, and the first logger keeps logging. A new call after
stop()captures again. - Errors thrown by scripts from another origin only give
Script error., without a stack. Serve the scripts with CORS headers and thecrossoriginattribute to get the details.
Vue error handler #
createVueErrorHandler(logger, options?) returns a handler for app.config.errorHandler (Vue 3) or Vue.config.errorHandler (Vue 2). It logs the error with the component name (component) and the Vue info string (render function, setup function, watcher callback...). It does not depend on vue.
| Option | Default | Description |
|---|---|---|
dedupeInterval | 5000 | Identical errors (same name, message, info and component) received within this delay, in milliseconds, are logged once, so an error thrown on each render does not flood the backend. 0 disables it. |
previous | – | Handler called after logging, with the same arguments: the handler set before, or the one of an error tracking tool. Errors it throws are ignored. |
Deduplicated errors are still printed and passed to previous.
import { createApp } from 'vue';
import { captureGlobalErrors, createVueErrorHandler } from 'kuzzle-logger/browser';
import App from './App.vue';
import { logger } from './logger';
const app = createApp(App);
app.config.errorHandler = createVueErrorHandler(logger.child('vue'), {
previous: app.config.errorHandler,
});
captureGlobalErrors(logger.child('global'));
app.mount('#app');Vue stops printing errors once an error handler is set: the handler prints them with console.error, as Vue does in production, whatever the logger console option.
Since Vue 3.4, production builds give info as a link (https://vuejs.org/error-reference/#runtime-1) instead of a string: the handler maps the known codes back to Vue's strings (render function), and keeps unknown ones as they are.
Privacy #
Browser logs are sent to your backend and stored with your other logs. Do not log personal data or secrets. The backend redacts the values of sensitive keys (password, token, authorization...) and tokens found in strings, but it cannot detect everything.