Skip to content

EbParsa/Telesense

Repository files navigation

Telesense

Lightweight, zero-dependency behavioural telemetry SDK. Drop one script tag — get clicks, scrolls, keypresses, heatmap data, and a clean JS API.

CI npm version bundle size license TypeScript

Live demo →


Why Telesense?

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

Quick start

Script tag (no build step)

<!-- 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 / bundler

npm install telesense
import tele from 'telesense';

tele.config({ endpoint: '/telemetry' });

tele.on('*', ev => console.log(ev));
tele.track('signup_complete', { plan: 'pro' });

Captured events

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


API reference

tele.on(type, fn)this

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

tele.off(type, fn)this

Remove a specific listener.

tele.track(type, data?)this

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

tele.flush()Promise<void>

Send the in-memory queue immediately (outside the normal interval).

tele.getQueue()TeleEvent[]

Snapshot of all queued events not yet flushed.

tele.reset()this

Clear the queue without sending (useful for logout / session change).

tele.config(patch)this

Update config at runtime — safe to call after the SDK loads.

// Set endpoint after user authenticates
tele.config({ endpoint: `/telemetry?userId=${user.id}` });

tele.start() / tele.stop()

Attach or detach all DOM listeners and the auto-flush timer.

tele.create(config?)TeleInstance

Create a completely isolated second instance — separate queue, session, and config.

const adminTele = tele.create({ sessionId: `admin-${uid}`, endpoint: '/admin-tele' });

tele.sessionId (read-only)

The session ID for this instance (auto-generated or from config.sessionId).

tele.version (read-only)

Current version string, e.g. "1.0.0".


Configuration

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

Transport options

1. Your own backend

tele.config({ endpoint: 'https://your-api.com/telemetry' });

Expected shape: POST with Content-Type: application/json, body { sessionId, events[] }.

2. Supabase (serverless, no backend required)

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

3. Custom transport

tele.config({
  onFlush: async (events) => {
    await fetch('/my-endpoint', {
      method: 'POST',
      body: JSON.stringify(events),
    });
  },
});

4. localStorage (offline / zero config)

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.


Backend (Node.js / Express)

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

TypeScript

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

Local development

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

Contributing

  1. Fork → branch → PR against main.
  2. Tests must pass: npm test.
  3. Keep the minified build under 20 kB.
  4. One feature / fix per PR — keep diffs focused.

License

MIT © EbParsa