Hulo Global
Plugins Roadmap

Visitor Analytics changelog

Every release of @huloglobal/vendure-plugin-visitor-analytics. Latest release: v0.18.5 — 28 September 2026.

v0.18.5

28 September 2026 Latest
Fixed
  • MySQL/MariaDB regression in 0.18.4. The HAVING rewrite made trending, topQueries and zeroResultQueries fail on MySQL/MariaDB ("Unknown column 'meta' in 'HAVING'": MySQL resolves only the output alias there, Postgres only the expression). The filters now sit in WHERE as the expression, which both accept. 0.18.4 is deprecated on npm.
  • The corpus test now also runs every statement through a server-side PREPARE on MySQL/MariaDB (HULO_MYSQL_URL), so a fix for one dialect can no longer break the other unnoticed.

v0.18.4

28 September 2026
Fixed
  • Postgres. Every camelCase column on the entity tables (visitor_event, conversion_goal, abandoned_cart, product_co_view) and on the Vendure tables the plugin reads (customer, order, order_channels_channel, product, product_variant, product_translation, product_variant_translation) is quoted; NOW() - INTERVAL … became DATE_SUB(…); aliases are no longer used in HAVING; the personal-recommendations seed query no longer combines DISTINCT with an ORDER BY on an unselected column; the CAST(… AS UNSIGNED) on product ids / result counts is guarded by a digits check (Postgres raises where MySQL returns 0); the co-view upsert quotes its ON CONFLICT target; SUM(boolean) in the session summary became MAX(CASE …); boolean columns are compared with TRUE/FALSE; the abandoned-cart controller's queries go through the dialect adapter; and the boot-time abandoned_cart ALTERs quote the column name (an install without the migration got a lowercase duplicate) behind an information-schema check (MySQL 8 has no ADD COLUMN IF NOT EXISTS). A corpus test (tests/pg-corpus.test.ts, skipped unless HULO_PG_URL is set) runs all 98 statements against PostgreSQL 17 with TypeORM-quoted stand-ins for the entity and Vendure tables.
  • POST /ees/track and customerId. The customer is resolved from the Vendure session (activeUserId → customer.userId, cached 10 min); the body value is ignored, so an anonymous beacon can no longer file events under an arbitrary customer id. A storefront that never sent the session (the bundled tracker does not) sees no change: those events were already unlinked.
  • CORS. OPTIONS /ees/track answers 204 with the same headers as the POST (a JSON fetch from another origin sends a preflight). A disallowed origin gets no Access-Control-Allow-Origin header at all — the previous null is itself a valid origin value.
Changed
  • Co-view aggregation. Pairs are accumulated in memory per (A, B, channel) and written as multi-row upserts in chunks of 500 (one statement per ordered pair before: a 20-product session cost 380 round trips). The end of the last aggregated window is persisted, so overlapping runs — aggregate-now after the 6-hourly sweep — count nothing twice; aggregate-now?force=1 rebuilds regardless.
  • Live feed. One query in flight per stream (a slow tick used to queue behind itself every 5 s) and a : keepalive comment every 15 s.
  • CSV exports. Visitors and abandoned carts stream in id-ordered chunks of 5 000 with back-pressure instead of materialising up to 200 000 / 50 000 rows first. Rows are in id (chronological) order.
  • Housekeeping. Monthly, on the worker: abandoned_cart rows in status expired / converted / dismissed older than 180 days and product_co_view pairs not refreshed in 90 days are deleted in batches of 5 000.

v0.18.3

28 September 2026
Changed
  • normaliseUrl and boundedMeta moved to src/ingest-utils.ts (decorator-free, unit-tested). No behaviour change.

v0.18.2

28 September 2026
Fixed
  • Storefront identity across origins. hulo.js now keeps a first-party visitor id (localStorage) and a 30-minute session id and sends them with every batch; on a storefront hosted on a different origin than the API the cookies never travelled, so uniques equalled page views.
  • URLs. hulo.js sends location.href; events are now stored as path + query, so funnels, conversion goals and source reports match again.
  • Permissions. Self-update requires SuperAdmin; licence, purchase, portal and goal changes require UpdateSettings (everything was reachable with ReadCustomer).
  • The GraphQL visitor summary aggregates bounce rate and duration in SQL instead of loading every session row; SUM(boolean) replaced with SUM(CASE …) everywhere (Postgres).
  • A cookie containing a bare % no longer turns every beacon into a 500; UTM fields are capped to their column widths (a long utm_source used to drop the whole batch); oversized meta (large cart snapshots) is shrunk item-wise instead of cut mid-JSON, so abandoned carts are still detected.
  • The rate limiter keys on the same resolved address as everything else; proxy headers are only honoured when listed in trustedIpHeaders.
  • Public trending is capped at 7 days and memoised for a minute; CSV exports quote cells that would run as spreadsheet formulas; the journey endpoint returns only the fields the drawer shows; goal stats count completions correctly (COUNT(v.id)); goal ids and query clamps are validated.
  • Abandoned-cart scanner: landing/last URL and referrer are the real first and last (the previous expression always returned an empty string), a scan cannot overlap the previous one, the Slack post times out after 8 s, and the opt-out GET shows a confirm button while the POST performs it (mail clients prefetch GETs).
  • The IP hash salt falls back to signingSecret before the public constant.
  • Admin UI: the update banner is no longer shown twice, the copy button copies a real @, the restart poll is cancelled on destroy.
Changed
  • retention defaults to { days: 400 } (false keeps everything) and the sweeper runs on the worker only.
  • Events are inserted with insert() (no reload round trip).

v0.18.1

11 September 2026

v0.18.0

11 September 2026
Added
  • Recovery links can resume an order. issueRecoveryLink(cartId, { resumeOrderCode }) (and POST /ees/abandoned-carts/:id/recovery-link with the same body) binds the link to the visitor's open Vendure order. GET /ees/recover-cart now returns orderCode, orderState and resumable, and the new public POST /ees/recover-cart/resume?t=… adds resumeOrderCode — set only while that order is still AddingItems / ArrangingPayment. Storefronts keep re-adding items as the universal fallback.
  • Attribution. New abandoned_cart columns recoveryStep (link_issued → link_opened → resumed → converted, monotonic), convertedAt, convertedOrderId, convertedOrderCode and resumeOrderCode. The storefront reports a recovered checkout with the token-bound public POST /ees/recover-cart/converted { t, orderCode }; the scanner's own checkout_completed match sets convertedAt too. GET /ees/abandoned-carts/summary gains an attribution block (link issued / opened / resumed / converted counts, value recovered via link, opt-outs) and the list endpoint returns the new columns.
  • Email opt-out. New abandoned_cart_opt_out table. GET|POST /ees/abandoned-carts/opt-out?e=<token> (public; POST is the RFC 8058 one-click form) records an opt-out for the address in the HMAC token. Service API: isOptedOut(email) (fails closed), optOut, optIn, listOptOuts, buildOptOutLink(email) and buildListUnsubscribeHeaders(email) for the List-Unsubscribe / List-Unsubscribe-Post headers. Admin: GET /ees/abandoned-carts/opt-outs, POST /ees/abandoned-carts/opt-outs/remove { email }; the detail endpoint reports optedOut. New option abandonment.optOutSecret (defaults to recoveryLinkSecret, then signingSecret).
  • Storefront helper. hulo.resumeCart(token) and hulo.recoveryConverted(orderCode) — the helper remembers the token from restoreCart / resumeCart in sessionStorage, so the thank-you page needs one call.
  • Pure helpers exported for hosts: isResumableOrderState, advanceRecoveryStep, normaliseEmail, hashEmail, buildOptOutToken, verifyOptOutToken, buildOptOutUrl, buildListUnsubscribeHeaders.
Changed
  • The public recovery endpoints are rate-limited per client IP (recover-cart and resume 30/min, converted and opt-out 10/min) and answer with Cache-Control: no-store.
  • The new columns and the opt-out table are created at boot with ADD COLUMN IF NOT EXISTS / CREATE TABLE IF NOT EXISTS (MariaDB and PostgreSQL), so installs that do not run TypeORM migrations for plugins need no manual step. Installs that do can generate a migration as usual — the entity declares the same columns.

v0.17.2

10 September 2026
Added
  • Per-channel recovery links. abandonment.storefrontBaseUrls (channel code → storefront origin) makes a cart abandoned on a second storefront link back to that storefront; unlisted channels use storefrontBaseUrl as before.
Fixed
  • README: the storefront restore route must show the basket the way the storefront does (drawer or page) rather than assume a /cart route, and must treat an ErrorResult from addItemToOrder as a failed line. The reference storefront implementation now does both.

v0.17.1

2 September 2026
Changed
  • Branding. The bundled hulo-global-logo.svg is now the HG wordmark on the HULO black rounded square (the same mark as the huloglobal.com header), with explicit -light and -dark variants alongside the auto-switching default.

v0.17.0

2 September 2026
Added
  • Licence & billing card in the admin. Always visible: the current state (free tier, free trial with first-charge date, monthly/annual subscription, lifetime, or master licence) with the actions that apply — start the 14-day free trial or subscribe, buy lifetime, Manage billing (Stripe customer portal: update card, cancel, switch plan) and Upgrade to lifetime (the old subscription stops billing at the end of its paid period). Requires licence SDK ^0.14.0.

v0.16.1

2 September 2026
Changed
  • The 14-day free trial is now card-backed. Unlicensed installs run in the free tier; start the trial from the admin banner (monthly or annual → *Start 14-day free trial*) — Stripe collects a card, nothing is charged until day 15, cancel any time before then, one trial per customer — and the licence installs itself within a minute. The automatic no-card evaluation window is retired (licence SDK ^0.13.0).

v0.16.0

2 September 2026
Added
  • Buy licence from the admin. The evaluation / free-tier banner now has a plan picker and a Buy licence button: checkout opens in a new tab and, once payment completes, the licence installs itself — no email round-trip, no .env edit, no restart. Renewed subscription keys are picked up automatically too. New admin endpoints licence/purchase-link and licence/claim-status.
Changed
  • Requires @huloglobal/vendure-licence-sdk ^0.12.0.
  • The 7-day card trial at checkout has been retired: every install already gets the 14-day no-card evaluation, and paid plans now bill from day one.

v0.15.3

2 September 2026
Changed
  • README. Licensing paragraph now describes the 14-day fully-featured evaluation (the plugin already behaved this way); pricing unchanged at £9.95/month or £199 lifetime.

v0.15.2

2 September 2026
Changed
  • Licence SDK ^0.11.0. Master licences (one key that activates every HULO plugin) and hardware-bound keys are now accepted by the runtime licence check.
  • Branding. Refreshed HULO Global logo (inline HG monogram) in the admin UI.

v0.15.1

1 September 2026
Fixed
  • Premium background jobs now respect the tier. The abandoned-cart sweep (with its Slack notifications) and the recommendations co-view aggregation kept running after the evaluation ended; both now check premium access on every tick — and re-enable instantly when a licence is activated, no restart needed. Admin endpoints were already gated.

v0.15.0

25 August 2026
Added
  • PostgreSQL support. All of the plugin's SQL now runs on Postgres as well as MySQL/MariaDB — the licence SDK's new dialect adapter translates queries transparently at runtime, so no configuration is needed: the plugin follows whatever database your Vendure dbConnectionOptions use. Verified against PostgreSQL 17. MySQL/MariaDB installs are unaffected (byte-identical passthrough).

v0.14.1

25 August 2026
Changed
  • The update banner's "What's new" link now opens the plugin's changelog page on huloglobal.com, so you can read exactly what a release contains before updating.

v0.14.0

23 August 2026
Added
  • One-click in-app updates. The update banner now has an "Update now" button: the plugin installs the new version via your project's own package manager (yarn/npm/pnpm auto-detected), verifies it landed, and gracefully restarts under your process supervisor (pm2/systemd). Admin-only; the target version is verified against the npm registry; a failed install never restarts anything. Disable with HULO_SELF_UPDATE=off; force restart without a detected supervisor with HULO_SELF_UPDATE=force. Note: a separate worker process picks the update up on its next restart, and the admin UI itself refreshes after your next admin build.

v0.13.1

23 August 2026
Added
  • Update notifications in the admin UI. When a newer version is on npm, a dismissible banner shows current → latest with a copy-ready install command and a link to what's new. (Update data comes from the existing daily registry check — no new network calls.)

v0.13.0

21 August 2026
Added
  • In-admin licence activation. A banner on the admin page shows the evaluation countdown (or free-tier state) with a paste-your-key box: the key is verified with the exact boot-time checks and activates instantly — no .env edit, no redeploy. Persisted in the shared hulo_licence_store table and restored on boot; env/init keys always take precedence. New licence/status, licence/activate and licence/deactivate admin endpoints.

v0.12.0

21 August 2026
Added
  • 14-day full-featured evaluation. Unlicensed installs now get the complete feature set for 14 days instead of the restricted free tier. Premium analytics features now also run during the evaluation window. The clock is anchored server-side (a hashed instance id — no personal data), so reinstalling does not restart it, and it fails open: if the licence server is unreachable the plugin keeps running fully. After the window the plugin drops to the free tier; all configuration is kept and reactivates instantly with a key.

v0.11.1

2 August 2026
Fixed
  • anonymizeIp produced a malformed address (e.g. fe80::1::) for already-abbreviated IPv6 inputs. Abbreviated addresses are now left unchanged; full-form IPv6 still truncates to the first three hextets.
Added
  • Unit test suites: URL glob matcher, bot detection + IP anonymisation, proxy-header extraction.

v0.8.4

4 July 2026
Added
  • GET /ees/abandoned-carts now includes each cart's parsed items array plus a short server-rendered itemsPreview string ("Windows 10 Pro, Office 2021 Pro +2 more"), sorted by quantity descending. Admin can see WHAT was abandoned without clicking through to the detail endpoint.
  • GET /ees/abandoned-carts/:id fills in missing name (and productId when it can) on every item by looking up product_variant_translation / product_translation. Only fills where the storefront snapshot didn't already capture a name — a stored name at cart time is more accurate than a live catalog lookup (products can be renamed after abandonment).
Changed
  • List endpoint drops the raw itemsJson string from its response — clients that need the JSON blob can hit the detail endpoint. Response payload stays lean.

v0.8.3

4 July 2026
Added
  • Recommendation endpoints (trending, also-viewed, personal) now enrich every row with productName and productSlug via a single bulk lookup against product_translation. English translation preferred for multi-locale stores; single-locale installs land on their only translation. Soft-deleted products are excluded.
  • New exported type: RecommendedProduct.
Changed
  • Response row shape is now { productId, productName, productSlug, score, views } — the addition is backwards-compatible for consumers that only read productId, but callers that render the row can now show the name without a second round-trip. Admin UI updated in the parallel ee.software commit to render "Windows Server 2022 Datacenter #42" instead of just "#42".

v0.8.2

4 July 2026
Documentation
  • README rewritten to cover every 0.8.x feature: the drop-in /ees/hulo.js storefront helper, cart-abandonment configuration and lifecycle, recommendation endpoints, search analytics, journey drawer buffs. Includes a walkthrough of the storefront-side /cart/restore route customers implement to consume recovery links.
  • Endpoints table split into Public (browser-safe, CORS-permissive) vs Admin, with every 0.8.0-introduced endpoint marked inline.
  • No runtime changes — bumped to ship the new README with the npm tarball.

v0.8.1

4 July 2026
Added
  • /ees/hulo.js — the plugin now serves a drop-in typed storefront helper at this path. One <script src> tag and every event API (hulo.cartSnapshot, hulo.productView, hulo.search, hulo.checkoutCompleted, hulo.restoreCart) is available on window.hulo. Handles batching, sendBeacon on unload, and installs auto rage-click + dead-click detectors. Served with a 10-minute browser TTL + 24-hour stale-while-revalidate and permissive CORS so it works cross-origin from any storefront.

v0.8.0

4 July 2026
Added
  • New AbandonedCart entity, keyed on session id. One row per abandoned session, refreshed in place until it either converts (order placed) or expires (recovery window elapses).
  • AbandonedCartService.scan() — periodic sweep finds sessions with cart_snapshot events but no checkout_completed in the abandonment window (default 30 min). Auto-promotes previously abandoned rows to converted when the customer later checks out.
  • Ships with a boot-time timer (worker-only, 5-minute cadence). Idempotent — safe to horizontally scale, only the worker runs it.
  • Signed recovery links — POST /ees/abandoned-carts/:id/recovery-link returns a time-bounded opaque token the storefront exchanges via GET /ees/recover-cart?t=… to rebuild the exact cart. Storefront never sees the underlying items until the token is presented.
  • Slack notification for high-value abandonments — configurable threshold and webhook via the abandonment plugin option.
  • Admin API: - GET /ees/abandoned-carts — paginated list with status, value and email filters. - GET /ees/abandoned-carts/summary — totals, recovery rate, recovered vs. lost value in the window. - GET /ees/abandoned-carts/:id — detail incl. parsed items. - POST /ees/abandoned-carts/:id/status — mark recovered / dismissed / re-open. - GET /ees/abandoned-carts/export.csv — CSV export.
  • New ProductCoView aggregate table. Scanner walks recent product_view events per session, extracts every ordered pair, and increments a per-triple counter. Bounded to 20 events per session so runaway bot sessions can't skew the table.
  • Denormalised — both (A, B) and (B, A) stored — so read-side lookups are one indexed scan.
  • Runs every 6 hours on the worker; also exposed as GET /ees/recommendations/aggregate-now for admins to kick a fresh run after a data backfill.
  • Public read endpoints (safe from the storefront): - GET /ees/recommendations/also-viewed?productId=… — the "customers who viewed X also viewed…" rail on a product page. - GET /ees/recommendations/personal?visitorId=… — personalised recs for a returning visitor, from their last 10 product views over 30 days. Excludes seeds. - GET /ees/recommendations/trending?hours=24 — most-viewed products in the window. Reflects real interest, not search- console clicks.
  • Reads back over visitor_event where the storefront has fired hulo.search(query, resultsCount). Zero new schema.
  • GET /ees/search-analytics/top — top queries by volume with average results count.
  • GET /ees/search-analytics/no-results — top zero-result queries. Direct catalogue-gap intel.
  • GET /ees/search-analytics/conversion — of sessions that searched, what fraction went on to fire add_to_cart.
  • Rage-click + dead-click aggregation, keyed on URL. Store-wide hot-spot lists for pages where visitors are stuck or frustrated.
  • Per-visitor session summary with a heuristic intent label (purchase / abandon / frustrate / consider / browse / bounce) computed from event history — one glance per session in the Journey drawer instead of scrolling event rows.
  • hulo.cartSnapshot({ currency, totalMinor, itemCount, items, email })
  • hulo.productView(productId, productVariantId?)
  • hulo.search(query, resultsCount)
  • hulo.rageClick(url, selector?) / hulo.deadClick(url, selector)
  • hulo.checkoutCompleted()
  • All flow into the existing POST /ees/track endpoint with a standard shape, so admins can also fire them from any language.
Changed
  • checkout_completed is now a first-class recognised event type — the abandonment scanner uses it to auto-close matched rows.

v0.7.0

4 July 2026
Added
  • Boot-time compatibility check via the new SDK helper warnIfIncompatibleVendure(). Logs a non-fatal warning when the runtime @vendure/core version is outside the tested range. Silent when inside; fail-open on unparseable versions.
Changed
  • Peer dep on @vendure/core tightened to >=3.5.0 <4.0.0 — Vendure 3.5, 3.6 and 3.7 are all covered. Anything under 3.5 has never been tested; anything from 4.0 upwards is deferred until the changelog is reviewed.
  • Uses @huloglobal/vendure-licence-sdk@^0.6.0.

v0.6.0

23 June 2026
Added
  • Vendure Admin API GraphQL extensions. Operator endpoints are now first-class GraphQL queries alongside the existing REST admin endpoints: huloVisitorSummary, huloVisitorSources, huloVisitorTopPages, huloVisitorFunnel, huloVisitorJourney.
  • Storefront path (POST /ees/track) stays REST — it's anonymous, high-frequency, and can ingest millions of events a day; the resolver stack would add pointless overhead per event.

v0.5.0

23 June 2026
Added
  • Tier-gating on every premium feature via the SDK's isLicensed() helper. Unlicensed installs get: - a hard 100 events / UTC day cap on POST /ees/track (returns {skipped: 'free-tier-cap'} after that); - live SSE feed 402; - conversion-goal creation 402; - CSV export 402.
  • Anti-tamper heartbeat via the SDK.
Changed
  • Relicensed the GitHub source to AGPL-3.0. Published npm builds remain under the commercial licence documented at <https://huloglobal.com/legal/terms/>.
  • npm builds now include Sigstore provenance attestations.

v0.4.3

21 June 2026
Fixed
  • Dropped the conflicting display: block on mobile tables that broke the row / cell alignment.

v0.4.2

21 June 2026
Changed
  • 44px minimum tap targets on every interactive element in the admin UI.

v0.4.1

21 June 2026
Changed
  • Comprehensive README refresh — documents the full v0.4 feature set with the storefront snippet, every privacy + security option, and the conversion-goals API.

v0.4.0

20 June 2026
Added
  • Signed visitor + session cookies via the licence-sdk signValue / verifySignedValue helpers — tampered cookies are rejected.
  • Secure cookie flag is set automatically when serving over HTTPS.
  • Rate limiter (240 requests / 60s default) on POST /ees/track.
  • corsAllowedOrigins option restricts CORS reflection to the configured list (legacy wildcard preserved when empty).
  • Security headers on every response.
  • Opt-in retention sweeper via options.retention.

v0.3.3

20 June 2026
Changed
  • Mobile-friendly admin UI — summary cards reflow, tables scroll inside their card, profile drawer goes full-width.

v0.3.2

20 June 2026
Changed
  • Republish targeting @huloglobal/vendure-licence-sdk@^0.2.0.

v0.3.1

20 June 2026
Added
  • UpdateChecker integration — /ees/visitors/status endpoint returns version + update info; admin banner appears on new releases.

v0.3.0

20 June 2026
Added
  • Conversion goals — new ConversionGoal entity with a URL-glob matcher (* within segment, ** across segments). Pageviews matching a goal are tagged with goalId at ingest. CRUD endpoints (GET /ees/goals, POST /ees/goals, PUT /ees/goals/:id, DELETE /ees/goals/:id) and GET /ees/goals/stats for completion totals per period.
  • Bot detection — UA-classified isBot boolean on every event. Default keeps bot events for visibility; new dropBotEvents option skips ingest entirely.
  • Privacy controls — - honorDoNotTrack (default true) — DNT: 1 returns {stored:0, skipped:'dnt'} - anonymizeIp (default true) — IPv4 last octet / IPv6 last 80 bits dropped before storage; ipHash still uses the raw IP - requireConsent (default false) — gate ingest behind a body consent:true or cookie ees_consent=1
  • CSV export — GET /ees/visitors/export.csv?days=N (max 90).

v0.2.0

19 June 2026
Added
  • UTM attribution — utmSource / utmMedium / utmCampaign / utmTerm / utmContent and referrerDomain columns parsed from every incoming pageview URL. New GET /ees/visitors/sources admin endpoint groups visitors by (source, medium) plus per-source conversion counts (reached product page, reached cart/checkout).
  • Live-now widget — Server-Sent Events stream at GET /ees/visitors/live pushing the active-visitor count and the 20 most recent URLs every 5 seconds. SSE clients auto-reconnect.
  • Top events admin endpoint at GET /ees/visitors/top-events, paginated.
  • Custom event recipes — README section with copy-paste storefront snippets for add-to-cart, search, quote-request, newsletter signup.

v0.1.0

19 June 2026
Added
  • VisitorAnalyticsPlugin — ingest endpoint + admin dashboards.
  • VisitorEvent entity capturing pageview / unload / event rows with full UA parse, MaxMind geo enrichment, raw + hashed IP.
  • Proxy-aware IP / country / region extraction (Cloudflare, Akamai, Fastly headers all detected; falls back to MaxMind only if the upstream didn't already resolve country).
  • Admin endpoints: summary, top pages (paginated), exit pages (paginated), funnel, recent visitors (paginated), per-visitor profile + journey.
  • Admin UI: summary tiles, funnel bars, top + exit page tables, recent visitors table with clickable profile drawer.
  • Licence verification via @huloglobal/vendure-licence-sdk with revocation polling.