HG Hulo Global

Visitor Analytics — User Manual

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

curl -sSL https://huloglobal.com/vendure-plugins/visitor-analytics/install.sh | bash

Or by hand:

# 1. Install yarn add @huloglobal/vendure-plugin-visitor-analytics # 2. Register in vendure-config.ts import {{ VisitorAnalyticsPlugin }} from '@huloglobal/vendure-plugin-visitor-analytics'; export const config: VendureConfig = {{ plugins: [ VisitorAnalyticsPlugin.init({{ publicBaseUrl: 'https://shop.example.com', licenceKey: process.env.HULO_LICENCE_KEY_VISITOR_ANALYTICS, // Privacy options — these are the defaults honorDoNotTrack: true, anonymizeIp: true, requireConsent: false, dropBotEvents: false, }}), ], }}; # 3. Migration yarn migration:generate AddVisitorAnalyticsTables yarn migration:run

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:

// utils/visitor-tracking.ts const ENDPOINT = 'https://shop.example.com/ees/track'; const CHANNEL_ID = 1; let queue: any[] = []; let flushTimer: any; export function recordPageview(url: string, title: string) {{ queue.push({{ type: 'pageview', url, title, clientTs: Date.now() }}); scheduleFlush(); }} export function recordEvent(type: string, meta: any) {{ queue.push({{ type, url: location.pathname + location.search, meta, clientTs: Date.now() }}); scheduleFlush(); }} function scheduleFlush() {{ clearTimeout(flushTimer); flushTimer = setTimeout(flush, 1000); }} function flush() {{ if (!queue.length) return; const body = JSON.stringify({{ channelId: CHANNEL_ID, events: queue }}); queue = []; // sendBeacon survives navigation navigator.sendBeacon?.(ENDPOINT, body) || fetch(ENDPOINT, {{ method: 'POST', body, headers: {{ 'content-type': 'application/json' }}, keepalive: true }}); }}

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

PatternMatches
/checkout/thank-you/*Order confirmation page
/signupExact: signup landing
**/wishlistAny wishlist page on any subdomain
/contact?*Contact form with any query

Creating a goal

curl -X POST https://shop.example.com/ees/goals \ -H "Content-Type: application/json" \ -d '{{ "channelId": 1, "name": "Checkout completed", "urlPattern": "/checkout/thank-you/*", "valueMinor": 5000, "enabled": true }}'

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:

OptionDefaultEffect
honorDoNotTracktrueIf the visitor's request has DNT: 1 or Sec-GPC: 1, the endpoint returns 200 with skipped: 'dnt' and writes nothing.
anonymizeIptrueThe 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.
requireConsentfalseIf on, the endpoint returns skipped: 'no-consent' unless the body sets consent: true or the request has cookie ees_consent=1.
dropBotEventsfalseIf 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

Visitor Journey — Summary Admin panel mockup. Last 30 days — 42,135 visitors / 89,210 sessions / 312,447 pageviews / avg 2:18 on page — Daily series — ▁▂▄▅▇▆▇█▇▆▅▄▃▄▅▆▇▆▅▄▃▂▃▄▅▆▇█▇▆ — Top sources — #1 google.com 18,402 visits — #2 direct 12,089 visits — #3 twitter.com 3,541 visits — #4 facebook.com 2,170 visits Vendure Admin Visitor Journey — Summary Last 30 days42,135 visitors 89,210 sessions 312,447 pageviews avg 2:18 on page Daily series▁▂▄▅▇▆▇█▇▆▅▄▃▄▅▆▇▆▅▄▃▂▃▄▅▆▇█▇▆ Top sources#1 google.com 18,402 visits#2 direct 12,089 visits#3 twitter.com 3,541 visits#4 facebook.com 2,170 visits
Summary — top-line counters, daily series, top sources, top countries.

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

MethodPathDescription
POST/ees/trackPublic: ingest batch of events
GET/ees/visitors/summaryAdmin: top-line + daily series
GET/ees/visitors/sourcesAdmin: top sources
GET/ees/visitors/top-pagesAdmin: most-visited URLs
GET/ees/visitors/funnelAdmin: configurable funnel
GET/ees/visitors/exit-pagesAdmin: top exit pages
GET/ees/visitors/top-eventsAdmin: top custom events
GET/ees/visitors/liveAdmin: SSE live-now stream
GET/ees/visitors/journey/:visitorIdAdmin: per-visitor timeline
GET/ees/visitors/recentAdmin: recent events
GET/ees/visitors/export.csvAdmin: CSV export (max 90 days)
GET/ees/goalsAdmin: list conversion goals
POST/ees/goalsAdmin: create a goal
PUT/ees/goals/:idAdmin: update a goal
DELETE/ees/goals/:idAdmin: delete a goal
GET/ees/goals/statsAdmin: 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.