@huloglobal/vendure-plugin-visitor-analytics 0.8.1 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +231 -57
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,6 +7,14 @@ and a per-visitor profile drawer with parsed user-agent and MaxMind
|
|
|
7
7
|
geo. Privacy-first defaults: DNT, IP anonymisation, optional consent
|
|
8
8
|
gate.
|
|
9
9
|
|
|
10
|
+
Since 0.8.0 the plugin also ships **cart abandonment** (detection,
|
|
11
|
+
signed recovery links, Slack notification, admin dashboard),
|
|
12
|
+
**co-view product recommendations** (`also-viewed` / `personal` /
|
|
13
|
+
`trending`), **site search analytics** (top queries, zero-result
|
|
14
|
+
queries, search-to-cart conversion) and **journey-drawer buffs**
|
|
15
|
+
(rage-click + dead-click hot-spot lists, per-session `intent`
|
|
16
|
+
labels).
|
|
17
|
+
|
|
10
18
|
Maintained by Wayne Garrison.
|
|
11
19
|
|
|
12
20
|
## Buy
|
|
@@ -45,55 +53,111 @@ export const config: VendureConfig = {
|
|
|
45
53
|
|
|
46
54
|
// -- Retention (opt-in) --
|
|
47
55
|
retention: { days: 365, maxRows: 50_000_000 },
|
|
56
|
+
|
|
57
|
+
// -- Cart abandonment (opt-in, since 0.8.0) --
|
|
58
|
+
// Storefront must fire cart_snapshot events (see below).
|
|
59
|
+
abandonment: {
|
|
60
|
+
windowMinutes: 30,
|
|
61
|
+
slackMinValueMinor: 5000,
|
|
62
|
+
slackWebhookUrl: process.env.HULO_ABANDONMENT_SLACK_URL,
|
|
63
|
+
recoveryLinkSecret: process.env.HULO_ABANDONMENT_SECRET,
|
|
64
|
+
recoveryLinkTtlHours: 72,
|
|
65
|
+
storefrontBaseUrl: 'https://shop.example.com',
|
|
66
|
+
},
|
|
48
67
|
}),
|
|
49
68
|
],
|
|
50
69
|
};
|
|
51
70
|
```
|
|
52
71
|
|
|
53
72
|
Add `VisitorAnalyticsPlugin.uiExtensions` to your `compileUiExtensions`
|
|
54
|
-
config.
|
|
73
|
+
config to pick up the Abandoned Carts + Analytics Insights admin pages.
|
|
74
|
+
|
|
75
|
+
## Storefront helpers
|
|
76
|
+
|
|
77
|
+
The plugin ships a **drop-in JS helper** at `/ees/hulo.js` — one script
|
|
78
|
+
tag and every event API below is available on `window.hulo`. It handles
|
|
79
|
+
batching, `sendBeacon` on unload, auto rage-click + dead-click
|
|
80
|
+
detection, and an on-mount `pageview`. Bare minimum:
|
|
81
|
+
|
|
82
|
+
```html
|
|
83
|
+
<script src="https://shop.example.com/ees/hulo.js" defer></script>
|
|
84
|
+
```
|
|
55
85
|
|
|
56
|
-
|
|
86
|
+
For a first-party integration (recommended — one bundle instead of a
|
|
87
|
+
second script tag), copy the equivalent typed helpers into your
|
|
88
|
+
storefront. The [elite.charity Qwik storefront](https://elite-software.co.uk)
|
|
89
|
+
uses this pattern. Every helper below is a thin wrapper around
|
|
90
|
+
`POST /ees/track` with a specific `meta.eventType` — the plugin's
|
|
91
|
+
server-side scanners look those event types up by name.
|
|
92
|
+
|
|
93
|
+
| Helper | When to call | What it feeds |
|
|
94
|
+
| --- | --- | --- |
|
|
95
|
+
| `hulo.pageview()` | first mount + every route change | pageview funnel, exit-page report |
|
|
96
|
+
| `hulo.productView(productId, variantId?)` | on the PDP | co-view aggregation, `also-viewed`, `trending`, `personal` recs |
|
|
97
|
+
| `hulo.addToCart(variantId, qty, unitPriceMinor)` | on the "add" button | search-to-cart conversion |
|
|
98
|
+
| `hulo.cartSnapshot({ currency, totalMinor, itemCount, items, email? })` | every cart change (add / remove / qty) | **cart abandonment detection** |
|
|
99
|
+
| `hulo.search(query, resultsCount)` | on every executed search | top-queries, zero-result queries |
|
|
100
|
+
| `hulo.checkoutCompleted(orderCode, totalMinor)` | on the thank-you page | closes any open `abandoned_cart` row for this session |
|
|
101
|
+
| `hulo.rageClick(selector)` / `hulo.deadClick(selector)` | fire yourself if you have a better signal than the auto-detector | rage-click / dead-click hot-spot lists |
|
|
102
|
+
| `hulo.restoreCart(token)` | on your `/cart/restore?t=...` route | rebuild a cart from a signed recovery link |
|
|
103
|
+
|
|
104
|
+
Full payload shapes:
|
|
57
105
|
|
|
58
106
|
```ts
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
});
|
|
75
|
-
scheduleFlush();
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function scheduleFlush() {
|
|
79
|
-
clearTimeout(flushTimer);
|
|
80
|
-
flushTimer = setTimeout(flush, 1000);
|
|
81
|
-
}
|
|
82
|
-
function flush() {
|
|
83
|
-
if (!queue.length) return;
|
|
84
|
-
const body = JSON.stringify({ channelId: CHANNEL_ID, events: queue });
|
|
85
|
-
queue = [];
|
|
86
|
-
navigator.sendBeacon?.(ENDPOINT, body) ||
|
|
87
|
-
fetch(ENDPOINT, {
|
|
88
|
-
method: 'POST', body,
|
|
89
|
-
headers: { 'content-type': 'application/json' }, keepalive: true,
|
|
90
|
-
});
|
|
91
|
-
}
|
|
107
|
+
hulo.cartSnapshot({
|
|
108
|
+
currency: 'GBP', // ISO-4217
|
|
109
|
+
totalMinor: 4995, // in pence / cents
|
|
110
|
+
itemCount: 2,
|
|
111
|
+
items: [
|
|
112
|
+
{ variantId: 42, name: 'Blue T-shirt (M)', qty: 1, unitPriceMinor: 1995, sku: 'BT-M' },
|
|
113
|
+
{ variantId: 88, name: 'Wool socks', qty: 1, unitPriceMinor: 3000 },
|
|
114
|
+
],
|
|
115
|
+
email: 'buyer@example.com', // optional — captured at checkout step 1
|
|
116
|
+
countryCode: 'GB', // optional
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
hulo.productView(product.id, selectedVariant.id);
|
|
120
|
+
hulo.search('rgb keyboard', 42); // (query, resultsCount)
|
|
121
|
+
hulo.checkoutCompleted('S2BZ54TEK', 12500); // (orderCode, totalMinor)
|
|
92
122
|
```
|
|
93
123
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
124
|
+
### Cart-restore route
|
|
125
|
+
|
|
126
|
+
The recovery link the admin mints (see below) lands on
|
|
127
|
+
`https://shop.example.com/cart/restore?t=<token>`. Your storefront
|
|
128
|
+
needs a route that:
|
|
129
|
+
|
|
130
|
+
1. Reads `?t=` from the URL
|
|
131
|
+
2. Calls `GET /ees/recover-cart?t=<token>` to fetch `{ items: [...] }`
|
|
132
|
+
3. Re-adds each `{ variantId, qty }` via your Vendure order API (usually
|
|
133
|
+
`addItemToOrder(productVariantId, quantity)`)
|
|
134
|
+
4. Navigates to `/cart` when done
|
|
135
|
+
|
|
136
|
+
Guard against silently overwriting a live cart — if the visitor
|
|
137
|
+
already has items, show a "you already have items in your cart"
|
|
138
|
+
message and let them reconcile. See
|
|
139
|
+
[elite.charity's `src/routes/cart/restore/index.tsx`](https://github.com/exceeded/elite-software-frontend/blob/main/src/routes/cart/restore/index.tsx)
|
|
140
|
+
for a working reference implementation.
|
|
141
|
+
|
|
142
|
+
### Legacy: hand-rolled tracker
|
|
143
|
+
|
|
144
|
+
If you prefer to skip `/ees/hulo.js`, the raw POST shape is unchanged:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
const body = JSON.stringify({
|
|
148
|
+
channelId: 1,
|
|
149
|
+
events: [{
|
|
150
|
+
type: 'event',
|
|
151
|
+
url: location.href,
|
|
152
|
+
meta: { eventType: 'product_view', productId: 42 },
|
|
153
|
+
}],
|
|
154
|
+
});
|
|
155
|
+
navigator.sendBeacon('/ees/track', body) ||
|
|
156
|
+
fetch('/ees/track', {
|
|
157
|
+
method: 'POST', body, credentials: 'include',
|
|
158
|
+
headers: { 'content-type': 'application/json' }, keepalive: true,
|
|
159
|
+
});
|
|
160
|
+
```
|
|
97
161
|
|
|
98
162
|
## Feature tour
|
|
99
163
|
|
|
@@ -143,6 +207,89 @@ Stats at `GET /ees/goals/stats?days=30&channelId=1`.
|
|
|
143
207
|
body field or an `ees_consent=1` cookie before ingest.
|
|
144
208
|
- `dropBotEvents: false` (default) — flip on to skip bot UAs entirely.
|
|
145
209
|
|
|
210
|
+
### Cart abandonment (since 0.8.0)
|
|
211
|
+
|
|
212
|
+
Detects sessions that got as far as putting items in the cart but
|
|
213
|
+
never fired `checkout_completed`. Turns them into `AbandonedCart` rows
|
|
214
|
+
you can send a recovery email against.
|
|
215
|
+
|
|
216
|
+
**How detection works.**
|
|
217
|
+
The plugin runs a worker-only sweep every 5 minutes. It looks at every
|
|
218
|
+
session that fired at least one `cart_snapshot` event, and:
|
|
219
|
+
|
|
220
|
+
- If a `checkout_completed` landed later — do nothing (or if an
|
|
221
|
+
`abandoned_cart` row already exists, promote it to `converted`).
|
|
222
|
+
- If the last `cart_snapshot` is older than `abandonment.windowMinutes`
|
|
223
|
+
(default 30) — open an `abandoned_cart` row, keyed on `sessionId`
|
|
224
|
+
(unique — you can't double-open the same session).
|
|
225
|
+
- Otherwise leave the session alone. It may still convert.
|
|
226
|
+
|
|
227
|
+
**Recovery link.**
|
|
228
|
+
`POST /ees/abandoned-carts/:id/recovery-link` mints a signed opaque
|
|
229
|
+
token and returns `{ ok: true, url: '<storefront>/cart/restore?t=...' }`.
|
|
230
|
+
The token is time-bounded (`recoveryLinkTtlHours`, default 72) and
|
|
231
|
+
non-reusable. The storefront exchanges it via
|
|
232
|
+
`GET /ees/recover-cart?t=<token>` to get back the persisted item list.
|
|
233
|
+
|
|
234
|
+
Set `abandonment.recoveryLinkSecret` in plugin options to enable this —
|
|
235
|
+
without it, the endpoint returns `{ error: 'recovery-disabled-or-not-found' }`.
|
|
236
|
+
|
|
237
|
+
**Slack notification.**
|
|
238
|
+
`abandonment.slackWebhookUrl` + `abandonment.slackMinValueMinor`
|
|
239
|
+
control an at-most-once Slack post per abandonment above the value
|
|
240
|
+
threshold. Useful for sales teams that follow up on high-value drops
|
|
241
|
+
manually.
|
|
242
|
+
|
|
243
|
+
**Admin dashboard.**
|
|
244
|
+
Under **Analytics → Abandoned carts**. Filters by status / min value /
|
|
245
|
+
email / window. Actions per row: mint recovery link (copies URL to
|
|
246
|
+
clipboard), mark recovered manually, dismiss. CSV export.
|
|
247
|
+
|
|
248
|
+
### Product recommendations (since 0.8.0)
|
|
249
|
+
|
|
250
|
+
A `ProductCoView` aggregate table holds a per-triple counter
|
|
251
|
+
`(productIdA, productIdB, channelId) → viewsTogether`. Rebuilt every 6
|
|
252
|
+
hours from the last 24h of `product_view` events, bounded to 20 events
|
|
253
|
+
per session so runaway bot sessions can't skew the table.
|
|
254
|
+
Denormalised — we store both `(A, B)` and `(B, A)` — so read-side
|
|
255
|
+
lookups are one indexed scan.
|
|
256
|
+
|
|
257
|
+
Three endpoints, all safe from the storefront (no PII):
|
|
258
|
+
|
|
259
|
+
| Endpoint | Use |
|
|
260
|
+
| --- | --- |
|
|
261
|
+
| `GET /ees/recommendations/also-viewed?productId=42&limit=10` | product-page rail: "customers who viewed X also viewed…" |
|
|
262
|
+
| `GET /ees/recommendations/personal?visitorId=abc&limit=10` | homepage / cart recs for a returning visitor. Uses their last 10 `product_view` events over 30 days, excludes the seeds so the same product never appears |
|
|
263
|
+
| `GET /ees/recommendations/trending?hours=24&limit=10` | homepage rail: most-viewed products in the window. Reflects real intent (not search-console clicks) |
|
|
264
|
+
|
|
265
|
+
`GET /ees/recommendations/aggregate-now` (SuperAdmin only) forces a
|
|
266
|
+
sweep — useful after a big backfill or spike.
|
|
267
|
+
|
|
268
|
+
### Site search analytics (since 0.8.0)
|
|
269
|
+
|
|
270
|
+
Zero-schema-cost queries over the existing `visitor_event` table where
|
|
271
|
+
the storefront has fired `hulo.search(query, resultsCount)` events.
|
|
272
|
+
|
|
273
|
+
| Endpoint | Use |
|
|
274
|
+
| --- | --- |
|
|
275
|
+
| `GET /ees/search-analytics/top?days=7` | top queries by volume with average results count |
|
|
276
|
+
| `GET /ees/search-analytics/no-results?days=7` | queries that returned zero hits — direct catalogue-gap intel |
|
|
277
|
+
| `GET /ees/search-analytics/conversion?days=7` | of sessions that searched, what fraction went on to `add_to_cart` |
|
|
278
|
+
|
|
279
|
+
### Journey drawer buffs (since 0.8.0)
|
|
280
|
+
|
|
281
|
+
| Endpoint | Use |
|
|
282
|
+
| --- | --- |
|
|
283
|
+
| `GET /ees/journey/rage-clicks?days=7` | rage-click hot-spot list per URL. Pages where visitors are frustrated |
|
|
284
|
+
| `GET /ees/journey/dead-clicks?days=7` | dead-click hot-spot list per URL. Elements that LOOK clickable but aren't |
|
|
285
|
+
| `GET /ees/journey/session-summary?visitorId=abc` | per-session summary with a heuristic `intent` label (`purchase` / `abandon` / `frustrate` / `consider` / `browse` / `bounce`) |
|
|
286
|
+
|
|
287
|
+
Rage-click auto-detector fires on ≥3 pointerdown events within 500ms
|
|
288
|
+
and a 20-pixel radius. Dead-click auto-detector fires when a click
|
|
289
|
+
lands on a non-interactive element and no navigation / significant
|
|
290
|
+
scroll follows within 400ms. Both are conservative heuristics — the
|
|
291
|
+
signal is direction-of-frustration, not a metric to optimise against.
|
|
292
|
+
|
|
146
293
|
### Live-now widget
|
|
147
294
|
|
|
148
295
|
SSE stream at `GET /ees/visitors/live` pushes the active-visitor count
|
|
@@ -160,25 +307,52 @@ events with full enrichment.
|
|
|
160
307
|
|
|
161
308
|
## HTTP endpoints
|
|
162
309
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
|
167
|
-
|
|
|
168
|
-
| `
|
|
169
|
-
| `GET`
|
|
170
|
-
| `GET`
|
|
171
|
-
| `GET`
|
|
172
|
-
| `GET`
|
|
173
|
-
| `GET`
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
|
179
|
-
|
|
|
180
|
-
| `GET`
|
|
181
|
-
| `GET`
|
|
310
|
+
**Public** (no auth — CORS-permissive for browser calls from any
|
|
311
|
+
storefront origin):
|
|
312
|
+
|
|
313
|
+
| Method | Path | Purpose |
|
|
314
|
+
| --- | --- | --- |
|
|
315
|
+
| `POST` | `/ees/track` | ingest a batch of visitor events |
|
|
316
|
+
| `GET` | `/ees/hulo.js` | typed storefront helper JS (since 0.8.1) |
|
|
317
|
+
| `GET` | `/ees/recover-cart?t=<token>` | resolve a recovery token → items |
|
|
318
|
+
| `GET` | `/ees/recommendations/also-viewed?productId=…` | co-view recs |
|
|
319
|
+
| `GET` | `/ees/recommendations/personal?visitorId=…` | personalised recs |
|
|
320
|
+
| `GET` | `/ees/recommendations/trending?hours=…` | most-viewed products |
|
|
321
|
+
|
|
322
|
+
**Admin** (Vendure `ReadCustomer` unless noted; requires a
|
|
323
|
+
Vendure admin session cookie):
|
|
324
|
+
|
|
325
|
+
| Method | Path | Purpose |
|
|
326
|
+
| --- | --- | --- |
|
|
327
|
+
| `GET` | `/ees/visitors/summary` | top-line + daily series |
|
|
328
|
+
| `GET` | `/ees/visitors/sources` | top sources by visits |
|
|
329
|
+
| `GET` | `/ees/visitors/top-pages` | most-visited URLs |
|
|
330
|
+
| `GET` | `/ees/visitors/funnel` | configurable funnel |
|
|
331
|
+
| `GET` | `/ees/visitors/exit-pages` | top exit pages |
|
|
332
|
+
| `GET` | `/ees/visitors/top-events` | top custom events |
|
|
333
|
+
| `GET` | `/ees/visitors/live` | SSE live-now stream |
|
|
334
|
+
| `GET` | `/ees/visitors/journey/:visitorId` | per-visitor timeline |
|
|
335
|
+
| `GET` | `/ees/visitors/recent` | recent events |
|
|
336
|
+
| `GET` | `/ees/visitors/export.csv` | CSV export |
|
|
337
|
+
| `GET` | `/ees/goals` | list conversion goals |
|
|
338
|
+
| `POST` | `/ees/goals` | create a goal |
|
|
339
|
+
| `PUT` | `/ees/goals/:id` | update a goal |
|
|
340
|
+
| `DELETE`| `/ees/goals/:id` | delete a goal |
|
|
341
|
+
| `GET` | `/ees/goals/stats` | per-goal completion stats |
|
|
342
|
+
| `GET` | `/ees/visitors/status` | version + update status |
|
|
343
|
+
| `GET` | `/ees/abandoned-carts` | paginated list w/ filters (0.8.0) |
|
|
344
|
+
| `GET` | `/ees/abandoned-carts/summary` | totals + recovery rate (0.8.0) |
|
|
345
|
+
| `GET` | `/ees/abandoned-carts/:id` | detail incl. parsed items (0.8.0) |
|
|
346
|
+
| `POST` | `/ees/abandoned-carts/:id/recovery-link` | mint signed URL (0.8.0, `UpdateCustomer`) |
|
|
347
|
+
| `POST` | `/ees/abandoned-carts/:id/status` | mark recovered/dismissed (0.8.0, `UpdateCustomer`) |
|
|
348
|
+
| `GET` | `/ees/abandoned-carts/export.csv` | CSV export (0.8.0) |
|
|
349
|
+
| `GET` | `/ees/recommendations/aggregate-now` | force co-view sweep (0.8.0, `SuperAdmin`) |
|
|
350
|
+
| `GET` | `/ees/search-analytics/top` | top queries (0.8.0) |
|
|
351
|
+
| `GET` | `/ees/search-analytics/no-results` | zero-result queries (0.8.0) |
|
|
352
|
+
| `GET` | `/ees/search-analytics/conversion` | search→cart rate (0.8.0) |
|
|
353
|
+
| `GET` | `/ees/journey/rage-clicks` | rage-click hot spots (0.8.0) |
|
|
354
|
+
| `GET` | `/ees/journey/dead-clicks` | dead-click hot spots (0.8.0) |
|
|
355
|
+
| `GET` | `/ees/journey/session-summary?visitorId=…` | per-session intent labels (0.8.0) |
|
|
182
356
|
|
|
183
357
|
## Documentation
|
|
184
358
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@huloglobal/vendure-plugin-visitor-analytics",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "Full-funnel visitor analytics for Vendure storefronts \u2014 pageviews, time-on-page, session journey, exit pages, funnel drop-off, and per-visitor profile drawer with parsed user-agent + MaxMind GeoLite2 enrichment. Auto-issues visitor + session cookies on first request; logs guest and signed-in events against the same visitor id so the journey survives login.",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"author": "Wayne Garrison <wayne@garrison.me.uk>",
|