@huloglobal/vendure-plugin-visitor-analytics 0.4.0 → 0.4.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/CHANGELOG.md +36 -0
- package/README.md +150 -121
- package/package.json +5 -5
- package/ui/components/visitors.component.ts +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,42 @@ documented here. The format follows
|
|
|
5
5
|
[Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this
|
|
6
6
|
project adheres to [semantic versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.4.1]
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- Comprehensive README refresh — documents the full v0.4 feature set
|
|
12
|
+
with the storefront snippet, every privacy + security option, and
|
|
13
|
+
the conversion-goals API.
|
|
14
|
+
|
|
15
|
+
## [0.4.0]
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- Signed visitor + session cookies via the licence-sdk `signValue` /
|
|
19
|
+
`verifySignedValue` helpers — tampered cookies are rejected.
|
|
20
|
+
- `Secure` cookie flag is set automatically when serving over HTTPS.
|
|
21
|
+
- Rate limiter (240/60s default) on `POST /ees/track`.
|
|
22
|
+
- `corsAllowedOrigins` option restricts CORS reflection to the
|
|
23
|
+
configured list (legacy wildcard preserved when empty).
|
|
24
|
+
- Security headers on every response.
|
|
25
|
+
- Opt-in retention sweeper via `options.retention`.
|
|
26
|
+
|
|
27
|
+
## [0.3.3]
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
- Mobile-friendly admin UI — summary cards reflow, tables scroll
|
|
31
|
+
inside their card, profile drawer goes full-width.
|
|
32
|
+
|
|
33
|
+
## [0.3.2]
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
- Republish targeting `@huloglobal/vendure-licence-sdk@^0.2.0`.
|
|
37
|
+
|
|
38
|
+
## [0.3.1]
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
- `UpdateChecker` integration — `/ees/visitors/status` endpoint returns
|
|
42
|
+
version + update info; admin banner appears on new releases.
|
|
43
|
+
|
|
8
44
|
## [0.3.0]
|
|
9
45
|
|
|
10
46
|
### Added
|
package/README.md
CHANGED
|
@@ -1,34 +1,18 @@
|
|
|
1
1
|
# @huloglobal/vendure-plugin-visitor-analytics
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
Self-hosted full-funnel visitor analytics for Vendure storefronts.
|
|
4
|
+
Pageviews, time-on-page, exit pages, configurable funnel, UTM
|
|
5
|
+
attribution, conversion goals with URL-glob matching, bot detection,
|
|
6
|
+
and a per-visitor profile drawer with parsed user-agent and MaxMind
|
|
7
|
+
geo. Privacy-first defaults: DNT, IP anonymisation, optional consent
|
|
8
|
+
gate.
|
|
8
9
|
|
|
9
10
|
Maintained by Wayne Garrison.
|
|
10
11
|
|
|
11
|
-
##
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
|
|
15
|
-
(`ees_vid`, 2 years) and a sliding session cookie (`ees_sid`,
|
|
16
|
-
30-minute idle).
|
|
17
|
-
- **Auto-enrichment** at ingest time:
|
|
18
|
-
- User-agent parsed via `ua-parser-js` → browser / browser version /
|
|
19
|
-
OS / OS version / device type. Bots auto-detected.
|
|
20
|
-
- IP-to-geo via MaxMind GeoLite2-City (no MaxMind account required,
|
|
21
|
-
DB fetched at install via `geolite2-redist`). Or use the upstream
|
|
22
|
-
proxy's resolved country / region when Cloudflare / Akamai /
|
|
23
|
-
Fastly is in front — saves the lookup.
|
|
24
|
-
- Raw IP is kept; a SHA-256 salted hash is stored alongside for
|
|
25
|
-
spot-the-same-bot work.
|
|
26
|
-
- **Admin endpoints**: summary tiles, top pages, exit pages, funnel,
|
|
27
|
-
recent visitors, per-visitor profile + journey timeline. All
|
|
28
|
-
paginated.
|
|
29
|
-
- **Admin UI**: top-line tiles, funnel bars, top + exit page tables,
|
|
30
|
-
recent visitors with a clickable profile drawer showing every field
|
|
31
|
-
+ per-session breakdown + the full event timeline.
|
|
12
|
+
## Buy
|
|
13
|
+
|
|
14
|
+
7-day free trial then **£9.95/month**, or **£199 one-off lifetime** at
|
|
15
|
+
[elite.charity/licence/buy/vendure-plugin-visitor-analytics](https://elite.charity/licence/buy/vendure-plugin-visitor-analytics).
|
|
32
16
|
|
|
33
17
|
## Install
|
|
34
18
|
|
|
@@ -36,132 +20,177 @@ Maintained by Wayne Garrison.
|
|
|
36
20
|
yarn add @huloglobal/vendure-plugin-visitor-analytics
|
|
37
21
|
```
|
|
38
22
|
|
|
39
|
-
## Wire up
|
|
40
|
-
|
|
41
23
|
```ts
|
|
42
24
|
import { VisitorAnalyticsPlugin } from '@huloglobal/vendure-plugin-visitor-analytics';
|
|
43
25
|
|
|
44
26
|
export const config: VendureConfig = {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
27
|
+
plugins: [
|
|
28
|
+
VisitorAnalyticsPlugin.init({
|
|
29
|
+
publicBaseUrl: 'https://shop.example.com',
|
|
30
|
+
licenceKey: process.env.HULO_LICENCE_KEY_VISITOR_ANALYTICS,
|
|
31
|
+
|
|
32
|
+
// -- Privacy (defaults shown) --
|
|
33
|
+
honorDoNotTrack: true,
|
|
34
|
+
anonymizeIp: true,
|
|
35
|
+
requireConsent: false,
|
|
36
|
+
dropBotEvents: false,
|
|
37
|
+
|
|
38
|
+
// -- Security (recommended in production) --
|
|
39
|
+
signingSecret: process.env.HULO_VISITOR_SIGNING_SECRET,
|
|
40
|
+
corsAllowedOrigins: [
|
|
41
|
+
'https://shop.example.com',
|
|
42
|
+
'https://www.example.com',
|
|
43
|
+
],
|
|
44
|
+
rateLimit: { capacity: 240, windowMs: 60_000 },
|
|
45
|
+
|
|
46
|
+
// -- Retention (opt-in) --
|
|
47
|
+
retention: { days: 365, maxRows: 50_000_000 },
|
|
48
|
+
}),
|
|
49
|
+
],
|
|
51
50
|
};
|
|
52
51
|
```
|
|
53
52
|
|
|
54
|
-
Add to your
|
|
53
|
+
Add `VisitorAnalyticsPlugin.uiExtensions` to your `compileUiExtensions`
|
|
54
|
+
config.
|
|
55
|
+
|
|
56
|
+
## Storefront snippet
|
|
55
57
|
|
|
56
58
|
```ts
|
|
57
|
-
|
|
59
|
+
// utils/visitor-tracking.ts
|
|
60
|
+
const ENDPOINT = 'https://shop.example.com/ees/track';
|
|
61
|
+
const CHANNEL_ID = 1;
|
|
62
|
+
let queue: any[] = [];
|
|
63
|
+
let flushTimer: any;
|
|
64
|
+
|
|
65
|
+
export function recordPageview(url: string, title: string) {
|
|
66
|
+
queue.push({ type: 'pageview', url, title, clientTs: Date.now() });
|
|
67
|
+
scheduleFlush();
|
|
68
|
+
}
|
|
58
69
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
70
|
+
export function recordEvent(type: string, meta: any) {
|
|
71
|
+
queue.push({
|
|
72
|
+
type, url: location.pathname + location.search,
|
|
73
|
+
meta, clientTs: Date.now(),
|
|
74
|
+
});
|
|
75
|
+
scheduleFlush();
|
|
76
|
+
}
|
|
64
77
|
|
|
65
|
-
|
|
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
|
+
}
|
|
92
|
+
```
|
|
66
93
|
|
|
67
|
-
|
|
68
|
-
|
|
94
|
+
Call `recordPageview()` on every route change. For custom events
|
|
95
|
+
(add-to-cart, search, signup, …) call `recordEvent(type, meta)` at the
|
|
96
|
+
appropriate point.
|
|
69
97
|
|
|
70
|
-
|
|
71
|
-
// utils/tracker.ts
|
|
72
|
-
const TRACK_URL = 'https://shop.example.com/ees/track';
|
|
73
|
-
let lastPath = '';
|
|
74
|
-
let pageOpenedAt = 0;
|
|
75
|
-
|
|
76
|
-
export function recordPageView(): void {
|
|
77
|
-
const url = location.pathname + location.search;
|
|
78
|
-
const events: any[] = [];
|
|
79
|
-
if (lastPath && lastPath !== url) {
|
|
80
|
-
events.push({ type: 'unload', url: lastPath, timeOnPageMs: Date.now() - pageOpenedAt });
|
|
81
|
-
}
|
|
82
|
-
events.push({ type: 'pageview', url, title: document.title, referrer: document.referrer });
|
|
83
|
-
lastPath = url;
|
|
84
|
-
pageOpenedAt = Date.now();
|
|
85
|
-
fetch(TRACK_URL, {
|
|
86
|
-
method: 'POST',
|
|
87
|
-
credentials: 'include',
|
|
88
|
-
headers: { 'content-type': 'application/json' },
|
|
89
|
-
body: JSON.stringify({ channelId: 1, events }),
|
|
90
|
-
keepalive: true,
|
|
91
|
-
}).catch(() => undefined);
|
|
92
|
-
}
|
|
98
|
+
## Feature tour
|
|
93
99
|
|
|
94
|
-
|
|
95
|
-
window.addEventListener('pagehide', () => {
|
|
96
|
-
const blob = new Blob([JSON.stringify({
|
|
97
|
-
channelId: 1,
|
|
98
|
-
events: [{ type: 'unload', url: lastPath, timeOnPageMs: Date.now() - pageOpenedAt }],
|
|
99
|
-
})], { type: 'application/json' });
|
|
100
|
-
navigator.sendBeacon(TRACK_URL, blob);
|
|
101
|
-
});
|
|
102
|
-
```
|
|
100
|
+
### Lightweight ingest
|
|
103
101
|
|
|
104
|
-
|
|
102
|
+
- `POST /ees/track` accepts a batch of up to 50 events at once.
|
|
103
|
+
- Visitor + session cookies (`ees_vid`, `ees_sid`) issued + refreshed
|
|
104
|
+
automatically. When `signingSecret` is set, cookies are HMAC-signed
|
|
105
|
+
and tampered values are rejected — the visitor gets a fresh id.
|
|
106
|
+
- `Secure` flag is set automatically when serving over HTTPS.
|
|
105
107
|
|
|
106
|
-
|
|
107
|
-
visitor does something interesting — add-to-cart, search, signup,
|
|
108
|
-
quote-request — and the event shows up in the admin "Top events" table
|
|
109
|
-
straight away.
|
|
108
|
+
### Auto-enrichment
|
|
110
109
|
|
|
111
|
-
|
|
112
|
-
function recordEvent(type, meta) {
|
|
113
|
-
navigator.sendBeacon(TRACK_URL, new Blob([JSON.stringify({
|
|
114
|
-
channelId: 1,
|
|
115
|
-
events: [{ type, url: location.pathname + location.search, meta }],
|
|
116
|
-
})], { type: 'application/json' }));
|
|
117
|
-
}
|
|
110
|
+
Per event:
|
|
118
111
|
|
|
119
|
-
|
|
120
|
-
|
|
112
|
+
- **User-agent** parsed via `ua-parser-js` → browser, version, OS,
|
|
113
|
+
device.
|
|
114
|
+
- **Geo** via MaxMind GeoLite2-City (no MaxMind account required — DB
|
|
115
|
+
fetched at install via `geolite2-redist`). Skipped when the upstream
|
|
116
|
+
proxy already provides a country (Cloudflare, Akamai, Fastly).
|
|
117
|
+
- **UTM attribution** parsed server-side from every pageview URL:
|
|
118
|
+
`utmSource`, `utmMedium`, `utmCampaign`, `utmTerm`, `utmContent`. Plus
|
|
119
|
+
`referrerDomain` for grouping by source even when UTM is absent.
|
|
120
|
+
- **Bot flag** — known crawler / monitoring / library UAs (Googlebot,
|
|
121
|
+
Bingbot, UptimeRobot, Datadog, curl, Puppeteer, …) marked `isBot=true`.
|
|
121
122
|
|
|
122
|
-
|
|
123
|
-
recordEvent('search', { query: 'windows server 2022' });
|
|
123
|
+
### Configurable conversion goals
|
|
124
124
|
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
A goal is a URL glob that, when matched, counts the visitor as having
|
|
126
|
+
converted. Supports `*` (within segment) and `**` (across segments).
|
|
127
127
|
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
```bash
|
|
129
|
+
curl -X POST https://shop.example.com/ees/goals -H 'content-type: application/json' \
|
|
130
|
+
-d '{"channelId":1,"name":"Checkout completed","urlPattern":"/checkout/thank-you/*","valueMinor":5000}'
|
|
130
131
|
```
|
|
131
132
|
|
|
132
|
-
|
|
133
|
-
slice on it later from the admin UI.
|
|
133
|
+
Stats at `GET /ees/goals/stats?days=30&channelId=1`.
|
|
134
134
|
|
|
135
|
-
|
|
135
|
+
### Privacy controls
|
|
136
136
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
137
|
+
- `honorDoNotTrack: true` (default) — `DNT: 1` and `Sec-GPC: 1` requests
|
|
138
|
+
get a 200 with `{stored:0, skipped:'dnt'}`.
|
|
139
|
+
- `anonymizeIp: true` (default) — IPv4 last octet dropped before
|
|
140
|
+
storage; IPv6 reduced to the first 3 hextets. `ipHash` still uses the
|
|
141
|
+
raw IP so unique-visitor counts stay accurate.
|
|
142
|
+
- `requireConsent: false` (default) — flip on to require a `consent: true`
|
|
143
|
+
body field or an `ees_consent=1` cookie before ingest.
|
|
144
|
+
- `dropBotEvents: false` (default) — flip on to skip bot UAs entirely.
|
|
142
145
|
|
|
143
|
-
|
|
144
|
-
inbound link and it surfaces automatically — no extra config.
|
|
146
|
+
### Live-now widget
|
|
145
147
|
|
|
146
|
-
|
|
148
|
+
SSE stream at `GET /ees/visitors/live` pushes the active-visitor count
|
|
149
|
+
and the 20 most recent URLs every 5 seconds. Auto-reconnects.
|
|
147
150
|
|
|
148
|
-
|
|
149
|
-
count + the URLs they're on in real time via Server-Sent Events
|
|
150
|
-
(`GET /ees/visitors/live`). Updates every 5 seconds, reconnects
|
|
151
|
-
automatically if the connection drops. Active = at least one event in
|
|
152
|
-
the last 5 minutes.
|
|
151
|
+
### Per-visitor journey
|
|
153
152
|
|
|
154
|
-
|
|
153
|
+
Click any visitor for the full timeline: pages, custom events,
|
|
154
|
+
time-on-page, country, browser, OS.
|
|
155
155
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
156
|
+
### CSV export
|
|
157
|
+
|
|
158
|
+
`GET /ees/visitors/export.csv?days=N` (max 90 days) returns the raw
|
|
159
|
+
events with full enrichment.
|
|
160
160
|
|
|
161
|
-
|
|
162
|
-
|
|
161
|
+
## HTTP endpoints
|
|
162
|
+
|
|
163
|
+
| Method | Path | Auth | Purpose |
|
|
164
|
+
| --- | --- | --- | --- |
|
|
165
|
+
| `POST` | `/ees/track` | public | ingest batch of events |
|
|
166
|
+
| `GET` | `/ees/visitors/summary` | admin | top-line + daily series |
|
|
167
|
+
| `GET` | `/ees/visitors/sources` | admin | top sources by visits |
|
|
168
|
+
| `GET` | `/ees/visitors/top-pages` | admin | most-visited URLs |
|
|
169
|
+
| `GET` | `/ees/visitors/funnel` | admin | configurable funnel |
|
|
170
|
+
| `GET` | `/ees/visitors/exit-pages` | admin | top exit pages |
|
|
171
|
+
| `GET` | `/ees/visitors/top-events` | admin | top custom events |
|
|
172
|
+
| `GET` | `/ees/visitors/live` | admin | SSE live-now stream |
|
|
173
|
+
| `GET` | `/ees/visitors/journey/:visitorId` | admin | per-visitor timeline |
|
|
174
|
+
| `GET` | `/ees/visitors/recent` | admin | recent events |
|
|
175
|
+
| `GET` | `/ees/visitors/export.csv` | admin | CSV export |
|
|
176
|
+
| `GET` | `/ees/goals` | admin | list conversion goals |
|
|
177
|
+
| `POST` | `/ees/goals` | admin | create a goal |
|
|
178
|
+
| `PUT` | `/ees/goals/:id` | admin | update a goal |
|
|
179
|
+
| `DELETE` | `/ees/goals/:id` | admin | delete a goal |
|
|
180
|
+
| `GET` | `/ees/goals/stats` | admin | per-goal completion stats |
|
|
181
|
+
| `GET` | `/ees/visitors/status` | admin | version + update status |
|
|
182
|
+
|
|
183
|
+
## Documentation
|
|
184
|
+
|
|
185
|
+
User manual + screenshots:
|
|
186
|
+
[huloglobal.com/vendure-plugins/visitor-analytics/docs/](https://huloglobal.com/vendure-plugins/visitor-analytics/docs/)
|
|
187
|
+
|
|
188
|
+
## Lost your licence key?
|
|
189
|
+
|
|
190
|
+
Re-send every active key on file at
|
|
191
|
+
[elite.charity/licence/forgot](https://elite.charity/licence/forgot).
|
|
163
192
|
|
|
164
193
|
## Licence
|
|
165
194
|
|
|
166
|
-
Commercial
|
|
167
|
-
(
|
|
195
|
+
Commercial. Buy at
|
|
196
|
+
[elite.charity/licence/buy/vendure-plugin-visitor-analytics](https://elite.charity/licence/buy/vendure-plugin-visitor-analytics).
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@huloglobal/vendure-plugin-visitor-analytics",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "Full-funnel visitor analytics for Vendure storefronts
|
|
3
|
+
"version": "0.4.2",
|
|
4
|
+
"description": "Full-funnel visitor analytics for Vendure storefronts — 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": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": "Wayne Garrison <wayne@garrison.me.uk>",
|
|
7
7
|
"homepage": "https://github.com/exceeded/vendure-plugin-visitor-analytics",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"prepublishOnly": "yarn build"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@huloglobal/vendure-licence-sdk": "^0.3.
|
|
26
|
+
"@huloglobal/vendure-licence-sdk": "^0.3.1",
|
|
27
27
|
"ua-parser-js": "^1.0.37",
|
|
28
28
|
"geolite2-redist": "^3.1.0",
|
|
29
29
|
"@maxmind/geoip2-node": "^5.0.0"
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"typeorm": ">=0.3.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@huloglobal/vendure-licence-sdk": "^0.3.
|
|
37
|
+
"@huloglobal/vendure-licence-sdk": "^0.3.1",
|
|
38
38
|
"@nestjs/common": "^10.0.0",
|
|
39
39
|
"@vendure/core": "^3.6.0",
|
|
40
40
|
"typeorm": "^0.3.0",
|
|
@@ -48,4 +48,4 @@
|
|
|
48
48
|
"funnel",
|
|
49
49
|
"session-journey"
|
|
50
50
|
]
|
|
51
|
-
}
|
|
51
|
+
}
|
|
@@ -411,6 +411,12 @@ interface VisitorProfile {
|
|
|
411
411
|
|
|
412
412
|
/* Mobile under 768px */
|
|
413
413
|
@media (max-width: 767px) {
|
|
414
|
+
/* 44px tap targets on every interactive element in our component */
|
|
415
|
+
:host button, :host .btn { min-height: 40px; }
|
|
416
|
+
:host vdr-action-bar { flex-wrap: wrap; gap: 6px; }
|
|
417
|
+
:host vdr-action-bar button { min-height: 40px; padding: 6px 12px; }
|
|
418
|
+
.range { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; margin: 8px 0; }
|
|
419
|
+
.range .btn { min-height: 40px; min-width: 48px; padding: 6px 12px; font-size: 13px; }
|
|
414
420
|
.summary-row { gap: 8px; }
|
|
415
421
|
.summary-card { min-width: 0; flex-basis: calc(50% - 4px); padding: 12px 14px; }
|
|
416
422
|
.summary-card .num { font-size: 20px; }
|