@huloglobal/vendure-plugin-visitor-analytics 0.6.0 → 0.8.0
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 +83 -15
- package/dist/abandoned-cart.controller.d.ts +78 -0
- package/dist/abandoned-cart.controller.d.ts.map +1 -0
- package/dist/abandoned-cart.controller.js +251 -0
- package/dist/abandoned-cart.controller.js.map +1 -0
- package/dist/abandoned-cart.entity.d.ts +61 -0
- package/dist/abandoned-cart.entity.d.ts.map +1 -0
- package/dist/abandoned-cart.entity.js +143 -0
- package/dist/abandoned-cart.entity.js.map +1 -0
- package/dist/abandoned-cart.scanner.d.ts +34 -0
- package/dist/abandoned-cart.scanner.d.ts.map +1 -0
- package/dist/abandoned-cart.scanner.js +85 -0
- package/dist/abandoned-cart.scanner.js.map +1 -0
- package/dist/abandoned-cart.service.d.ts +92 -0
- package/dist/abandoned-cart.service.d.ts.map +1 -0
- package/dist/abandoned-cart.service.js +276 -0
- package/dist/abandoned-cart.service.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/journey-buffs.controller.d.ts +43 -0
- package/dist/journey-buffs.controller.d.ts.map +1 -0
- package/dist/journey-buffs.controller.js +79 -0
- package/dist/journey-buffs.controller.js.map +1 -0
- package/dist/journey-buffs.service.d.ts +53 -0
- package/dist/journey-buffs.service.d.ts.map +1 -0
- package/dist/journey-buffs.service.js +141 -0
- package/dist/journey-buffs.service.js.map +1 -0
- package/dist/plugin.d.ts +5 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +35 -3
- package/dist/plugin.js.map +1 -1
- package/dist/product-co-view.entity.d.ts +19 -0
- package/dist/product-co-view.entity.d.ts.map +1 -0
- package/dist/product-co-view.entity.js +52 -0
- package/dist/product-co-view.entity.js.map +1 -0
- package/dist/recommendations.controller.d.ts +72 -0
- package/dist/recommendations.controller.d.ts.map +1 -0
- package/dist/recommendations.controller.js +123 -0
- package/dist/recommendations.controller.js.map +1 -0
- package/dist/recommendations.service.d.ts +62 -0
- package/dist/recommendations.service.d.ts.map +1 -0
- package/dist/recommendations.service.js +167 -0
- package/dist/recommendations.service.js.map +1 -0
- package/dist/search-analytics.controller.d.ts +36 -0
- package/dist/search-analytics.controller.d.ts.map +1 -0
- package/dist/search-analytics.controller.js +86 -0
- package/dist/search-analytics.controller.js.map +1 -0
- package/dist/search-analytics.service.d.ts +36 -0
- package/dist/search-analytics.service.d.ts.map +1 -0
- package/dist/search-analytics.service.js +105 -0
- package/dist/search-analytics.service.js.map +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { RequestContext } from '@vendure/core';
|
|
2
|
+
import { RecommendationsService } from './recommendations.service';
|
|
3
|
+
/**
|
|
4
|
+
* Public + admin API for the co-view recommendations feature.
|
|
5
|
+
*
|
|
6
|
+
* The `also-viewed`, `personal` and `trending` endpoints are safe to
|
|
7
|
+
* hit from the storefront directly — they return only product ids +
|
|
8
|
+
* scores, no PII. Restricting them to Vendure admin would break the
|
|
9
|
+
* primary use case (rendering a recs rail on the product page).
|
|
10
|
+
*/
|
|
11
|
+
export declare class RecommendationsController {
|
|
12
|
+
private readonly service;
|
|
13
|
+
constructor(service: RecommendationsService);
|
|
14
|
+
/**
|
|
15
|
+
* "Customers who viewed X also viewed…"
|
|
16
|
+
* /ees/recommendations/also-viewed?productId=42&channelId=1&limit=10
|
|
17
|
+
*/
|
|
18
|
+
alsoViewed(pRaw?: string, chRaw?: string, lRaw?: string): Promise<{
|
|
19
|
+
error: string;
|
|
20
|
+
productId?: undefined;
|
|
21
|
+
items?: undefined;
|
|
22
|
+
} | {
|
|
23
|
+
productId: number;
|
|
24
|
+
items: {
|
|
25
|
+
productId: number;
|
|
26
|
+
score: number;
|
|
27
|
+
}[];
|
|
28
|
+
error?: undefined;
|
|
29
|
+
}>;
|
|
30
|
+
/**
|
|
31
|
+
* Personalised recs for a returning visitor:
|
|
32
|
+
* /ees/recommendations/personal?visitorId=abc&channelId=1&limit=10
|
|
33
|
+
*
|
|
34
|
+
* Storefronts should send the current `ees_vid` cookie value —
|
|
35
|
+
* the same one the ingest endpoint issues.
|
|
36
|
+
*/
|
|
37
|
+
personal(vRaw?: string, chRaw?: string, lRaw?: string): Promise<{
|
|
38
|
+
error: string;
|
|
39
|
+
visitorId?: undefined;
|
|
40
|
+
items?: undefined;
|
|
41
|
+
} | {
|
|
42
|
+
visitorId: string;
|
|
43
|
+
items: {
|
|
44
|
+
productId: number;
|
|
45
|
+
score: number;
|
|
46
|
+
}[];
|
|
47
|
+
error?: undefined;
|
|
48
|
+
}>;
|
|
49
|
+
/**
|
|
50
|
+
* Most-viewed products in the window:
|
|
51
|
+
* /ees/recommendations/trending?channelId=1&hours=24&limit=10
|
|
52
|
+
*/
|
|
53
|
+
trending(chRaw?: string, hRaw?: string, lRaw?: string): Promise<{
|
|
54
|
+
channelId: number;
|
|
55
|
+
hours: number;
|
|
56
|
+
items: {
|
|
57
|
+
productId: number;
|
|
58
|
+
views: number;
|
|
59
|
+
}[];
|
|
60
|
+
}>;
|
|
61
|
+
/**
|
|
62
|
+
* Admin-only: manually kick the aggregation cron. Normally runs
|
|
63
|
+
* nightly, but this is useful for smoke-testing after a data
|
|
64
|
+
* backfill, or for pushing a big spike through immediately.
|
|
65
|
+
*/
|
|
66
|
+
aggregateNow(ctx: RequestContext, hRaw?: string): Promise<{
|
|
67
|
+
pairs: number;
|
|
68
|
+
ok: boolean;
|
|
69
|
+
hours: number;
|
|
70
|
+
}>;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=recommendations.controller.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,123 @@
|
|
|
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.RecommendationsController = void 0;
|
|
16
|
+
const common_1 = require("@nestjs/common");
|
|
17
|
+
const core_1 = require("@vendure/core");
|
|
18
|
+
const recommendations_service_1 = require("./recommendations.service");
|
|
19
|
+
/**
|
|
20
|
+
* Public + admin API for the co-view recommendations feature.
|
|
21
|
+
*
|
|
22
|
+
* The `also-viewed`, `personal` and `trending` endpoints are safe to
|
|
23
|
+
* hit from the storefront directly — they return only product ids +
|
|
24
|
+
* scores, no PII. Restricting them to Vendure admin would break the
|
|
25
|
+
* primary use case (rendering a recs rail on the product page).
|
|
26
|
+
*/
|
|
27
|
+
let RecommendationsController = class RecommendationsController {
|
|
28
|
+
constructor(service) {
|
|
29
|
+
this.service = service;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* "Customers who viewed X also viewed…"
|
|
33
|
+
* /ees/recommendations/also-viewed?productId=42&channelId=1&limit=10
|
|
34
|
+
*/
|
|
35
|
+
async alsoViewed(pRaw, chRaw, lRaw) {
|
|
36
|
+
const productId = parseInt(pRaw || '0', 10);
|
|
37
|
+
if (!productId)
|
|
38
|
+
return { error: 'productId-required' };
|
|
39
|
+
const channelId = parseInt(chRaw || '1', 10) || 1;
|
|
40
|
+
const limit = parseInt(lRaw || '10', 10) || 10;
|
|
41
|
+
const items = await this.service.alsoViewed(productId, channelId, limit);
|
|
42
|
+
return { productId, items };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Personalised recs for a returning visitor:
|
|
46
|
+
* /ees/recommendations/personal?visitorId=abc&channelId=1&limit=10
|
|
47
|
+
*
|
|
48
|
+
* Storefronts should send the current `ees_vid` cookie value —
|
|
49
|
+
* the same one the ingest endpoint issues.
|
|
50
|
+
*/
|
|
51
|
+
async personal(vRaw, chRaw, lRaw) {
|
|
52
|
+
const visitorId = String(vRaw || '').trim();
|
|
53
|
+
if (!visitorId)
|
|
54
|
+
return { error: 'visitorId-required' };
|
|
55
|
+
const channelId = parseInt(chRaw || '1', 10) || 1;
|
|
56
|
+
const limit = parseInt(lRaw || '10', 10) || 10;
|
|
57
|
+
const items = await this.service.personalRecommendations(visitorId, channelId, limit);
|
|
58
|
+
return { visitorId, items };
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Most-viewed products in the window:
|
|
62
|
+
* /ees/recommendations/trending?channelId=1&hours=24&limit=10
|
|
63
|
+
*/
|
|
64
|
+
async trending(chRaw, hRaw, lRaw) {
|
|
65
|
+
const channelId = parseInt(chRaw || '1', 10) || 1;
|
|
66
|
+
const hours = Math.min(Math.max(1, parseInt(hRaw || '24', 10) || 24), 24 * 30);
|
|
67
|
+
const limit = parseInt(lRaw || '10', 10) || 10;
|
|
68
|
+
const items = await this.service.trending(channelId, hours, limit);
|
|
69
|
+
return { channelId, hours, items };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Admin-only: manually kick the aggregation cron. Normally runs
|
|
73
|
+
* nightly, but this is useful for smoke-testing after a data
|
|
74
|
+
* backfill, or for pushing a big spike through immediately.
|
|
75
|
+
*/
|
|
76
|
+
async aggregateNow(ctx, hRaw) {
|
|
77
|
+
const hours = Math.min(Math.max(1, parseInt(hRaw || '24', 10) || 24), 24 * 30);
|
|
78
|
+
const result = await this.service.aggregateCoViews(hours);
|
|
79
|
+
return { ok: true, hours, ...result };
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
exports.RecommendationsController = RecommendationsController;
|
|
83
|
+
__decorate([
|
|
84
|
+
(0, common_1.Get)('recommendations/also-viewed'),
|
|
85
|
+
__param(0, (0, common_1.Query)('productId')),
|
|
86
|
+
__param(1, (0, common_1.Query)('channelId')),
|
|
87
|
+
__param(2, (0, common_1.Query)('limit')),
|
|
88
|
+
__metadata("design:type", Function),
|
|
89
|
+
__metadata("design:paramtypes", [String, String, String]),
|
|
90
|
+
__metadata("design:returntype", Promise)
|
|
91
|
+
], RecommendationsController.prototype, "alsoViewed", null);
|
|
92
|
+
__decorate([
|
|
93
|
+
(0, common_1.Get)('recommendations/personal'),
|
|
94
|
+
__param(0, (0, common_1.Query)('visitorId')),
|
|
95
|
+
__param(1, (0, common_1.Query)('channelId')),
|
|
96
|
+
__param(2, (0, common_1.Query)('limit')),
|
|
97
|
+
__metadata("design:type", Function),
|
|
98
|
+
__metadata("design:paramtypes", [String, String, String]),
|
|
99
|
+
__metadata("design:returntype", Promise)
|
|
100
|
+
], RecommendationsController.prototype, "personal", null);
|
|
101
|
+
__decorate([
|
|
102
|
+
(0, common_1.Get)('recommendations/trending'),
|
|
103
|
+
__param(0, (0, common_1.Query)('channelId')),
|
|
104
|
+
__param(1, (0, common_1.Query)('hours')),
|
|
105
|
+
__param(2, (0, common_1.Query)('limit')),
|
|
106
|
+
__metadata("design:type", Function),
|
|
107
|
+
__metadata("design:paramtypes", [String, String, String]),
|
|
108
|
+
__metadata("design:returntype", Promise)
|
|
109
|
+
], RecommendationsController.prototype, "trending", null);
|
|
110
|
+
__decorate([
|
|
111
|
+
(0, common_1.Get)('recommendations/aggregate-now'),
|
|
112
|
+
(0, core_1.Allow)(core_1.Permission.SuperAdmin),
|
|
113
|
+
__param(0, (0, core_1.Ctx)()),
|
|
114
|
+
__param(1, (0, common_1.Query)('hours')),
|
|
115
|
+
__metadata("design:type", Function),
|
|
116
|
+
__metadata("design:paramtypes", [core_1.RequestContext, String]),
|
|
117
|
+
__metadata("design:returntype", Promise)
|
|
118
|
+
], RecommendationsController.prototype, "aggregateNow", null);
|
|
119
|
+
exports.RecommendationsController = RecommendationsController = __decorate([
|
|
120
|
+
(0, common_1.Controller)('ees'),
|
|
121
|
+
__metadata("design:paramtypes", [recommendations_service_1.RecommendationsService])
|
|
122
|
+
], RecommendationsController);
|
|
123
|
+
//# sourceMappingURL=recommendations.controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recommendations.controller.js","sourceRoot":"","sources":["../src/recommendations.controller.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { TransactionalConnection } from '@vendure/core';
|
|
2
|
+
/**
|
|
3
|
+
* Product recommendations driven by observed co-viewing behaviour.
|
|
4
|
+
*
|
|
5
|
+
* The scanner walks recent `product_view` custom events grouped by
|
|
6
|
+
* session, extracts every ordered pair, and increments a counter per
|
|
7
|
+
* `(productIdA, productIdB, channelId)` triple. We denormalise both
|
|
8
|
+
* directions (`A → B` AND `B → A`) so read-side lookups are one
|
|
9
|
+
* indexed scan.
|
|
10
|
+
*
|
|
11
|
+
* "Also viewed" is far simpler than "also bought" — you need orders
|
|
12
|
+
* data for the latter, and small stores don't have enough of it for
|
|
13
|
+
* useful signals. Co-views come from every browsing session, so a
|
|
14
|
+
* store with 1k daily visitors gets useful recs from day one.
|
|
15
|
+
*/
|
|
16
|
+
export declare class RecommendationsService {
|
|
17
|
+
private connection;
|
|
18
|
+
constructor(connection: TransactionalConnection);
|
|
19
|
+
/**
|
|
20
|
+
* Rebuild the co-view aggregate from `visitor_event`. Idempotent
|
|
21
|
+
* per (A, B, channelId) — repeated calls over the same window
|
|
22
|
+
* accumulate, so schedule this as a nightly cron with a rolling
|
|
23
|
+
* lookback (e.g. last 24h) rather than a full-history rescan.
|
|
24
|
+
*
|
|
25
|
+
* Returns the number of pair-triples updated. Uses a bounded
|
|
26
|
+
* ordered-pair extractor (max 20 events per session) so a single
|
|
27
|
+
* bot session can't skew the table.
|
|
28
|
+
*/
|
|
29
|
+
aggregateCoViews(sinceHours?: number): Promise<{
|
|
30
|
+
pairs: number;
|
|
31
|
+
}>;
|
|
32
|
+
/**
|
|
33
|
+
* 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.
|
|
37
|
+
*/
|
|
38
|
+
alsoViewed(productId: number, channelId?: number, limit?: number): Promise<Array<{
|
|
39
|
+
productId: number;
|
|
40
|
+
score: number;
|
|
41
|
+
}>>;
|
|
42
|
+
/**
|
|
43
|
+
* Personalised recommendations for a returning visitor: look at
|
|
44
|
+
* the last N products they viewed, and rank co-viewed products by
|
|
45
|
+
* combined score across those seed products (excluding the seeds
|
|
46
|
+
* themselves).
|
|
47
|
+
*/
|
|
48
|
+
personalRecommendations(visitorId: string, channelId?: number, limit?: number): Promise<Array<{
|
|
49
|
+
productId: number;
|
|
50
|
+
score: number;
|
|
51
|
+
}>>;
|
|
52
|
+
/**
|
|
53
|
+
* "Trending now" — most-viewed products in the last N hours.
|
|
54
|
+
* Useful for a homepage rail; uses `product_view` events, so it
|
|
55
|
+
* reflects real interest, not just search-console clicks.
|
|
56
|
+
*/
|
|
57
|
+
trending(channelId?: number, sinceHours?: number, limit?: number): Promise<Array<{
|
|
58
|
+
productId: number;
|
|
59
|
+
views: number;
|
|
60
|
+
}>>;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=recommendations.service.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -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"}
|