@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.
@@ -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: the SDK never mints a
5
- * second one beside it.
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=60, stale-while-revalidate=600`) saves the round trip.
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. The key alone selects the tenant, the
42
- * property and test or live, as with Stripe and LaunchDarkly; a host is only for a first-party proxy.
43
- * Until production is live this is the nonprod host, reachable only from allowlisted networks, so
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.uat.thgingenuity.com';
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
- function pruneNulls(value) {
41
- if (Array.isArray(value)) return value.map(pruneNulls);
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
- const out = {};
44
- for (const [k, v] of Object.entries(value)) {
45
- if (isNullish(v)) continue;
46
- out[k] = pruneNulls(v);
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
 
@@ -1,4 +1,4 @@
1
1
  // Generated from the spec by scripts/build-types.mjs. DO NOT EDIT: run `pnpm run build:types`.
2
2
 
3
3
  /** The event specification this SDK emits, stamped on every event as context.spec.version. */
4
- export const SPEC_VERSION = '1.8';
4
+ export const SPEC_VERSION = '1.12';
@@ -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 names none, expiring 30
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
- this.queue.push(event);
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
- const delay =
307
- Math.min(1000 * 2 ** attempt, 30_000) * (0.5 + Math.random());
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.17.1';
1
+ export const VERSION = '3.19.0';
2
2
  export const LIB = '@thg-agentic/hyper-personalisation-web';
@@ -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;
@@ -1,9 +1,7 @@
1
- export type ConsentPurpose =
2
- | 'analytics'
3
- | 'marketing'
4
- | 'personalisation'
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.8
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.8';
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
- export type PageType =
49
- | 'home'
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). One place per source, never a fallback between two:
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: a visitor without it has no device id.
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
- page(properties?: PageProperties, options?: CallOptions): CdpEvent;
251
- identify(traits?: IdentifyTraits, options?: CallOptions): CdpEvent;
252
- /** Clear identity and the page chain. */
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). */