Overview
Visitor Analytics is a self-hosted, privacy-aware visitor journey tracker. Page views, time-on-page, exit pages, configurable funnel, conversion goals, UTM attribution, bot detection. All data lives in your own database; nothing is sent to a third party.
Survives login — a visitor's pre-signin events and post-signin events share the same visitorId, so funnel analysis works across the auth boundary.
Install
Or by hand:
Storefront integration
The plugin ships an ingest endpoint at POST /ees/track. Your storefront calls it with batches of events.
Drop a small client into your storefront to record pageviews automatically:
Then call recordPageview() on every route change. For custom events (add-to-cart, search, signup, etc.) call recordEvent(type, meta) at the appropriate point.
Conversion goals
A conversion goal is a URL glob that, when matched by a pageview, counts that visitor as having completed the goal. Patterns support:
*— match zero or more chars within a path segment**— match zero or more segments (including/)- everything else is a literal substring (case-insensitive)
Examples
| Pattern | Matches |
|---|---|
/checkout/thank-you/* | Order confirmation page |
/signup | Exact: signup landing |
**/wishlist | Any wishlist page on any subdomain |
/contact?* | Contact form with any query |
Creating a goal
Once created, every matching pageview is tagged with the goalId on its visitor_event row. The admin stats endpoint at /ees/goals/stats?days=30 aggregates completions per goal.
Privacy controls
The plugin defaults to privacy-respecting behaviour. Toggle as needed:
| Option | Default | Effect |
|---|---|---|
honorDoNotTrack | true | If the visitor's request has DNT: 1 or Sec-GPC: 1, the endpoint returns 200 with skipped: 'dnt' and writes nothing. |
anonymizeIp | true | The stored ip column drops the last octet of IPv4 (or last 80 bits of IPv6). The ipHash column still uses the raw IP so "unique visitor" counts remain accurate. |
requireConsent | false | If on, the endpoint returns skipped: 'no-consent' unless the body sets consent: true or the request has cookie ees_consent=1. |
dropBotEvents | false | If on, known bot UAs are dropped entirely. Default off so bot share is visible on the dashboard. |
Bot detection
Every event is checked against an embedded list of ~45 bot UA patterns: Googlebot, Bingbot, Facebook scrapers, monitoring probes (UptimeRobot, Datadog, Pingdom), HTTP libraries (curl, wget, axios, requests, node-fetch), headless browsers (HeadlessChrome, Puppeteer, Playwright).
By default these events are stored with isBot: true so you can see bot share but they're excluded from "real human" counts in the admin dashboards. Flip dropBotEvents: true to skip ingest entirely.
Admin UI tour
Other admin views:
- Funnel — drop-off per configured step
- Exit pages — where visitors leave
- Top events — custom event distribution
- Top pages — most-visited URLs
- Live — SSE-streamed real-time count
- Journey — full per-visitor timeline (drill from any of the views above)
HTTP endpoints
| Method | Path | Description |
|---|---|---|
| POST | /ees/track | Public: ingest batch of events |
| GET | /ees/visitors/summary | Admin: top-line + daily series |
| GET | /ees/visitors/sources | Admin: top sources |
| GET | /ees/visitors/top-pages | Admin: most-visited URLs |
| GET | /ees/visitors/funnel | Admin: configurable funnel |
| GET | /ees/visitors/exit-pages | Admin: top exit pages |
| GET | /ees/visitors/top-events | Admin: top custom events |
| GET | /ees/visitors/live | Admin: SSE live-now stream |
| GET | /ees/visitors/journey/:visitorId | Admin: per-visitor timeline |
| GET | /ees/visitors/recent | Admin: recent events |
| GET | /ees/visitors/export.csv | Admin: CSV export (max 90 days) |
| GET | /ees/goals | Admin: list conversion goals |
| POST | /ees/goals | Admin: create a goal |
| PUT | /ees/goals/:id | Admin: update a goal |
| DELETE | /ees/goals/:id | Admin: delete a goal |
| GET | /ees/goals/stats | Admin: per-goal completion stats |
Troubleshooting
Visitors aren't being counted
Check the response of POST /ees/track — if skipped: 'dnt' the visitor is sending a Do-Not-Track header (and your honorDoNotTrack option is on, which it is by default). Set the option to false to override.
Goals don't seem to fire
The matcher only runs for type: 'pageview' events — custom events (type: 'event') don't trigger goals. Also, the goal cache refreshes every 60s; new goals start counting after that.
MaxMind geo isn't populating
The plugin uses the geolite2-redist package to download the GeoLite2 City DB on first use. If the download fails (network, sandboxed env), geo fields stay null. You can force a re-download with npx geolite2-redist refresh from your Vendure project root.