@thg-agentic/hyper-personalisation-web 3.31.0 → 3.34.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 CHANGED
@@ -516,8 +516,9 @@ itself: `import { startOnsite } from '@thg-agentic/hyper-personalisation-web/ons
516
516
  `startOnsite(client, options)`. On the CDN, `hp.min.js` loads `onsite.min.js` (and that loads
517
517
  `onsite-render.min.js` once a trigger fires, `onsite-wheel.min.js` for a wheel,
518
518
  `onsite-answers.min.js` for a date or number question, `onsite-products.min.js` for a products
519
- block, `onsite-audiences.min.js` for a form limited to audiences) from beside itself; a self-hosted
520
- copy keeps all six beside `hp.min.js`. The runtime watches triggers, caps and consent and asks for the
519
+ block, `onsite-audiences.min.js` for a form limited to audiences, `onsite-values.min.js` for a form
520
+ whose text has shopper values) from beside itself; a self-hosted copy keeps all seven beside
521
+ `hp.min.js`. The runtime watches triggers, caps and consent and asks for the
521
522
  decision; the renderer and its stylesheet are fetched only when a trigger fires, alongside the
522
523
  decision request, so a page whose forms never fire never downloads them. If the renderer cannot
523
524
  load, the form is not shown (`hp.onsite.debug()` says `the renderer did not load`) and the page goes
@@ -570,7 +571,8 @@ on desktop arms after `delay_ms` (5 s) and once the pointer has been below the t
570
571
  viewport, and fires when it leaves through the top edge, moving up. On mobile it is a fast upward
571
572
  flick (120px at 0.5 px/ms) after 30% of the page, or a return to the tab after 10 s away. Pages
572
573
  (include/exclude globs on the path, `*` matching anything, in any case), devices (`mobile` is a coarse pointer
573
- or under 768px), the campaign the visit arrived from (`arrived_from`), dates and caps are checked
574
+ or under 768px), the campaign the visit arrived from (`arrived_from`), what the page says
575
+ (`page_facts`), dates and caps are checked
574
576
  first; then `POST /v1/decisions` is asked for that one
575
577
  slot. A held-out visitor, or no decision, sees nothing. One overlay per page view: of the forms that
576
578
  fire within 50 ms, the highest `priority`; once one has shown (or the visitor was held out of it),
@@ -587,17 +589,50 @@ property's forms, `thg_hp_answers:<property>` in localStorage: the latest answer
587
589
  question (its key and the option values chosen; never a date, a number, typed text or contact
588
590
  data), each for 90 days from when it was given, written from a quiz's `Preferences Stated` (so only with analytics consent too) and a
589
591
  sign-up form's answers once the gateway took them. A form limited to
590
- an early audience, one whose every rule is an answer or about the visit, is then decided from those
591
- answers with the visit, so it shows on the next page view without waiting for the warehouse. The
592
- datafile ships it as a MIXED audience (`rules` its visit rules) with `early: true` and its
593
- `answer_rules`: `answer.<question>` with `in` (is one of), `not_in` (is none of; holds with no
594
- answer kept) or `all` (includes all of) and a list of answers. The runtime sends the early audiences
592
+ an early audience, one whose every rule is about the visit or an answer the shopper gave (is one
593
+ of, includes all of), is then decided from those answers with the visit, so it shows on the next
594
+ page view without waiting for the warehouse. An audience with an "is none of" answer is never
595
+ early: a device that keeps no answer cannot tell, so the warehouse decides it. The datafile ships
596
+ an early audience as a MIXED audience (`rules` its visit rules) with `early: true` and its
597
+ `answer_rules`: `answer.<question>` with `in` (is one of) or `all` (includes all of) and a list of
598
+ answers. A datafile written before 2026-11 may still carry `not_in` (is none of), which holds with
599
+ no answer kept. The runtime sends the early audiences
595
600
  that hold on the kept answers as `answered_audiences`. The answers go when personalisation is
596
601
  refused or withdrawn, at a sign-out and when another user signs in. Anywhere else (the page's own
597
602
  `./session`, which carries no answers, an answer given on another device, an earlier SDK) an early
598
603
  audience is MIXED: met where the warehouse qualified the shopper on its answers and its visit rules
599
604
  hold, from the next profile update.
600
605
 
606
+ **Shopper values in text (3.32).** A heading or paragraph may hold a value node,
607
+ `{ v, fallback, format? }`, filled in the browser as a text node (never markup): trimmed, without
608
+ control or invisible characters, at most 60 characters with an ellipsis. `v` is one of
609
+ `basket_value` (the basket this visit, as a basket bar reads it, as money in the `Cart Updated`'s
610
+ own `currency`, else the client's), `basket_items` (the items its lines hold: each line's whole
611
+ `quantity`, a line without one or with a fractional one once, zero or less none), `first_name` (what the page gave, below) or `answer.<question>` (the label of the
612
+ shopper's answer in the form's language: one given in this form, else the one kept under
613
+ personalisation consent; none without it). `format.style` is `number`, `money`, `date`,
614
+ `relative` or `text`, formatted with `Intl` in the form's locale, and `format.plural`
615
+ (`{ one, other }`) follows a number with its word ("3 items"). A value the page does not know shows
616
+ its `fallback`; with the block's `missing: "hide"` the block is not shown instead. The page gives
617
+ what only it knows with `hp.onsite.values({ first_name: 'Alex' })` (or `startOnsite`'s `values`):
618
+ kept in the page's memory only, drawn as text, never sent, stored or logged; each call replaces the
619
+ last, `values({})` forgets it. The formatting is its own chunk (`onsite-values.min.js`), loaded
620
+ only for a form whose text has a value; if it does not load, the form is not shown
621
+ (`values-failed`). The public API serves such a form only to 3.32 or later.
622
+
623
+ **Daily cut-off.** A form's `window.daily` (3.33 or later) is a cut-off that comes back each day, such
624
+ as "order by 1 AM for next-day delivery". The server works out when it falls, in the shop's time
625
+ zone on the days it is set for, and the settings list those moments: `next`, epoch milliseconds,
626
+ earliest first, for the next 8 days. So the browser compares numbers and carries no time-zone code,
627
+ and every shopper counts to the same moment, whatever their own clock. It is targeting, checked with
628
+ the form's pages and dates before any decision is asked for, so held-out and shown visitors are
629
+ filtered alike: the form is not eligible while none of the moments is still ahead (`no cut-off
630
+ ahead`) or, with `within_hours`, while the next is further away than that (`outside its daily
631
+ window`). A countdown with `daily: true`, on any step and as many as the form has, counts to the
632
+ first ahead: at 23:00 a 01:00 cut-off is two hours away, and as one passes the countdown moves on to
633
+ the next. A preview shows the form whatever the time. The public API serves a form with a daily
634
+ cut-off, or a daily countdown, only to 3.33 or later.
635
+
601
636
  **Arrived from.** A form with `arrived_from` shows only to a visit that landed from a matching
602
637
  campaign: `[{ field, match, values }]`, `field` one of `source`, `medium`, `campaign`, `content`,
603
638
  `term` (the link's `utm_*` tags), `match` `is` or `contains`, compared trimmed and in any case.
@@ -617,6 +652,27 @@ form unless the rules hold, so an SDK older than 3.26 never shows one. A preview
617
652
  URL's tags: the console's Preview on site link adds them. Click ids
618
653
  (`fbclid`, `gclid`) are not a campaign.
619
654
 
655
+ **Page facts (3.34).** A form with `page_facts` shows only on a page that says so:
656
+ `[{ fact, op, values }]`, `op` one of `is`, `is_not` (any of `values` meets it, or none does),
657
+ `at_least` and `at_most` (one number). The page gives its facts with
658
+ `hp.onsite.facts({ next_day_eligible: true, delivery_tier: 'express' })`: keys of lower-case
659
+ letters, digits and `_` (up to 40), values `true`, `false`, a number or text (up to 100
660
+ characters), at most 20. Each call replaces the last; `facts({})` forgets them. They hold for the
661
+ page path they were given on only, so a single-page app's next page starts with none, and are never
662
+ stored. The rules are judged when the form's trigger fires, so facts the page gives after it loads
663
+ (or again for another variant) count for a form that waits on one (exit intent, a delay, a scroll,
664
+ time or idle). An inline form, or one that shows on arrival with no delay, is judged as onsite
665
+ starts: give its facts before then (before `startOnsite`, or as the page renders). A preview shows
666
+ whatever the page says. Every rule must hold, a fact the page did not give meets no rule, and a rule this SDK cannot
667
+ read never matches (`not eligible: the page does not say so`). The decision request for such a form
668
+ carries the facts as `page_facts` (never logged), and the public API answers no form unless the
669
+ rules hold, so an SDK older than 3.34 never shows one.
670
+
671
+ **Products.** A form whose settings carry `products: true` shows only on the page of a chosen
672
+ product: the request carries the page's product, the `product_id` of the last `Product Viewed` on
673
+ this page, as `visit_context.product_id`, and the public API checks it against the form's product
674
+ filters in the catalogue. A page with no product does not ask (`not eligible: not a product page`).
675
+
620
676
  | Option | |
621
677
  |---|---|
622
678
  | `audiences()` | The visitor's session audiences, when the page evaluates them: a form targeting none of them is skipped. Without it HP's server decides, from the visitor's stored audiences and the in-visit audiences whose rules hold in this visit (the runtime evaluates them from `GET /v1/audiences/datafile` and sends them with the decision). |
@@ -624,6 +680,7 @@ URL's tags: the console's Preview on site link adds them. Click ids
624
680
  | `servingHost`, `gatewayHost` | Where decisions and claims go (default: the host the SDK was pointed at) and capture goes (default: where events go). |
625
681
  | `debug` | Log why each form did or did not show. Also on with `?hp_debug=onsite`. |
626
682
  | `settings` | Development and tests: these forms instead of the settings'. |
683
+ | `values` | What the page knows of the shopper for a form's text, `{ first_name }`, as `hp.onsite.values` (3.32). |
627
684
 
628
685
  `hp.onsite.debug()` returns each form's state and reasons (`armed`, `not eligible: per_session cap`,
629
686
  `held out`, `shown`, …), and since 3.29 a `code`, which stays the same across releases while the
@@ -635,9 +692,10 @@ on it, not on the reasons. `code` is always present, so a trace built by hand ne
635
692
  | `hp.onsite` | |
636
693
  |---|---|
637
694
  | `open(name)`, `show(name)` | Show a form now, past its trigger; resolves with whether it showed. `name` is a form's slot, else one of a form's `aliases` (the settings' names for it: `footer_signup` for the site's own sign-up button), matched exactly; a slot wins over an alias. A `manual` form, or one opened by an alias, shows each time (see above); any other keeps to its caps. From there it is that form, by its slot, in every event. |
638
- | `on(name, fn)` | `shown`, `submitted`, `apply`, `closed`, `held` or `nothing`, with `{ slot, arm, format }` (`nothing`: `{ slot, format, code }`); `submitted` and `apply` add `coupon` (`{ code, prize }` or `null`), `closed` adds `reason`. `held` is a held-out visitor whose trigger fired (a form the site opened says so by `open` resolving `false` instead), so a site replacing its own popup with HP's shows its own there and the holdout compares the two. `nothing` (since 3.29) is a covering form (any but `inline`) whose own trigger fired and for which HP will show nothing on this page view; the site shows its own there too. `code` says why: `decision-failed` (a network error, any status but 200, a 200 without a list of decisions, or no answer within 3 s), `no-creative`, `no-code` (a reminder with no code to remind of), `renderer-failed` or `products-failed` (the chunk did not load within 3 s), or `error` (the form failed while it was being opened). It comes at most 3.05 s after the trigger: the 50 ms in which forms that fire together compete, then one 3 s deadline they share; an answer after it is never shown, so the site needs no timer of its own. It is not said when another covering form that fired with it shows, or the visitor is held out (`held` is said instead); for a form the site opened (`open` resolves `false`); on a suppressed page (`suppress`: the site is keeping its own screen clear); when its caps, the gap between popups, the consent banner or a form already on screen keep it away; or for an empty basket, which is tried again when the basket changes. Of the forms that fire together, only the highest HP has nothing for says it: the slot whose place the site takes. Like a holdout, it spends the page view's allowance, so no other HP popup, sheet, bar or teaser follows it on that page view (a later trigger is `allowance-spent`; a form the shopper asks for, `manual` or by an alias, still opens). A form with prizes says `submitted` once, when its claim settles: with the code, or `null` if the claim failed. Returns a function that stops listening. A throwing listener is logged, never breaks the form. |
695
+ | `on(name, fn)` | `shown`, `submitted`, `apply`, `closed`, `held` or `nothing`, with `{ slot, arm, format }` (`nothing`: `{ slot, format, code }`); `submitted` and `apply` add `coupon` (`{ code, prize }` or `null`), `closed` adds `reason`. `held` is a held-out visitor whose trigger fired (a form the site opened says so by `open` resolving `false` instead), so a site replacing its own popup with HP's shows its own there and the holdout compares the two. `nothing` (since 3.29) is a covering form (any but `inline`) whose own trigger fired and for which HP will show nothing on this page view; the site shows its own there too. `code` says why: `decision-failed` (a network error, any status but 200, a 200 without a list of decisions, or no answer within 3 s), `no-creative`, `no-code` (a reminder with no code to remind of), `renderer-failed`, `products-failed` or `values-failed` (the chunk did not load within 3 s), or `error` (the form failed while it was being opened). It comes at most 3.05 s after the trigger: the 50 ms in which forms that fire together compete, then one 3 s deadline they share; an answer after it is never shown, so the site needs no timer of its own. It is not said when another covering form that fired with it shows, or the visitor is held out (`held` is said instead); for a form the site opened (`open` resolves `false`); on a suppressed page (`suppress`: the site is keeping its own screen clear); when its caps, the gap between popups, the consent banner or a form already on screen keep it away; or for an empty basket, which is tried again when the basket changes. Of the forms that fire together, only the highest HP has nothing for says it: the slot whose place the site takes. Like a holdout, it spends the page view's allowance, so no other HP popup, sheet, bar or teaser follows it on that page view (a later trigger is `allowance-spent`; a form the shopper asks for, `manual` or by an alias, still opens). A form with prizes says `submitted` once, when its claim settles: with the code, or `null` if the claim failed. Returns a function that stops listening. A throwing listener is logged, never breaks the form. |
639
696
  | `ready()` | Resolves once the page knows whether onsite forms run on it: each form's state, as `debug()`, once the runtime has started, or `[]` once none will (not opted in, no live form, the runtime failed to load). A site keeping its own popup where HP has none for the visitor reads it. |
640
697
  | `suppress(reason)`, `unsuppress(reason?)` | Take any form off the screen quietly (not a dismissal) and keep forms away while any reason holds; `unsuppress()` lifts all. |
698
+ | `values(given)` | What the page knows of the shopper for a form's text, `{ first_name }` (3.32): in memory only, never sent; each call replaces the last. |
641
699
 
642
700
  **Formats.** A `teaser` is a tab (its first step) that opens the rest as a popup; the tab is kept on
643
701
  the session's later pages until opened or dismissed, and never traps focus. A `bar` with a
@@ -651,7 +709,8 @@ image or muted, looping video media; see ADR 0064.
651
709
  **Preview.** `?hp_preview=<token>` asks `POST /v1/decisions` for the previewed version and counts
652
710
  nothing. For an editor, `import { mountPreview } from
653
711
  '@thg-agentic/hyper-personalisation-web/onsite/preview'`; `mountPreview(host, creative, { step,
654
- device: 'desktop' | 'mobile', consentText, smsConsentText, questions, prizes, basketValue })` draws the creative
712
+ device: 'desktop' | 'mobile', consentText, smsConsentText, questions, prizes, basketValue, values })`
713
+ draws the creative (with `values`, a returning shopper's sample values; without, every fallback)
655
714
  inside `host` with no request, event or counter, and returns `{ update(creative, options), destroy(),
656
715
  step() }`. It is not part of the runtime chunk.
657
716
 
@@ -714,7 +773,8 @@ each form did or did not show, and a preview link (`?hp_preview=`) counts nothin
714
773
  (about 15.7 KiB), so a page that shows a form loads about 22.3 KiB of onsite code and one whose
715
774
  forms never fire about 6.6 KiB; `onsite-wheel.min.js` under 8 KiB (about 1.8 KiB),
716
775
  `onsite-answers.min.js` under 2 KiB (about 1.5 KiB), `onsite-products.min.js` under 2.6 KiB (about
717
- 2.5 KiB), `onsite-audiences.min.js` under 1.63 KiB (about 1.61 KiB). The core carries only the loader.
776
+ 2.5 KiB), `onsite-audiences.min.js` under 1.63 KiB (about 1.61 KiB), `onsite-values.min.js` under
777
+ 1.5 KiB (about 1.35 KiB). The core carries only the loader.
718
778
 
719
779
  **Products in a form.** A creative's `products` block (ADR 0064 as amended 2026-11-02) is filled by
720
780
  the public API in the decision that serves the form, as the block's `served`. For a form whose