npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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.

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-tracker

That'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)   └──────────────────────────┘
        └──────────────────────────────────────────────┘
  1. Add the collector to your backend (one line). It mounts the ingest API and the dashboard at /tracker.
  2. Add the tracker to your frontend (one line). It autocaptures behaviour and ships it to the collector.
  3. 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 IPs

Express

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/tracker

NestJS — 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 3server/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>

Astroastro.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_request events 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" (or ph-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 visitor

Framework 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 4319
import { createTracker } from "full-tracker/browser";
createTracker({ apiHost: "https://analytics.example.com" });   // your collector origin

CORS 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_PASSWORD and a long random FULLTRACKER_SECRET (a stable secret keeps admin sessions valid across restarts).
  • Serve over HTTPS in production (the session cookie is marked Secure when 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.