Lightweight, zero-dependency behavioural telemetry SDK. Drop one script tag — get clicks, scrolls, keypresses, heatmap data, and a clean JS API.
| Telesense | Heap | Mixpanel | Hotjar | |
|---|---|---|---|---|
| Self-hosted | ✅ | ❌ | ❌ | ❌ |
| Zero dependencies | ✅ | ❌ | ❌ | ❌ |
| Bundle size | <20 kB | ~120 kB | ~80 kB | ~60 kB |
| Custom transport | ✅ | ❌ | ❌ | ❌ |
| TypeScript types | ✅ | partial | partial | ❌ |
| Open source | ✅ | ❌ | ❌ | ❌ |
<!-- 1. Optional config -->
<script>
window.TELE_CONFIG = {
endpoint: '/telemetry', // your backend
flushInterval: 5000 // ms between auto-flushes
};
</script>
<!-- 2. Load SDK -->
<script src="https://cdn.jsdelivr.net/npm/telesense/dist/tele.umd.min.js"></script>
<!-- 3. tele is globally available -->
<script>
tele.on('click', ev => console.log('click at', ev.x, ev.y));
</script>npm install telesenseimport tele from 'telesense';
tele.config({ endpoint: '/telemetry' });
tele.on('*', ev => console.log(ev));
tele.track('signup_complete', { plan: 'pro' });| Type | Payload fields |
|---|---|
click |
button, x, y, element |
mousemove |
x, y (throttled 100 ms) |
scroll |
scrollX, scrollY, percentX, percentY |
keydown / keyup |
key, code, target — sensitive fields auto-masked |
visibility |
state (active | inactive) |
resize |
width, height |
| custom | anything you pass to tele.track() |
Sensitive inputs (type="password", name="card*", autocomplete="cc-*") are detected automatically — key is replaced with [masked].
Subscribe to an event type. Use '*' to receive all events.
tele.on('click', ev => { /* ev.x, ev.y, ev.element */ });
tele.on('*', ev => sendToAnalytics(ev));Remove a specific listener.
Record a custom event. Merged with sessionId, ts, page.
tele.track('video_play', { videoId: 'abc123', position: 0 });
tele.track('form_submit', { formId: 'checkout', valid: true });Send the in-memory queue immediately (outside the normal interval).
Snapshot of all queued events not yet flushed.
Clear the queue without sending (useful for logout / session change).
Update config at runtime — safe to call after the SDK loads.
// Set endpoint after user authenticates
tele.config({ endpoint: `/telemetry?userId=${user.id}` });Attach or detach all DOM listeners and the auto-flush timer.
Create a completely isolated second instance — separate queue, session, and config.
const adminTele = tele.create({ sessionId: `admin-${uid}`, endpoint: '/admin-tele' });The session ID for this instance (auto-generated or from config.sessionId).
Current version string, e.g. "1.0.0".
window.TELE_CONFIG = {
// ── transports (pick one, or use onFlush for a custom transport)
endpoint: '', // POST URL
supabaseUrl: '', // Supabase project URL
supabaseAnonKey: '', // Supabase anon/public key
// ── behaviour
flushInterval: 5000, // ms between auto-flushes
maxQueue: 500, // max in-memory events before forced flush
maxStored: 20000, // max events in localStorage fallback
mouseThrottle: 100, // ms between mousemove samples
sessionId: null, // supply your own or one is generated
// ── toggle individual event types
capture: {
click: true,
mousemove: true,
scroll: true,
keydown: true,
keyup: true,
visibility: true,
resize: true,
},
// ── custom transport (overrides endpoint + supabase)
onFlush: null, // (events: TeleEvent[]) => void
// ── per-event hook (fires synchronously, before queue)
onEvent: null, // (event: TeleEvent) => void
};tele.config({ endpoint: 'https://your-api.com/telemetry' });Expected shape: POST with Content-Type: application/json, body { sessionId, events[] }.
-- Run once in Supabase SQL editor
create table telemetry_events (
id bigint generated always as identity primary key,
session_id text not null,
event_json jsonb not null,
created_at timestamptz default now()
);
alter table telemetry_events enable row level security;
create policy "anon_insert" on telemetry_events for insert to anon with check (true);
create policy "anon_select" on telemetry_events for select to anon using (true);tele.config({
supabaseUrl: 'https://xxxx.supabase.co',
supabaseAnonKey: 'your-anon-key',
});tele.config({
onFlush: async (events) => {
await fetch('/my-endpoint', {
method: 'POST',
body: JSON.stringify(events),
});
},
});If no transport is configured, events are stored in localStorage under tele_events (up to maxStored entries). This is the default fallback when any transport fails.
A minimal reference backend is included in demo/server.js. It exposes:
| Method | Path | Description |
|---|---|---|
POST |
/telemetry |
Receive a batch of events |
GET |
/events |
Retrieve all stored events |
GET |
/events/stats |
Event counts by type + session count |
DELETE |
/events/reset |
Clear the store |
node demo/server.js
# or with a custom port
PORT=4000 node demo/server.jsFull types ship with the package — no @types/ install needed.
import tele, { TeleEvent, TeleConfig, TeleInstance } from 'telesense';
tele.on('click', (ev: TeleEvent) => {
console.log(ev.x, ev.y, ev.element?.tag);
});
const cfg: TeleConfig = {
endpoint: '/telemetry',
onEvent: (ev: TeleEvent) => updateUI(ev),
};
tele.config(cfg);git clone https://github.com/EbParsa/telesense
cd telesense
npm install
# Build (produces dist/)
npm run build
# Build + watch
npm run build:watch
# Tests (23 tests, <1 s)
npm test
# Run the demo with a live backend
npm run demo
# → http://localhost:3000- Fork → branch → PR against
main. - Tests must pass:
npm test. - Keep the minified build under 20 kB.
- One feature / fix per PR — keep diffs focused.
MIT © EbParsa