npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

cerbero

v2.0.1

Published

Tracking your users interations

Downloads

44

Readme

npm codecov CI - Test GitHub license Open Source Love svg1

🐾 Cerbero

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.

Table of Contents

  1. Install
  2. Quick Start
  3. Constructor Options
  4. Public API
  5. Event Reference
  6. Plugin System
  7. Selective Tracking
  8. Batching
  9. Breaking Changes from v1.x
  10. Contributing
  11. License

Install

npm install cerbero
# or
yarn add cerbero

CDN (UMD, no bundler needed):

<script src="https://unpkg.com/cerbero/dist/cerbero.umd.js"></script>
<!-- window.cerbero.Cerbero is now available -->

Quick Start

import { Cerbero } from 'cerbero';

const cerbero = new Cerbero();

cerbero.addEventListener((event) => {
  console.log(event.type, event.data);
});

Constructor Options

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,
});

Public API

addEventListener(callback)

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);
});

setTimeInPageInterval(ms)

Change how often the timeInPage event fires. Default: 2500 ms.

cerbero.setTimeInPageInterval(5000);

observeElements(selector)

Track element visibility via IntersectionObserver. Fires an elementVisible event when a matching element is ≥50% in the viewport.

cerbero.observeElements('.hero-cta, .pricing-card');

use(plugin)

Register a plugin that emits custom events through the Cerbero pipeline. Returns this for chaining.

import { fingerprintPlugin } from './plugins/fingerprint';

cerbero.use(fingerprintPlugin());

destroy()

Remove all event listeners, disconnect observers, clear intervals, and terminate the Web Worker.

cerbero.destroy();

Event Reference

All events share a base shape:

{
  "type": "click",
  "time": 1720000000000,
  "after": 1234,
  "data": { ... }
}
  • time — Unix timestamp (ms) when the event was processed
  • after — ms elapsed since page load
  • data — event-specific payload

click

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" }
    }
  }
}

rageClick

Fires when the same element is clicked 3+ times within 500 ms.

{
  "type": "rageClick",
  "data": {
    "count": 4,
    "target": { "identifier": "button#submit", "domType": "HTMLButtonElement" }
  }
}

scroll

Fires once per scroll stop (debounced at 100 ms).

{
  "type": "scroll",
  "data": {
    "scroll": { "fromTop": 1240, "domHeight": 5800, "percentage": 21.37 }
  }
}

mouseExit

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" }
    }
  }
}

selection

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 }
    }
  }
}

timeInPage

Fires on a repeating interval (default 2.5 s). Pauses automatically when the tab is hidden (Page Visibility API).

{
  "type": "timeInPage",
  "data": { "timePassed": 7500 }
}

performance

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
    }
  }
}

webVitals

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"
  }
}

environment

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
  }
}

network

Fires at init and again on connection change. Requires navigator.connection (Chrome/Edge).

{
  "type": "network",
  "data": { "effectiveType": "4g", "downlink": 10, "rtt": 50, "saveData": false }
}

longTask

Fires for every main-thread task exceeding 50 ms. Requires longtask PerformanceObserver support (Chrome/Edge).

{
  "type": "longTask",
  "data": { "duration": 87.3, "startTime": 1234.5 }
}

slowResource

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
  }
}

browserAI

One-shot detection of browser AI APIs (Chrome 127+). Never invokes the model.

{
  "type": "browserAI",
  "data": { "hasChromeAI": true, "chromeAIModel": "readily", "hasWebNN": false }
}

privacySignals

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 }
}

orientationChange

Fires when the screen orientation changes (mobile/tablet).

{
  "type": "orientationChange",
  "data": { "orientationType": "landscape-primary", "angle": 90 }
}

elementVisible

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" } }
}

navigation

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" }
}

pageUnload

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 your addEventListener callback when you receive this event.


keyboardShortcut

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
  }
}

Plugin System

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());

Built-in opt-in plugins

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());

Selective Tracking

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.


Batching

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));
});

Breaking Changes from v1.x

| 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 |


Contributing

See CONTRIBUTING.md.

License

MIT