🚀 Live demo: https://pathpulse-tracker.onrender.com
A modern, self-hosted website behaviour analytics tool. It tracks how visitors interact with a site — clicks, cursor movement, scrolling, attention, frustration — and turns those signals into live dashboards, heatmaps, scroll maps and full session replays.
| Signal | Description |
|---|---|
| Clicks | Element tag, id, text, classes, page + viewport coordinates, and whether the target was interactive |
| Rage clicks | Automatic detection of 3+ rapid clicks in the same spot — a classic frustration signal |
| Dead clicks | Clicks on non-interactive elements — hints at missing affordances |
| Cursor movement | Throttled mouse tracking with distance + velocity stats |
| Scroll behaviour | Scroll events, max depth %, per-section reach |
| Section dwell | Time spent in each page section (IntersectionObserver based) |
| Engaged vs idle time | 15s no-activity idle detection; engagement time excludes idle and hidden-tab time |
| Tab visibility | Tab switches away and back |
| Form interactions | Field focus/blur (names only, never content), keystroke counts, submissions |
| Navigation | In-page nav clicks with destination |
| Device & context | Viewport, screen, device class (desktop/tablet/mobile), language, platform, referrer, new vs returning visitor |
Privacy-first by design: anonymous visitor ids, no cookies, no PII, no keystroke content.
- Live monitor — active sessions, real-time event stream (3s polling), rage-click alerts, per-visitor "watch" buttons
- Overview — KPI cards (engagement, scroll depth, clicks/session, rage, dead clicks, form submits), activity timeline, scroll-depth histogram, device split, section dwell chart, most-clicked elements, frustration signals
- Heatmaps — true canvas density heatmaps (alpha accumulation + colour LUT, additive glow rendering that pops on the dark demo page), click dot map, and a scroll reach map showing how far visitors get, rendered over a scaled clone of the real page, with desktop/tablet/mobile filters and opacity control
- Sessions — filterable table (device, live-only, search) with quality badges and one-click delete
- Session replay — watch any recorded visit as a video: animated cursor, scroll replay, click/rage pings, play/pause, 1–8× speed, scrubable timeline with click markers, synced event stream
- Sample data — one click seeds 12 realistic simulated sessions (buyers, browsers, rage-clickers, form fillers…) so every view is instantly explorable; coordinates are calibrated against the real page layout
- Dark mode, responsive layout, live REC indicator and click-dot overlay on the tracked site
- Next.js 16 (App Router, TypeScript)
- Tailwind CSS 4 + shadcn/ui (teal/amber dashboard design system, dark mode via
next-themes) - Glassmorphism demo storefront (backdrop-blur glass panels over an animated aurora gradient + starfield, see
src/app/globals.css) - Prisma ORM + SQLite (zero-config persistence in
db/custom.db) - TanStack Query (live polling data flow)
- Recharts (analytics charts)
- Lucide icons, canvas 2D heatmap engine (no external heatmap lib)
- Node.js 18+ (Node 20 LTS recommended) — install from nodejs.org or with
nvm install 20or Bun 1.0+ — install from bun.sh - Git (to clone the repo) — or download the project as a ZIP
- That's it: no external database, no API keys, no
.envrequired. Persistence uses a bundled SQLite file.
Works the same on macOS, Windows and Linux.
git clone <your-repo-url> pathpulse
cd pathpulse
# (or unzip the downloaded archive and cd into it)npm install # npm
# or
bun install # bun — noticeably faster
# or
pnpm install # pnpm and yarn also worknpx prisma db push # npm / pnpm users
bunx prisma db push # bun usersThis creates db/custom.db and the TrackedSession + TrackedEvent tables. The Prisma datasource is preconfigured in prisma/schema.prisma, so no environment variables are needed.
Optional: custom database location or MySQL/Postgres
- Move the SQLite file: set
DATABASE_URL="file:./wherever/you/want.db"in a.envfile at the project root, then re-runnpx prisma db push. - Switch engine: replace the
datasource db { provider = "sqlite" }block inprisma/schema.prismawithpostgresqlormysqland a matchingDATABASE_URL, then runnpx prisma db pushagain. The rest of the app is engine-agnostic.
npm run dev
# or
bun run devOpen http://localhost:3000 — the demo site is tracked from your very first click.
npm run build && npm run start
# or
bun run build && bun run start| Step | Command (npm) | Command (bun) |
|---|---|---|
| Install | npm install |
bun install |
| Database | npx prisma db push |
bunx prisma db push |
| Develop | npm run dev |
bun run dev |
| Build | npm run build |
bun run build |
| Serve | npm run start |
bun run start |
| Symptom | Fix |
|---|---|
Port 3000 is already in use |
Stop the other process, or run npm run dev -- -p 3001 and open http://localhost:3001 |
PrismaClient did not initialize / schema errors |
Re-run npx prisma db push and restart the dev server so the client regenerates |
| Install fails on Node < 18 | Upgrade Node (nvm install 20 && nvm use 20) |
| Charts flicker after schema changes | Clear db/custom.db (delete the file, re-run npx prisma db push) or press Clear all in the dashboard |
| Windows PowerShell blocked scripts | Run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned once, or use npx commands directly |
PathPulse needs a Node.js server (Next.js + SQLite/Prisma), so it cannot run on purely static hosts like GitHub Pages. Render provides a free tier that supports persistent disks for SQLite.
# Inside the project folder
git init # if not already a git repo
git add .
git commit -m "feat: initial PathPulse release"
# Create a repo on github.com, then:
git remote add origin https://github.com/<your-username>/<your-repo>.git
git branch -M main
git push -u origin main- Go to render.com and sign in (GitHub login is easiest).
- Click New → Web Service and connect your GitHub repo.
- Render will auto-detect the
render.yamlblueprint. Accept the defaults:
| Setting | Value |
|---|---|
| Build command | npm install && npx prisma generate && npx prisma db push && npm run build && cp -r .next/static .next/standalone/.next/static && cp -r public .next/standalone/public |
| Start command | node .next/standalone/server.js |
| Node version | 20 (set via Environment → NODE_VERSION=20) |
| Disk | Mount at /opt/render/project/src/db, 1 GB |
- Add these Environment Variables in the Render dashboard:
| Key | Value |
|---|---|
NODE_ENV |
production |
DATABASE_URL |
file:/opt/render/project/src/db/custom.db |
NODE_VERSION |
20 |
-
Click Deploy. First build takes ~3 minutes. When it goes green, your public URL (e.g.
https://pathpulse.onrender.com) is live. -
Update this README — paste your live URL in the badge at the top:
> 🚀 **Live demo:** https://pathpulse.onrender.com
Note: The Render free plan spins down after 15 minutes of inactivity. The first request after a cold start takes ~30 seconds to wake up. Upgrade to the $7/month Starter plan to keep it always-on.
- You land on the Lumina Voyages demo site — a glassmorphism aurora-travel storefront that is fully tracked. Click things, scroll, fill the contact form.
- Notice the REC badge and live stats (engaged time, clicks, depth) in the top bar, plus amber dots marking your clicks.
- Switch to Dashboard → the Live tab shows your own session in real time.
- Press Load sample data to add 12 simulated visitors, then explore:
- Overview for aggregate charts,
- Heatmaps for click density and scroll reach,
- Sessions → Replay to watch a recorded visit,
- the Watch buttons on the Live tab to jump straight into a replay.
- Toggle dark mode from the top bar any time.
src/
├── app/
│ ├── page.tsx # App shell: site/dashboard views + tracker wiring
│ ├── layout.tsx # Theme provider, metadata, toaster
│ ├── globals.css # Teal design tokens, scrollbars, glass/starfield/aurora utilities
│ └── api/
│ ├── track/route.ts # POST — event ingest (session upsert)
│ ├── analytics/route.ts # GET — aggregates for dashboard (KPIs, charts, heatmap points)
│ ├── sessions/route.ts # GET/DELETE — session list / clear all
│ ├── sessions/[id]/route.ts# GET/DELETE — full session + events
│ └── seed/route.ts # POST/DELETE — sample data generator (layout-calibrated)
├── lib/tracker/
│ ├── tracker.ts # PathPulseTracker SDK (capture, batching, sendBeacon)
│ ├── types.ts # Shared types (events, sessions, DTOs)
│ ├── heatmap.ts # Canvas density heatmap + reach colour engine
│ └── format.ts # Formatting helpers
└── components/
├── demo/DemoSite.tsx # Lumina Voyages glass storefront (live + static modes)
└── dashboard/
├── Dashboard.tsx # Tab shell, queries, seed/clear
├── LiveTab.tsx # Real-time monitor
├── OverviewTab.tsx # KPIs + charts
├── HeatmapTab.tsx # Heatmap / dots / scroll reach
├── SessionsTab.tsx # Session table
├── ReplayTab.tsx # Session replay player
└── shared.tsx # KPI cards, event meta, badges
prisma/schema.prisma # TrackedSession + TrackedEvent models
| Endpoint | Method | Purpose |
|---|---|---|
/api/track |
POST | Ingest a batch: { sessionId, visitorId, startedAt, ended, device, returning, summary, events[] } |
/api/analytics |
GET | Aggregates: totals, active sessions, recent events, rage events, top targets, scroll histogram, devices, section dwell, timeline, heatmap points |
/api/sessions?limit=100 |
GET | Session list with live status and event counts |
/api/sessions |
DELETE | Clear all data |
/api/sessions/:id |
GET | Full session detail + ordered events |
/api/sessions/:id |
DELETE | Delete one session |
/api/seed |
POST {count} |
Generate simulated sessions |
/api/seed |
DELETE | Alias for clearing all data |
A window.PathPulse handle is exposed in the browser console:
window.PathPulse.stats() // live session stats
window.PathPulse.flush() // force-upload the event buffer- Capture —
PathPulseTrackerattaches to the tracked container and listens (capture phase) for clicks, moves, keys, focus, submit, scroll, visibility and resizes. Coordinates are recorded relative to the page content, so replays and heatmaps stay aligned at any viewport size. - Enrichment — each event is timestamped (
tms since session start) and enriched: dead/rage flags, section context, element labels, viewport geometry (sw/sh). - Ship — events are batched and POSTed every 4 seconds;
navigator.sendBeaconguarantees delivery when the tab closes. - Store — the API upserts the session summary and appends events in SQLite (capped at 20k events per session).
- Visualise — the dashboard polls aggregates; the heatmap normalises click points per layout width; the replay interpolates cursor position and scroll position between recorded events.
- Anonymous, rotating per-browser visitor id (localStorage) — no cookies
- No IP storage, no keystroke content, no form input values
- Rage/dead click detection runs client-side; only the flags are uploaded
- All data stays in your own SQLite file — delete everything with one button
- Track your own page: wrap any page in a container
divand pass it toPathPulseTracker.attach(el)(seesrc/app/page.tsx). Mark sections withdata-section="..."for dwell analytics. - Branding: design tokens live in
src/app/globals.css(--primary, chart colours). - Tracking knobs: idle timeout, throttle intervals and buffer caps are constants at the top of
src/lib/tracker/tracker.ts.
MIT — do whatever you like, attribution appreciated.