full-tracker
v0.1.1
Published
Drop-in, full-stack user analytics for any JS framework. One install adds client autocapture (clicks, inputs, scroll depth, time-on-page, rage-clicks, route changes), a backend collector, and a password-protected /tracker dashboard.
Maintainers
Readme
full-tracker
Drop-in, full-stack user analytics for any JavaScript stack. One install adds:
- 🧲 Client autocapture — pageviews, time-on-page, clicks, scroll depth, input changes, rage-clicks, form submits, SPA route changes and JS errors.
- 🛰️ A backend collector — receives events, times your backend requests, and stores everything (SQLite by default — zero config).
- 📊 A built-in dashboard at
/tracker— a password-protected control panel showing every user, session, page and click. No third-party service, no data leaves your server.
Works with React, Vue, Svelte, Astro, Node, Express, NestJS, Next.js and Nuxt from a single package.
⚠️ Privacy first. By default, values typed into password / email / credit-card / and other sensitive fields are masked in the browser before they are ever sent. You are responsible for using this in line with your privacy policy and laws (GDPR/CCPA/etc.). See Privacy & masking.
Install
npm install full-trackerThat's it — one package. Subpath imports give you the right piece for your framework (full-tracker/react, full-tracker/express, …). The default storage (SQLite) works with no database setup.
How it works
┌─────────────┐ events (batched, beacon) ┌──────────────────────────┐
│ Browser │ ───────────────────────────▶ │ Collector (your backend)│
│ tracker │ POST /tracker/api/i │ • validates + stores │
└─────────────┘ │ • SQLite / PG / Mongo │
│ • serves /tracker dash │
▲ GET /tracker (super-admin only) └──────────────────────────┘
└──────────────────────────────────────────────┘- Add the collector to your backend (one line). It mounts the ingest API and the dashboard at
/tracker. - Add the tracker to your frontend (one line). It autocaptures behaviour and ships it to the collector.
- Open
/tracker, sign in with your admin password, and watch everything.
No JS backend? Run the standalone collector and point your frontend at it.
Quick start
1. Backend — mount the collector
Set two environment variables first:
export FULLTRACKER_ADMIN_PASSWORD="choose-a-strong-password" # gate the /tracker dashboard
export FULLTRACKER_SECRET="a-long-random-string" # signs admin sessions + hashes IPsExpress
import express from "express";
import { mountTracker } from "full-tracker/express";
const app = express();
mountTracker(app); // adds /tracker (collector + dashboard) and backend request tracking
app.listen(3000);Node (no framework) — standalone collector:
import { startTrackerServer } from "full-tracker/node";
await startTrackerServer({ port: 4319 });
// dashboard → http://localhost:4319/trackerNestJS — register the module:
import { Module } from "@nestjs/common";
import { TrackerModule } from "full-tracker/nest";
@Module({ imports: [TrackerModule.forRoot({ trackRequests: true })] })
export class AppModule {}Next.js (App Router) — app/tracker/[[...slug]]/route.ts:
import { createNextHandler } from "full-tracker/next";
export const { GET, POST, OPTIONS } = createNextHandler();Nuxt 3 — server/routes/tracker/[...].ts:
import { trackerNuxtHandler } from "full-tracker/nuxt";
export default defineEventHandler(trackerNuxtHandler());2. Frontend — start tracking
React — wrap your app (mark it a client component in Next.js):
import { TrackerProvider } from "full-tracker/react";
export default function App({ children }) {
return <TrackerProvider config={{ /* apiHost if cross-origin */ }}>{children}</TrackerProvider>;
}Vue 3
import { createApp } from "vue";
import { FullTrackerPlugin } from "full-tracker/vue";
createApp(App).use(FullTrackerPlugin, {}).mount("#app");Svelte — in your root +layout.svelte:
<script>
import { initTracker } from "full-tracker/svelte";
initTracker();
</script>Astro — astro.config.mjs:
import fullTracker from "full-tracker/astro";
export default defineConfig({ integrations: [fullTracker()] });Plain JS / any framework — drop a script tag (served by the collector):
<script src="/tracker/tracker.js"></script>Then open /tracker and sign in. Done. 🎉
The client posts to a blocker-friendly path (
/tracker/api/i) by default so ad/tracker blockers don't drop your events. If one still interferes, see Troubleshooting.
The /tracker dashboard
A self-contained dark-mode dashboard (no external assets) served straight from your backend:
- Overview — visitors, sessions, pageviews, avg session, avg scroll, bounce rate, clicks, rage-clicks, errors, backend requests + a traffic chart (auto-refreshes every 15s).
- Live — visitors active in the last 5 minutes and what they're viewing.
- Pages — views, unique visitors, average scroll depth and time per page.
- Clicks & Events — most-clicked elements (with rage-click counts) and a live event stream.
- Inputs — what users typed (sensitive fields shown masked 🔒).
- Sessions — every session with a full event timeline drill-down.
- Users — visitors and identified users, first/last seen.
- Countries — visitor breakdown by country with flags, derived from timezone (or a custom IP lookup), shown on the Overview.
- Date range — presets (24h / 7d / 30d / 90d) or a custom from/to picker; the chart bucket auto-switches between hourly and daily.
- CSV export — every panel has a ⤓ CSV button; ⤓ Export all (ZIP) in the top bar downloads every dataset as a single zip; the events panel also has ⤓ All events for the full event log (every column, up to 10k rows).
- Heatmaps — per-page click heatmap rendered from captured x/y coordinates.
- Filter & segment — filter every view by search / path / country / device, and toggle Hide bots.
- Compare to previous period — ▲/▼ deltas on the Overview KPIs vs the prior equal-length window.
- Privacy tools — export or erase a single visitor's data, and prune events older than N days (GDPR).
Access is restricted to whoever knows FULLTRACKER_ADMIN_PASSWORD. Login issues a signed, httpOnly session cookie (12h), with brute-force throttling. Want to use your own auth instead? Pass an isAdmin hook.
What gets tracked
| Event | When |
|---|---|
| pageview / route_change | Page load and SPA navigations (history is patched automatically) |
| page_leave | On tab hide / unload — carries active time on page and max scroll depth |
| click | Clicks on links, buttons, inputs and other interactive/labelled elements (selector, text, x/y) |
| rage_click | 3+ clicks on the same element within 1s |
| scroll_depth | 25 / 50 / 75 / 100% thresholds |
| input_change / input_focus | Field focus and committed values (masked where sensitive) |
| form_submit | Form submissions |
| error | Uncaught errors and unhandled promise rejections |
| custom | Anything you send via track() |
| backend_request | Each backend request (method, path, status, duration) |
Each event carries device/browser/OS, referrer, UTM params, viewport, anonymous id, session id and (if set) user id.
backend_requestevents are tracked separately and are never counted as visitors, sessions or users — they appear as their own "Backend reqs" metric and in the event stream.
Privacy & masking
By default captureInputs: "mask":
- Values in
input[type=password|email|tel|hidden], credit-card autocomplete fields, and any field whose name/id/placeholder looks sensitive (password, token, card, cvv, ssn, iban, …) are replaced with••••••in the browser. - Add
class="ft-mask"to mask any element's input value. - Add
class="ft-ignore"(orph-no-capture) to skip an element entirely. - The browser Do-Not-Track signal is respected by default.
- Visitors can be opted out with
tracker.optOut()(persisted).
Modes (captureInputs):
| Mode | Behaviour |
|---|---|
| "mask" (default) | Capture text, but mask sensitive fields |
| "meta" | Never capture characters — only field, length and focus/typing metadata |
| "all" | Capture everything verbatim (includes secrets — use only with consent) |
| false | Don't capture input values at all |
Custom events & identify
import { getTracker } from "full-tracker/browser";
const tracker = getTracker(); // or the instance from createTracker()
tracker.track("Signup completed", { plan: "pro" });
tracker.identify("user_123", { email: "[email protected]" }); // associate the visitor with a user
tracker.optOut(); // stop tracking this visitorFramework helpers: useTracker() (React/Vue/Svelte), useTrackEvent() (React).
No backend? (static sites)
Run the collector as its own process and point your frontend at it:
FULLTRACKER_ADMIN_PASSWORD=secret npx full-tracker --port 4319import { createTracker } from "full-tracker/browser";
createTracker({ apiHost: "https://analytics.example.com" }); // your collector originCORS for the ingest endpoint is open by default (configurable). The dashboard lives at the collector's /tracker.
Client configuration
createTracker({
apiHost: "", // collector origin; default = same origin
apiPath: "/tracker/api/i", // ingest path (kept blocker-friendly; must match server collectPath)
projectId: undefined, // when one collector serves multiple apps
autocapture: true, // master switch for clicks/inputs/forms
captureInputs: "mask", // "mask" | "meta" | "all" | true | false
captureScroll: true,
capturePageviews: true,
captureErrors: true,
maskSelectors: [], // extra selectors to mask
ignoreSelectors: [], // extra selectors to never capture
scrollThresholds: [25, 50, 75, 100],
batchSize: 20,
flushInterval: 5000,
sessionTimeout: 1800000, // 30 min inactivity
sampleRate: 1, // 0..1
respectDoNotTrack: true,
debug: false,
beforeSend: (event) => event // redact or drop events (return null to drop)
});Server configuration
mountTracker(app, {
basePath: "/tracker",
collectPath: "/tracker/api/i", // ingest path; change it (and client apiPath) if a blocker interferes
adminPassword: process.env.FULLTRACKER_ADMIN_PASSWORD,
secret: process.env.FULLTRACKER_SECRET,
storage: "sqlite", // "sqlite" | "file" | "postgres" | "mongo" | custom adapter
storagePath: "./.fulltracker/events.db",
databaseUrl: process.env.DATABASE_URL, // for postgres/mongo
projectId: "default",
trackRequests: true, // record backend_request events
ipMode: "hash", // "hash" | "store" | "none"
cors: true, // true | string[] | (origin) => boolean
rateLimit: { windowMs: 60000, max: 600 },
sessionTtl: 43200000, // 12h
retentionDays: undefined, // auto-delete older events
trustProxy: false, // read client IP from X-Forwarded-For
isAdmin: undefined, // (req) => boolean — use your own auth instead of the password
geoFromIp: undefined, // (ip) => country — precise geo; defaults to timezone-based
silent: false, // suppress all server-side console output (incl. startup warnings)
});Geo / country
Visitor country is derived from the client's timezone by default — offline, no IP geolocation, and consistent with the IP-hashing privacy stance. For precise IP-based geo, plug in any resolver (e.g. MaxMind):
import maxmind from "maxmind";
const lookup = await maxmind.open("./GeoLite2-Country.mmdb");
mountTracker(app, { geoFromIp: (ip) => lookup.get(ip)?.country?.names?.en });Logging
The library is quiet by default. Browser logs only appear when you pass debug: true
(or data-debug="true" on the script tag). Server-side, only one-time startup warnings
for misconfiguration print; pass silent: true to suppress those too.
Storage backends
| Backend | Setup | Notes |
|---|---|---|
| SQLite (default) | none | Embedded via better-sqlite3. Falls back to a file (NDJSON) store if the native module isn't available. |
| File | none | Pure-JS append-only NDJSON. Great for tiny/dev usage. |
| Postgres | npm i pg + DATABASE_URL | storage: "postgres" |
| MongoDB | npm i mongodb + DATABASE_URL | storage: "mongo" |
Troubleshooting: events captured but not arriving
If your browser console shows events being captured but the dashboard stays empty, an
ad/tracker blocker (uBlock Origin, Firefox ETP, Brave Shields) is almost certainly
dropping the ingest request. The default ingest path is already kept neutral (/api/i,
not /collect), but strict block lists can still match. Options:
- Verify in the Network tab whether the ingest request is
blocked. - Move the ingest endpoint to a custom path and point the client at it:
// server
mountTracker(app, { collectPath: "/api/m" });
// client
createTracker({ apiPath: "/api/m" });Because the collector is first-party (same origin as your app), a neutral path is rarely
blocked. The /tracker dashboard itself loads fine (it's a normal page navigation).
Security notes
- Always set a strong
FULLTRACKER_ADMIN_PASSWORDand a long randomFULLTRACKER_SECRET(a stable secret keeps admin sessions valid across restarts). - Serve over HTTPS in production (the session cookie is marked
Securewhen the request is HTTPS). - IPs are hashed by default (
ipMode: "hash"); no raw IPs are stored. - The ingest endpoint is rate-limited per IP.
License
MIT — see LICENSE.
