@thg-agentic/hyper-personalisation-web 3.22.2 → 3.23.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 +143 -1
- package/dist/hp.min.js +3 -3
- package/dist/onsite-answers.min.js +1 -0
- package/dist/onsite-render.min.js +178 -0
- package/dist/onsite-wheel.min.js +1 -0
- package/dist/onsite.min.js +1 -0
- package/dist/snippet.min.js +1 -1
- package/package.json +19 -3
- package/src/bootstrap.js +33 -2
- package/src/context.js +20 -7
- package/src/hp.js +2 -0
- package/src/index.js +96 -1
- package/src/onsite/answers-loader.js +6 -0
- package/src/onsite/answers.js +177 -0
- package/src/onsite/api.js +93 -0
- package/src/onsite/counters.js +144 -0
- package/src/onsite/index.js +446 -0
- package/src/onsite/pow.js +127 -0
- package/src/onsite/preview.js +97 -0
- package/src/onsite/render-loader.js +5 -0
- package/src/onsite/render.js +1234 -0
- package/src/onsite/rich-text.js +73 -0
- package/src/onsite/rules.js +237 -0
- package/src/onsite/styles.js +208 -0
- package/src/onsite/triggers.js +165 -0
- package/src/onsite/wheel-loader.js +5 -0
- package/src/onsite/wheel.js +148 -0
- package/src/onsite-facade.js +99 -0
- package/src/onsite-loader.js +5 -0
- package/src/settings.js +18 -0
- package/src/spec-version.js +1 -1
- package/src/storage-scope.js +5 -0
- package/src/version.js +1 -1
- package/types/bootstrap.d.ts +15 -1
- package/types/events.d.ts +68 -5
- package/types/hp.d.ts +4 -1
- package/types/index.d.ts +10 -0
- package/types/onsite-preview.d.ts +58 -0
- package/types/onsite-wheel.d.ts +11 -0
- package/types/onsite.d.ts +195 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The spin-to-win wheel (`./onsite/wheel`), a chunk of its own: an SVG of the form's prize labels
|
|
3
|
+
* that only animates, in the theme's colours: the accent and its tints, a rim of lights, a hub and
|
|
4
|
+
* a pointer. The prize is drawn on the server (`POST /v1/offers/claim`), so the wheel is told where
|
|
5
|
+
* to stop; the segments are equal, and say nothing about the odds. The angle math is ours, after
|
|
6
|
+
* spin-wheel's spinToItem (CrazyTim, MIT).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { restAngle } from './rules.js';
|
|
10
|
+
|
|
11
|
+
const NS = 'http://www.w3.org/2000/svg';
|
|
12
|
+
const SPIN_MS = 4500;
|
|
13
|
+
const MAX_LABEL = 16;
|
|
14
|
+
const ACCENT = 'var(--hp-accent,#111)';
|
|
15
|
+
const ON_ACCENT = 'var(--hp-on-accent,#fff)';
|
|
16
|
+
const BG = 'var(--hp-bg,#fff)';
|
|
17
|
+
const TEXT = 'var(--hp-text,#111)';
|
|
18
|
+
|
|
19
|
+
function make(tag, attrs = {}) {
|
|
20
|
+
const node = document.createElementNS(NS, tag);
|
|
21
|
+
for (const [key, value] of Object.entries(attrs))
|
|
22
|
+
node.setAttribute(key, String(value));
|
|
23
|
+
return node;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Colours as style, not attributes, so the theme's custom properties resolve. */
|
|
27
|
+
function paint(node, fill, stroke, opacity = 1) {
|
|
28
|
+
node.style.fill = fill;
|
|
29
|
+
if (opacity < 1) node.style.fillOpacity = String(opacity);
|
|
30
|
+
if (stroke) node.style.stroke = stroke;
|
|
31
|
+
return node;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const reducedMotion = () =>
|
|
35
|
+
globalThis.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches === true;
|
|
36
|
+
|
|
37
|
+
const point = (deg, r = 1) => {
|
|
38
|
+
const rad = ((deg - 90) * Math.PI) / 180;
|
|
39
|
+
return `${(r * Math.cos(rad)).toFixed(4)} ${(r * Math.sin(rad)).toFixed(4)}`;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Draw the wheel for `prizes` (`[{ key, label }]`) into `container`. Null for fewer than two.
|
|
44
|
+
*
|
|
45
|
+
* @returns {{ spinTo(index: number): Promise<void>, remove(): void } | null}
|
|
46
|
+
*/
|
|
47
|
+
export function mountWheel(container, prizes) {
|
|
48
|
+
const labels = (Array.isArray(prizes) ? prizes : []).map((p) =>
|
|
49
|
+
String(p?.label ?? p?.key ?? ''),
|
|
50
|
+
);
|
|
51
|
+
const n = labels.length;
|
|
52
|
+
if (n < 2) return null;
|
|
53
|
+
const size = 360 / n;
|
|
54
|
+
const svg = make('svg', {
|
|
55
|
+
viewBox: '-1.2 -1.2 2.4 2.4',
|
|
56
|
+
role: 'img',
|
|
57
|
+
'aria-label': `Prize wheel: ${labels.join(', ')}`,
|
|
58
|
+
});
|
|
59
|
+
const wheel = make('g');
|
|
60
|
+
// Spun about its own box, the unit circle round 0 0. A view-box origin sits at the user space's
|
|
61
|
+
// 0 0, not the viewBox's corner, so 50% there is off centre and the wheel swings out of its rim.
|
|
62
|
+
wheel.style.transformBox = 'fill-box';
|
|
63
|
+
wheel.style.transformOrigin = '50% 50%';
|
|
64
|
+
// The accent, a tint of it over the background, and for an odd count a third tone, so no two
|
|
65
|
+
// neighbours match.
|
|
66
|
+
const tones = [
|
|
67
|
+
[1, ON_ACCENT],
|
|
68
|
+
[0.14, TEXT],
|
|
69
|
+
[0.72, ON_ACCENT],
|
|
70
|
+
];
|
|
71
|
+
wheel.append(paint(make('circle', { r: 1 }), BG));
|
|
72
|
+
const lights = [];
|
|
73
|
+
labels.forEach((label, i) => {
|
|
74
|
+
const [tone, ink] = tones[n % 2 === 1 && i === n - 1 ? 2 : i % 2];
|
|
75
|
+
wheel.append(
|
|
76
|
+
paint(
|
|
77
|
+
make('path', {
|
|
78
|
+
d: `M0 0L${point(i * size)}A1 1 0 ${size > 180 ? 1 : 0} 1 ${point((i + 1) * size)}Z`,
|
|
79
|
+
'stroke-width': 0.012,
|
|
80
|
+
}),
|
|
81
|
+
ACCENT,
|
|
82
|
+
BG,
|
|
83
|
+
tone,
|
|
84
|
+
),
|
|
85
|
+
);
|
|
86
|
+
// Along its slice's middle; on the left half turned about its own centre, so at rest no label
|
|
87
|
+
// reads upside down.
|
|
88
|
+
const angle = (i + 0.5) * size - 90;
|
|
89
|
+
const text = paint(
|
|
90
|
+
make('text', {
|
|
91
|
+
transform: `rotate(${angle}) translate(0.6 0)${angle > 90 && angle < 270 ? ' rotate(180)' : ''}`,
|
|
92
|
+
'text-anchor': 'middle',
|
|
93
|
+
'dominant-baseline': 'central',
|
|
94
|
+
'font-size': n > 6 ? 0.09 : 0.105,
|
|
95
|
+
'font-weight': 700,
|
|
96
|
+
'aria-hidden': 'true',
|
|
97
|
+
}),
|
|
98
|
+
ink,
|
|
99
|
+
);
|
|
100
|
+
text.textContent =
|
|
101
|
+
label.length > MAX_LABEL ? `${label.slice(0, MAX_LABEL - 1)}…` : label;
|
|
102
|
+
wheel.append(text);
|
|
103
|
+
for (const at of [i * size, (i + 0.5) * size]) {
|
|
104
|
+
const [cx, cy] = point(at, 1.075).split(' ');
|
|
105
|
+
lights.push(paint(make('circle', { cx, cy, r: 0.024 }), BG));
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
svg.append(
|
|
109
|
+
paint(make('circle', { r: 1.13 }), TEXT),
|
|
110
|
+
...lights,
|
|
111
|
+
wheel,
|
|
112
|
+
paint(make('circle', { r: 0.17, 'stroke-width': 0.035 }), BG, TEXT),
|
|
113
|
+
paint(make('circle', { r: 0.065 }), ACCENT),
|
|
114
|
+
paint(
|
|
115
|
+
make('path', {
|
|
116
|
+
d: 'M-0.12 -1.2L0.12 -1.2L0 -0.86Z',
|
|
117
|
+
'stroke-width': 0.03,
|
|
118
|
+
'stroke-linejoin': 'round',
|
|
119
|
+
}),
|
|
120
|
+
ACCENT,
|
|
121
|
+
BG,
|
|
122
|
+
),
|
|
123
|
+
);
|
|
124
|
+
container.append(svg);
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
spinTo(index) {
|
|
128
|
+
const still = reducedMotion();
|
|
129
|
+
wheel.style.transition = still
|
|
130
|
+
? 'none'
|
|
131
|
+
: `transform ${SPIN_MS}ms cubic-bezier(.15,.7,.1,1)`;
|
|
132
|
+
wheel.style.transform = `rotate(${restAngle(index, labels.length, still ? 0 : 5)}deg)`;
|
|
133
|
+
if (still) return Promise.resolve();
|
|
134
|
+
return new Promise((resolve) => {
|
|
135
|
+
const done = setTimeout(resolve, SPIN_MS + 200);
|
|
136
|
+
wheel.addEventListener(
|
|
137
|
+
'transitionend',
|
|
138
|
+
() => {
|
|
139
|
+
clearTimeout(done);
|
|
140
|
+
resolve();
|
|
141
|
+
},
|
|
142
|
+
{ once: true },
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
},
|
|
146
|
+
remove: () => svg.remove(),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hp.onsite`: open (`show`) a form by hand, listen to the forms, keep the screen clear, or read why
|
|
3
|
+
* each form did or did not show, all callable before the onsite runtime has loaded. Listeners and
|
|
4
|
+
* suppressions wait on the page for the runtime. An `open` made before it started waits for it, and resolves with what
|
|
5
|
+
* the runtime did, or `false` once the page knows no runtime will start; nothing here throws.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const CONTROLLER = Symbol.for('@thg-agentic/hyper-personalisation-web.onsite');
|
|
9
|
+
const PENDING = Symbol.for(
|
|
10
|
+
'@thg-agentic/hyper-personalisation-web.onsite.pending',
|
|
11
|
+
);
|
|
12
|
+
const LISTENERS = Symbol.for(
|
|
13
|
+
'@thg-agentic/hyper-personalisation-web.onsite.listeners',
|
|
14
|
+
);
|
|
15
|
+
const SUPPRESSED = Symbol.for(
|
|
16
|
+
'@thg-agentic/hyper-personalisation-web.onsite.suppressed',
|
|
17
|
+
);
|
|
18
|
+
const EVENTS = ['shown', 'submitted', 'apply', 'closed'];
|
|
19
|
+
const MAX_PENDING = 10;
|
|
20
|
+
|
|
21
|
+
const page = () => (typeof window === 'undefined' ? null : window);
|
|
22
|
+
|
|
23
|
+
/** No onsite runtime will start on this page: every `open` waiting for one resolves `false`. */
|
|
24
|
+
export function dropPendingOpens() {
|
|
25
|
+
try {
|
|
26
|
+
for (const { resolve } of page()?.[PENDING]?.splice(0) ?? [])
|
|
27
|
+
resolve(false);
|
|
28
|
+
} catch {
|
|
29
|
+
/* never into the page */
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const onsite = Object.freeze({
|
|
34
|
+
/** Show the form on `slot` now, if its rules allow it. Resolves to whether it was shown. */
|
|
35
|
+
open(slot) {
|
|
36
|
+
try {
|
|
37
|
+
const w = page();
|
|
38
|
+
if (!w || typeof slot !== 'string') return Promise.resolve(false);
|
|
39
|
+
if (w[CONTROLLER]) return w[CONTROLLER].open(slot);
|
|
40
|
+
w[PENDING] ??= [];
|
|
41
|
+
if (w[PENDING].length < MAX_PENDING)
|
|
42
|
+
return new Promise((resolve) => w[PENDING].push({ slot, resolve }));
|
|
43
|
+
} catch {
|
|
44
|
+
/* never into the page */
|
|
45
|
+
}
|
|
46
|
+
return Promise.resolve(false);
|
|
47
|
+
},
|
|
48
|
+
/** The same as `open`, by the name the console's snippets use. */
|
|
49
|
+
show(slot) {
|
|
50
|
+
return onsite.open(slot);
|
|
51
|
+
},
|
|
52
|
+
/**
|
|
53
|
+
* Listen to the forms: `shown`, `submitted` (`{ coupon }`: the claimed code for a form that
|
|
54
|
+
* spins, else null), `apply` (`{ coupon }`: a reminder's code to apply) and `closed`
|
|
55
|
+
* (`{ reason }`). Each detail carries the `slot`, `arm` and `format`. Returns a function that
|
|
56
|
+
* stops listening.
|
|
57
|
+
*/
|
|
58
|
+
on(name, fn) {
|
|
59
|
+
try {
|
|
60
|
+
const w = page();
|
|
61
|
+
if (!w || !EVENTS.includes(name) || typeof fn !== 'function')
|
|
62
|
+
return () => {};
|
|
63
|
+
w[LISTENERS] ??= {};
|
|
64
|
+
(w[LISTENERS][name] ??= new Set()).add(fn);
|
|
65
|
+
return () => w[LISTENERS][name].delete(fn);
|
|
66
|
+
} catch {
|
|
67
|
+
return () => {};
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
/** Keep the screen clear, for `reason`: a form showing goes, quietly, and none shows until every reason is lifted. */
|
|
71
|
+
suppress(reason = 'page') {
|
|
72
|
+
try {
|
|
73
|
+
const w = page();
|
|
74
|
+
if (!w) return;
|
|
75
|
+
(w[SUPPRESSED] ??= new Set()).add(String(reason));
|
|
76
|
+
w[CONTROLLER]?.suppress();
|
|
77
|
+
} catch {
|
|
78
|
+
/* never into the page */
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
/** Lift `reason`, or every reason when none is given. */
|
|
82
|
+
unsuppress(reason) {
|
|
83
|
+
try {
|
|
84
|
+
const set = page()?.[SUPPRESSED];
|
|
85
|
+
if (reason === undefined) set?.clear();
|
|
86
|
+
else set?.delete(String(reason));
|
|
87
|
+
} catch {
|
|
88
|
+
/* never into the page */
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
/** Each form's eligibility on this page, and why: `[]` until the runtime has started. */
|
|
92
|
+
debug() {
|
|
93
|
+
try {
|
|
94
|
+
return page()?.[CONTROLLER]?.debug() ?? [];
|
|
95
|
+
} catch {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
});
|
package/src/settings.js
CHANGED
|
@@ -156,6 +156,22 @@ function readAnonymousId(raw) {
|
|
|
156
156
|
: null;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
/**
|
|
160
|
+
* The live onsite forms (`hp.onsite`), or null when there are none. Only the shape is checked here:
|
|
161
|
+
* ./onsite reads each form itself, so the core never grows with the forms' rules.
|
|
162
|
+
*/
|
|
163
|
+
function readOnsite(raw) {
|
|
164
|
+
const forms = Array.isArray(raw?.forms) ? raw.forms.filter(isObject) : [];
|
|
165
|
+
if (forms.length === 0) return null;
|
|
166
|
+
return {
|
|
167
|
+
forms,
|
|
168
|
+
capture: isObject(raw.capture) ? raw.capture : null,
|
|
169
|
+
...(Number.isInteger(raw.overlay_gap_minutes)
|
|
170
|
+
? { overlay_gap_minutes: raw.overlay_gap_minutes }
|
|
171
|
+
: {}),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
159
175
|
/**
|
|
160
176
|
* Whether a 2xx body is a settings answer at all: an `hp` block saying on or off. Anything else (a
|
|
161
177
|
* captive portal's page, a proxy's error) is no answer, so the last good settings and what was
|
|
@@ -174,6 +190,7 @@ export function isSettingsAnswer(body) {
|
|
|
174
190
|
export function readSettings(body) {
|
|
175
191
|
const hp = isObject(body) ? body.hp : null;
|
|
176
192
|
if (!isObject(hp) || hp.enabled !== true) return DISABLED;
|
|
193
|
+
const onsite = readOnsite(hp.onsite);
|
|
177
194
|
return {
|
|
178
195
|
enabled: true,
|
|
179
196
|
propertyId:
|
|
@@ -192,6 +209,7 @@ export function readSettings(body) {
|
|
|
192
209
|
// The share of visitors sent for (3.18). Anything that is not a share sends for everyone.
|
|
193
210
|
sampleRate: isFraction(hp.sampleRate) ? hp.sampleRate : 1,
|
|
194
211
|
debug: readDebug(hp.debug),
|
|
212
|
+
...(onsite && { onsite }),
|
|
195
213
|
plan:
|
|
196
214
|
isObject(body.plan) && isObject(body.plan.track)
|
|
197
215
|
? { track: body.plan.track }
|
package/src/spec-version.js
CHANGED
package/src/storage-scope.js
CHANGED
|
@@ -59,6 +59,11 @@ export const DEFAULT_STORAGE_NAMES = Object.freeze({
|
|
|
59
59
|
* (`?hp_debug=`), checked against the settings on every page (3.18).
|
|
60
60
|
*/
|
|
61
61
|
debug: 'thg_hp_debug',
|
|
62
|
+
/**
|
|
63
|
+
* localStorage and sessionStorage, per property: onsite forms' opaque counters per slot (shown,
|
|
64
|
+
* last closed, submitted) and a subscribed flag. Never contact data (onsite).
|
|
65
|
+
*/
|
|
66
|
+
onsite: 'thg_hp_onsite',
|
|
62
67
|
});
|
|
63
68
|
|
|
64
69
|
/**
|
package/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const VERSION = '3.
|
|
1
|
+
export const VERSION = '3.23.0';
|
|
2
2
|
export const LIB = '@thg-agentic/hyper-personalisation-web';
|
package/types/bootstrap.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
PageType,
|
|
20
20
|
StorageNames,
|
|
21
21
|
} from './index';
|
|
22
|
+
import type { OnsiteFacade, OnsiteOptions, OnsiteSettings } from './onsite';
|
|
22
23
|
|
|
23
24
|
/** Everything the page itself configures: the rest is the tenant's, and comes from settings. */
|
|
24
25
|
export interface HpConfig {
|
|
@@ -68,6 +69,8 @@ export interface SettingsResponse {
|
|
|
68
69
|
string,
|
|
69
70
|
{ share: number; salt: string; readoutSlots?: string[] }
|
|
70
71
|
>;
|
|
72
|
+
/** The live onsite forms (ADR 0064). Started only for a page that passes `onsite`. */
|
|
73
|
+
onsite?: OnsiteSettings;
|
|
71
74
|
};
|
|
72
75
|
}
|
|
73
76
|
|
|
@@ -148,6 +151,17 @@ export interface BootstrapOptions {
|
|
|
148
151
|
settings?: SettingsResponse;
|
|
149
152
|
/** createAnalytics options that are functions, e.g. getRecommendations or getExtensions. */
|
|
150
153
|
extend?: Partial<AnalyticsOptions>;
|
|
154
|
+
/**
|
|
155
|
+
* Onsite forms: `true` (or the options) loads `./onsite` as a separate chunk when the settings
|
|
156
|
+
* have a live form, and starts it. Absent or false: never loaded.
|
|
157
|
+
*/
|
|
158
|
+
onsite?: boolean | OnsiteOptions;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The client bootstrap resolves to: the lower-level client, plus `onsite`. */
|
|
162
|
+
export interface BootstrappedClient extends AnalyticsClient {
|
|
163
|
+
/** Open a form by hand, or read why each form did or did not show (`[]` before it started). */
|
|
164
|
+
onsite: OnsiteFacade;
|
|
151
165
|
}
|
|
152
166
|
|
|
153
167
|
/**
|
|
@@ -157,7 +171,7 @@ export interface BootstrapOptions {
|
|
|
157
171
|
export declare function bootstrap(
|
|
158
172
|
config: HpConfig,
|
|
159
173
|
options: BootstrapOptions,
|
|
160
|
-
): Promise<
|
|
174
|
+
): Promise<BootstrappedClient | null>;
|
|
161
175
|
/** Every reason the config and options cannot be used; empty when they can. */
|
|
162
176
|
export declare function validateConfig(
|
|
163
177
|
config: unknown,
|
package/types/events.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Generated from spec/registry.json by scripts/generate-sdk-models.mjs.
|
|
2
2
|
// DO NOT EDIT. Change the schema and regenerate — the schema is the contract,
|
|
3
3
|
// and a hand-edit here is an SDK that disagrees with the ingest.
|
|
4
|
-
// Spec version: 1.
|
|
4
|
+
// Spec version: 1.18
|
|
5
5
|
|
|
6
6
|
/** context.source.environment. Environment the producer runs in. */
|
|
7
7
|
export type CdpSourceEnvironment = 'production' | 'staging' | 'development';
|
|
@@ -76,12 +76,28 @@ export interface DecisionMade {
|
|
|
76
76
|
latency_ms?: number | null;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
/** A
|
|
79
|
+
/** A triggered journey fired for one shopper: an entry event happened, the journey's delay passed, its exit event did not happen, and the shopper passed its gates (marketing consent, suppression, frequency cap, quiet hours). */
|
|
80
|
+
export interface JourneyTriggered {
|
|
81
|
+
trigger_id: string;
|
|
82
|
+
journey: string;
|
|
83
|
+
entry_event: string;
|
|
84
|
+
entry_message_id: string;
|
|
85
|
+
entry_at: string;
|
|
86
|
+
delay_seconds: number;
|
|
87
|
+
holdout: boolean;
|
|
88
|
+
holdout_share?: number | null;
|
|
89
|
+
cart_id?: string | null;
|
|
90
|
+
currency?: string | null;
|
|
91
|
+
products?: CdpProduct[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A visitor acted on a personalised surface: clicked it (for example opened a product from a rail), dismissed it, or submitted a step of an onsite form. */
|
|
80
95
|
export interface SlotEngaged {
|
|
81
96
|
decision_id: string;
|
|
82
97
|
slot: string;
|
|
83
|
-
engagement: 'view' | 'click' | 'dismiss';
|
|
98
|
+
engagement: 'view' | 'click' | 'dismiss' | 'submit';
|
|
84
99
|
arm?: string | null;
|
|
100
|
+
step?: string | null;
|
|
85
101
|
position?: number | null;
|
|
86
102
|
products?: CdpProduct[] | null;
|
|
87
103
|
}
|
|
@@ -432,13 +448,18 @@ export interface SignedUp {
|
|
|
432
448
|
/** The shopper signed out. */
|
|
433
449
|
export type SignedOut = Record<string, never>;
|
|
434
450
|
|
|
435
|
-
/** The shopper opted in to marketing messages on the site or in the app, for example through a footer form,
|
|
451
|
+
/** The shopper opted in to marketing messages on the site or in the app, for example through a footer form, an onsite sign-up form, or a checkbox at checkout or in their account. */
|
|
436
452
|
export interface NewsletterSubscribed {
|
|
437
453
|
email_sha256: string;
|
|
438
454
|
requires_verification?: boolean | null;
|
|
439
455
|
placement?: string | null;
|
|
440
456
|
channel?: 'email' | 'sms' | 'post' | 'push' | null;
|
|
441
457
|
phone_sha256?: string | null;
|
|
458
|
+
slot?: string | null;
|
|
459
|
+
arm?: string | null;
|
|
460
|
+
decision_id?: string | null;
|
|
461
|
+
receipt_id?: string | null;
|
|
462
|
+
consent_text?: string | null;
|
|
442
463
|
}
|
|
443
464
|
|
|
444
465
|
/** A shopper submitted a review of a product. */
|
|
@@ -630,12 +651,47 @@ export interface ApplicationUpdated {
|
|
|
630
651
|
build: string;
|
|
631
652
|
}
|
|
632
653
|
|
|
654
|
+
/** The shopper answered a preference question, for example what they are shopping for or which categories they want to hear about. */
|
|
655
|
+
export interface PreferencesStated {
|
|
656
|
+
question: string;
|
|
657
|
+
values: string[];
|
|
658
|
+
source: 'onsite' | 'account' | 'quiz';
|
|
659
|
+
slot?: string | null;
|
|
660
|
+
arm?: string | null;
|
|
661
|
+
decision_id?: string | null;
|
|
662
|
+
step?: string | null;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/** A shopper who signed up through an onsite form claimed its offer, for example the prize of a spin-to-win or a welcome discount. */
|
|
666
|
+
export interface OfferClaimed {
|
|
667
|
+
slot: string;
|
|
668
|
+
prize: string;
|
|
669
|
+
email_sha256: string;
|
|
670
|
+
fallback: boolean;
|
|
671
|
+
arm?: string | null;
|
|
672
|
+
decision_id?: string | null;
|
|
673
|
+
pool?: string | null;
|
|
674
|
+
code_sha256?: string | null;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/** Hyper-Personalisation delivered a sign-up from an onsite form to one of the brand's systems of record, such as its account service or its email platform, or gave up trying. */
|
|
678
|
+
export interface SignUpRelayed {
|
|
679
|
+
receipt_id: string;
|
|
680
|
+
destination: string;
|
|
681
|
+
kind: 'thg' | 'listrak' | 'klaviyo' | 'webhook';
|
|
682
|
+
outcome: 'delivered' | 'duplicate' | 'verify' | 'failed';
|
|
683
|
+
attempts: number;
|
|
684
|
+
email_sha256?: string | null;
|
|
685
|
+
phone_sha256?: string | null;
|
|
686
|
+
}
|
|
687
|
+
|
|
633
688
|
/** The spec version these models implement. SDKs stamp it on context.spec.version. */
|
|
634
|
-
export const SPEC_VERSION = '1.
|
|
689
|
+
export const SPEC_VERSION = '1.18';
|
|
635
690
|
|
|
636
691
|
/** Every event name the registry accepts. Anything else is rejected at the ingest. */
|
|
637
692
|
export type CdpEventName =
|
|
638
693
|
| 'Decision Made'
|
|
694
|
+
| 'Journey Triggered'
|
|
639
695
|
| 'Slot Engaged'
|
|
640
696
|
| 'Recommendations Viewed'
|
|
641
697
|
| 'Promotion Viewed'
|
|
@@ -690,11 +746,15 @@ export type CdpEventName =
|
|
|
690
746
|
| 'Application Opened'
|
|
691
747
|
| 'Application Backgrounded'
|
|
692
748
|
| 'Application Updated'
|
|
749
|
+
| 'Preferences Stated'
|
|
750
|
+
| 'Offer Claimed'
|
|
751
|
+
| 'Sign-up Relayed'
|
|
693
752
|
;
|
|
694
753
|
|
|
695
754
|
/** Event name -> its properties, so a generic emitter stays type-safe. */
|
|
696
755
|
export interface CdpEventProperties {
|
|
697
756
|
'Decision Made': DecisionMade;
|
|
757
|
+
'Journey Triggered': JourneyTriggered;
|
|
698
758
|
'Slot Engaged': SlotEngaged;
|
|
699
759
|
'Recommendations Viewed': RecommendationsViewed;
|
|
700
760
|
'Promotion Viewed': PromotionViewed;
|
|
@@ -749,6 +809,9 @@ export interface CdpEventProperties {
|
|
|
749
809
|
'Application Opened': ApplicationOpened;
|
|
750
810
|
'Application Backgrounded': ApplicationBackgrounded;
|
|
751
811
|
'Application Updated': ApplicationUpdated;
|
|
812
|
+
'Preferences Stated': PreferencesStated;
|
|
813
|
+
'Offer Claimed': OfferClaimed;
|
|
814
|
+
'Sign-up Relayed': SignUpRelayed;
|
|
752
815
|
}
|
|
753
816
|
|
|
754
817
|
/** Which producers the registry permits for each event. Emitting from another is refused. */
|
package/types/hp.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
CallOptions,
|
|
5
5
|
RecommendationContext,
|
|
6
6
|
} from './index';
|
|
7
|
+
import type { OnsiteFacade } from './onsite';
|
|
7
8
|
|
|
8
9
|
type Methods = {
|
|
9
10
|
[K in keyof AnalyticsClient as AnalyticsClient[K] extends (
|
|
@@ -40,7 +41,7 @@ interface HpTrack {
|
|
|
40
41
|
* into the caller.
|
|
41
42
|
*/
|
|
42
43
|
export declare const hp: {
|
|
43
|
-
[K in Exclude<keyof Methods, 'track' | 'recommendationContext'>]: (
|
|
44
|
+
[K in Exclude<keyof Methods, 'track' | 'recommendationContext' | 'onsite'>]: (
|
|
44
45
|
...args: Parameters<Methods[K]>
|
|
45
46
|
) => void;
|
|
46
47
|
} & {
|
|
@@ -61,4 +62,6 @@ export declare const hp: {
|
|
|
61
62
|
* client has started, and without personalisation consent. Since 3.11.
|
|
62
63
|
*/
|
|
63
64
|
recommendationContext(): RecommendationContext;
|
|
65
|
+
/** Onsite forms: `hp.onsite.open(slot)` opens a form by hand; `debug()` says why each did or did not show. */
|
|
66
|
+
onsite: OnsiteFacade;
|
|
64
67
|
};
|
package/types/index.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
NewsletterUnsubscribed,
|
|
23
23
|
PaymentInfoEntered,
|
|
24
24
|
PersonalisationExposed,
|
|
25
|
+
PreferencesStated,
|
|
25
26
|
ProductAdded,
|
|
26
27
|
ProductAddedToWishlist,
|
|
27
28
|
ProductClicked,
|
|
@@ -335,6 +336,11 @@ export interface AnalyticsClient {
|
|
|
335
336
|
newsletterSubscribed: Emit<NewsletterSubscribed>;
|
|
336
337
|
/** An on-site opt-out. The ESP reports its own as Email Unsubscribed. */
|
|
337
338
|
newsletterUnsubscribed: Emit<NewsletterUnsubscribed>;
|
|
339
|
+
/**
|
|
340
|
+
* Answers to an approved preference question (spec 1.18). Answers sent with an onsite sign-up are
|
|
341
|
+
* the gateway's to report, so never send those.
|
|
342
|
+
*/
|
|
343
|
+
preferencesStated: Emit<PreferencesStated>;
|
|
338
344
|
/** An alert request for one product: back in stock, a price drop or a launch. */
|
|
339
345
|
backInStockSubscribed: Emit<BackInStockSubscribed>;
|
|
340
346
|
productReviewed: Emit<ProductReviewed>;
|
|
@@ -436,6 +442,10 @@ export interface ServingBody {
|
|
|
436
442
|
* computes it from the settings. `default` is no decision and is not put in force. Since 3.16.
|
|
437
443
|
*/
|
|
438
444
|
variant?: 'bandit' | 'default';
|
|
445
|
+
/** The A/B test the arm was assigned in, when the slot runs one. Since 3.23. */
|
|
446
|
+
experiment?: string;
|
|
447
|
+
/** The test's split token, sent back with the pin. Since 3.23. */
|
|
448
|
+
split?: string;
|
|
439
449
|
}>;
|
|
440
450
|
recommendation_request_id?: string;
|
|
441
451
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** A question the form's settings publish, as the preview renders it. */
|
|
2
|
+
export interface PreviewQuestion {
|
|
3
|
+
key: string;
|
|
4
|
+
type?: 'one' | 'many' | 'date' | 'number';
|
|
5
|
+
label?: string;
|
|
6
|
+
/** A choice's options. */
|
|
7
|
+
options?: Array<{ value: string; label?: string }>;
|
|
8
|
+
/** A date's or a number's format, as the settings publish it. */
|
|
9
|
+
format?: {
|
|
10
|
+
kind?: 'birthday' | 'date';
|
|
11
|
+
year?: 'none' | 'optional' | 'required';
|
|
12
|
+
min?: number;
|
|
13
|
+
max?: number;
|
|
14
|
+
decimals?: boolean;
|
|
15
|
+
unit?: string;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface PreviewOptions {
|
|
20
|
+
/** The step to show; the first when left out or unknown. */
|
|
21
|
+
step?: string;
|
|
22
|
+
/** The small-screen layout (`mobile`) or the large one, whatever the host's own width. */
|
|
23
|
+
device?: 'desktop' | 'mobile';
|
|
24
|
+
/** The form's consent text; a sample sentence when left out. */
|
|
25
|
+
consentText?: string;
|
|
26
|
+
/** The consent text a step taking a phone number shows; a sample sentence when left out. */
|
|
27
|
+
smsConsentText?: string;
|
|
28
|
+
/**
|
|
29
|
+
* The form's language (the settings' `locale`, e.g. `en-GB`): a birthday's field order and month
|
|
30
|
+
* names, and a phone field's first country. The page's `<html lang>` when left out.
|
|
31
|
+
*/
|
|
32
|
+
locale?: string;
|
|
33
|
+
questions?: PreviewQuestion[];
|
|
34
|
+
prizes?: Array<{ key: string; label: string }>;
|
|
35
|
+
/** The basket a progress block is drawn against; 32 when left out. */
|
|
36
|
+
basketValue?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface OnsitePreview {
|
|
40
|
+
/** Re-render, on the step shown unless `options.step` names another. */
|
|
41
|
+
update(creative: unknown, options?: PreviewOptions): void;
|
|
42
|
+
destroy(): void;
|
|
43
|
+
/** The step on screen, or null when the creative could not be read. */
|
|
44
|
+
step(): string | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Render a creative with the shoppers' renderer inside `host`, for the console: no request, no
|
|
49
|
+
* event, no counter; its buttons only move between steps.
|
|
50
|
+
*/
|
|
51
|
+
export declare function mountPreview(
|
|
52
|
+
host: HTMLElement,
|
|
53
|
+
creative: unknown,
|
|
54
|
+
options?: PreviewOptions,
|
|
55
|
+
): OnsitePreview;
|
|
56
|
+
|
|
57
|
+
/** The countries a phone field's country code picker offers, by ISO code, with their calling codes. */
|
|
58
|
+
export declare const CALLING_CODES: Readonly<Record<string, string>>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface OnsiteWheel {
|
|
2
|
+
/** Turn to the prize at `index`, the one the server drew. Instant under reduced motion. */
|
|
3
|
+
spinTo(index: number): Promise<void>;
|
|
4
|
+
remove(): void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Draw the prize wheel into `container`. Null for fewer than two prizes. */
|
|
8
|
+
export declare function mountWheel(
|
|
9
|
+
container: Element,
|
|
10
|
+
prizes: Array<{ key: string; label: string }>,
|
|
11
|
+
): OnsiteWheel | null;
|