peekaboo-events
v1.0.0
Published
Non-blocking React analytics hook powered by sendBeacon
Maintainers
Readme
Peekaboo Events
Peekaboo Events is a lightweight, non-blocking analytics library for React. It batches events, enriches them automatically with device/browser/session context, and flushes them via sendBeacon (with a fetch fallback) — so tracking never blocks the UI or affects user experience.
📖 Documentation Guide
Start here:
| Your Goal | Read This | |-----------|-----------| | I'm using this package | CONSUMER_GUIDE.md ⭐ — Setup, auth, patterns, production checklist | | Try it out quickly | DEMO_SETUP.md — Run the demo in 2 minutes | | Understand how it works | ARCHITECTURE.md — Data flow, merge order, lifecycle | | Debug CORS errors | CORS_TROUBLESHOOTING.md — DevTools debugging guide | | API reference | This README (scroll down) |
Installation
npm install peekaboo-eventsor using Yarn:
yarn add peekaboo-eventsua-parser-js is installed automatically as a dependency (used for browser/OS/device detection). react is a peer dependency — your app must already have React ≥16.8 installed.
Setup
Call initAnalytics once, at your app's entry point, before any component tries to track events.
// main.tsx / App.tsx
import { initAnalytics } from 'peekaboo-events';
initAnalytics({
trackingUrl: 'https://my-backend.com/api/analytics',
instanceId: 'your-tenant-id', // identifies this instance to your backend
sessionProperties: { // optional, session-level properties added to every event
sessionKey: 'abc-123',
userId: '[email protected]',
},
defaultProperties: { // optional, default properties for every event
appVersion: '2.4.1',
environment: 'production',
},
flushIntervalMs: 5000, // optional, default 5000
maxQueueSize: 20, // optional, default 20
});Events tracked before initAnalytics runs are dropped — make sure this call happens early (e.g. before your router mounts).
Usage
import { useAnalytics } from 'peekaboo-events';
const MyComponent = () => {
const { trackEvent } = useAnalytics('MyComponent');
return (
<button onClick={() => trackEvent({ event: 'button_clicked', properties: { buttonId: 'submit' } })}>
Submit
</button>
);
};
export default MyComponent;Automatic lifecycle tracking
The hook automatically fires a component_unmounted event when the component unmounts — no need to call trackEvent manually for this.
Custom events and extra data
properties accepts any additional fields you want attached to a specific event — including nested objects for arbitrary metadata:
trackEvent({
event: 'form_submitted',
properties: {
formId: 'signup',
plan: 'pro',
meta: { experimentVariant: 'B', referrer: 'email' },
},
});Sending data with every event
If you have data that should go out with every tracked event (app version, environment, build number, feature flags, etc.), set it once via defaultProperties in initAnalytics rather than passing it on every trackEvent call:
initAnalytics({
trackingUrl: 'https://my-backend.com/api/analytics',
instanceId: 'tenant-abc-123',
key: 'encrypted-identifier-string',
defaultProperties: {
appVersion: '2.4.1',
environment: 'production',
},
});Merge order per event is: auto-collected properties → defaultProperties → per-call properties. A per-call value always wins if the same key is used at more than one level.
Overriding the screen name
By default, onScreen is set to the component name passed into useAnalytics(componentName). You can override it per-call:
trackEvent({ event: 'modal_opened', onScreen: 'UpgradeModal' });Identifying users
By default, events are attributed to an auto-generated, persisted distinctId (stored in localStorage, stable across sessions until cleared). Once you know who the user is, call identify:
import { identify, reset } from 'peekaboo-events';
// after login
identify(user.id);
// after logout
reset();Once identified, distinctId reflects the known user ID instead of the anonymous one.
Middleware
Register middlewares to inspect, transform, or veto events before they're queued. Middlewares run in registration order — the output of one feeds into the next.
import { addMiddleware } from 'peekaboo-events';
// consent gate — drop events until the user accepts tracking
addMiddleware((event) => (hasUserConsented() ? event : null));
// scrub sensitive fields before they ever leave the browser
addMiddleware((event) => {
delete event.properties.email;
return event;
});
// dev-only logging, doesn't affect what's sent
addMiddleware((event) => {
if (process.env.NODE_ENV === 'development') console.log('[peekaboo]', event);
return event;
});- Return the event (modified or not) to let it continue to the next middleware and eventually the queue.
- Return
nullto drop the event entirely — no further middlewares run, and nothing is queued. - If a middleware throws, only that event is dropped; tracking continues normally for subsequent events.
- Middlewares can be registered any time — via
addMiddleware()directly, or up front throughinitAnalytics({ middlewares: [...] }).
How it works
- Batching: Events are queued in memory and flushed together, not sent one-by-one.
- Flush triggers: Automatic on an interval (
flushIntervalMs), when the queue hitsmaxQueueSize, when the tab is hidden, and onbeforeunload. - Non-blocking delivery: Flushes use
navigator.sendBeaconwhere available; falls back tofetchwithkeepalive: trueif the beacon fails or isn't supported. - Automatic enrichment: Every event is stamped with a
uuid,timestamp, and apropertiesobject containing device, browser, OS, session, and identity context — collected automatically from the browser. - Lifecycle tracking:
component_unmountedfires automatically per hook instance.
Event shape
Each tracked event looks like this:
{
"uuid": "019f4b6e-d6f7-7c10-9237-d21ade04159a",
"event": "button_clicked",
"timestamp": "2026-07-10T09:49:43.591Z",
"properties": {
"deviceType": "Desktop",
"browser": "Chrome",
"browserVersion": "148.0.0.0",
"browserLanguagePrefix": "en",
"rawUserAgent": "Mozilla/5.0 ...",
"os": "Mac OS X",
"osVersion": "10.15.7",
"screenWidth": 3440,
"sessionId": "019f4b54-7f96-7419-a82a-0987938a936c",
"distinctId": "019f3bf9-0f7e-7149-80f8-0d9df0163053",
"timestamp": "2026-07-10T09:49:43.030Z",
"key": "encrypted-identifier-string",
"onScreen": "MyComponent",
"buttonId": "submit"
}
}Fields collected automatically: uuid, timestamp, deviceType, browser, browserVersion, browserLanguagePrefix, rawUserAgent, os, osVersion, screenWidth, sessionId, distinctId, key, onScreen (defaulted). Anything in config.defaultProperties or passed in properties on trackEvent is merged in alongside these.
createdAt is intentionally not set client-side — your backend should stamp this on ingest.
API reference
| Export | Description |
|---|---|
| initAnalytics(config) | Configures tracking URL, instance ID, identifier key, default properties, and flush behavior. Call once at bootstrap. |
| useAnalytics(componentName) | React hook. Returns { trackEvent } scoped to the given component name. |
| identify(userId) | Associates future events with a known user's distinctId. |
| reset() | Clears identity; future events fall back to the anonymous distinctId. |
| track(input) | Low-level, framework-agnostic event tracking — use outside React if needed. |
| flush() | Manually force an immediate flush of the queue. |
| addMiddleware(fn) | Register a middleware to inspect, transform, or veto events before they're queued. |
initAnalytics config
| Option | Type | Required | Default | Purpose |
|---|---|---|---|---|
| trackingUrl | string | Yes | — | Endpoint that receives event batches |
| instanceId | string | Yes | — | Identifies this app instance (sent separately, not in events) |
| sessionProperties | Record<string, unknown> | No | undefined | Session-level data added to every event (e.g., sessionKey, userId) |
| defaultProperties | Record<string, unknown> | No | undefined | Default data merged into every event (e.g., appVersion) |
| flushIntervalMs | number | No | 5000 | How often to send queued events (milliseconds) |
| maxQueueSize | number | No | 20 | Send immediately when queue reaches this size |
| middlewares | Middleware[] | No | undefined | Event processors/filters (run before queueing) |
Backend integration
Each flush POSTs a single JSON body to trackingUrl:
{
"instanceId": "your-tenant-id",
"events": [ /* array of event objects, shape above */ ]
}Your backend can use instanceId to route each batch to the correct downstream provider (e.g. CleverTap, Adobe) with the correct per-tenant credentials, and should stamp createdAt on receipt.
Files included
src/: Source code —core.ts(framework-agnostic queueing, enrichment, and flush logic) anduseAnalytics.ts(the React hook).dist/: Compiled output for distribution.package.json: Package metadata and build scripts.
📚 Documentation
- CONSUMER_GUIDE.md — Complete guide (setup, auth, patterns, security, production)
- ARCHITECTURE.md — Technical details (data flow, lifecycle, design decisions)
- DEMO_SETUP.md — Quick demo walkthrough
- CORS_TROUBLESHOOTING.md — Debugging CORS issues
- examples/README.md — Example implementations and auth patterns
Contributing
Feel free to open issues or submit pull requests. Contributions are welcome!
License
MIT License. See LICENSE for details.
