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

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.
Files changed (62) hide show
  1. package/CHANGELOG.md +165 -15
  2. package/dist/abandoned-cart.controller.d.ts +78 -0
  3. package/dist/abandoned-cart.controller.d.ts.map +1 -0
  4. package/dist/abandoned-cart.controller.js +251 -0
  5. package/dist/abandoned-cart.controller.js.map +1 -0
  6. package/dist/abandoned-cart.entity.d.ts +61 -0
  7. package/dist/abandoned-cart.entity.d.ts.map +1 -0
  8. package/dist/abandoned-cart.entity.js +143 -0
  9. package/dist/abandoned-cart.entity.js.map +1 -0
  10. package/dist/abandoned-cart.scanner.d.ts +34 -0
  11. package/dist/abandoned-cart.scanner.d.ts.map +1 -0
  12. package/dist/abandoned-cart.scanner.js +85 -0
  13. package/dist/abandoned-cart.scanner.js.map +1 -0
  14. package/dist/abandoned-cart.service.d.ts +92 -0
  15. package/dist/abandoned-cart.service.d.ts.map +1 -0
  16. package/dist/abandoned-cart.service.js +276 -0
  17. package/dist/abandoned-cart.service.js.map +1 -0
  18. package/dist/hulo-storefront.d.ts +17 -0
  19. package/dist/hulo-storefront.d.ts.map +1 -0
  20. package/dist/hulo-storefront.js +289 -0
  21. package/dist/hulo-storefront.js.map +1 -0
  22. package/dist/index.d.ts +6 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +13 -1
  25. package/dist/index.js.map +1 -1
  26. package/dist/journey-buffs.controller.d.ts +43 -0
  27. package/dist/journey-buffs.controller.d.ts.map +1 -0
  28. package/dist/journey-buffs.controller.js +79 -0
  29. package/dist/journey-buffs.controller.js.map +1 -0
  30. package/dist/journey-buffs.service.d.ts +53 -0
  31. package/dist/journey-buffs.service.d.ts.map +1 -0
  32. package/dist/journey-buffs.service.js +141 -0
  33. package/dist/journey-buffs.service.js.map +1 -0
  34. package/dist/plugin.d.ts +5 -0
  35. package/dist/plugin.d.ts.map +1 -1
  36. package/dist/plugin.js +30 -3
  37. package/dist/plugin.js.map +1 -1
  38. package/dist/product-co-view.entity.d.ts +19 -0
  39. package/dist/product-co-view.entity.d.ts.map +1 -0
  40. package/dist/product-co-view.entity.js +52 -0
  41. package/dist/product-co-view.entity.js.map +1 -0
  42. package/dist/recommendations.controller.d.ts +72 -0
  43. package/dist/recommendations.controller.d.ts.map +1 -0
  44. package/dist/recommendations.controller.js +123 -0
  45. package/dist/recommendations.controller.js.map +1 -0
  46. package/dist/recommendations.service.d.ts +62 -0
  47. package/dist/recommendations.service.d.ts.map +1 -0
  48. package/dist/recommendations.service.js +167 -0
  49. package/dist/recommendations.service.js.map +1 -0
  50. package/dist/search-analytics.controller.d.ts +36 -0
  51. package/dist/search-analytics.controller.d.ts.map +1 -0
  52. package/dist/search-analytics.controller.js +86 -0
  53. package/dist/search-analytics.controller.js.map +1 -0
  54. package/dist/search-analytics.service.d.ts +36 -0
  55. package/dist/search-analytics.service.d.ts.map +1 -0
  56. package/dist/search-analytics.service.js +105 -0
  57. package/dist/search-analytics.service.js.map +1 -0
  58. package/dist/storefront-helper.controller.d.ts +16 -0
  59. package/dist/storefront-helper.controller.d.ts.map +1 -0
  60. package/dist/storefront-helper.controller.js +56 -0
  61. package/dist/storefront-helper.controller.js.map +1 -0
  62. package/package.json +1 -1
@@ -0,0 +1,167 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.RecommendationsService = void 0;
13
+ const common_1 = require("@nestjs/common");
14
+ const core_1 = require("@vendure/core");
15
+ const loggerCtx = 'HuloRecommendationsService';
16
+ /**
17
+ * Product recommendations driven by observed co-viewing behaviour.
18
+ *
19
+ * The scanner walks recent `product_view` custom events grouped by
20
+ * session, extracts every ordered pair, and increments a counter per
21
+ * `(productIdA, productIdB, channelId)` triple. We denormalise both
22
+ * directions (`A → B` AND `B → A`) so read-side lookups are one
23
+ * indexed scan.
24
+ *
25
+ * "Also viewed" is far simpler than "also bought" — you need orders
26
+ * data for the latter, and small stores don't have enough of it for
27
+ * useful signals. Co-views come from every browsing session, so a
28
+ * store with 1k daily visitors gets useful recs from day one.
29
+ */
30
+ let RecommendationsService = class RecommendationsService {
31
+ constructor(connection) {
32
+ this.connection = connection;
33
+ }
34
+ /**
35
+ * Rebuild the co-view aggregate from `visitor_event`. Idempotent
36
+ * per (A, B, channelId) — repeated calls over the same window
37
+ * accumulate, so schedule this as a nightly cron with a rolling
38
+ * lookback (e.g. last 24h) rather than a full-history rescan.
39
+ *
40
+ * Returns the number of pair-triples updated. Uses a bounded
41
+ * ordered-pair extractor (max 20 events per session) so a single
42
+ * bot session can't skew the table.
43
+ */
44
+ async aggregateCoViews(sinceHours = 24) {
45
+ const conn = this.connection.rawConnection;
46
+ const since = new Date(Date.now() - sinceHours * 3600000);
47
+ // Extract every session's product-view sequence within the
48
+ // window. Bounded to 20 events per session to keep pathological
49
+ // sessions from blowing up the table.
50
+ const sessions = await conn.query(`SELECT
51
+ sessionId,
52
+ channelId,
53
+ GROUP_CONCAT(
54
+ SUBSTRING_INDEX(SUBSTRING_INDEX(meta, '"productId":', -1), ',', 1)
55
+ ORDER BY createdAt
56
+ SEPARATOR ','
57
+ ) AS ids
58
+ FROM visitor_event
59
+ WHERE type = 'event'
60
+ AND meta LIKE '%"eventType":"product_view"%'
61
+ AND createdAt >= ?
62
+ GROUP BY sessionId, channelId
63
+ HAVING COUNT(*) BETWEEN 2 AND 20`, [since]);
64
+ let pairs = 0;
65
+ for (const s of sessions) {
66
+ const rawIds = String(s.ids || '')
67
+ .split(',')
68
+ .map((x) => parseInt(x.replace(/[^0-9]/g, ''), 10))
69
+ .filter((n) => Number.isFinite(n) && n > 0);
70
+ if (rawIds.length < 2)
71
+ continue;
72
+ const uniq = Array.from(new Set(rawIds));
73
+ if (uniq.length < 2)
74
+ continue;
75
+ for (let i = 0; i < uniq.length; i++) {
76
+ for (let j = 0; j < uniq.length; j++) {
77
+ if (i === j)
78
+ continue;
79
+ const a = uniq[i];
80
+ const b = uniq[j];
81
+ // Upsert-with-increment via ON DUPLICATE KEY UPDATE.
82
+ await conn.query(`INSERT INTO product_co_view
83
+ (productIdA, productIdB, channelId, viewsTogether, lastUpdated)
84
+ VALUES (?, ?, ?, 1, NOW(3))
85
+ ON DUPLICATE KEY UPDATE
86
+ viewsTogether = viewsTogether + 1,
87
+ lastUpdated = NOW(3)`, [a, b, s.channelId || 1]);
88
+ pairs += 1;
89
+ }
90
+ }
91
+ }
92
+ common_1.Logger.log(`Co-view aggregation: ${sessions.length} sessions, ${pairs} pair updates`, loggerCtx);
93
+ return { pairs };
94
+ }
95
+ /**
96
+ * 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.
100
+ */
101
+ async alsoViewed(productId, channelId = 1, limit = 10) {
102
+ const rows = await this.connection.rawConnection.query(`SELECT productIdB AS productId, viewsTogether AS score
103
+ FROM product_co_view
104
+ WHERE productIdA = ? AND channelId = ?
105
+ ORDER BY viewsTogether DESC, lastUpdated DESC
106
+ LIMIT ?`, [productId, channelId, Math.min(Math.max(1, limit), 50)]);
107
+ return rows.map(r => ({ productId: Number(r.productId), score: Number(r.score) }));
108
+ }
109
+ /**
110
+ * Personalised recommendations for a returning visitor: look at
111
+ * the last N products they viewed, and rank co-viewed products by
112
+ * combined score across those seed products (excluding the seeds
113
+ * themselves).
114
+ */
115
+ async personalRecommendations(visitorId, channelId = 1, limit = 10) {
116
+ const conn = this.connection.rawConnection;
117
+ const seeds = await conn.query(`SELECT DISTINCT
118
+ CAST(SUBSTRING_INDEX(SUBSTRING_INDEX(meta, '"productId":', -1), ',', 1) AS UNSIGNED) AS productId
119
+ FROM visitor_event
120
+ WHERE visitorId = ?
121
+ AND type = 'event'
122
+ AND meta LIKE '%"eventType":"product_view"%'
123
+ AND createdAt >= (NOW() - INTERVAL 30 DAY)
124
+ ORDER BY createdAt DESC
125
+ LIMIT 10`, [visitorId]);
126
+ const seedIds = seeds.map(s => Number(s.productId)).filter(n => n > 0);
127
+ if (!seedIds.length)
128
+ return [];
129
+ const placeholders = seedIds.map(() => '?').join(',');
130
+ const rows = await conn.query(`SELECT productIdB AS productId, SUM(viewsTogether) AS score
131
+ FROM product_co_view
132
+ WHERE productIdA IN (${placeholders})
133
+ AND channelId = ?
134
+ AND productIdB NOT IN (${placeholders})
135
+ GROUP BY productIdB
136
+ ORDER BY score DESC
137
+ LIMIT ?`, [...seedIds, channelId, ...seedIds, Math.min(Math.max(1, limit), 50)]);
138
+ return rows.map(r => ({ productId: Number(r.productId), score: Number(r.score) }));
139
+ }
140
+ /**
141
+ * "Trending now" — most-viewed products in the last N hours.
142
+ * Useful for a homepage rail; uses `product_view` events, so it
143
+ * reflects real interest, not just search-console clicks.
144
+ */
145
+ async trending(channelId = 1, sinceHours = 24, limit = 10) {
146
+ const since = new Date(Date.now() - sinceHours * 3600000);
147
+ const rows = await this.connection.rawConnection.query(`SELECT
148
+ CAST(SUBSTRING_INDEX(SUBSTRING_INDEX(meta, '"productId":', -1), ',', 1) AS UNSIGNED) AS productId,
149
+ COUNT(*) AS views
150
+ FROM visitor_event
151
+ WHERE type = 'event'
152
+ AND meta LIKE '%"eventType":"product_view"%'
153
+ AND channelId = ?
154
+ AND createdAt >= ?
155
+ GROUP BY productId
156
+ HAVING productId > 0
157
+ ORDER BY views DESC
158
+ LIMIT ?`, [channelId, since, Math.min(Math.max(1, limit), 50)]);
159
+ return rows.map(r => ({ productId: Number(r.productId), views: Number(r.views) }));
160
+ }
161
+ };
162
+ exports.RecommendationsService = RecommendationsService;
163
+ exports.RecommendationsService = RecommendationsService = __decorate([
164
+ (0, common_1.Injectable)(),
165
+ __metadata("design:paramtypes", [core_1.TransactionalConnection])
166
+ ], RecommendationsService);
167
+ //# sourceMappingURL=recommendations.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recommendations.service.js","sourceRoot":"","sources":["../src/recommendations.service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import { RequestContext } from '@vendure/core';
2
+ import { SearchAnalyticsService } from './search-analytics.service';
3
+ /**
4
+ * Admin API for search analytics. The storefront just needs to fire
5
+ * `hulo.search(query, resultsCount)` for every executed search;
6
+ * everything else is aggregation over the resulting custom events.
7
+ */
8
+ export declare class SearchAnalyticsController {
9
+ private readonly service;
10
+ constructor(service: SearchAnalyticsService);
11
+ top(ctx: RequestContext, dRaw?: string, chRaw?: string, lRaw?: string): Promise<{
12
+ days: number;
13
+ channelId: number;
14
+ items: {
15
+ query: string;
16
+ searches: number;
17
+ avgResults: number;
18
+ }[];
19
+ }>;
20
+ noResults(ctx: RequestContext, dRaw?: string, chRaw?: string, lRaw?: string): Promise<{
21
+ days: number;
22
+ channelId: number;
23
+ items: {
24
+ query: string;
25
+ searches: number;
26
+ }[];
27
+ }>;
28
+ conversion(ctx: RequestContext, dRaw?: string, chRaw?: string): Promise<{
29
+ sessionsSearched: number;
30
+ sessionsAdded: number;
31
+ conversion: number;
32
+ days: number;
33
+ channelId: number;
34
+ }>;
35
+ }
36
+ //# sourceMappingURL=search-analytics.controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-analytics.controller.d.ts","sourceRoot":"","sources":["../src/search-analytics.controller.ts"],"names":[],"mappings":"AACA,OAAO,EAAO,cAAc,EAAqB,MAAM,eAAe,CAAC;AACvE,OAAO,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAEpE;;;;GAIG;AACH,qBACa,yBAAyB;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,sBAAsB;IAItD,GAAG,CACE,GAAG,EAAE,cAAc,EACX,IAAI,CAAC,EAAE,MAAM,EACR,KAAK,CAAC,EAAE,MAAM,EAClB,IAAI,CAAC,EAAE,MAAM;;;;;;;;;IAW3B,SAAS,CACJ,GAAG,EAAE,cAAc,EACX,IAAI,CAAC,EAAE,MAAM,EACR,KAAK,CAAC,EAAE,MAAM,EAClB,IAAI,CAAC,EAAE,MAAM;;;;;;;;IAW3B,UAAU,CACL,GAAG,EAAE,cAAc,EACX,IAAI,CAAC,EAAE,MAAM,EACR,KAAK,CAAC,EAAE,MAAM;;;;;;;CAOzC"}
@@ -0,0 +1,86 @@
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.SearchAnalyticsController = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const core_1 = require("@vendure/core");
18
+ const search_analytics_service_1 = require("./search-analytics.service");
19
+ /**
20
+ * Admin API for search analytics. The storefront just needs to fire
21
+ * `hulo.search(query, resultsCount)` for every executed search;
22
+ * everything else is aggregation over the resulting custom events.
23
+ */
24
+ let SearchAnalyticsController = class SearchAnalyticsController {
25
+ constructor(service) {
26
+ this.service = service;
27
+ }
28
+ async top(ctx, dRaw, chRaw, lRaw) {
29
+ const days = Math.min(Math.max(1, parseInt(dRaw || '7', 10) || 7), 365);
30
+ const channelId = parseInt(chRaw || '1', 10) || 1;
31
+ const limit = parseInt(lRaw || '50', 10) || 50;
32
+ const rows = await this.service.topQueries(days, channelId, limit);
33
+ return { days, channelId, items: rows };
34
+ }
35
+ async noResults(ctx, dRaw, chRaw, lRaw) {
36
+ const days = Math.min(Math.max(1, parseInt(dRaw || '7', 10) || 7), 365);
37
+ const channelId = parseInt(chRaw || '1', 10) || 1;
38
+ const limit = parseInt(lRaw || '50', 10) || 50;
39
+ const rows = await this.service.zeroResultQueries(days, channelId, limit);
40
+ return { days, channelId, items: rows };
41
+ }
42
+ async conversion(ctx, dRaw, chRaw) {
43
+ const days = Math.min(Math.max(1, parseInt(dRaw || '7', 10) || 7), 365);
44
+ const channelId = parseInt(chRaw || '1', 10) || 1;
45
+ const summary = await this.service.searchToAddRate(days, channelId);
46
+ return { days, channelId, ...summary };
47
+ }
48
+ };
49
+ exports.SearchAnalyticsController = SearchAnalyticsController;
50
+ __decorate([
51
+ (0, common_1.Get)('search-analytics/top'),
52
+ (0, core_1.Allow)(core_1.Permission.ReadCustomer),
53
+ __param(0, (0, core_1.Ctx)()),
54
+ __param(1, (0, common_1.Query)('days')),
55
+ __param(2, (0, common_1.Query)('channelId')),
56
+ __param(3, (0, common_1.Query)('limit')),
57
+ __metadata("design:type", Function),
58
+ __metadata("design:paramtypes", [core_1.RequestContext, String, String, String]),
59
+ __metadata("design:returntype", Promise)
60
+ ], SearchAnalyticsController.prototype, "top", null);
61
+ __decorate([
62
+ (0, common_1.Get)('search-analytics/no-results'),
63
+ (0, core_1.Allow)(core_1.Permission.ReadCustomer),
64
+ __param(0, (0, core_1.Ctx)()),
65
+ __param(1, (0, common_1.Query)('days')),
66
+ __param(2, (0, common_1.Query)('channelId')),
67
+ __param(3, (0, common_1.Query)('limit')),
68
+ __metadata("design:type", Function),
69
+ __metadata("design:paramtypes", [core_1.RequestContext, String, String, String]),
70
+ __metadata("design:returntype", Promise)
71
+ ], SearchAnalyticsController.prototype, "noResults", null);
72
+ __decorate([
73
+ (0, common_1.Get)('search-analytics/conversion'),
74
+ (0, core_1.Allow)(core_1.Permission.ReadCustomer),
75
+ __param(0, (0, core_1.Ctx)()),
76
+ __param(1, (0, common_1.Query)('days')),
77
+ __param(2, (0, common_1.Query)('channelId')),
78
+ __metadata("design:type", Function),
79
+ __metadata("design:paramtypes", [core_1.RequestContext, String, String]),
80
+ __metadata("design:returntype", Promise)
81
+ ], SearchAnalyticsController.prototype, "conversion", null);
82
+ exports.SearchAnalyticsController = SearchAnalyticsController = __decorate([
83
+ (0, common_1.Controller)('ees'),
84
+ __metadata("design:paramtypes", [search_analytics_service_1.SearchAnalyticsService])
85
+ ], SearchAnalyticsController);
86
+ //# sourceMappingURL=search-analytics.controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-analytics.controller.js","sourceRoot":"","sources":["../src/search-analytics.controller.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import { TransactionalConnection } from '@vendure/core';
2
+ /**
3
+ * Zero-schema-cost search analytics: reads back over the existing
4
+ * `visitor_event` table where the storefront has posted `type='event'`,
5
+ * `eventType='search'` custom events, and aggregates them per query
6
+ * string.
7
+ *
8
+ * The storefront just needs to fire:
9
+ * hulo.search('rgb keyboard', 42) // (query, resultsCount)
10
+ *
11
+ * Everything else lives in the read side.
12
+ */
13
+ export declare class SearchAnalyticsService {
14
+ private connection;
15
+ constructor(connection: TransactionalConnection);
16
+ /** Top search queries in the window, by volume. */
17
+ topQueries(sinceDays?: number, channelId?: number, limit?: number): Promise<Array<{
18
+ query: string;
19
+ searches: number;
20
+ avgResults: number;
21
+ }>>;
22
+ /** Zero-result queries — catalogue-gap intel. */
23
+ zeroResultQueries(sinceDays?: number, channelId?: number, limit?: number): Promise<Array<{
24
+ query: string;
25
+ searches: number;
26
+ }>>;
27
+ /** Search-to-cart-add conversion — of sessions that searched, what
28
+ * fraction went on to fire an add_to_cart. Coarse, but the direction
29
+ * of movement is what matters for catalogue tuning. */
30
+ searchToAddRate(sinceDays?: number, channelId?: number): Promise<{
31
+ sessionsSearched: number;
32
+ sessionsAdded: number;
33
+ conversion: number;
34
+ }>;
35
+ }
36
+ //# sourceMappingURL=search-analytics.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-analytics.service.d.ts","sourceRoot":"","sources":["../src/search-analytics.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAExD;;;;;;;;;;GAUG;AACH,qBACa,sBAAsB;IACnB,OAAO,CAAC,UAAU;gBAAV,UAAU,EAAE,uBAAuB;IAEvD,mDAAmD;IAC7C,UAAU,CAAC,SAAS,SAAI,EAAE,SAAS,SAAI,EAAE,KAAK,SAAK,GAAG,OAAO,CAAC,KAAK,CAAC;QACtE,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,EAAE,MAAM,CAAC;KACtB,CAAC,CAAC;IAyBH,iDAAiD;IAC3C,iBAAiB,CAAC,SAAS,SAAI,EAAE,SAAS,SAAI,EAAE,KAAK,SAAK,GAAG,OAAO,CAAC,KAAK,CAAC;QAC7E,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,MAAM,CAAC;KACpB,CAAC,CAAC;IAqBH;;4DAEwD;IAClD,eAAe,CAAC,SAAS,SAAI,EAAE,SAAS,SAAI,GAAG,OAAO,CAAC;QACzD,gBAAgB,EAAE,MAAM,CAAC;QACzB,aAAa,EAAE,MAAM,CAAC;QACtB,UAAU,EAAE,MAAM,CAAC;KACtB,CAAC;CA6BL"}
@@ -0,0 +1,105 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.SearchAnalyticsService = void 0;
13
+ const common_1 = require("@nestjs/common");
14
+ const core_1 = require("@vendure/core");
15
+ /**
16
+ * Zero-schema-cost search analytics: reads back over the existing
17
+ * `visitor_event` table where the storefront has posted `type='event'`,
18
+ * `eventType='search'` custom events, and aggregates them per query
19
+ * string.
20
+ *
21
+ * The storefront just needs to fire:
22
+ * hulo.search('rgb keyboard', 42) // (query, resultsCount)
23
+ *
24
+ * Everything else lives in the read side.
25
+ */
26
+ let SearchAnalyticsService = class SearchAnalyticsService {
27
+ constructor(connection) {
28
+ this.connection = connection;
29
+ }
30
+ /** Top search queries in the window, by volume. */
31
+ async topQueries(sinceDays = 7, channelId = 1, limit = 50) {
32
+ const since = new Date(Date.now() - sinceDays * 86400000);
33
+ const rows = await this.connection.rawConnection.query(`SELECT
34
+ LOWER(TRIM(BOTH '"' FROM SUBSTRING_INDEX(SUBSTRING_INDEX(meta, '"query":"', -1), '"', 1))) AS query,
35
+ COUNT(*) AS searches,
36
+ AVG(CAST(SUBSTRING_INDEX(SUBSTRING_INDEX(meta, '"resultsCount":', -1), ',', 1) AS UNSIGNED)) AS avgResults
37
+ FROM visitor_event
38
+ WHERE type = 'event'
39
+ AND meta LIKE '%"eventType":"search"%'
40
+ AND channelId = ?
41
+ AND createdAt >= ?
42
+ GROUP BY query
43
+ HAVING query <> ''
44
+ ORDER BY searches DESC
45
+ LIMIT ?`, [channelId, since, Math.min(Math.max(1, limit), 500)]);
46
+ return rows.map(r => ({
47
+ query: String(r.query || ''),
48
+ searches: Number(r.searches),
49
+ avgResults: Math.round(Number(r.avgResults || 0) * 10) / 10,
50
+ }));
51
+ }
52
+ /** Zero-result queries — catalogue-gap intel. */
53
+ async zeroResultQueries(sinceDays = 7, channelId = 1, limit = 50) {
54
+ const since = new Date(Date.now() - sinceDays * 86400000);
55
+ const rows = await this.connection.rawConnection.query(`SELECT
56
+ LOWER(TRIM(BOTH '"' FROM SUBSTRING_INDEX(SUBSTRING_INDEX(meta, '"query":"', -1), '"', 1))) AS query,
57
+ COUNT(*) AS searches
58
+ FROM visitor_event
59
+ WHERE type = 'event'
60
+ AND meta LIKE '%"eventType":"search"%'
61
+ AND meta LIKE '%"resultsCount":0%'
62
+ AND channelId = ?
63
+ AND createdAt >= ?
64
+ GROUP BY query
65
+ HAVING query <> ''
66
+ ORDER BY searches DESC
67
+ LIMIT ?`, [channelId, since, Math.min(Math.max(1, limit), 500)]);
68
+ return rows.map(r => ({ query: String(r.query || ''), searches: Number(r.searches) }));
69
+ }
70
+ /** Search-to-cart-add conversion — of sessions that searched, what
71
+ * fraction went on to fire an add_to_cart. Coarse, but the direction
72
+ * of movement is what matters for catalogue tuning. */
73
+ async searchToAddRate(sinceDays = 7, channelId = 1) {
74
+ const since = new Date(Date.now() - sinceDays * 86400000);
75
+ const rows = await this.connection.rawConnection.query(`SELECT
76
+ COUNT(DISTINCT ve.sessionId) AS sessionsSearched,
77
+ COUNT(DISTINCT CASE WHEN adds.sessionId IS NOT NULL THEN ve.sessionId END) AS sessionsAdded
78
+ FROM visitor_event ve
79
+ LEFT JOIN (
80
+ SELECT DISTINCT sessionId FROM visitor_event
81
+ WHERE type = 'event'
82
+ AND meta LIKE '%"eventType":"add_to_cart"%'
83
+ AND channelId = ?
84
+ AND createdAt >= ?
85
+ ) adds ON adds.sessionId = ve.sessionId
86
+ WHERE ve.type = 'event'
87
+ AND ve.meta LIKE '%"eventType":"search"%'
88
+ AND ve.channelId = ?
89
+ AND ve.createdAt >= ?`, [channelId, since, channelId, since]);
90
+ const r = (rows === null || rows === void 0 ? void 0 : rows[0]) || {};
91
+ const searched = Number(r.sessionsSearched || 0);
92
+ const added = Number(r.sessionsAdded || 0);
93
+ return {
94
+ sessionsSearched: searched,
95
+ sessionsAdded: added,
96
+ conversion: searched ? Math.round((added / searched) * 1000) / 10 : 0,
97
+ };
98
+ }
99
+ };
100
+ exports.SearchAnalyticsService = SearchAnalyticsService;
101
+ exports.SearchAnalyticsService = SearchAnalyticsService = __decorate([
102
+ (0, common_1.Injectable)(),
103
+ __metadata("design:paramtypes", [core_1.TransactionalConnection])
104
+ ], SearchAnalyticsService);
105
+ //# sourceMappingURL=search-analytics.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-analytics.service.js","sourceRoot":"","sources":["../src/search-analytics.service.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.7.0",
3
+ "version": "0.8.1",
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>",