@thg-agentic/hyper-personalisation-web 3.17.1 → 3.19.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 +112 -39
- package/dist/hp.min.js +3 -3
- package/dist/snippet.min.js +1 -1
- package/package.json +2 -2
- package/src/bootstrap.js +40 -9
- package/src/consent-gate.js +8 -2
- package/src/debug.js +45 -0
- package/src/device-storage.js +35 -22
- package/src/guard.js +46 -0
- package/src/identity-option.js +6 -0
- package/src/index.js +159 -27
- package/src/recommendation-context.js +10 -1
- package/src/sampling.js +19 -0
- package/src/segment.js +15 -2
- package/src/session/record.js +23 -9
- package/src/session/signals.js +4 -0
- package/src/session-id.js +2 -2
- package/src/settings.js +25 -6
- package/src/shape.js +16 -7
- package/src/spec-version.js +1 -1
- package/src/storage-scope.js +6 -1
- package/src/transport.js +42 -4
- package/src/version.js +1 -1
- package/types/bootstrap.d.ts +11 -0
- package/types/consent.d.ts +4 -6
- package/types/events.d.ts +77 -2
- package/types/index.d.ts +28 -21
package/src/session/signals.js
CHANGED
|
@@ -67,6 +67,7 @@ function readUnscoped() {
|
|
|
67
67
|
parsed &&
|
|
68
68
|
(parsed.productViews > 0 ||
|
|
69
69
|
parsed.listViews > 0 ||
|
|
70
|
+
parsed.basketValue != null ||
|
|
70
71
|
parsed.recentProducts?.length ||
|
|
71
72
|
parsed.recentBrowse?.length);
|
|
72
73
|
return busy ? parsed : null;
|
|
@@ -86,6 +87,9 @@ function emptySignals() {
|
|
|
86
87
|
recentCategories: [],
|
|
87
88
|
recentBrowse: [],
|
|
88
89
|
basketProducts: [],
|
|
90
|
+
// The last Cart Updated's `value`, in its currency. Unknown (so every rule on it fails closed)
|
|
91
|
+
// until the visit sends one: a basket carried over from an earlier visit is not zero.
|
|
92
|
+
basketValue: null,
|
|
89
93
|
recentSearches: [],
|
|
90
94
|
// Shown-and-ignored counts, kept apart from `recentProducts` so a rail's own impressions never
|
|
91
95
|
// read as interest. The threshold that makes them meaningful lives server-side in `fatigue.ts`.
|
package/src/session-id.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The session id the SDK keeps when the host names none (3.15): a first-party cookie holding a
|
|
3
3
|
* random id, ended by 30 minutes without an event, as Amplitude and GA4 sessionise. A host that
|
|
4
|
-
* has its own session (`getSessionId`, `identityCookies.sessionId`) keeps it
|
|
5
|
-
*
|
|
4
|
+
* has its own session (`getSessionId`, `identityCookies.sessionId`) keeps it; this one stands in
|
|
5
|
+
* only while the host's is missing or empty (3.18), so no event is sent without a session.
|
|
6
6
|
*
|
|
7
7
|
* The cookie is shared by the site's tabs and readable by its servers. Under strict consent it is
|
|
8
8
|
* written only once analytics is granted; until then the id lives in memory, and it is deleted on
|
package/src/settings.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* The last good answer is cached, only under an analytics grant (it is a device write), and a page
|
|
11
11
|
* with a cache starts from it and revalidates in the background. Until the grant the HTTP cache
|
|
12
|
-
* (`max-age=
|
|
12
|
+
* (`max-age=10` and an `ETag`) saves the round trip or answers a `304`.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { storageKey, storageName } from './storage-scope.js';
|
|
@@ -38,12 +38,11 @@ export function settingsUrl(apiHost, writeKey) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
|
-
* Where the SDK talks to when the integrator names no host
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* for test keys, UAT and previews; the release that moves it to production is the one to go live on.
|
|
41
|
+
* Where the SDK talks to when the integrator names no host: the one host every client uses, for
|
|
42
|
+
* test and live keys alike. The key alone selects the tenant, the property and test or live, as
|
|
43
|
+
* with Stripe and LaunchDarkly; a host is only for a first-party proxy or our own nonprod testing.
|
|
45
44
|
*/
|
|
46
|
-
export const DEFAULT_API_HOST = 'https://hp.
|
|
45
|
+
export const DEFAULT_API_HOST = 'https://hp.thgingenuity.com';
|
|
47
46
|
|
|
48
47
|
/** The integrator's `apiHost` as an origin, DEFAULT_API_HOST when none is given, null when invalid. */
|
|
49
48
|
export function resolveApiHost(host) {
|
|
@@ -76,6 +75,23 @@ function readPurposes(raw) {
|
|
|
76
75
|
const isFraction = (value) =>
|
|
77
76
|
typeof value === 'number' && value >= 0 && value <= 1;
|
|
78
77
|
|
|
78
|
+
const SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Console logging (3.18): on for everyone, or for a session opened with `?hp_debug=<token>`, checked
|
|
82
|
+
* against the token's hash. Off when the settings do not say.
|
|
83
|
+
*/
|
|
84
|
+
function readDebug(raw) {
|
|
85
|
+
if (!isObject(raw)) return { enabled: false, tokenSha256: null };
|
|
86
|
+
return {
|
|
87
|
+
enabled: raw.enabled === true,
|
|
88
|
+
tokenSha256:
|
|
89
|
+
typeof raw.tokenSha256 === 'string' && SHA256_HEX.test(raw.tokenSha256)
|
|
90
|
+
? raw.tokenSha256
|
|
91
|
+
: null,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
79
95
|
/**
|
|
80
96
|
* The personalisation holdout per property, `{ share, ramp, salt, readoutSlots }`, or null when the settings carry
|
|
81
97
|
* none. An entry that is not a share in [0, 1] with a non-empty salt, or whose ramp is neither null
|
|
@@ -150,6 +166,9 @@ export function readSettings(body) {
|
|
|
150
166
|
anonymousId: readAnonymousId(hp.anonymousId),
|
|
151
167
|
apiHost: apiOrigin(hp.apiHost),
|
|
152
168
|
holdouts: readHoldouts(hp.holdouts),
|
|
169
|
+
// The share of visitors sent for (3.18). Anything that is not a share sends for everyone.
|
|
170
|
+
sampleRate: isFraction(hp.sampleRate) ? hp.sampleRate : 1,
|
|
171
|
+
debug: readDebug(hp.debug),
|
|
153
172
|
plan:
|
|
154
173
|
isObject(body.plan) && isObject(body.plan.track)
|
|
155
174
|
? { track: body.plan.track }
|
package/src/shape.js
CHANGED
|
@@ -37,14 +37,23 @@ const NEVER_PRUNE = new Set(['products']);
|
|
|
37
37
|
/** Restore an exempt key only when it was null; an array stays pruned. */
|
|
38
38
|
const isNullish = (v) => v === null || v === undefined;
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
/**
|
|
41
|
+
* A Date becomes the ISO string JSON would make of it. A cycle is left in place, not followed: the
|
|
42
|
+
* transport drops an event it cannot serialise, where following it would overflow the stack.
|
|
43
|
+
*/
|
|
44
|
+
function pruneNulls(value, ancestors = new WeakSet()) {
|
|
45
|
+
if (value instanceof Date) return value.toJSON();
|
|
42
46
|
if (value === null || typeof value !== 'object') return value;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
if (ancestors.has(value)) return value;
|
|
48
|
+
ancestors.add(value);
|
|
49
|
+
const out = Array.isArray(value)
|
|
50
|
+
? value.map((v) => pruneNulls(v, ancestors))
|
|
51
|
+
: Object.fromEntries(
|
|
52
|
+
Object.entries(value)
|
|
53
|
+
.filter(([, v]) => !isNullish(v))
|
|
54
|
+
.map(([k, v]) => [k, pruneNulls(v, ancestors)]),
|
|
55
|
+
);
|
|
56
|
+
ancestors.delete(value);
|
|
48
57
|
return out;
|
|
49
58
|
}
|
|
50
59
|
|
package/src/spec-version.js
CHANGED
package/src/storage-scope.js
CHANGED
|
@@ -45,7 +45,7 @@ export const DEFAULT_STORAGE_NAMES = Object.freeze({
|
|
|
45
45
|
*/
|
|
46
46
|
anonymousId: 'thg_hp_anonymous_id',
|
|
47
47
|
/**
|
|
48
|
-
* Cookie, one per browser: the session id the SDK keeps when the host
|
|
48
|
+
* Cookie, one per browser: the session id the SDK keeps when the host has none, expiring 30
|
|
49
49
|
* minutes after the last event (3.15). Only under a grant.
|
|
50
50
|
*/
|
|
51
51
|
session: 'thg_hp_session',
|
|
@@ -54,6 +54,11 @@ export const DEFAULT_STORAGE_NAMES = Object.freeze({
|
|
|
54
54
|
* (`?hp_preview=`), for the site's server to forward to `/v1/assignments` (3.15).
|
|
55
55
|
*/
|
|
56
56
|
preview: 'thg_hp_preview',
|
|
57
|
+
/**
|
|
58
|
+
* sessionStorage, for the browser session: the debug token a developer opened the site with
|
|
59
|
+
* (`?hp_debug=`), checked against the settings on every page (3.18).
|
|
60
|
+
*/
|
|
61
|
+
debug: 'thg_hp_debug',
|
|
57
62
|
});
|
|
58
63
|
|
|
59
64
|
/**
|
package/src/transport.js
CHANGED
|
@@ -32,12 +32,30 @@ const DEFAULTS = {
|
|
|
32
32
|
};
|
|
33
33
|
|
|
34
34
|
const now = () => Date.now();
|
|
35
|
+
const MAX_BACKOFF_MS = 30_000;
|
|
36
|
+
|
|
37
|
+
/** The largest event the SDK sends: with its batch envelope it still fits one keepalive request. */
|
|
38
|
+
export const MAX_EVENT_BYTES = 55 * 1024;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Retry-After as milliseconds from now: delay-seconds or an HTTP date (RFC 9110 §10.2.3); 0 when
|
|
42
|
+
* absent or unreadable. Cross-origin, the browser exposes it only if the ingest lists it in
|
|
43
|
+
* Access-Control-Expose-Headers; a first-party proxy always can.
|
|
44
|
+
*/
|
|
45
|
+
export function retryAfterMs(value, at = now()) {
|
|
46
|
+
if (typeof value !== 'string') return 0;
|
|
47
|
+
const text = value.trim();
|
|
48
|
+
if (/^\d+$/.test(text)) return Number(text) * 1000;
|
|
49
|
+
if (text === '') return 0;
|
|
50
|
+
const date = Date.parse(text);
|
|
51
|
+
return Number.isNaN(date) ? 0 : Math.max(0, date - at);
|
|
52
|
+
}
|
|
35
53
|
|
|
36
54
|
/**
|
|
37
55
|
* UTF-8 bytes, the unit of sendBeacon's ceiling and the keepalive budget. String length counts
|
|
38
56
|
* UTF-16 code units and undercounts any non-ASCII catalogue.
|
|
39
57
|
*/
|
|
40
|
-
const byteLength = (s) =>
|
|
58
|
+
export const byteLength = (s) =>
|
|
41
59
|
typeof TextEncoder !== 'undefined'
|
|
42
60
|
? new TextEncoder().encode(s).length
|
|
43
61
|
: Buffer.byteLength(s);
|
|
@@ -111,8 +129,23 @@ export class Transport {
|
|
|
111
129
|
}
|
|
112
130
|
}
|
|
113
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Queued as its JSON form, made once here: an event JSON cannot serialise (a BigInt, a cycle, a
|
|
134
|
+
* throwing toJSON) is dropped on its own, rather than throwing from every later flush and holding
|
|
135
|
+
* every event behind it.
|
|
136
|
+
*/
|
|
114
137
|
enqueue(event) {
|
|
115
|
-
|
|
138
|
+
let wire;
|
|
139
|
+
try {
|
|
140
|
+
wire = JSON.parse(JSON.stringify(event));
|
|
141
|
+
} catch (err) {
|
|
142
|
+
this.o.onError?.(
|
|
143
|
+
`event ${event?.messageId ?? ''} cannot be serialised; dropped`,
|
|
144
|
+
err,
|
|
145
|
+
);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
this.queue.push(wire);
|
|
116
149
|
if (this.queue.length > this.o.maxQueue) {
|
|
117
150
|
// Drop oldest: recent behaviour is more useful than a complete-but-stale tail.
|
|
118
151
|
this.queue.splice(0, this.queue.length - this.o.maxQueue);
|
|
@@ -267,6 +300,7 @@ export class Transport {
|
|
|
267
300
|
async #send(batch, attempt, persisted = null) {
|
|
268
301
|
if (persisted) this.sending.add(persisted);
|
|
269
302
|
const body = this.#body(batch);
|
|
303
|
+
let wait = 0;
|
|
270
304
|
try {
|
|
271
305
|
const res = await fetch(this.#url(), {
|
|
272
306
|
method: 'POST',
|
|
@@ -285,6 +319,7 @@ export class Transport {
|
|
|
285
319
|
// 4xx (except 429) means the payload is wrong; retrying cannot fix it and
|
|
286
320
|
// would loop forever. Drop and surface.
|
|
287
321
|
const retryable = res.status === 429 || res.status >= 500;
|
|
322
|
+
if (retryable) wait = retryAfterMs(res.headers?.get?.('retry-after'));
|
|
288
323
|
if (!retryable) {
|
|
289
324
|
this.#settle(persisted);
|
|
290
325
|
this.o.onError?.(
|
|
@@ -303,8 +338,11 @@ export class Transport {
|
|
|
303
338
|
);
|
|
304
339
|
return false;
|
|
305
340
|
}
|
|
306
|
-
|
|
307
|
-
|
|
341
|
+
// The ingest's Retry-After is a floor on the backoff, within the same cap.
|
|
342
|
+
const delay = Math.max(
|
|
343
|
+
Math.min(1000 * 2 ** attempt, MAX_BACKOFF_MS) * (0.5 + Math.random()),
|
|
344
|
+
Math.min(wait, MAX_BACKOFF_MS),
|
|
345
|
+
);
|
|
308
346
|
await new Promise((r) => setTimeout(r, delay));
|
|
309
347
|
return this.#send(batch, attempt + 1, persisted);
|
|
310
348
|
}
|
package/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const VERSION = '3.
|
|
1
|
+
export const VERSION = '3.19.0';
|
|
2
2
|
export const LIB = '@thg-agentic/hyper-personalisation-web';
|
package/types/bootstrap.d.ts
CHANGED
|
@@ -56,6 +56,13 @@ export interface SettingsResponse {
|
|
|
56
56
|
queryParamAllowlist?: string[];
|
|
57
57
|
/** Host for ingest and for the API. */
|
|
58
58
|
apiHost?: string;
|
|
59
|
+
/** The share of visitors sent for, decided per anonymous id (since 3.18). Absent: 1. */
|
|
60
|
+
sampleRate?: number;
|
|
61
|
+
/**
|
|
62
|
+
* Console logging for everyone, or for a session opened with `?hp_debug=<token>` whose SHA-256
|
|
63
|
+
* is `tokenSha256` (since 3.18). Absent: off.
|
|
64
|
+
*/
|
|
65
|
+
debug?: { enabled: boolean; tokenSha256: string | null };
|
|
59
66
|
/** Each property's personalisation holdout, keyed by property id. */
|
|
60
67
|
holdouts?: Record<
|
|
61
68
|
string,
|
|
@@ -132,6 +139,10 @@ export interface BootstrapOptions {
|
|
|
132
139
|
dataLayer?: { name?: string; ignore?: string[] };
|
|
133
140
|
/** Rename what the SDK stores. Defaults are `thg_hp_*`. */
|
|
134
141
|
storageNames?: StorageNames;
|
|
142
|
+
/**
|
|
143
|
+
* Log every event to the console. Also on when the settings turn it on (`hp.debug`), for everyone
|
|
144
|
+
* or for a session opened with `?hp_debug=<token>`, without a release of the page.
|
|
145
|
+
*/
|
|
135
146
|
debug?: boolean;
|
|
136
147
|
/** Development and tests: use this settings response and never fetch or cache one. */
|
|
137
148
|
settings?: SettingsResponse;
|
package/types/consent.d.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
| 'ad_user_data'
|
|
6
|
-
| 'ad_personalization';
|
|
1
|
+
import type { CdpConsentPurpose } from './events';
|
|
2
|
+
|
|
3
|
+
/** The purposes a visitor answers for: the spec's, generated. */
|
|
4
|
+
export type ConsentPurpose = CdpConsentPurpose;
|
|
7
5
|
|
|
8
6
|
/** schemas/common/consent-state.json: true granted, false refused, null never asked. */
|
|
9
7
|
export type ConsentState = Record<ConsentPurpose, boolean | null>;
|
package/types/events.d.ts
CHANGED
|
@@ -1,7 +1,39 @@
|
|
|
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.12
|
|
5
|
+
|
|
6
|
+
/** context.source.environment. Lets non-production traffic be filtered out of the warehouse without a separate writeKey. */
|
|
7
|
+
export type CdpSourceEnvironment = 'production' | 'staging' | 'development';
|
|
8
|
+
export declare const CDP_SOURCE_ENVIRONMENT_VALUES: readonly CdpSourceEnvironment[];
|
|
9
|
+
|
|
10
|
+
/** context.identity_trust. Whether anonymousId on THIS event identifies a single real visitor. */
|
|
11
|
+
export type CdpIdentityTrust = 'device' | 'shared_render' | 'none';
|
|
12
|
+
export declare const CDP_IDENTITY_TRUST_VALUES: readonly CdpIdentityTrust[];
|
|
13
|
+
|
|
14
|
+
/** context.page.type. Normalised page taxonomy. */
|
|
15
|
+
export type CdpPageType = 'home' | 'list' | 'product' | 'search' | 'basket' | 'checkout' | 'order_confirmation' | 'login' | 'register' | 'account' | 'info' | 'blog' | 'other';
|
|
16
|
+
export declare const CDP_PAGE_TYPE_VALUES: readonly CdpPageType[];
|
|
17
|
+
|
|
18
|
+
/** context.device.type. The kind of thing this is. */
|
|
19
|
+
export type CdpDeviceType = 'desktop' | 'mobile' | 'tablet' | 'bot' | 'app';
|
|
20
|
+
export declare const CDP_DEVICE_TYPE_VALUES: readonly CdpDeviceType[];
|
|
21
|
+
|
|
22
|
+
/** context.device.network. How the device was connected when the event happened, as a native app reports it. */
|
|
23
|
+
export type CdpDeviceNetwork = 'wifi' | 'cellular' | 'wired' | 'offline' | 'other';
|
|
24
|
+
export declare const CDP_DEVICE_NETWORK_VALUES: readonly CdpDeviceNetwork[];
|
|
25
|
+
|
|
26
|
+
/** context.consent.basis. Who decided whether this event may be collected (spec 1.5). */
|
|
27
|
+
export type CdpConsentBasis = 'visitor' | 'host';
|
|
28
|
+
export declare const CDP_CONSENT_BASIS_VALUES: readonly CdpConsentBasis[];
|
|
29
|
+
|
|
30
|
+
/** context.decisions[].kind. The KIND of decision this was. */
|
|
31
|
+
export type CdpDecisionKind = 'recommendation' | 'search_rank' | 'banner' | 'offer' | 'layout' | 'copy' | 'price' | 'other';
|
|
32
|
+
export declare const CDP_DECISION_KIND_VALUES: readonly CdpDecisionKind[];
|
|
33
|
+
|
|
34
|
+
/** context.consent. The purposes a visitor answers for: context.consent and Consent Updated `state`. */
|
|
35
|
+
export type CdpConsentPurpose = 'analytics' | 'marketing' | 'personalisation' | 'ad_user_data' | 'ad_personalization';
|
|
36
|
+
export declare const CDP_CONSENT_PURPOSE_VALUES: readonly CdpConsentPurpose[];
|
|
5
37
|
|
|
6
38
|
/** The product shape shared by every commerce event. */
|
|
7
39
|
export interface CdpProduct {
|
|
@@ -36,6 +68,7 @@ export interface DecisionMade {
|
|
|
36
68
|
propensity: number;
|
|
37
69
|
arm: string;
|
|
38
70
|
kind?: 'recommendation' | 'search_rank' | 'banner' | 'offer' | 'layout' | 'copy' | 'price' | 'other' | null;
|
|
71
|
+
learning_reset_at?: string | null;
|
|
39
72
|
explored?: boolean | null;
|
|
40
73
|
context_features?: Record<string, unknown> | null;
|
|
41
74
|
context_key?: string | null;
|
|
@@ -137,6 +170,13 @@ export interface ProductsSearched {
|
|
|
137
170
|
search_request_id?: string | null;
|
|
138
171
|
}
|
|
139
172
|
|
|
173
|
+
/** A search box suggested products for a partly typed query, and they were seen. */
|
|
174
|
+
export interface SearchSuggestionsViewed {
|
|
175
|
+
query: string;
|
|
176
|
+
products: CdpProduct[];
|
|
177
|
+
search_request_id?: string | null;
|
|
178
|
+
}
|
|
179
|
+
|
|
140
180
|
/** A product was added to the basket. */
|
|
141
181
|
export interface ProductAdded {
|
|
142
182
|
products: CdpProduct[];
|
|
@@ -260,6 +300,7 @@ export interface OrderUpdated {
|
|
|
260
300
|
taxes_included?: boolean | null;
|
|
261
301
|
discount?: number | null;
|
|
262
302
|
coupon?: string | null;
|
|
303
|
+
payment_method?: string | null;
|
|
263
304
|
products?: CdpProduct[] | null;
|
|
264
305
|
reason?: 'line_added' | 'line_removed' | 'quantity_changed' | 'address_changed' | 'delivery_changed' | 'payment_changed' | 'price_adjusted' | 'partial_cancellation' | 'other' | 'status_changed' | null;
|
|
265
306
|
status?: 'processing' | 'despatched' | 'partially_despatched' | 'delivered' | 'collected' | 'held' | 'payment_problem' | null;
|
|
@@ -565,8 +606,32 @@ export interface EmailMarkedAsSpam {
|
|
|
565
606
|
email_id?: string | null;
|
|
566
607
|
}
|
|
567
608
|
|
|
609
|
+
/** The app was launched for the first time after being installed. */
|
|
610
|
+
export interface ApplicationInstalled {
|
|
611
|
+
version: string;
|
|
612
|
+
build: string;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/** The app was launched, or came back to the foreground. */
|
|
616
|
+
export interface ApplicationOpened {
|
|
617
|
+
from_background: boolean;
|
|
618
|
+
version: string;
|
|
619
|
+
build: string;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/** The app went to the background. */
|
|
623
|
+
export type ApplicationBackgrounded = Record<string, never>;
|
|
624
|
+
|
|
625
|
+
/** The app was launched for the first time after an update to a different version or build. */
|
|
626
|
+
export interface ApplicationUpdated {
|
|
627
|
+
previous_version: string;
|
|
628
|
+
previous_build: string;
|
|
629
|
+
version: string;
|
|
630
|
+
build: string;
|
|
631
|
+
}
|
|
632
|
+
|
|
568
633
|
/** The spec version these models implement. SDKs stamp it on context.spec.version. */
|
|
569
|
-
export const SPEC_VERSION = '1.
|
|
634
|
+
export const SPEC_VERSION = '1.12';
|
|
570
635
|
|
|
571
636
|
/** Every event name the registry accepts. Anything else is rejected at the ingest. */
|
|
572
637
|
export type CdpEventName =
|
|
@@ -580,6 +645,7 @@ export type CdpEventName =
|
|
|
580
645
|
| 'Product Clicked'
|
|
581
646
|
| 'Product List Filtered'
|
|
582
647
|
| 'Products Searched'
|
|
648
|
+
| 'Search Suggestions Viewed'
|
|
583
649
|
| 'Product Added'
|
|
584
650
|
| 'Product Removed'
|
|
585
651
|
| 'Cart Viewed'
|
|
@@ -620,6 +686,10 @@ export type CdpEventName =
|
|
|
620
686
|
| 'Order Returned'
|
|
621
687
|
| 'Email Bounced'
|
|
622
688
|
| 'Email Marked as Spam'
|
|
689
|
+
| 'Application Installed'
|
|
690
|
+
| 'Application Opened'
|
|
691
|
+
| 'Application Backgrounded'
|
|
692
|
+
| 'Application Updated'
|
|
623
693
|
;
|
|
624
694
|
|
|
625
695
|
/** Event name -> its properties, so a generic emitter stays type-safe. */
|
|
@@ -634,6 +704,7 @@ export interface CdpEventProperties {
|
|
|
634
704
|
'Product Clicked': ProductClicked;
|
|
635
705
|
'Product List Filtered': ProductListFiltered;
|
|
636
706
|
'Products Searched': ProductsSearched;
|
|
707
|
+
'Search Suggestions Viewed': SearchSuggestionsViewed;
|
|
637
708
|
'Product Added': ProductAdded;
|
|
638
709
|
'Product Removed': ProductRemoved;
|
|
639
710
|
'Cart Viewed': CartViewed;
|
|
@@ -674,6 +745,10 @@ export interface CdpEventProperties {
|
|
|
674
745
|
'Order Returned': OrderReturned;
|
|
675
746
|
'Email Bounced': EmailBounced;
|
|
676
747
|
'Email Marked as Spam': EmailMarkedAsSpam;
|
|
748
|
+
'Application Installed': ApplicationInstalled;
|
|
749
|
+
'Application Opened': ApplicationOpened;
|
|
750
|
+
'Application Backgrounded': ApplicationBackgrounded;
|
|
751
|
+
'Application Updated': ApplicationUpdated;
|
|
677
752
|
}
|
|
678
753
|
|
|
679
754
|
/** Which producers the registry permits for each event. Emitting from another is refused. */
|
package/types/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
CartViewed,
|
|
7
7
|
CdpEventName,
|
|
8
8
|
CdpEventProperties,
|
|
9
|
+
CdpPageType,
|
|
9
10
|
CdpProduct,
|
|
10
11
|
CheckoutFailed,
|
|
11
12
|
CheckoutStarted,
|
|
@@ -36,6 +37,7 @@ import type {
|
|
|
36
37
|
PromotionClicked,
|
|
37
38
|
PromotionViewed,
|
|
38
39
|
RecommendationsViewed,
|
|
40
|
+
SearchSuggestionsViewed,
|
|
39
41
|
SignedIn,
|
|
40
42
|
SignedUp,
|
|
41
43
|
SlotEngaged,
|
|
@@ -45,20 +47,8 @@ import type {
|
|
|
45
47
|
export type { ConsentAdapter, ConsentBasis, ConsentState } from './consent';
|
|
46
48
|
export type * from './events';
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
| 'list'
|
|
51
|
-
| 'product'
|
|
52
|
-
| 'search'
|
|
53
|
-
| 'basket'
|
|
54
|
-
| 'checkout'
|
|
55
|
-
| 'order_confirmation'
|
|
56
|
-
| 'login'
|
|
57
|
-
| 'register'
|
|
58
|
-
| 'account'
|
|
59
|
-
| 'info'
|
|
60
|
-
| 'blog'
|
|
61
|
-
| 'other';
|
|
50
|
+
/** context.page.type: the spec's page taxonomy, generated. */
|
|
51
|
+
export type PageType = CdpPageType;
|
|
62
52
|
|
|
63
53
|
export interface PageProperties {
|
|
64
54
|
/** Stored as context.page.type and inherited by later events on the page. */
|
|
@@ -171,6 +161,11 @@ export interface AnalyticsOptions extends AnalyticsContextSources {
|
|
|
171
161
|
|
|
172
162
|
/** The property's personalisation holdout, from the settings (`hp.holdouts`). */
|
|
173
163
|
holdout?: Holdout | null;
|
|
164
|
+
/**
|
|
165
|
+
* The share of visitors to send for, from the settings (`hp.sampleRate`): a visitor whose
|
|
166
|
+
* anonymous id falls outside it sends nothing. Default 1. Since 3.18.
|
|
167
|
+
*/
|
|
168
|
+
sampleRate?: number;
|
|
174
169
|
|
|
175
170
|
/** Keep only these query params (anchored, case-insensitive patterns), plus campaign params. */
|
|
176
171
|
queryParamAllowlist?: string[];
|
|
@@ -184,10 +179,10 @@ export interface AnalyticsOptions extends AnalyticsContextSources {
|
|
|
184
179
|
|
|
185
180
|
/**
|
|
186
181
|
* Where device ids come from, as the source's settings serve it (`hp.anonymousId`, chosen in the
|
|
187
|
-
* console)
|
|
182
|
+
* console):
|
|
188
183
|
*
|
|
189
184
|
* - `site_cookie`: the site's own first-party cookie, the one named here or, with `cookie: null`,
|
|
190
|
-
* whatever `getAnonymousId` reads. Read only
|
|
185
|
+
* whatever `getAnonymousId` reads. Read only; a visitor without it gets the SDK's own (3.18).
|
|
191
186
|
* - `hp_cookie`: Hyper-Personalisation's own first-party cookie (`storageNames.anonymousId`,
|
|
192
187
|
* `thg_hp_anonymous_id`), which the SDK mints and keeps; `getAnonymousId` is ignored.
|
|
193
188
|
*
|
|
@@ -231,11 +226,11 @@ export interface AnalyticsOptions extends AnalyticsContextSources {
|
|
|
231
226
|
};
|
|
232
227
|
}
|
|
233
228
|
|
|
234
|
-
/** The event as queued, or null when the tracking plan disables it. */
|
|
229
|
+
/** The event as queued, or null when the tracking plan disables it or the visitor is outside the sample. */
|
|
235
230
|
type Emit<P> = (properties: P, options?: CallOptions) => CdpEvent | null;
|
|
236
231
|
|
|
237
232
|
export interface AnalyticsClient {
|
|
238
|
-
/** Null when the tracking plan disables the event. */
|
|
233
|
+
/** Null when the tracking plan disables the event or the visitor is outside the sample. */
|
|
239
234
|
track<E extends CdpEventName>(
|
|
240
235
|
event: E,
|
|
241
236
|
properties?: CdpEventProperties[E],
|
|
@@ -247,9 +242,13 @@ export interface AnalyticsClient {
|
|
|
247
242
|
properties?: Record<string, unknown>,
|
|
248
243
|
options?: CallOptions,
|
|
249
244
|
): CdpEvent | null;
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
245
|
+
/** Null when the event is not sent: outside the sample, dropped by `beforeSend`, or failed. */
|
|
246
|
+
page(properties?: PageProperties, options?: CallOptions): CdpEvent | null;
|
|
247
|
+
identify(traits?: IdentifyTraits, options?: CallOptions): CdpEvent | null;
|
|
248
|
+
/**
|
|
249
|
+
* Sign-out on a shared device: forget the user, the page chain, the SDK's session, the decisions
|
|
250
|
+
* in force and the recommendation context. The device id stays.
|
|
251
|
+
*/
|
|
253
252
|
reset(): void;
|
|
254
253
|
|
|
255
254
|
productViewed: Emit<ProductViewed>;
|
|
@@ -288,7 +287,13 @@ export interface AnalyticsClient {
|
|
|
288
287
|
personalisationGroup(): PersonalisationGroup;
|
|
289
288
|
productClicked: Emit<ProductClicked>;
|
|
290
289
|
productListFiltered: Emit<ProductListFiltered>;
|
|
290
|
+
/** A submitted search and the results page it produced. */
|
|
291
291
|
productsSearched: Emit<ProductsSearched>;
|
|
292
|
+
/**
|
|
293
|
+
* The search box's suggested products for a partly typed query, once seen. Never productsSearched,
|
|
294
|
+
* which is a submitted search's results page.
|
|
295
|
+
*/
|
|
296
|
+
searchSuggestionsViewed: Emit<SearchSuggestionsViewed>;
|
|
292
297
|
productAdded: Emit<ProductAdded>;
|
|
293
298
|
productRemoved: Emit<ProductRemoved>;
|
|
294
299
|
/**
|
|
@@ -473,6 +478,8 @@ export interface StorageNames {
|
|
|
473
478
|
session?: string;
|
|
474
479
|
/** Session cookie: the preview token from `?hp_preview=` (3.15). Default 'thg_hp_preview'. */
|
|
475
480
|
preview?: string;
|
|
481
|
+
/** sessionStorage: the debug token from `?hp_debug=` (3.18). Default 'thg_hp_debug'. */
|
|
482
|
+
debug?: string;
|
|
476
483
|
}
|
|
477
484
|
|
|
478
485
|
/** Where a source's device ids come from: the settings' `hp.anonymousId` (since 3.6). */
|