Set Up Browser Logging #
This guide walks through a typical setup: a Kuzzle application (the backend) and a Vue frontend that talks to it with the Kuzzle SDK. At the end, frontend logs and uncaught errors are written by the backend, next to its own logs (stdout, Loki...).
Frontend (kuzzle-logger/browser) ── browser-logs:push ──▶ Backend (kuzzle-logger/kuzzle) ──▶ app.log transportsYou need kuzzle-logger 1.6.0 or later, installed in both the backend and the frontend.
1. Register the controller in the backend #
npm install kuzzle-loggerKuzzle 2.59 and earlier install their own kuzzle-logger 1.4 for app.log. To have app.log use the same version as your application, add an npm overrides entry for kuzzle-logger and check in package-lock.json that only one copy is installed: some npm versions (11.13, for example) ignore overrides without any error.
// app.ts
import { Backend, BadRequestError } from 'kuzzle';
import { createBrowserLogsController } from 'kuzzle-logger/kuzzle';
const app = new Backend('my-app');
app.controller.register(
'browser-logs',
createBrowserLogsController(app.log, {
// Without it, an invalid batch gets a 500 instead of a 400, and the browser retries it
badRequest: (message) => new BadRequestError(message),
}),
);This adds the browser-logs:push action and its POST /_/browser-logs/_push HTTP route. The defaults are fine for most applications: see Ingesting browser logs in Kuzzle for the limits, sanitization and options.
2. Grant the action #
Add the action to the roles of the users who run the frontend:
{
"controllers": {
"browser-logs": {
"actions": {
"push": true
}
}
}
}To also receive the errors raised before login (a login page that fails to load, an expired session), add it to the anonymous role and set a rateLimit on the anonymous profile. Read Anonymous users first.
3. Create the frontend logger #
npm install kuzzle-loggerCreate the logger once, in its own module, next to the SDK instance:
// src/plugins/logger.js
import {
KuzzleLogger,
captureGlobalErrors,
createKuzzleSender,
createVueErrorHandler,
} from 'kuzzle-logger/browser';
import { name, version } from '../../package.json';
import { kuzzle } from './kuzzle'; // your Kuzzle SDK instance
export const logger = new KuzzleLogger({
// Everything in the devtools console in development, warnings and errors in production
console: import.meta.env.DEV ? true : 'warn',
level: 'info',
namespace: 'web',
sender: createKuzzleSender(kuzzle, {
app: { name, version },
// HTTP route of the action: batches sent while the page closes use fetch + keepalive
keepaliveUrl: 'https://api.example.com/_/browser-logs/_push',
}),
});
export function captureErrors(app) {
captureGlobalErrors(logger.child('global'));
app.config.errorHandler = createVueErrorHandler(logger.child('vue'));
}createKuzzleSenderuses the SDK: requests are authenticated as the logged-in user, and anonymous before login.keepaliveUrlis optional but recommended: without it, the entries logged just before the page closes may be lost.- Without a Kuzzle SDK in the frontend, use
createHttpSenderinstead (see HTTP sender).
4. Capture uncaught errors #
Call captureErrors before the other plugins are registered, so that their errors are captured too:
// src/main.js
import { createApp } from 'vue';
import App from './App.vue';
import { captureErrors } from './plugins/logger';
const app = createApp(App);
captureErrors(app);
// app.use(router), app.use(pinia)...
app.mount('#app');Then log where errors are handled, as you would with console:
import { logger } from '@/plugins/logger';
const mapLogger = logger.child('map'); // namespace "web:map"
try {
await loadMap(mapId);
} catch (error) {
mapLogger.error({ err: error, mapId }, 'Failed to load the map');
}Do not log personal data or secrets: see Privacy.
5. Allow the requests #
If the frontend is served with a Content Security Policy, its connect-src directive must include the backend origin, for both the WebSocket (wss://api.example.com) and the keepaliveUrl (https://api.example.com).
When the frontend and the backend have different origins, the keepaliveUrl request is a cross-origin request. Kuzzle accepts any origin by default: if you restricted http.accessControlAllowOrigin, it must include the frontend origin.
6. Check that it works #
Send a test batch with Kourou, with the credentials of a user allowed to call the action:
kourou browser-logs:push --body '{
"version": 1,
"entries": [{ "level": "error", "msg": "Browser logging test", "namespace": "test" }]
}'The response is { "accepted": 1, "rejected": [] }, and the backend logs the entry under the browser:test namespace, with source: "browser".
Then open the frontend, run logger.error('Hello from the browser') from your code, and find it with the backend logs. With Loki:
{service_name="my-app"} | json | source="browser"See Grafana dashboards for more queries and a ready-made dashboard.
Troubleshooting #
| Symptom | Cause |
|---|---|
403 on browser-logs:push | The user's roles do not grant the action, or the user is anonymous and the anonymous role does not grant it. |
500 on an invalid batch, and the browser keeps retrying it | The badRequest option is missing in createBrowserLogsController. |
| Entries in the devtools console, but nothing in the backend | No sender, a level above the logged entries, or blocked requests (CSP connect-src, network tab). |
| Entries in the backend stdout, but not in Loki or Elasticsearch | The backend logger transport: check server.appLogs in the Kuzzle configuration (see Getting started). |
Uncaught errors logged as Script error., without a stack | The script comes from another origin: serve it with CORS headers and the crossorigin attribute. |
| Entries logged just before a navigation are lost | Set keepaliveUrl on the Kuzzle sender. |
Entries logged under browser, with a clientNamespace field | More than maxNamespaces (50) distinct namespaces: keep namespaces static (no IDs in them). |