easy-analytics-tracker
v0.1.0
Published
Lightweight, dependency-free browser client for first-party behavioural analytics. Batches events in memory and delivers them to your own HTTP endpoint via sendBeacon or fetch.
Maintainers
Readme
easy-analytics-tracker
A lightweight, dependency-free browser client for first-party behavioural
analytics. It batches track() calls in memory and delivers them to an HTTP
endpoint that you operate. No data is sent to third-party services.
The package provides a framework-agnostic core and an optional React adapter.
Installation
npm install easy-analytics-trackerreact and react-dom are optional peer dependencies, required only when
importing from easy-analytics-tracker/react.
Quick start
import { createTracker } from "easy-analytics-tracker";
export const tracker = createTracker({
endpoint: "https://api.example.com/analytics/events",
source: "web-app",
getToken: () => sessionStorage.getItem("access_token"),
});
tracker.track("menu_item_clicked", { item: "agenda" });Omit getToken on surfaces where users are not authenticated. Events are then
delivered without an Authorization header and treated as anonymous by the
receiving service.
React
import { TrackerProvider, useTracker } from "easy-analytics-tracker/react";
function Root() {
return (
<TrackerProvider
config={{
endpoint: "https://api.example.com/analytics/events",
source: "web-app",
getToken: () => sessionStorage.getItem("access_token"),
}}
>
<App />
</TrackerProvider>
);
}
function SaveButton() {
const { track } = useTracker();
return (
<button onClick={() => track("document_saved", { format: "pdf" })}>
Save
</button>
);
}TrackerProvider reads config once, when it mounts. To apply a different
endpoint or source at runtime, remount the provider with a new key.
Behaviour
Non-blocking. track() never throws and never awaits network activity on the
calling path. Each event is appended to an in-memory ring buffer and delivered
later as part of a batch.
Batching. A flush is triggered by whichever of the following occurs first:
the flush interval elapses (10 seconds by default), the buffer reaches the
flushAt threshold (20 events by default), the document becomes hidden, or the
page begins to unload. Each request carries at most 50 events and stays under
64 KB; larger backlogs are split across consecutive requests.
Transport. Anonymous batches under 64 KB are sent with
navigator.sendBeacon, which is guaranteed to complete during page unload.
Authenticated batches, and any batch that exceeds the beacon size limit, are
sent with fetch using keepalive: true, because the Beacon API cannot set an
Authorization header.
Retries. A network failure, a 429, or a 5xx response causes the batch to
be returned to the buffer and retried with exponential backoff (10 s, 20 s,
40 s, 80 s, then 120 s between attempts). Any other 4xx response is treated as
a permanent rejection and the batch is discarded. The buffer holds 1,000 events;
when full, the oldest events are dropped. Nothing is persisted across page
reloads.
Identity. The client never transmits a user identifier. When getToken
returns a value, it is sent as a bearer token and the receiving service is
expected to resolve the user from it. Anonymous sessions are keyed by a random
UUID stored in localStorage under eds_anon_id, which persists until reset()
is called.
API
createTracker(config): Tracker
| Option | Default | Description |
| -------------------- | ---------- | ------------------------------------------------------------------ |
| endpoint | required | Absolute URL that batches are posted to. |
| source | required | Identifier for the emitting application, included in every batch. |
| getToken | — | Returns the current bearer token, or null/undefined if none. |
| flushIntervalMs | 10000 | Interval between timer-driven flushes. |
| flushAt | 20 | Buffer size that triggers an immediate flush. |
| bufferCap | 1000 | Maximum buffered events; the oldest are dropped beyond this. |
| maxPropertiesBytes | 12288 | Events whose serialised properties exceed this size are dropped. |
| debug | false | Log dropped events and transport failures to the console. |
getToken is invoked on every flush and its result is never cached. If it
throws, the batch is sent as anonymous.
Tracker
| Method | Description |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| track(name, properties?) | Queue an event. name must match ^[a-z0-9]+(_[a-z0-9]+)*$ and be at most 100 characters. properties is an optional JSON object and should not contain personal data. |
| identify(userId) | Call after authentication. Emits a user_identified event and flushes immediately so that events queued while anonymous are delivered under the same session. Repeat calls with the same id are ignored; the id itself is not transmitted. |
| reset() | Call on sign-out. Clears the identified user and rotates the anonymous id. |
| flush() | Send buffered events now, bypassing any active backoff. Resolves when the attempt has settled. |
| shutdown() | Remove timers and event listeners. The tracker is inert afterwards. |
Server contract
Each batch is delivered as a single POST request with
Content-Type: application/json. When getToken returns a value, the request
also carries Authorization: Bearer <token>.
{
"source": "web-app",
"events": [
{
"event_id": "d94f9c8e-4a2b-4c1d-9e3f-1a2b3c4d5e6f",
"event_name": "menu_item_clicked",
"occurred_at": "2026-09-01T10:32:11.204Z",
"anonymous_id": "b1c2d3e4-5f6a-4b7c-8d9e-0f1a2b3c4d5e",
"properties": { "item": "agenda" },
"url": "https://app.example.com/agenda"
}
]
}| Field | Description |
| -------------- | ------------------------------------------------------------------------------------------------------------ |
| event_id | UUID generated per event. A retry following a partial failure may resend an event, so deduplicate on this key if exactly-once semantics are required. |
| event_name | The name passed to track(). |
| occurred_at | ISO 8601 UTC timestamp of the track() call. |
| anonymous_id | The per-browser anonymous id described above. |
| properties | The object passed to track(), if any. |
| url | The page URL at the time of the call, with the query string removed. |
The receiving service should respond with any 2xx status; the client does not
read the response body. Respond with 429 or 5xx to request a retry, or with
any other 4xx to have the batch discarded.
Development
npm install
npm run typecheck
npm test
npm run buildnpm run build uses tsup to emit ESM, CommonJS, and type declarations to
dist/.
Releases are published to npm from CI when a v* tag is pushed.
License
MIT
