Cerbero is a lightweight TypeScript library for tracking user interactions in the browser. It collects clicks, scrolls, text selections, mouse exits, time on page, performance metrics, Web Vitals, and many more signals — and offloads all processing to a Web Worker so the main thread stays free.
→ Live Demo — see every event type and every plugin in action.
- Install
- Quick Start
- Constructor Options
- Public API
- Event Reference
- Plugin System
- Selective Tracking
- Batching
- Breaking Changes from v1.x
- Contributing
- License
npm install cerbero
# or
yarn add cerberoCDN (UMD, no bundler needed):
<script src="https://unpkg.com/cerbero/dist/cerbero.umd.js"></script>
<!-- window.cerbero.Cerbero is now available -->import { Cerbero } from 'cerbero';
const cerbero = new Cerbero();
cerbero.addEventListener((event) => {
console.log(event.type, event.data);
});new Cerbero(config?: CerberoConfig)| Option | Type | Default | Description |
|---|---|---|---|
events |
string[] |
(all) | Whitelist of event types to track. Omit to track everything. |
domPath |
boolean |
false |
Include full DOM ancestor path in serialized elements (e.g. > html > body > div > button). Results are cached per-element via WeakMap. |
domPosition |
boolean |
false |
Include element bounding box via getBoundingClientRect(). Forces a synchronous layout reflow on every event. |
batchSize |
number |
— | Flush callback when N events have accumulated. |
batchInterval |
number |
— | Flush callback every N milliseconds. |
const cerbero = new Cerbero({
events: ['click', 'scroll', 'webVitals'],
domPath: true,
batchSize: 10,
});Register the callback that receives every event (or batch).
cerbero.addEventListener((event) => {
// event is a single object (or unknown[] if batching is enabled)
sendToMyBackend(event);
});Change how often the timeInPage event fires. Default: 2500 ms.
cerbero.setTimeInPageInterval(5000);Track element visibility via IntersectionObserver. Fires an elementVisible event when a matching element is ≥50% in the viewport.
cerbero.observeElements('.hero-cta, .pricing-card');Register a plugin that emits custom events through the Cerbero pipeline. Returns this for chaining.
import { fingerprintPlugin } from './plugins/fingerprint';
cerbero.use(fingerprintPlugin());Remove all event listeners, disconnect observers, clear intervals, and terminate the Web Worker.
cerbero.destroy();All events share a base shape:
{
"type": "click",
"time": 1720000000000,
"after": 1234,
"data": { ... }
}time— Unix timestamp (ms) when the event was processedafter— ms elapsed since page loaddata— event-specific payload
Fires on every pointer-up (pointerup event, capturing mouse, touch, and pen input).
{
"type": "click",
"data": {
"position": { "screenX": 752, "screenY": 244, "clientX": 752, "clientY": 164,
"pageX": 752, "pageY": 3609, "x": 752, "y": 164,
"offsetX": 388, "offsetY": 414 },
"keys": { "ctrlKey": false, "shiftKey": false, "altKey": false, "metaKey": false },
"pointerType": "mouse",
"elements": {
"target": { "id": "", "type": "BUTTON", "domType": "HTMLButtonElement",
"identifier": "button#submit", "className": "btn-primary" }
}
}
}Fires when the same element is clicked 3+ times within 500 ms.
{
"type": "rageClick",
"data": {
"count": 4,
"target": { "identifier": "button#submit", "domType": "HTMLButtonElement" }
}
}Fires once per scroll stop (debounced at 100 ms).
{
"type": "scroll",
"data": {
"scroll": { "fromTop": 1240, "domHeight": 5800, "percentage": 21.37 }
}
}Fires when the pointer leaves the browser window.
{
"type": "mouseExit",
"data": {
"position": { "screenX": 744, "screenY": 0, "clientX": 744, "clientY": 0,
"pageX": 744, "pageY": 820, "x": 744, "y": 0 },
"keys": { "ctrlKey": false, "shiftKey": false, "altKey": false, "metaKey": false },
"elements": {
"target": { "id": "hero", "type": "DIV", "domType": "HTMLDivElement", "identifier": "div#hero" }
}
}
}Fires when the user finishes a text selection.
{
"type": "selection",
"data": {
"text": "the selected text string",
"elements": {
"anchorNode": { "type": "#text", "data": "full text of node", "offset": 4 },
"focusNode": { "type": "#text", "data": "full text of node", "offset": 22 }
}
}
}Fires on a repeating interval (default 2.5 s). Pauses automatically when the tab is hidden (Page Visibility API).
{
"type": "timeInPage",
"data": { "timePassed": 7500 }
}One-shot at page load. Uses PerformanceNavigationTiming (via PerformanceObserver).
{
"type": "performance",
"data": {
"timing": {
"domContentLoadedEventEnd": 312.4,
"loadEventEnd": 541.8,
"responseStart": 84.2,
"responseEnd": 190.1,
"domInteractive": 295.0
}
}
}Fires for each Core Web Vital as it becomes available: CLS, FCP, INP, LCP, TTFB.
{
"type": "webVitals",
"data": {
"name": "LCP",
"value": 1842.5,
"rating": "good",
"delta": 1842.5,
"id": "v3-1720000000000-1234"
}
}One-shot at init. Rich device and session context.
{
"type": "environment",
"data": {
"darkMode": true,
"reducedMotion": false,
"colorGamut": "p3",
"cpuCores": 10,
"deviceMemory": 8,
"devicePixelRatio": 2,
"screenWidth": 2560,
"screenHeight": 1440,
"viewportWidth": 1440,
"viewportHeight": 900,
"pointerType": "mouse",
"hasHover": true,
"maxTouchPoints": 0,
"language": "en-US",
"languages": ["en-US", "it-IT"],
"timezone": "Europe/Rome",
"hourCycle": "h23",
"referrer": "https://google.com",
"historyLength": 3,
"isIframe": false,
"isStandalone": false,
"serviceWorker": true,
"webAssembly": true
}
}Fires at init and again on connection change. Requires navigator.connection (Chrome/Edge).
{
"type": "network",
"data": { "effectiveType": "4g", "downlink": 10, "rtt": 50, "saveData": false }
}Fires for every main-thread task exceeding 50 ms. Requires longtask PerformanceObserver support (Chrome/Edge).
{
"type": "longTask",
"data": { "duration": 87.3, "startTime": 1234.5 }
}Fires when a network resource takes more than 1 000 ms to load.
{
"type": "slowResource",
"data": {
"name": "https://example.com/large-image.jpg",
"duration": 1842,
"initiatorType": "img",
"startTime": 400.2
}
}One-shot detection of browser AI APIs (Chrome 127+). Never invokes the model.
{
"type": "browserAI",
"data": { "hasChromeAI": true, "chromeAIModel": "readily", "hasWebNN": false }
}Fires first, before any other listener is set up. If gpc === true (Global Privacy Control), Cerbero calls destroy() and emits no further events.
{
"type": "privacySignals",
"data": { "gpc": false, "doNotTrack": false, "gpcAutoDisabled": false }
}Fires when the screen orientation changes (mobile/tablet).
{
"type": "orientationChange",
"data": { "orientationType": "landscape-primary", "angle": 90 }
}Fires when an observed element enters the viewport (≥50% visible). Requires a call to observeElements().
{
"type": "elementVisible",
"data": { "selector": ".hero-cta", "visibleRatio": 0.82,
"element": { "identifier": "div.hero-cta", "type": "DIV" } }
}Fires on SPA route changes. Uses Navigation API when available; falls back to popstate + hashchange.
{
"type": "navigation",
"data": { "from": "https://example.com/home", "to": "https://example.com/about",
"navigationType": "push" }
}Fires on pagehide. Gives you the total time-on-page before the user leaves.
{
"type": "pageUnload",
"data": { "timeOnPage": 42300 }
}Note: to send a beacon to your own server on unload, call
navigator.sendBeacon()from youraddEventListenercallback when you receive this event.
Fires only when a modifier key (Ctrl / Meta / Alt) is held. Never records bare keystrokes.
{
"type": "keyboardShortcut",
"data": {
"key": "s",
"combo": "ctrl+s",
"ctrlKey": true, "metaKey": false, "altKey": false, "shiftKey": false
}
}Plugins push custom events through the same worker pipeline as built-in events.
import type { CerberoPlugin, PluginEmitter } from 'cerbero';
const myPlugin = (): CerberoPlugin => ({
name: 'myPlugin',
init(emit: PluginEmitter) {
// emit any event type
emit('myEvent', { foo: 'bar' });
// optionally return a cleanup function called on destroy()
const timer = setInterval(() => emit('myEvent', { tick: Date.now() }), 5000);
return () => clearInterval(timer);
},
});
cerbero.use(myPlugin());The library ships four ready-made grey-area plugins (opt-in — you decide whether to use them):
| Plugin | Import | Emits | Description |
|---|---|---|---|
| Fingerprint | src/plugins/fingerprint |
fingerprint |
Canvas, WebGL, and AudioContext hashes |
| Extended Env | src/plugins/extended-env |
extendedEnv |
Speech voices, media device counts, storage, permissions, keyboard layout |
| Behavioral | src/plugins/behavioral |
behavioralMouse, behavioralKeyboard |
Mouse velocity/acceleration + keystroke timing (no key values) |
| WebRTC IP | src/plugins/webrtc |
webrtcIp |
Local and public IPs via RTCPeerConnection ICE candidates |
import { fingerprintPlugin } from './src/plugins/fingerprint';
import { extendedEnvPlugin } from './src/plugins/extended-env';
import { behavioralPlugin } from './src/plugins/behavioral';
import { webrtcPlugin } from './src/plugins/webrtc';
cerbero
.use(fingerprintPlugin())
.use(extendedEnvPlugin())
.use(behavioralPlugin({ mouseDynamics: true, keystrokeDynamics: true, emitInterval: 5000 }))
.use(webrtcPlugin());Pass an events array to only activate the trackers you need:
const cerbero = new Cerbero({
events: ['click', 'scroll', 'webVitals', 'environment'],
});privacySignals always runs regardless of this list — it is a legal requirement for GPC support.
Collect multiple events before flushing to your callback. Useful for sendBeacon or rate-limited endpoints.
// Flush every 10 events
const cerbero = new Cerbero({ batchSize: 10 });
// Flush every 2 seconds
const cerbero = new Cerbero({ batchInterval: 2000 });
cerbero.addEventListener((events) => {
// events is unknown[] when batching is enabled
navigator.sendBeacon('/analytics', JSON.stringify(events));
});| Change | v1.x | v2.x |
|---|---|---|
| No singleton export | import cerbero from 'cerbero' |
import { Cerbero } from 'cerbero'; const c = new Cerbero() |
addEventListner typo |
cerbero.addEventListner(cb) |
cerbero.addEventListener(cb) (deprecated alias kept) |
mouseExit payload key |
data.elements.fromElement |
data.elements.target |
performance timestamps |
Absolute epoch ms (navigationStart: 1611...) |
Relative ms from navigation start (domContentLoadedEventEnd: 312.4) |
| Web Vitals | FID | INP (Interaction to Next Paint, replacing FID in web-vitals v3+) |
scroll payload |
Included full elements.target DOM node |
Only data.scroll — no elements |
See CONTRIBUTING.md.
Thecreazy Core |
TheBous Core |

