A tiny, dependency-free, offline-first web telemetry and error-tracking SDK you fully own.
telemetry-lite gives you privacy-friendly product analytics and automatic crash reporting without a third-party SaaS vendor. Drop telemetry.js into any web app, point it at an HTTP endpoint you control, and own your event stream.
- 🛡️ Zero Third Parties: Direct
POSTto your own endpoint. No vendor accounts, no tracking cookies, and no cross-site identifiers. - 💾 Offline-First & Durable: Events queue in
localStorageand survive page reloads and network loss. Batches retry automatically with single-flight locking so no events duplicate or drop. - 🚨 Automatic Crash Reporting: Traps uncaught errors and unhandled promise rejections with the trail of recent screen views and actions that caused them.
- 🔒 Privacy-by-Design: User IDs are SHA-256 hashed strictly on-device before transmission, URLs strip query parameters to avoid leaking secrets, and breadcrumb trails carry names only—never sensitive values.
- ⚡ Zero Dependencies, No Build Step: One standalone ~9 KB script. Just include it via
<script>or bundle with your favorite framework.
<script src="telemetry.js"></script>
<script>
Telemetry.init({ appId: "my-app", endpoint: "https://example.com/events" });
Telemetry.track("signup_completed", { plan: "pro" });
Telemetry.screen("dashboard");
</script>Include the script directly or load it asynchronously:
<script src="telemetry.js"></script>Telemetry.init({
appId: "my-app",
endpoint: "https://example.com/events",
appVersion: "1.4.0", // optional: stamped on every event
});// Track custom product events
Telemetry.track("checkout_completed", { items: 3, currency: "USD" });
// Track page / screen views
Telemetry.screen("pricing");
// Track one-time activation milestones (fires at most once ever per device)
Telemetry.trackOnce("activated", "first_project_created");
// Attach an on-device hashed user identifier
await Telemetry.identifyUser("user_12345");Flush cadence: Events flush automatically every 15 seconds, whenever a batch reaches 25 events, and cleanly on page hide / unload (
visibilitychange).
Run the complete telemetry loop locally on your machine with zero configuration:
# 1. Start the reference ingest endpoint (zero-dependency Node server)
node server/ingest.mjs # listens on http://localhost:8787/events
# 2. Serve the demo page
python3 -m http.server 8000 # open http://localhost:8000/demo/- Click the test buttons in the demo page to produce events and simulated errors.
- Watch batches print live in your ingest server terminal.
- Stop the server, keep clicking, and restart the server—the local queue automatically drains without dropping a single event.
npm test # or: node telemetry.test.js| Option | Default | Description |
|---|---|---|
appId |
(required) | Identifier for the application or website |
endpoint |
(required) | URL endpoint receiving POST { events: [...] } |
flushIntervalMs |
15000 |
Periodic timer interval between batch flushes (ms) |
maxBatch |
25 |
Maximum events per flush batch & auto-flush threshold |
storage |
localStorage |
Storage adapter { getItem, setItem } (pluggable) |
platform |
"web" |
Platform label stamped on every event payload |
appVersion |
undefined |
Version string stamped on every event payload |
errors |
true |
Automatically capture uncaught exceptions and unhandled rejections |
fetchErrors |
false |
Report failed fetch requests (status >= 500 or network drops) |
debug |
false |
Enable verbose internal console.log logging |
Telemetry.track(event, props)— Records an event with an optional flat dictionary of properties.Telemetry.screen(name, props)— Shorthand for recording ascreen_viewevent.Telemetry.trackOnce(flagKey, event, props)— Fires an event at most once per device lifetime.Telemetry.identifyUser(rawId)— Hashes the ID with SHA-256 on device and associates subsequent events withuser_hash.Telemetry.reportError(message, stack, extra)— Manually reports a caught exception through the rate-limited crash pipe with breadcrumbs.Telemetry.flush()— Returns a Promise forcing an immediate queue flush.
Batches arrive at your endpoint formatted as { "events": [ ... ] }:
{
"event_id": "b78b6716-e57c-4731-b3b4-52d83b27bcfb",
"app_id": "my-app",
"anon_id": "9b2a758e-d9a2-4a0b-9689-91894d075253",
"user_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"session_id": "7d448108-c70e-436f-b2b0-9602bc6825c9",
"event": "checkout_completed",
"props": { "items": 3, "currency": "USD" },
"platform": "web",
"app_version": "1.4.0",
"schema_version": 1,
"occurred_at": "2026-01-01T00:00:00.000Z"
}The client SDK enforces strict invariants verified by the test suite:
- Single Flight Flush: Concurrent triggers (timer, tab hide, batch threshold) share a single in-flight network request to prevent duplicated payloads.
- ID-Based Queue Eviction: Events are evicted by
event_idrather than queue index, ensuring new items enqueued during transit are never lost. - Queue Clamping: Local storage is capped at 500 items, discarding the oldest entries under extended offline periods.
- Rate-Limited Crash Reporting: Crash captures are throttled to a maximum of 10 errors per minute per device with deduplication.
MIT © pekth