@huloglobal/vendure-plugin-visitor-analytics 0.8.1 → 0.8.3

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,32 @@ 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.2] — 2026-07-04
9
+
10
+ ### Documentation
11
+ - README rewritten to cover every 0.8.x feature: the drop-in
12
+ `/ees/hulo.js` storefront helper, cart-abandonment configuration
13
+ and lifecycle, recommendation endpoints, search analytics,
14
+ journey drawer buffs. Includes a walkthrough of the
15
+ storefront-side `/cart/restore` route customers implement to
16
+ consume recovery links.
17
+ - Endpoints table split into Public (browser-safe, CORS-permissive)
18
+ vs Admin, with every 0.8.0-introduced endpoint marked inline.
19
+ - No runtime changes — bumped to ship the new README with the npm
20
+ tarball.
21
+
22
+ ## [0.8.1] — 2026-07-04
23
+
24
+ ### Added
25
+ - `/ees/hulo.js` — the plugin now serves a drop-in typed storefront
26
+ helper at this path. One `<script src>` tag and every event API
27
+ (`hulo.cartSnapshot`, `hulo.productView`, `hulo.search`,
28
+ `hulo.checkoutCompleted`, `hulo.restoreCart`) is available on
29
+ `window.hulo`. Handles batching, `sendBeacon` on unload, and
30
+ installs auto rage-click + dead-click detectors. Served with a
31
+ 10-minute browser TTL + 24-hour stale-while-revalidate and
32
+ permissive CORS so it works cross-origin from any storefront.
33
+
8
34
  ## [0.8.0] — 2026-07-04
9
35
 
10
36
  ### Added
@@ -230,6 +256,8 @@ adheres to [semantic versioning](https://semver.org/spec/v2.0.0.html).
230
256
  - Licence verification via `@huloglobal/vendure-licence-sdk` with
231
257
  revocation polling.
232
258
 
259
+ [0.8.2]: https://github.com/exceeded/vendure-plugin-visitor-analytics/releases/tag/v0.8.2
260
+ [0.8.1]: https://github.com/exceeded/vendure-plugin-visitor-analytics/releases/tag/v0.8.1
233
261
  [0.8.0]: https://github.com/exceeded/vendure-plugin-visitor-analytics/releases/tag/v0.8.0
234
262
  [0.7.0]: https://github.com/exceeded/vendure-plugin-visitor-analytics/releases/tag/v0.7.0
235
263
  [0.6.0]: https://github.com/exceeded/vendure-plugin-visitor-analytics/releases/tag/v0.6.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
 
package/dist/index.d.ts CHANGED
@@ -7,7 +7,7 @@ export { VisitorEvent } from './visitor-event.entity';
7
7
  export { AbandonedCart, AbandonedCartStatus } from './abandoned-cart.entity';
8
8
  export { ProductCoView } from './product-co-view.entity';
9
9
  export { AbandonedCartService, CartSnapshotMeta, AbandonmentOptions, } from './abandoned-cart.service';
10
- export { RecommendationsService } from './recommendations.service';
10
+ export { RecommendationsService, RecommendedProduct } from './recommendations.service';
11
11
  export { SearchAnalyticsService } from './search-analytics.service';
12
12
  export { JourneyBuffsService } from './journey-buffs.service';
13
13
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,sBAAsB,EAAE,6BAA6B,EAAE,MAAM,UAAU,CAAC;AACjF,OAAO,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACpE,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC7E,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,EACH,oBAAoB,EACpB,gBAAgB,EAChB,kBAAkB,GACrB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AACnE,OAAO,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACpE,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,sBAAsB,EAAE,6BAA6B,EAAE,MAAM,UAAU,CAAC;AACjF,OAAO,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACpE,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC7E,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,EACH,oBAAoB,EACpB,gBAAgB,EAChB,kBAAkB,GACrB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AACvF,OAAO,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACpE,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC"}
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;GAEG;;;AAEH,mCAAiF;AAAxE,gHAAA,sBAAsB,OAAA;AAC/B,uEAAoE;AAA3D,kIAAA,sBAAsB,OAAA;AAC/B,+DAAsD;AAA7C,oHAAA,YAAY,OAAA;AACrB,iEAA6E;AAApE,sHAAA,aAAa,OAAA;AACtB,mEAAyD;AAAhD,uHAAA,aAAa,OAAA;AACtB,mEAIkC;AAH9B,8HAAA,oBAAoB,OAAA;AAIxB,qEAAmE;AAA1D,iIAAA,sBAAsB,OAAA;AAC/B,uEAAoE;AAA3D,kIAAA,sBAAsB,OAAA;AAC/B,iEAA8D;AAArD,4HAAA,mBAAmB,OAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;GAEG;;;AAEH,mCAAiF;AAAxE,gHAAA,sBAAsB,OAAA;AAC/B,uEAAoE;AAA3D,kIAAA,sBAAsB,OAAA;AAC/B,+DAAsD;AAA7C,oHAAA,YAAY,OAAA;AACrB,iEAA6E;AAApE,sHAAA,aAAa,OAAA;AACtB,mEAAyD;AAAhD,uHAAA,aAAa,OAAA;AACtB,mEAIkC;AAH9B,8HAAA,oBAAoB,OAAA;AAIxB,qEAAuF;AAA9E,iIAAA,sBAAsB,OAAA;AAC/B,uEAAoE;AAA3D,kIAAA,sBAAsB,OAAA;AAC/B,iEAA8D;AAArD,4HAAA,mBAAmB,OAAA"}
@@ -21,10 +21,7 @@ export declare class RecommendationsController {
21
21
  items?: undefined;
22
22
  } | {
23
23
  productId: number;
24
- items: {
25
- productId: number;
26
- score: number;
27
- }[];
24
+ items: import("./recommendations.service").RecommendedProduct[];
28
25
  error?: undefined;
29
26
  }>;
30
27
  /**
@@ -40,10 +37,7 @@ export declare class RecommendationsController {
40
37
  items?: undefined;
41
38
  } | {
42
39
  visitorId: string;
43
- items: {
44
- productId: number;
45
- score: number;
46
- }[];
40
+ items: import("./recommendations.service").RecommendedProduct[];
47
41
  error?: undefined;
48
42
  }>;
49
43
  /**
@@ -53,10 +47,7 @@ export declare class RecommendationsController {
53
47
  trending(chRaw?: string, hRaw?: string, lRaw?: string): Promise<{
54
48
  channelId: number;
55
49
  hours: number;
56
- items: {
57
- productId: number;
58
- views: number;
59
- }[];
50
+ items: import("./recommendations.service").RecommendedProduct[];
60
51
  }>;
61
52
  /**
62
53
  * Admin-only: manually kick the aggregation cron. Normally runs
@@ -1 +1 @@
1
- {"version":3,"file":"recommendations.controller.d.ts","sourceRoot":"","sources":["../src/recommendations.controller.ts"],"names":[],"mappings":"AACA,OAAO,EAAO,cAAc,EAAqB,MAAM,eAAe,CAAC;AACvE,OAAO,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAEnE;;;;;;;GAOG;AACH,qBACa,yBAAyB;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,sBAAsB;IAE5D;;;OAGG;IAEG,UAAU,CACQ,IAAI,CAAC,EAAE,MAAM,EACb,KAAK,CAAC,EAAE,MAAM,EACd,IAAI,CAAC,EAAE,MAAM;;;;;;;;;;;;IAUrC;;;;;;OAMG;IAEG,QAAQ,CACU,IAAI,CAAC,EAAE,MAAM,EACb,KAAK,CAAC,EAAE,MAAM,EACd,IAAI,CAAC,EAAE,MAAM;;;;;;;;;;;;IAUrC;;;OAGG;IAEG,QAAQ,CACU,KAAK,CAAC,EAAE,MAAM,EACd,IAAI,CAAC,EAAE,MAAM,EACb,IAAI,CAAC,EAAE,MAAM;;;;;;;;IASrC;;;;OAIG;IAGG,YAAY,CAAQ,GAAG,EAAE,cAAc,EAAkB,IAAI,CAAC,EAAE,MAAM;;;;;CAK/E"}
1
+ {"version":3,"file":"recommendations.controller.d.ts","sourceRoot":"","sources":["../src/recommendations.controller.ts"],"names":[],"mappings":"AACA,OAAO,EAAO,cAAc,EAAqB,MAAM,eAAe,CAAC;AACvE,OAAO,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAEnE;;;;;;;GAOG;AACH,qBACa,yBAAyB;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,sBAAsB;IAE5D;;;OAGG;IAEG,UAAU,CACQ,IAAI,CAAC,EAAE,MAAM,EACb,KAAK,CAAC,EAAE,MAAM,EACd,IAAI,CAAC,EAAE,MAAM;;;;;;;;;IAUrC;;;;;;OAMG;IAEG,QAAQ,CACU,IAAI,CAAC,EAAE,MAAM,EACb,KAAK,CAAC,EAAE,MAAM,EACd,IAAI,CAAC,EAAE,MAAM;;;;;;;;;IAUrC;;;OAGG;IAEG,QAAQ,CACU,KAAK,CAAC,EAAE,MAAM,EACd,IAAI,CAAC,EAAE,MAAM,EACb,IAAI,CAAC,EAAE,MAAM;;;;;IASrC;;;;OAIG;IAGG,YAAY,CAAQ,GAAG,EAAE,cAAc,EAAkB,IAAI,CAAC,EAAE,MAAM;;;;;CAK/E"}
@@ -1,4 +1,19 @@
1
1
  import { TransactionalConnection } from '@vendure/core';
2
+ /** Row shape returned by every read-side endpoint. `productName` and
3
+ * `productSlug` are best-effort enrichments — they'll be null when
4
+ * the product has been deleted or when the `product_translation`
5
+ * table doesn't have a row for it in any language. */
6
+ export interface RecommendedProduct {
7
+ productId: number;
8
+ productName: string | null;
9
+ productSlug: string | null;
10
+ /** Numeric score returned by whichever endpoint produced this row.
11
+ * Callers should read `score` for co-view based responses and
12
+ * `views` for the raw trending endpoint — both are populated
13
+ * here so a UI can render one column regardless. */
14
+ score: number;
15
+ views: number;
16
+ }
2
17
  /**
3
18
  * Product recommendations driven by observed co-viewing behaviour.
4
19
  *
@@ -12,6 +27,13 @@ import { TransactionalConnection } from '@vendure/core';
12
27
  * data for the latter, and small stores don't have enough of it for
13
28
  * useful signals. Co-views come from every browsing session, so a
14
29
  * store with 1k daily visitors gets useful recs from day one.
30
+ *
31
+ * Every read-side response is enriched with the product NAME and
32
+ * SLUG (via a single bulk lookup against `product_translation`) so
33
+ * the admin UI + a first-party storefront can render "Windows Server
34
+ * 2022 Datacenter #42" without a second round-trip. Bare product ids
35
+ * are still returned so storefronts that prefer their own hydration
36
+ * path can ignore the extra fields.
15
37
  */
16
38
  export declare class RecommendationsService {
17
39
  private connection;
@@ -31,32 +53,33 @@ export declare class RecommendationsService {
31
53
  }>;
32
54
  /**
33
55
  * Read side: "customers who viewed X also viewed Y" for one
34
- * product, ordered by co-view score, capped at `limit`. Returns
35
- * bare product ids so the storefront can hydrate names / images
36
- * from its usual product-fetch path.
56
+ * product, ordered by co-view score, capped at `limit`.
37
57
  */
38
- alsoViewed(productId: number, channelId?: number, limit?: number): Promise<Array<{
39
- productId: number;
40
- score: number;
41
- }>>;
58
+ alsoViewed(productId: number, channelId?: number, limit?: number): Promise<RecommendedProduct[]>;
42
59
  /**
43
60
  * Personalised recommendations for a returning visitor: look at
44
61
  * the last N products they viewed, and rank co-viewed products by
45
62
  * combined score across those seed products (excluding the seeds
46
63
  * themselves).
47
64
  */
48
- personalRecommendations(visitorId: string, channelId?: number, limit?: number): Promise<Array<{
49
- productId: number;
50
- score: number;
51
- }>>;
65
+ personalRecommendations(visitorId: string, channelId?: number, limit?: number): Promise<RecommendedProduct[]>;
52
66
  /**
53
67
  * "Trending now" — most-viewed products in the last N hours.
54
68
  * Useful for a homepage rail; uses `product_view` events, so it
55
69
  * reflects real interest, not just search-console clicks.
56
70
  */
57
- trending(channelId?: number, sinceHours?: number, limit?: number): Promise<Array<{
58
- productId: number;
59
- views: number;
60
- }>>;
71
+ trending(channelId?: number, sinceHours?: number, limit?: number): Promise<RecommendedProduct[]>;
72
+ /**
73
+ * Bulk-fetch product names + slugs for a list of product ids in
74
+ * one round-trip. Prefers the English translation when a product
75
+ * has more than one, then falls back to whichever translation
76
+ * MariaDB returns first. Returns two maps keyed by productId; keys
77
+ * absent from the maps mean the product either doesn't exist any
78
+ * more or has no translation row at all (unusual — Vendure
79
+ * always creates one on product create).
80
+ */
81
+ private fetchProductInfo;
82
+ /** Apply the bulk name lookup to a set of raw rec rows. */
83
+ private enrichWithNames;
61
84
  }
62
85
  //# sourceMappingURL=recommendations.service.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"recommendations.service.d.ts","sourceRoot":"","sources":["../src/recommendations.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAIxD;;;;;;;;;;;;;GAaG;AACH,qBACa,sBAAsB;IACnB,OAAO,CAAC,UAAU;gBAAV,UAAU,EAAE,uBAAuB;IAEvD;;;;;;;;;OASG;IACG,gBAAgB,CAAC,UAAU,SAAK,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAyDnE;;;;;OAKG;IACG,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,SAAI,EAAE,KAAK,SAAK,GAAG,OAAO,CAAC,KAAK,CAAC;QAC1E,SAAS,EAAE,MAAM,CAAC;QAClB,KAAK,EAAE,MAAM,CAAC;KACjB,CAAC,CAAC;IAYH;;;;;OAKG;IACG,uBAAuB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,SAAI,EAAE,KAAK,SAAK,GAAG,OAAO,CAAC,KAAK,CAAC;QACvF,SAAS,EAAE,MAAM,CAAC;QAClB,KAAK,EAAE,MAAM,CAAC;KACjB,CAAC,CAAC;IA+BH;;;;OAIG;IACG,QAAQ,CAAC,SAAS,SAAI,EAAE,UAAU,SAAK,EAAE,KAAK,SAAK,GAAG,OAAO,CAAC,KAAK,CAAC;QACtE,SAAS,EAAE,MAAM,CAAC;QAClB,KAAK,EAAE,MAAM,CAAC;KACjB,CAAC,CAAC;CAmBN"}
1
+ {"version":3,"file":"recommendations.service.d.ts","sourceRoot":"","sources":["../src/recommendations.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAIxD;;;uDAGuD;AACvD,MAAM,WAAW,kBAAkB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B;;;yDAGqD;IACrD,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBACa,sBAAsB;IACnB,OAAO,CAAC,UAAU;gBAAV,UAAU,EAAE,uBAAuB;IAEvD;;;;;;;;;OASG;IACG,gBAAgB,CAAC,UAAU,SAAK,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAyDnE;;;OAGG;IACG,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,SAAI,EAAE,KAAK,SAAK,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC;IAgB7F;;;;;OAKG;IACG,uBAAuB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,SAAI,EAAE,KAAK,SAAK,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC;IAmC1G;;;;OAIG;IACG,QAAQ,CAAC,SAAS,SAAI,EAAE,UAAU,SAAK,EAAE,KAAK,SAAK,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC;IAwBzF;;;;;;;;OAQG;YACW,gBAAgB;IAuC9B,2DAA2D;YAC7C,eAAe;CAehC"}
@@ -26,6 +26,13 @@ const loggerCtx = 'HuloRecommendationsService';
26
26
  * data for the latter, and small stores don't have enough of it for
27
27
  * useful signals. Co-views come from every browsing session, so a
28
28
  * store with 1k daily visitors gets useful recs from day one.
29
+ *
30
+ * Every read-side response is enriched with the product NAME and
31
+ * SLUG (via a single bulk lookup against `product_translation`) so
32
+ * the admin UI + a first-party storefront can render "Windows Server
33
+ * 2022 Datacenter #42" without a second round-trip. Bare product ids
34
+ * are still returned so storefronts that prefer their own hydration
35
+ * path can ignore the extra fields.
29
36
  */
30
37
  let RecommendationsService = class RecommendationsService {
31
38
  constructor(connection) {
@@ -94,9 +101,7 @@ let RecommendationsService = class RecommendationsService {
94
101
  }
95
102
  /**
96
103
  * Read side: "customers who viewed X also viewed Y" for one
97
- * product, ordered by co-view score, capped at `limit`. Returns
98
- * bare product ids so the storefront can hydrate names / images
99
- * from its usual product-fetch path.
104
+ * product, ordered by co-view score, capped at `limit`.
100
105
  */
101
106
  async alsoViewed(productId, channelId = 1, limit = 10) {
102
107
  const rows = await this.connection.rawConnection.query(`SELECT productIdB AS productId, viewsTogether AS score
@@ -104,7 +109,11 @@ let RecommendationsService = class RecommendationsService {
104
109
  WHERE productIdA = ? AND channelId = ?
105
110
  ORDER BY viewsTogether DESC, lastUpdated DESC
106
111
  LIMIT ?`, [productId, channelId, Math.min(Math.max(1, limit), 50)]);
107
- return rows.map(r => ({ productId: Number(r.productId), score: Number(r.score) }));
112
+ return this.enrichWithNames(rows.map(r => ({
113
+ productId: Number(r.productId),
114
+ score: Number(r.score),
115
+ views: Number(r.score),
116
+ })));
108
117
  }
109
118
  /**
110
119
  * Personalised recommendations for a returning visitor: look at
@@ -135,7 +144,11 @@ let RecommendationsService = class RecommendationsService {
135
144
  GROUP BY productIdB
136
145
  ORDER BY score DESC
137
146
  LIMIT ?`, [...seedIds, channelId, ...seedIds, Math.min(Math.max(1, limit), 50)]);
138
- return rows.map(r => ({ productId: Number(r.productId), score: Number(r.score) }));
147
+ return this.enrichWithNames(rows.map(r => ({
148
+ productId: Number(r.productId),
149
+ score: Number(r.score),
150
+ views: Number(r.score),
151
+ })));
139
152
  }
140
153
  /**
141
154
  * "Trending now" — most-viewed products in the last N hours.
@@ -156,7 +169,68 @@ let RecommendationsService = class RecommendationsService {
156
169
  HAVING productId > 0
157
170
  ORDER BY views DESC
158
171
  LIMIT ?`, [channelId, since, Math.min(Math.max(1, limit), 50)]);
159
- return rows.map(r => ({ productId: Number(r.productId), views: Number(r.views) }));
172
+ return this.enrichWithNames(rows.map(r => ({
173
+ productId: Number(r.productId),
174
+ score: Number(r.views),
175
+ views: Number(r.views),
176
+ })));
177
+ }
178
+ /**
179
+ * Bulk-fetch product names + slugs for a list of product ids in
180
+ * one round-trip. Prefers the English translation when a product
181
+ * has more than one, then falls back to whichever translation
182
+ * MariaDB returns first. Returns two maps keyed by productId; keys
183
+ * absent from the maps mean the product either doesn't exist any
184
+ * more or has no translation row at all (unusual — Vendure
185
+ * always creates one on product create).
186
+ */
187
+ async fetchProductInfo(ids) {
188
+ const names = new Map();
189
+ const slugs = new Map();
190
+ if (!ids.length)
191
+ return { names, slugs };
192
+ const placeholders = ids.map(() => '?').join(',');
193
+ try {
194
+ // `deletedAt IS NULL` on product so we don't surface
195
+ // soft-deleted products in the admin UI. Order by
196
+ // languageCode = 'en' DESC so English wins the tie for
197
+ // multi-locale stores; single-locale installs will
198
+ // naturally land on their only translation.
199
+ const rows = await this.connection.rawConnection.query(`SELECT pt.baseId AS productId, pt.name, pt.slug
200
+ FROM product_translation pt
201
+ JOIN product p ON p.id = pt.baseId AND p.deletedAt IS NULL
202
+ WHERE pt.baseId IN (${placeholders})
203
+ ORDER BY pt.baseId, pt.languageCode = 'en' DESC`, ids);
204
+ for (const r of rows) {
205
+ const id = Number(r.productId);
206
+ // First-write wins — the ORDER BY puts English first
207
+ // for each baseId, so we keep that one and skip
208
+ // subsequent translations.
209
+ if (!names.has(id))
210
+ names.set(id, String(r.name || ''));
211
+ if (!slugs.has(id))
212
+ slugs.set(id, String(r.slug || ''));
213
+ }
214
+ }
215
+ catch (e) {
216
+ // Never break the recs endpoint on a name-lookup failure —
217
+ // an admin can still see the id and click through to the
218
+ // Vendure catalog page manually.
219
+ common_1.Logger.warn(`Product info lookup failed: ${e === null || e === void 0 ? void 0 : e.message}`, loggerCtx);
220
+ }
221
+ return { names, slugs };
222
+ }
223
+ /** Apply the bulk name lookup to a set of raw rec rows. */
224
+ async enrichWithNames(rows) {
225
+ const ids = rows.map(r => r.productId);
226
+ const { names, slugs } = await this.fetchProductInfo(ids);
227
+ return rows.map(r => ({
228
+ productId: r.productId,
229
+ productName: names.get(r.productId) || null,
230
+ productSlug: slugs.get(r.productId) || null,
231
+ score: r.score,
232
+ views: r.views,
233
+ }));
160
234
  }
161
235
  };
162
236
  exports.RecommendationsService = RecommendationsService;
@@ -1 +1 @@
1
- {"version":3,"file":"recommendations.service.js","sourceRoot":"","sources":["../src/recommendations.service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"recommendations.service.js","sourceRoot":"","sources":["../src/recommendations.service.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAAoD;AACpD,wCAAwD;AAExD,MAAM,SAAS,GAAG,4BAA4B,CAAC;AAkB/C;;;;;;;;;;;;;;;;;;;;GAoBG;AAEI,IAAM,sBAAsB,GAA5B,MAAM,sBAAsB;IAC/B,YAAoB,UAAmC;QAAnC,eAAU,GAAV,UAAU,CAAyB;IAAG,CAAC;IAE3D;;;;;;;;;OASG;IACH,KAAK,CAAC,gBAAgB,CAAC,UAAU,GAAG,EAAE;QAClC,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC;QAC3C,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,UAAU,GAAG,OAAQ,CAAC,CAAC;QAC3D,2DAA2D;QAC3D,gEAAgE;QAChE,sCAAsC;QACtC,MAAM,QAAQ,GAAU,MAAM,IAAI,CAAC,KAAK,CACpC;;;;;;;;;;;;;8CAakC,EAClC,CAAC,KAAK,CAAC,CACV,CAAC;QAEF,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;YACvB,MAAM,MAAM,GAAa,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,EAAE,CAAC;iBACvC,KAAK,CAAC,GAAG,CAAC;iBACV,GAAG,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;iBAC1D,MAAM,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;YACxD,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;gBAAE,SAAS;YAChC,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;YACzC,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;gBAAE,SAAS;YAE9B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;gBACnC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;oBACnC,IAAI,CAAC,KAAK,CAAC;wBAAE,SAAS;oBACtB,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;oBAClB,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;oBAClB,qDAAqD;oBACrD,MAAM,IAAI,CAAC,KAAK,CACZ;;;;;gDAKwB,EACxB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,CAC3B,CAAC;oBACF,KAAK,IAAI,CAAC,CAAC;gBACf,CAAC;YACL,CAAC;QACL,CAAC;QACD,eAAM,CAAC,GAAG,CAAC,wBAAwB,QAAQ,CAAC,MAAM,cAAc,KAAK,eAAe,EAAE,SAAS,CAAC,CAAC;QACjG,OAAO,EAAE,KAAK,EAAE,CAAC;IACrB,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,UAAU,CAAC,SAAiB,EAAE,SAAS,GAAG,CAAC,EAAE,KAAK,GAAG,EAAE;QACzD,MAAM,IAAI,GAAU,MAAM,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,KAAK,CACzD;;;;qBAIS,EACT,CAAC,SAAS,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,CAC3D,CAAC;QACF,OAAO,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YACvC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;YAC9B,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC;YACtB,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC;SACzB,CAAC,CAAC,CAAC,CAAC;IACT,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,uBAAuB,CAAC,SAAiB,EAAE,SAAS,GAAG,CAAC,EAAE,KAAK,GAAG,EAAE;QACtE,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC;QAC3C,MAAM,KAAK,GAAU,MAAM,IAAI,CAAC,KAAK,CACjC;;;;;;;;sBAQU,EACV,CAAC,SAAS,CAAC,CACd,CAAC;QACF,MAAM,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QACvE,IAAI,CAAC,OAAO,CAAC,MAAM;YAAE,OAAO,EAAE,CAAC;QAC/B,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACtD,MAAM,IAAI,GAAU,MAAM,IAAI,CAAC,KAAK,CAChC;;oCAEwB,YAAY;;wCAER,YAAY;;;qBAG/B,EACT,CAAC,GAAG,OAAO,EAAE,SAAS,EAAE,GAAG,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,CACxE,CAAC;QACF,OAAO,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YACvC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;YAC9B,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC;YACtB,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC;SACzB,CAAC,CAAC,CAAC,CAAC;IACT,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,QAAQ,CAAC,SAAS,GAAG,CAAC,EAAE,UAAU,GAAG,EAAE,EAAE,KAAK,GAAG,EAAE;QACrD,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,UAAU,GAAG,OAAQ,CAAC,CAAC;QAC3D,MAAM,IAAI,GAAU,MAAM,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,KAAK,CACzD;;;;;;;;;;;qBAWS,EACT,CAAC,SAAS,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,CACvD,CAAC;QACF,OAAO,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YACvC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;YAC9B,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC;YACtB,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC;SACzB,CAAC,CAAC,CAAC,CAAC;IACT,CAAC;IAED;;;;;;;;OAQG;IACK,KAAK,CAAC,gBAAgB,CAAC,GAAa;QAIxC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;QACxC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;QACxC,IAAI,CAAC,GAAG,CAAC,MAAM;YAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;QACzC,MAAM,YAAY,GAAG,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAClD,IAAI,CAAC;YACD,qDAAqD;YACrD,kDAAkD;YAClD,uDAAuD;YACvD,mDAAmD;YACnD,4CAA4C;YAC5C,MAAM,IAAI,GAAU,MAAM,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,KAAK,CACzD;;;uCAGuB,YAAY;iEACc,EACjD,GAAG,CACN,CAAC;YACF,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;gBACnB,MAAM,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;gBAC/B,qDAAqD;gBACrD,gDAAgD;gBAChD,2BAA2B;gBAC3B,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;oBAAE,KAAK,CAAC,GAAG,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC;gBACxD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;oBAAE,KAAK,CAAC,GAAG,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC;YAC5D,CAAC;QACL,CAAC;QAAC,OAAO,CAAM,EAAE,CAAC;YACd,2DAA2D;YAC3D,yDAAyD;YACzD,iCAAiC;YACjC,eAAM,CAAC,IAAI,CAAC,+BAA+B,CAAC,aAAD,CAAC,uBAAD,CAAC,CAAE,OAAO,EAAE,EAAE,SAAS,CAAC,CAAC;QACxE,CAAC;QACD,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IAC5B,CAAC;IAED,2DAA2D;IACnD,KAAK,CAAC,eAAe,CAAC,IAI5B;QACE,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QACvC,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC;QAC1D,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YAClB,SAAS,EAAE,CAAC,CAAC,SAAS;YACtB,WAAW,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,IAAI;YAC3C,WAAW,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,IAAI;YAC3C,KAAK,EAAE,CAAC,CAAC,KAAK;YACd,KAAK,EAAE,CAAC,CAAC,KAAK;SACjB,CAAC,CAAC,CAAC;IACR,CAAC;CACJ,CAAA;AAhOY,wDAAsB;iCAAtB,sBAAsB;IADlC,IAAA,mBAAU,GAAE;qCAEuB,8BAAuB;GAD9C,sBAAsB,CAgOlC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@huloglobal/vendure-plugin-visitor-analytics",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
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>",