@huloglobal/vendure-plugin-visitor-analytics 0.8.0 → 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/CHANGELOG.md CHANGED
@@ -5,6 +5,87 @@ documented here. The format follows
5
5
  [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project
6
6
  adheres to [semantic versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.8.0] — 2026-07-04
9
+
10
+ ### Added
11
+
12
+ **Cart abandonment — end-to-end.**
13
+ - New `AbandonedCart` entity, keyed on session id. One row per
14
+ abandoned session, refreshed in place until it either converts
15
+ (order placed) or expires (recovery window elapses).
16
+ - `AbandonedCartService.scan()` — periodic sweep finds sessions with
17
+ `cart_snapshot` events but no `checkout_completed` in the
18
+ abandonment window (default 30 min). Auto-promotes previously
19
+ abandoned rows to `converted` when the customer later checks out.
20
+ - Ships with a boot-time timer (worker-only, 5-minute cadence).
21
+ Idempotent — safe to horizontally scale, only the worker runs it.
22
+ - Signed recovery links — `POST /ees/abandoned-carts/:id/recovery-link`
23
+ returns a time-bounded opaque token the storefront exchanges via
24
+ `GET /ees/recover-cart?t=…` to rebuild the exact cart. Storefront
25
+ never sees the underlying items until the token is presented.
26
+ - Slack notification for high-value abandonments — configurable
27
+ threshold and webhook via the `abandonment` plugin option.
28
+ - Admin API:
29
+ - `GET /ees/abandoned-carts` — paginated list with status,
30
+ value and email filters.
31
+ - `GET /ees/abandoned-carts/summary` — totals, recovery rate,
32
+ recovered vs. lost value in the window.
33
+ - `GET /ees/abandoned-carts/:id` — detail incl. parsed items.
34
+ - `POST /ees/abandoned-carts/:id/status` — mark recovered /
35
+ dismissed / re-open.
36
+ - `GET /ees/abandoned-carts/export.csv` — CSV export.
37
+
38
+ **Co-view product recommendations.**
39
+ - New `ProductCoView` aggregate table. Scanner walks recent
40
+ `product_view` events per session, extracts every ordered pair,
41
+ and increments a per-triple counter. Bounded to 20 events per
42
+ session so runaway bot sessions can't skew the table.
43
+ - Denormalised — both `(A, B)` and `(B, A)` stored — so read-side
44
+ lookups are one indexed scan.
45
+ - Runs every 6 hours on the worker; also exposed as
46
+ `GET /ees/recommendations/aggregate-now` for admins to kick a
47
+ fresh run after a data backfill.
48
+ - Public read endpoints (safe from the storefront):
49
+ - `GET /ees/recommendations/also-viewed?productId=…` — the
50
+ "customers who viewed X also viewed…" rail on a product page.
51
+ - `GET /ees/recommendations/personal?visitorId=…` — personalised
52
+ recs for a returning visitor, from their last 10 product views
53
+ over 30 days. Excludes seeds.
54
+ - `GET /ees/recommendations/trending?hours=24` — most-viewed
55
+ products in the window. Reflects real interest, not search-
56
+ console clicks.
57
+
58
+ **Site search analytics.**
59
+ - Reads back over `visitor_event` where the storefront has fired
60
+ `hulo.search(query, resultsCount)`. Zero new schema.
61
+ - `GET /ees/search-analytics/top` — top queries by volume with
62
+ average results count.
63
+ - `GET /ees/search-analytics/no-results` — top zero-result queries.
64
+ Direct catalogue-gap intel.
65
+ - `GET /ees/search-analytics/conversion` — of sessions that
66
+ searched, what fraction went on to fire `add_to_cart`.
67
+
68
+ **Journey drawer buffs.**
69
+ - Rage-click + dead-click aggregation, keyed on URL. Store-wide
70
+ hot-spot lists for pages where visitors are stuck or frustrated.
71
+ - Per-visitor session summary with a heuristic `intent` label
72
+ (`purchase` / `abandon` / `frustrate` / `consider` / `browse` /
73
+ `bounce`) computed from event history — one glance per session
74
+ in the Journey drawer instead of scrolling event rows.
75
+
76
+ **Storefront helper events (documented in the README).**
77
+ - `hulo.cartSnapshot({ currency, totalMinor, itemCount, items, email })`
78
+ - `hulo.productView(productId, productVariantId?)`
79
+ - `hulo.search(query, resultsCount)`
80
+ - `hulo.rageClick(url, selector?)` / `hulo.deadClick(url, selector)`
81
+ - `hulo.checkoutCompleted()`
82
+ - All flow into the existing `POST /ees/track` endpoint with a
83
+ standard shape, so admins can also fire them from any language.
84
+
85
+ ### Changed
86
+ - `checkout_completed` is now a first-class recognised event type
87
+ — the abandonment scanner uses it to auto-close matched rows.
88
+
8
89
  ## [0.7.0] — 2026-07-04
9
90
 
10
91
  ### Added
@@ -149,6 +230,7 @@ adheres to [semantic versioning](https://semver.org/spec/v2.0.0.html).
149
230
  - Licence verification via `@huloglobal/vendure-licence-sdk` with
150
231
  revocation polling.
151
232
 
233
+ [0.8.0]: https://github.com/exceeded/vendure-plugin-visitor-analytics/releases/tag/v0.8.0
152
234
  [0.7.0]: https://github.com/exceeded/vendure-plugin-visitor-analytics/releases/tag/v0.7.0
153
235
  [0.6.0]: https://github.com/exceeded/vendure-plugin-visitor-analytics/releases/tag/v0.6.0
154
236
  [0.5.0]: https://github.com/exceeded/vendure-plugin-visitor-analytics/releases/tag/v0.5.0
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
- ## Storefront snippet
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
- // 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
- }
69
-
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
- }
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
- Call `recordPageview()` on every route change. For custom events
95
- (add-to-cart, search, signup, …) call `recordEvent(type, meta)` at the
96
- appropriate point.
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
- | 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 |
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
 
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The single-file storefront helper the plugin serves at /ees/hulo.js.
3
+ *
4
+ * Written as a string constant (not a bundled module) because:
5
+ * - It's tiny (< 6kB minified) — a bundler adds more than the code
6
+ * - It ships to the browser verbatim, no compile step
7
+ * - Customers can inspect / audit it without a source map
8
+ * - It has ZERO npm dependencies at runtime
9
+ *
10
+ * The controller wraps this in a small template that injects the
11
+ * public base URL and a default channel id, so a storefront paste
12
+ * of `<script src="https://backend/ees/hulo.js"></script>` Just Works
13
+ * with no config. Advanced users can call `hulo.configure({ ... })`
14
+ * to override.
15
+ */
16
+ export declare const HULO_STOREFRONT_JS: (backendBaseUrl: string, defaultChannelId?: number) => string;
17
+ //# sourceMappingURL=hulo-storefront.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hulo-storefront.d.ts","sourceRoot":"","sources":["../src/hulo-storefront.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,kBAAkB,GAAI,gBAAgB,MAAM,EAAE,yBAAoB,KAAG,MA4QjF,CAAC"}
@@ -0,0 +1,289 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HULO_STOREFRONT_JS = void 0;
4
+ /**
5
+ * The single-file storefront helper the plugin serves at /ees/hulo.js.
6
+ *
7
+ * Written as a string constant (not a bundled module) because:
8
+ * - It's tiny (< 6kB minified) — a bundler adds more than the code
9
+ * - It ships to the browser verbatim, no compile step
10
+ * - Customers can inspect / audit it without a source map
11
+ * - It has ZERO npm dependencies at runtime
12
+ *
13
+ * The controller wraps this in a small template that injects the
14
+ * public base URL and a default channel id, so a storefront paste
15
+ * of `<script src="https://backend/ees/hulo.js"></script>` Just Works
16
+ * with no config. Advanced users can call `hulo.configure({ ... })`
17
+ * to override.
18
+ */
19
+ const HULO_STOREFRONT_JS = (backendBaseUrl, defaultChannelId = 1) => `
20
+ // hulo storefront helper — @huloglobal/vendure-plugin-visitor-analytics
21
+ // Public domain when served from your own store; do not resell.
22
+ (function (global) {
23
+ var CONFIG = {
24
+ endpoint: ${JSON.stringify(backendBaseUrl.replace(/\/$/, '') + '/ees/track')},
25
+ recoverEndpoint: ${JSON.stringify(backendBaseUrl.replace(/\/$/, '') + '/ees/recover-cart')},
26
+ channelId: ${defaultChannelId},
27
+ debug: false,
28
+ autoRageClick: true,
29
+ autoDeadClick: true,
30
+ autoScroll: false, // not implemented yet — flag reserved
31
+ };
32
+
33
+ // ── queue + batched send ─────────────────────────────────────────
34
+ // Every helper below pushes into a queue. Flush on requestIdleCallback
35
+ // or 2s max. Coalesces bursts of events into one HTTP roundtrip.
36
+ var QUEUE = [];
37
+ var FLUSH_TIMER = null;
38
+ function enqueue(event) {
39
+ event.url = event.url || location.href;
40
+ event.channelId = event.channelId || CONFIG.channelId;
41
+ QUEUE.push(event);
42
+ scheduleFlush();
43
+ if (CONFIG.debug) console.log('[hulo]', event);
44
+ }
45
+ function scheduleFlush() {
46
+ if (FLUSH_TIMER) return;
47
+ FLUSH_TIMER = setTimeout(flush, 1500);
48
+ }
49
+ function flush() {
50
+ FLUSH_TIMER = null;
51
+ if (!QUEUE.length) return;
52
+ var batch = QUEUE.splice(0, QUEUE.length);
53
+ var payload = JSON.stringify({ events: batch });
54
+ // Prefer sendBeacon on unload (survives page transitions) and
55
+ // fall back to fetch keepalive for normal calls.
56
+ try {
57
+ if (navigator.sendBeacon) {
58
+ var blob = new Blob([payload], { type: 'application/json' });
59
+ if (navigator.sendBeacon(CONFIG.endpoint, blob)) return;
60
+ }
61
+ } catch (_e) {}
62
+ try {
63
+ fetch(CONFIG.endpoint, {
64
+ method: 'POST', credentials: 'include', keepalive: true,
65
+ headers: { 'content-type': 'application/json' },
66
+ body: payload,
67
+ }).catch(function () { /* silent */ });
68
+ } catch (_e) {}
69
+ }
70
+ // Flush before the page dies.
71
+ window.addEventListener('pagehide', flush, { capture: true });
72
+ window.addEventListener('beforeunload', flush, { capture: true });
73
+
74
+ // ── public API ───────────────────────────────────────────────────
75
+ var hulo = {
76
+ configure: function (opts) { Object.assign(CONFIG, opts || {}); },
77
+ flush: flush,
78
+
79
+ /** Fire once per pageview. Optional — the ingest endpoint infers
80
+ * pageviews from the referrer / URL, but calling this explicitly
81
+ * captures the title too. */
82
+ pageview: function (extra) {
83
+ enqueue(Object.assign({ type: 'pageview', title: document.title }, extra || {}));
84
+ },
85
+
86
+ /** Product detail viewed. Store the product id you would use to
87
+ * fetch the product server-side (Vendure product.id). If your
88
+ * storefront uses variants primarily, pass variantId too. */
89
+ productView: function (productId, productVariantId) {
90
+ if (!productId && productId !== 0) return;
91
+ enqueue({
92
+ type: 'event',
93
+ meta: JSON.stringify({
94
+ eventType: 'product_view',
95
+ productId: Number(productId),
96
+ productVariantId: productVariantId != null ? Number(productVariantId) : undefined,
97
+ }),
98
+ });
99
+ },
100
+
101
+ /** Item added to cart. Fire on every add — not just the first. */
102
+ addToCart: function (variantId, qty, unitPriceMinor) {
103
+ enqueue({
104
+ type: 'event',
105
+ meta: JSON.stringify({
106
+ eventType: 'add_to_cart',
107
+ variantId: variantId != null ? Number(variantId) : undefined,
108
+ qty: qty != null ? Number(qty) : 1,
109
+ unitPriceMinor: unitPriceMinor != null ? Number(unitPriceMinor) : undefined,
110
+ }),
111
+ });
112
+ },
113
+
114
+ /** Cart snapshot — fire on every cart change (add / remove / qty).
115
+ * The abandonment scanner reads only this event, so fire it
116
+ * liberally. Payload shape:
117
+ * { currency, totalMinor, itemCount, items: [{ variantId, name, qty, unitPriceMinor }], email? } */
118
+ cartSnapshot: function (cart) {
119
+ if (!cart || !cart.items || !cart.items.length) return;
120
+ enqueue({
121
+ type: 'event',
122
+ meta: JSON.stringify({
123
+ eventType: 'cart_snapshot',
124
+ currency: (cart.currency || 'GBP').toUpperCase().slice(0, 3),
125
+ totalMinor: Number(cart.totalMinor || 0),
126
+ itemCount: Number(cart.itemCount || cart.items.length),
127
+ items: cart.items.map(function (i) {
128
+ return {
129
+ variantId: i.variantId != null ? Number(i.variantId) : undefined,
130
+ productId: i.productId != null ? Number(i.productId) : undefined,
131
+ name: i.name != null ? String(i.name).slice(0, 200) : undefined,
132
+ qty: Number(i.qty != null ? i.qty : (i.quantity != null ? i.quantity : 1)),
133
+ unitPriceMinor: Number(i.unitPriceMinor != null ? i.unitPriceMinor : (i.unitPrice != null ? i.unitPrice : 0)),
134
+ sku: i.sku != null ? String(i.sku).slice(0, 100) : undefined,
135
+ };
136
+ }).slice(0, 100),
137
+ email: cart.email || undefined,
138
+ countryCode: cart.countryCode || undefined,
139
+ }),
140
+ });
141
+ },
142
+
143
+ /** Storefront site search executed. Pass the raw query + the
144
+ * resulting hit count so no-result queries can be aggregated. */
145
+ search: function (query, resultsCount) {
146
+ var q = String(query || '').trim();
147
+ if (!q) return;
148
+ enqueue({
149
+ type: 'event',
150
+ meta: JSON.stringify({
151
+ eventType: 'search',
152
+ query: q.slice(0, 200),
153
+ resultsCount: Math.max(0, Number(resultsCount || 0)),
154
+ }),
155
+ });
156
+ },
157
+
158
+ /** Checkout finished — fires the counter that closes any open
159
+ * abandoned-cart row for this session. Call from your thank-you
160
+ * page. Optionally pass the order code + total. */
161
+ checkoutCompleted: function (orderCode, totalMinor) {
162
+ enqueue({
163
+ type: 'event',
164
+ meta: JSON.stringify({
165
+ eventType: 'checkout_completed',
166
+ orderCode: orderCode ? String(orderCode).slice(0, 64) : undefined,
167
+ totalMinor: totalMinor != null ? Number(totalMinor) : undefined,
168
+ }),
169
+ });
170
+ flush(); // don't wait — the visitor may close the tab immediately
171
+ },
172
+
173
+ /** Rage-click event — fire it yourself if you have a better signal
174
+ * than the built-in detector. */
175
+ rageClick: function (selector, extraMeta) {
176
+ enqueue({
177
+ type: 'event',
178
+ meta: JSON.stringify(Object.assign({
179
+ eventType: 'rage_click',
180
+ selector: selector ? String(selector).slice(0, 200) : undefined,
181
+ }, extraMeta || {})),
182
+ });
183
+ },
184
+
185
+ /** Click landed on a non-interactive element. */
186
+ deadClick: function (selector, extraMeta) {
187
+ enqueue({
188
+ type: 'event',
189
+ meta: JSON.stringify(Object.assign({
190
+ eventType: 'dead_click',
191
+ selector: selector ? String(selector).slice(0, 200) : undefined,
192
+ }, extraMeta || {})),
193
+ });
194
+ },
195
+
196
+ /**
197
+ * Restore a cart from a signed recovery token. Fetches the items
198
+ * from the backend and returns them. Your storefront is responsible
199
+ * for calling its own Vendure order API to actually re-add them,
200
+ * because different storefronts frame that call differently.
201
+ */
202
+ restoreCart: function (token) {
203
+ var t = String(token || '').trim();
204
+ if (!t) return Promise.reject(new Error('missing-token'));
205
+ return fetch(CONFIG.recoverEndpoint + '?t=' + encodeURIComponent(t), {
206
+ credentials: 'include',
207
+ }).then(function (r) { return r.json(); });
208
+ },
209
+ };
210
+
211
+ // ── auto rage-click detector ─────────────────────────────────────
212
+ // Any pointerdown ≥ 3 within 500ms in a 20x20px zone counts.
213
+ if (CONFIG.autoRageClick) {
214
+ var recent = [];
215
+ document.addEventListener('pointerdown', function (e) {
216
+ var now = Date.now();
217
+ recent = recent.filter(function (r) { return now - r.t < 500; });
218
+ recent.push({ t: now, x: e.clientX, y: e.clientY, target: e.target });
219
+ var close = recent.filter(function (r) {
220
+ return Math.abs(r.x - e.clientX) < 20 && Math.abs(r.y - e.clientY) < 20;
221
+ });
222
+ if (close.length >= 3) {
223
+ hulo.rageClick(cssPathOf(e.target));
224
+ recent = []; // reset so we don't fire again on the 4th, 5th, etc.
225
+ }
226
+ }, { capture: true, passive: true });
227
+ }
228
+
229
+ // ── auto dead-click detector ─────────────────────────────────────
230
+ // A click that lands on an element with no href / not a button /
231
+ // not a form control and doesn't produce a navigation within 400ms.
232
+ if (CONFIG.autoDeadClick) {
233
+ document.addEventListener('click', function (e) {
234
+ var target = e.target;
235
+ if (!target || target.nodeType !== 1) return;
236
+ if (isInteractive(target)) return;
237
+ var beforeUrl = location.href;
238
+ var beforeScroll = window.scrollY;
239
+ setTimeout(function () {
240
+ if (location.href !== beforeUrl) return;
241
+ if (Math.abs(window.scrollY - beforeScroll) > 50) return;
242
+ // Was there an ancestor with a click handler? Best-effort:
243
+ // walk up and check for onclick, role='button', tabindex.
244
+ var node = target;
245
+ for (var i = 0; i < 4 && node; i++) {
246
+ if (isInteractive(node) || node.onclick) return;
247
+ node = node.parentElement;
248
+ }
249
+ hulo.deadClick(cssPathOf(target));
250
+ }, 400);
251
+ }, { capture: true, passive: true });
252
+ }
253
+
254
+ function isInteractive(el) {
255
+ if (!el || !el.tagName) return false;
256
+ var tag = el.tagName.toLowerCase();
257
+ if (tag === 'a' || tag === 'button' || tag === 'input' || tag === 'select'
258
+ || tag === 'textarea' || tag === 'label' || tag === 'summary'
259
+ || tag === 'details') return true;
260
+ if (el.getAttribute && (el.getAttribute('role') === 'button'
261
+ || el.getAttribute('role') === 'link'
262
+ || el.hasAttribute('tabindex'))) return true;
263
+ return false;
264
+ }
265
+
266
+ function cssPathOf(el) {
267
+ if (!el || el.nodeType !== 1) return '';
268
+ var parts = [];
269
+ while (el && el.nodeType === 1 && parts.length < 5) {
270
+ var p = el.tagName.toLowerCase();
271
+ if (el.id) { p += '#' + el.id; parts.unshift(p); break; }
272
+ if (el.className && typeof el.className === 'string') {
273
+ var cls = el.className.trim().split(/\\s+/).slice(0, 2).join('.');
274
+ if (cls) p += '.' + cls;
275
+ }
276
+ parts.unshift(p);
277
+ el = el.parentElement;
278
+ }
279
+ return parts.join(' > ');
280
+ }
281
+
282
+ global.hulo = hulo;
283
+ // Auto pageview on load — most callers want it.
284
+ if (document.readyState === 'complete') hulo.pageview();
285
+ else window.addEventListener('load', function () { hulo.pageview(); });
286
+ })(window);
287
+ `;
288
+ exports.HULO_STOREFRONT_JS = HULO_STOREFRONT_JS;
289
+ //# sourceMappingURL=hulo-storefront.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hulo-storefront.js","sourceRoot":"","sources":["../src/hulo-storefront.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;GAcG;AACI,MAAM,kBAAkB,GAAG,CAAC,cAAsB,EAAE,gBAAgB,GAAG,CAAC,EAAU,EAAE,CAAC;;;;;gBAK5E,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,YAAY,CAAC;uBACzD,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,mBAAmB,CAAC;iBAC7E,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqQhC,CAAC;AA5QW,QAAA,kBAAkB,sBA4Q7B"}
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,IAAI,EAAiB,MAAM,eAAe,CAAC;AACxE,OAAO,EAAmC,aAAa,EAAE,gBAAgB,EAAqB,aAAa,EAA4C,MAAM,iCAAiC,CAAC;AAM/L,OAAO,EAAwB,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAgBpF,MAAM,WAAW,6BAA6B;IAC1C,qEAAqE;IACrE,aAAa,EAAE,MAAM,CAAC;IACtB;;;gDAG4C;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;IAGpB;6EACyE;IACzE,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;yEAEqE;IACrE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;qEAEiE;IACjE,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;6CAEyC;IACzC,aAAa,CAAC,EAAE,OAAO,CAAC;IAGxB,mDAAmD;IACnD,SAAS,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD;;yEAEqE;IACrE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;2EAEuE;IACvE,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAG9B,8DAA8D;IAC9D,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAG7B;;4DAEwD;IACxD,WAAW,CAAC,EAAE,kBAAkB,CAAC;CACpC;AAgBD,QAAA,MAAM,eAAe,EAAE,6BAMtB,CAAC;AAGF,wBAAgB,UAAU,IAAI,OAAO,eAAe,GAAG,6BAA6B,CAEnF;AAED;;;;;;;;;;;GAWG;AACH,qBAyBa,sBAAsB;IAC/B,OAAO,CAAC,MAAM,CAAC,UAAU,CAAkC;IAC3D,OAAO,CAAC,MAAM,CAAC,aAAa,CAA8B;IAC1D,OAAO,CAAC,MAAM,CAAC,SAAS,CAA0B;IAClD,OAAO,CAAC,MAAM,CAAC,aAAa,CAA8B;IAE1D,MAAM,CAAC,gBAAgB,IAAI,aAAa,GAAG,IAAI;IAC/C,MAAM,CAAC,iBAAiB,IAAI,MAAM;IAClC,MAAM,CAAC,cAAc,IAAI,MAAM;IAC/B,MAAM,CAAC,gBAAgB,IAAI,aAAa,GAAG,IAAI;IAE/C,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,6BAA6B,GAAG,IAAI,CAAC,sBAAsB,CAAC;IAoDjF,MAAM,CAAC,YAAY;;;;;;;;MAUjB;CACL"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,IAAI,EAAiB,MAAM,eAAe,CAAC;AACxE,OAAO,EAAmC,aAAa,EAAE,gBAAgB,EAAqB,aAAa,EAA4C,MAAM,iCAAiC,CAAC;AAM/L,OAAO,EAAwB,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAiBpF,MAAM,WAAW,6BAA6B;IAC1C,qEAAqE;IACrE,aAAa,EAAE,MAAM,CAAC;IACtB;;;gDAG4C;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;IAGpB;6EACyE;IACzE,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;yEAEqE;IACrE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;qEAEiE;IACjE,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;6CAEyC;IACzC,aAAa,CAAC,EAAE,OAAO,CAAC;IAGxB,mDAAmD;IACnD,SAAS,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD;;yEAEqE;IACrE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;2EAEuE;IACvE,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAG9B,8DAA8D;IAC9D,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAG7B;;4DAEwD;IACxD,WAAW,CAAC,EAAE,kBAAkB,CAAC;CACpC;AAgBD,QAAA,MAAM,eAAe,EAAE,6BAMtB,CAAC;AAGF,wBAAgB,UAAU,IAAI,OAAO,eAAe,GAAG,6BAA6B,CAEnF;AAED;;;;;;;;;;;GAWG;AACH,qBA0Ba,sBAAsB;IAC/B,OAAO,CAAC,MAAM,CAAC,UAAU,CAAkC;IAC3D,OAAO,CAAC,MAAM,CAAC,aAAa,CAA8B;IAC1D,OAAO,CAAC,MAAM,CAAC,SAAS,CAA0B;IAClD,OAAO,CAAC,MAAM,CAAC,aAAa,CAA8B;IAE1D,MAAM,CAAC,gBAAgB,IAAI,aAAa,GAAG,IAAI;IAC/C,MAAM,CAAC,iBAAiB,IAAI,MAAM;IAClC,MAAM,CAAC,cAAc,IAAI,MAAM;IAC/B,MAAM,CAAC,gBAAgB,IAAI,aAAa,GAAG,IAAI;IAE/C,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,6BAA6B,GAAG,IAAI,CAAC,sBAAsB,CAAC;IAoDjF,MAAM,CAAC,YAAY;;;;;;;;MAUjB;CACL"}
package/dist/plugin.js CHANGED
@@ -27,6 +27,7 @@ const abandoned_cart_controller_1 = require("./abandoned-cart.controller");
27
27
  const recommendations_controller_1 = require("./recommendations.controller");
28
28
  const search_analytics_controller_1 = require("./search-analytics.controller");
29
29
  const journey_buffs_controller_1 = require("./journey-buffs.controller");
30
+ const storefront_helper_controller_1 = require("./storefront-helper.controller");
30
31
  // eslint-disable-next-line @typescript-eslint/no-var-requires
31
32
  const PKG_VERSION = require('../package.json').version;
32
33
  const PKG_NAME = '@huloglobal/vendure-plugin-visitor-analytics';
@@ -148,6 +149,7 @@ exports.VisitorAnalyticsPlugin = VisitorAnalyticsPlugin = VisitorAnalyticsPlugin
148
149
  recommendations_controller_1.RecommendationsController,
149
150
  search_analytics_controller_1.SearchAnalyticsController,
150
151
  journey_buffs_controller_1.JourneyBuffsController,
152
+ storefront_helper_controller_1.StorefrontHelperController,
151
153
  ],
152
154
  entities: [visitor_event_entity_1.VisitorEvent, conversion_goal_entity_1.ConversionGoal, abandoned_cart_entity_1.AbandonedCart, product_co_view_entity_1.ProductCoView],
153
155
  compatibility: '^3.0.0',
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import { Request, Response } from 'express';
2
+ /**
3
+ * Serves the storefront helper at `/ees/hulo.js`. Storefronts embed
4
+ * one script tag and every helper (cartSnapshot, productView, search,
5
+ * rageClick+deadClick auto-detectors, checkoutCompleted, restoreCart)
6
+ * is available on `window.hulo`.
7
+ *
8
+ * Cache: 10-minute browser TTL + 24-hour SWR. The file's content is
9
+ * a function of plugin options; storefronts don't need to bust the
10
+ * cache to see config changes because the URL stays stable and 10
11
+ * minutes is short enough that dashboard tweaks propagate promptly.
12
+ */
13
+ export declare class StorefrontHelperController {
14
+ hulo(req: Request, res: Response): void;
15
+ }
16
+ //# sourceMappingURL=storefront-helper.controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storefront-helper.controller.d.ts","sourceRoot":"","sources":["../src/storefront-helper.controller.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAI5C;;;;;;;;;;GAUG;AACH,qBACa,0BAA0B;IAEnC,IAAI,CAAQ,GAAG,EAAE,OAAO,EAAS,GAAG,EAAE,QAAQ;CAYjD"}
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.StorefrontHelperController = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const hulo_storefront_1 = require("./hulo-storefront");
18
+ const plugin_1 = require("./plugin");
19
+ /**
20
+ * Serves the storefront helper at `/ees/hulo.js`. Storefronts embed
21
+ * one script tag and every helper (cartSnapshot, productView, search,
22
+ * rageClick+deadClick auto-detectors, checkoutCompleted, restoreCart)
23
+ * is available on `window.hulo`.
24
+ *
25
+ * Cache: 10-minute browser TTL + 24-hour SWR. The file's content is
26
+ * a function of plugin options; storefronts don't need to bust the
27
+ * cache to see config changes because the URL stays stable and 10
28
+ * minutes is short enough that dashboard tweaks propagate promptly.
29
+ */
30
+ let StorefrontHelperController = class StorefrontHelperController {
31
+ hulo(req, res) {
32
+ const opts = (0, plugin_1.getOptions)();
33
+ const base = opts.publicBaseUrl
34
+ || `https://${req.headers.host || 'localhost'}`;
35
+ const js = (0, hulo_storefront_1.HULO_STOREFRONT_JS)(base, 1);
36
+ res.setHeader('content-type', 'application/javascript; charset=utf-8');
37
+ res.setHeader('cache-control', 'public, max-age=600, stale-while-revalidate=86400');
38
+ // Storefront lives on a different origin from the backend —
39
+ // static-JS CORS is safe (no credentials) but be explicit.
40
+ res.setHeader('access-control-allow-origin', '*');
41
+ res.send(js);
42
+ }
43
+ };
44
+ exports.StorefrontHelperController = StorefrontHelperController;
45
+ __decorate([
46
+ (0, common_1.Get)('hulo.js'),
47
+ __param(0, (0, common_1.Req)()),
48
+ __param(1, (0, common_1.Res)()),
49
+ __metadata("design:type", Function),
50
+ __metadata("design:paramtypes", [Object, Object]),
51
+ __metadata("design:returntype", void 0)
52
+ ], StorefrontHelperController.prototype, "hulo", null);
53
+ exports.StorefrontHelperController = StorefrontHelperController = __decorate([
54
+ (0, common_1.Controller)('ees')
55
+ ], StorefrontHelperController);
56
+ //# sourceMappingURL=storefront-helper.controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storefront-helper.controller.js","sourceRoot":"","sources":["../src/storefront-helper.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAA2D;AAE3D,uDAAuD;AACvD,qCAAsC;AAEtC;;;;;;;;;;GAUG;AAEI,IAAM,0BAA0B,GAAhC,MAAM,0BAA0B;IAEnC,IAAI,CAAQ,GAAY,EAAS,GAAa;QAC1C,MAAM,IAAI,GAAG,IAAA,mBAAU,GAAE,CAAC;QAC1B,MAAM,IAAI,GAAI,IAAY,CAAC,aAAa;eACjC,WAAW,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,WAAW,EAAE,CAAC;QACpD,MAAM,EAAE,GAAG,IAAA,oCAAkB,EAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QACvC,GAAG,CAAC,SAAS,CAAC,cAAc,EAAE,uCAAuC,CAAC,CAAC;QACvE,GAAG,CAAC,SAAS,CAAC,eAAe,EAAE,mDAAmD,CAAC,CAAC;QACpF,4DAA4D;QAC5D,2DAA2D;QAC3D,GAAG,CAAC,SAAS,CAAC,6BAA6B,EAAE,GAAG,CAAC,CAAC;QAClD,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACjB,CAAC;CACJ,CAAA;AAdY,gEAA0B;AAEnC;IADC,IAAA,YAAG,EAAC,SAAS,CAAC;IACT,WAAA,IAAA,YAAG,GAAE,CAAA;IAAgB,WAAA,IAAA,YAAG,GAAE,CAAA;;;;sDAW/B;qCAbQ,0BAA0B;IADtC,IAAA,mBAAU,EAAC,KAAK,CAAC;GACL,0BAA0B,CActC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@huloglobal/vendure-plugin-visitor-analytics",
3
- "version": "0.8.0",
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>",