@thg-agentic/hyper-personalisation-web 3.34.0 → 3.35.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/README.md +10 -2
- package/dist/hp.min.js +2 -2
- package/dist/onsite-answers.min.js +2 -2
- package/dist/onsite-audiences.min.js +1 -1
- package/dist/onsite-products.min.js +1 -1
- package/dist/onsite-render.min.js +1 -1
- package/dist/onsite-wheel.min.js +1 -1
- package/dist/onsite.min.js +1 -1
- package/package.json +1 -1
- package/src/datalayer.js +7 -2
- package/src/index.js +51 -3
- package/src/onsite/index.js +33 -7
- package/src/onsite/rules.js +6 -1
- package/src/segment.js +2 -1
- package/src/session/record.js +61 -0
- package/src/session/rules.js +3 -1
- package/src/session/signals.js +3 -0
- package/src/shape.js +9 -6
- package/src/version.js +1 -1
- package/types/index.d.ts +1 -1
- package/types/onsite.d.ts +5 -0
package/src/session/record.js
CHANGED
|
@@ -77,6 +77,61 @@ export const basketItemsOf = (products) =>
|
|
|
77
77
|
return n + (Number.isInteger(q) ? q : 1);
|
|
78
78
|
}, 0);
|
|
79
79
|
|
|
80
|
+
/** The events that can change the basket's codes: every other leaves them as they are. */
|
|
81
|
+
export const BASKET_CODE_EVENTS = new Set([
|
|
82
|
+
'Cart Viewed',
|
|
83
|
+
'Cart Updated',
|
|
84
|
+
'Coupon Applied',
|
|
85
|
+
'Coupon Removed',
|
|
86
|
+
'Order Completed',
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
/** Codes compared as a shopper types them: `welcome10` and `WELCOME10 ` are one code. */
|
|
90
|
+
const sameCode = (a, b) => a.toLowerCase() === b.toLowerCase();
|
|
91
|
+
|
|
92
|
+
const codeText = (v) => (typeof v === 'string' && v.trim() ? v.trim() : null);
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The discount codes in the basket after `eventName`, from the spec's own fields: a cart event's
|
|
96
|
+
* `discounts` (one entry per code), else its `coupon`, replaces them; Coupon Applied and Coupon
|
|
97
|
+
* Removed adjust them between cart events, and an order uses them up. A cart event naming neither
|
|
98
|
+
* field leaves them as they were. Null is unknown, until an event names them, so nothing is decided
|
|
99
|
+
* on codes never reported.
|
|
100
|
+
*/
|
|
101
|
+
export function basketCodesAfter(codes, eventName, properties) {
|
|
102
|
+
switch (eventName) {
|
|
103
|
+
case 'Cart Viewed':
|
|
104
|
+
case 'Cart Updated': {
|
|
105
|
+
const { discounts, coupon } = properties ?? {};
|
|
106
|
+
if (Array.isArray(discounts))
|
|
107
|
+
return discounts
|
|
108
|
+
.map((d) => codeText(d?.coupon_name))
|
|
109
|
+
.filter(Boolean)
|
|
110
|
+
.filter((c, i, all) => all.findIndex((o) => sameCode(o, c)) === i);
|
|
111
|
+
if (coupon !== undefined) {
|
|
112
|
+
const code = codeText(coupon);
|
|
113
|
+
return code ? [code] : [];
|
|
114
|
+
}
|
|
115
|
+
// `discounts: null` is a basket without promotions; naming neither leaves them as they were.
|
|
116
|
+
return discounts === null ? [] : codes;
|
|
117
|
+
}
|
|
118
|
+
case 'Coupon Applied': {
|
|
119
|
+
const code = codeText(properties?.coupon_name);
|
|
120
|
+
if (!code) return codes;
|
|
121
|
+
const kept = codes ?? [];
|
|
122
|
+
return kept.some((c) => sameCode(c, code)) ? kept : [...kept, code];
|
|
123
|
+
}
|
|
124
|
+
case 'Coupon Removed': {
|
|
125
|
+
const code = codeText(properties?.coupon_name);
|
|
126
|
+
return code && codes ? codes.filter((c) => !sameCode(c, code)) : codes;
|
|
127
|
+
}
|
|
128
|
+
case 'Order Completed':
|
|
129
|
+
return codes?.length ? [] : codes;
|
|
130
|
+
default:
|
|
131
|
+
return codes;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
80
135
|
/**
|
|
81
136
|
* Fold one CDP event into the session signals.
|
|
82
137
|
*
|
|
@@ -95,6 +150,7 @@ export function recordSessionEvent(eventName, properties, campaign) {
|
|
|
95
150
|
const products = Array.isArray(properties?.products)
|
|
96
151
|
? properties.products
|
|
97
152
|
: [];
|
|
153
|
+
s.basketCodes = basketCodesAfter(s.basketCodes, eventName, properties);
|
|
98
154
|
|
|
99
155
|
switch (eventName) {
|
|
100
156
|
case 'Product Viewed':
|
|
@@ -197,6 +253,11 @@ export function recordSessionEvent(eventName, properties, campaign) {
|
|
|
197
253
|
if (products.length > 0) s.basketProducts = basketOf(products);
|
|
198
254
|
writeSignals(s);
|
|
199
255
|
return s;
|
|
256
|
+
case 'Coupon Applied':
|
|
257
|
+
case 'Coupon Removed':
|
|
258
|
+
case 'Order Completed':
|
|
259
|
+
writeSignals(s);
|
|
260
|
+
return s;
|
|
200
261
|
case 'Cart Updated': {
|
|
201
262
|
// The basket as it now stands, priced by the platform, so a merge, a repricing or a code is
|
|
202
263
|
// already in it. Summing the lines of Product Added would drift from that, and its line
|
package/src/session/rules.js
CHANGED
|
@@ -109,7 +109,9 @@ export function measureField(field, signals) {
|
|
|
109
109
|
const fn = MEASURES[name];
|
|
110
110
|
return fn && arg ? fn(signals, arg) : null;
|
|
111
111
|
}
|
|
112
|
-
|
|
112
|
+
// How many discount codes the basket holds: `eq 0` is a basket without one. Unknown fails closed.
|
|
113
|
+
const value =
|
|
114
|
+
field === 'basketCodeCount' ? signals.basketCodes?.length : signals[field];
|
|
113
115
|
return typeof value === 'number' ? value : null;
|
|
114
116
|
}
|
|
115
117
|
|
package/src/session/signals.js
CHANGED
|
@@ -68,6 +68,7 @@ function readUnscoped() {
|
|
|
68
68
|
(parsed.productViews > 0 ||
|
|
69
69
|
parsed.listViews > 0 ||
|
|
70
70
|
parsed.basketValue != null ||
|
|
71
|
+
parsed.basketCodes != null ||
|
|
71
72
|
parsed.recentProducts?.length ||
|
|
72
73
|
parsed.recentBrowse?.length ||
|
|
73
74
|
parsed.campaign);
|
|
@@ -95,6 +96,8 @@ function emptySignals() {
|
|
|
95
96
|
basketItems: null,
|
|
96
97
|
// The last Cart Updated's `currency`, the one its value is in; unknown when it named none.
|
|
97
98
|
basketCurrency: null,
|
|
99
|
+
// The discount codes in the basket (`basketCodesAfter`); unknown until a cart event names them.
|
|
100
|
+
basketCodes: null,
|
|
98
101
|
recentSearches: [],
|
|
99
102
|
// Shown-and-ignored counts, kept apart from `recentProducts` so a rail's own impressions never
|
|
100
103
|
// read as interest. The threshold that makes them meaningful lives server-side in `fatigue.ts`.
|
package/src/shape.js
CHANGED
|
@@ -30,9 +30,13 @@ export const PAGE_TYPES = new Set([
|
|
|
30
30
|
*
|
|
31
31
|
* For `properties` only. Context nulls are load-bearing (`consent.analytics: null` means the CMP
|
|
32
32
|
* has not answered). `products: null` is kept, so a failed product mapping still fails validation
|
|
33
|
-
* rather than looking like an empty result.
|
|
33
|
+
* rather than looking like an empty result. On a cart event `coupon: null` and `discounts: null`
|
|
34
|
+
* are kept too: they say the basket has no code, which a missing field does not. Other events'
|
|
35
|
+
* schemas are closed and may not have them, so there they are pruned as before.
|
|
34
36
|
*/
|
|
35
|
-
const NEVER_PRUNE =
|
|
37
|
+
const NEVER_PRUNE = ['products'];
|
|
38
|
+
const CART_NEVER_PRUNE = ['products', 'coupon', 'discounts'];
|
|
39
|
+
const CART_EVENTS = new Set(['Cart Viewed', 'Cart Updated']);
|
|
36
40
|
|
|
37
41
|
/** Restore an exempt key only when it was null; an array stays pruned. */
|
|
38
42
|
const isNullish = (v) => v === null || v === undefined;
|
|
@@ -58,12 +62,11 @@ function pruneNulls(value, ancestors = new WeakSet()) {
|
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
/** Properties as they go on the wire: nulls pruned, `products: null` kept visible. */
|
|
61
|
-
export function pruneProperties(properties) {
|
|
65
|
+
export function pruneProperties(properties, event) {
|
|
62
66
|
if (!properties) return properties;
|
|
63
67
|
const pruned = pruneNulls(properties);
|
|
64
|
-
for (const key of NEVER_PRUNE) {
|
|
65
|
-
if (key
|
|
66
|
-
pruned[key] = properties[key];
|
|
68
|
+
for (const key of CART_EVENTS.has(event) ? CART_NEVER_PRUNE : NEVER_PRUNE) {
|
|
69
|
+
if (properties[key] === null) pruned[key] = null;
|
|
67
70
|
}
|
|
68
71
|
return pruned;
|
|
69
72
|
}
|
package/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const VERSION = '3.
|
|
1
|
+
export const VERSION = '3.35.0';
|
|
2
2
|
export const LIB = '@thg-agentic/hyper-personalisation-web';
|
package/types/index.d.ts
CHANGED
|
@@ -535,7 +535,7 @@ export declare function toProducts(
|
|
|
535
535
|
): CdpProduct[];
|
|
536
536
|
export declare function pruneProperties<
|
|
537
537
|
T extends Record<string, unknown> | null | undefined,
|
|
538
|
-
>(properties: T): T;
|
|
538
|
+
>(properties: T, event?: string): T;
|
|
539
539
|
|
|
540
540
|
/** SHA-256 as lowercase hex. */
|
|
541
541
|
export declare function sha256(value: string): Promise<string>;
|
package/types/onsite.d.ts
CHANGED
|
@@ -105,6 +105,11 @@ export interface OnsiteForm {
|
|
|
105
105
|
/** Shown only with personalisation consent, and only to members as the server decides. */
|
|
106
106
|
audiences?: string[];
|
|
107
107
|
exclude_subscribed?: boolean;
|
|
108
|
+
/**
|
|
109
|
+
* Never shown while the basket holds a discount code, as the page's cart events name them
|
|
110
|
+
* (`discounts`, `coupon`, Coupon Applied and Removed); one held back tries again once it has none.
|
|
111
|
+
*/
|
|
112
|
+
exclude_code_applied?: boolean;
|
|
108
113
|
caps?: { per_session?: number; lifetime?: number; after_close_days?: number };
|
|
109
114
|
/**
|
|
110
115
|
* Its dates and its daily cut-off: with one, it shows only while one of its listed cut-offs is
|