@thg-agentic/hyper-personalisation-web 0.0.0-stage → 3.17.1
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/LICENSE +21 -0
- package/README.md +825 -2
- package/dist/hp.min.js +3 -0
- package/dist/snippet.min.js +1 -0
- package/package.json +83 -3
- package/src/assignment.js +75 -0
- package/src/auto/attributes.js +16 -0
- package/src/auto/exposures.js +45 -0
- package/src/auto/lists.js +136 -0
- package/src/auto/promotions.js +104 -0
- package/src/auto/rails.js +217 -0
- package/src/auto/viewability.js +150 -0
- package/src/auto.js +13 -0
- package/src/bootstrap.js +476 -0
- package/src/carry.js +74 -0
- package/src/conformance.js +530 -0
- package/src/consent-gate.js +324 -0
- package/src/consent.js +604 -0
- package/src/context.js +439 -0
- package/src/datalayer.js +411 -0
- package/src/decisions.js +90 -0
- package/src/device-storage.js +201 -0
- package/src/experiments.js +220 -0
- package/src/ga4.js +124 -0
- package/src/hash.js +138 -0
- package/src/hp.js +135 -0
- package/src/index.js +818 -0
- package/src/preview.js +44 -0
- package/src/recommendation-context.js +101 -0
- package/src/segment.js +375 -0
- package/src/session/category.js +40 -0
- package/src/session/record.js +163 -0
- package/src/session/rules.js +195 -0
- package/src/session/signals.js +146 -0
- package/src/session-id.js +62 -0
- package/src/session.js +28 -0
- package/src/settings.js +211 -0
- package/src/shape.js +111 -0
- package/src/social-proof.js +139 -0
- package/src/spec-version.js +4 -0
- package/src/storage-scope.js +159 -0
- package/src/transport.js +436 -0
- package/src/version.js +2 -0
- package/types/auto.d.ts +51 -0
- package/types/bootstrap.d.ts +154 -0
- package/types/consent.d.ts +126 -0
- package/types/datalayer.d.ts +76 -0
- package/types/events.d.ts +680 -0
- package/types/hp.d.ts +64 -0
- package/types/index.d.ts +610 -0
- package/types/segment.d.ts +68 -0
package/dist/hp.min.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
(()=>{var xt=e=>{throw TypeError(e)};var Xn=(e,t,n)=>t.has(e)||xt("Cannot "+n);var It=(e,t,n)=>t.has(e)?xt("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,n);var O=(e,t,n)=>(Xn(e,t,"access private method"),n);var pe=[];for(let e=2;pe.length<64;e++)pe.every(t=>e%t!==0)&&pe.push(e);var At=e=>(e-Math.floor(e))*2**32>>>0,Ho=pe.slice(0,8).map(e=>At(Math.sqrt(e))),Ko=pe.map(e=>At(Math.cbrt(e)));var Ve=(e,t)=>e<<t|e>>>32-t;function vt(e){let t=new Uint32Array((e.length+8>>6)*16+16);e.forEach((s,i)=>{t[i>>2]|=s<<24-i%4*8}),t[e.length>>2]|=128<<24-e.length%4*8,t[t.length-2]=Math.floor(e.length/2**29),t[t.length-1]=e.length*8;let n=Uint32Array.of(1732584193,4023233417,2562383102,271733878,3285377520),r=new Uint32Array(80);for(let s=0;s<t.length;s+=16){for(let f=0;f<80;f++)r[f]=f<16?t[s+f]:Ve(r[f-3]^r[f-8]^r[f-14]^r[f-16],1);let[i,c,l,d,m]=n;for(let f=0;f<80;f++){let g,b;f<20?(g=c&l|~c&d,b=1518500249):f<40?(g=c^l^d,b=1859775393):f<60?(g=c&l|c&d|l&d,b=2400959708):(g=c^l^d,b=3395469782);let A=Ve(i,5)+g+m+b+r[f]|0;[m,d,l,c,i]=[d,l,Ve(c,30),i,A]}[i,c,l,d,m].forEach((f,g)=>{n[g]+=f})}let o=new Uint8Array(20);return n.forEach((s,i)=>{o[i*4]=s>>>24,o[i*4+1]=s>>>16,o[i*4+2]=s>>>8,o[i*4+3]=s}),o}var Yn="e28e8ada-5b76-4209-bba9-79b823a0af75",Zn=e=>Array.from(e,t=>t.toString(16).padStart(2,"0")).join("");function kt(e,t){let n=Uint8Array.from(e.replace(/-/g,"").match(/../g),c=>Number.parseInt(c,16)),r=new TextEncoder().encode(t),o=new Uint8Array(16+r.length);o.set(n),o.set(r,16);let s=vt(o).slice(0,16);s[6]=s[6]&15|80,s[8]=s[8]&63|128;let i=Zn(s);return`${i.slice(0,8)}-${i.slice(8,12)}-${i.slice(12,16)}-${i.slice(16,20)}-${i.slice(20)}`}var J=(e,t)=>typeof e=="string"&&e!==""&&e.length<=t?e:null;function we(e,t){return J(e,64)??J(t,256)}function Et({sessionId:e,slot:t,arm:n}={}){let r=J(e,256);return!r||!J(t,256)||!J(n,256)?null:kt(Yn,`${r}:${t}:${n}`)}var Qn="4b1f2c7e-9a3d-4e58-b6c1-2d7f0a9e8c53";function Ct({sessionId:e,surface:t}={}){let n=J(e,256);return!n||!J(t,64)?null:kt(Qn,`${n}:${t}`)}var er=Array.from({length:11},(e,t)=>t/10);function tr(e,t){if(!e.isIntersecting)return!1;let{intersectionRect:n,boundingClientRect:r,rootBounds:o}=e;if(!n||!r)return!0;let s=n.width*n.height,i=r.width*r.height,c=o?Math.min(i,o.width*o.height):i;return c>0&&s/c>=t}function z(e){let{selector:t,onViewable:n,visibleRatio:r=.5,dwellMs:o=1e3,root:s=document}=e,i=new Map,c=new WeakSet,l=new Set,d=h=>{i.has(h)||i.set(h,setTimeout(()=>{i.delete(h),n(h)&&(f?.unobserve(h),l.delete(h))},o))},m=h=>{clearTimeout(i.get(h)),i.delete(h)},f=typeof IntersectionObserver>"u"?null:new IntersectionObserver(h=>{for(let x of h){let p=x.target;if(!tr(x,r)){l.delete(p),m(p);continue}l.add(p),g()&&d(p)}},{threshold:er}),g=()=>typeof document>"u"||document.visibilityState!=="hidden",b=()=>{if(g())for(let h of l)d(h);else for(let h of l)m(h)};document.addEventListener("visibilitychange",b);let A=()=>{for(let h of s.querySelectorAll(t))c.has(h)||(c.add(h),f?.observe(h))},k=typeof MutationObserver>"u"?null:new MutationObserver(()=>A()),w=()=>{A(),k?.observe(s===document?document.body:s,{childList:!0,subtree:!0})},L=s===document&&!document.body;return L?document.addEventListener("DOMContentLoaded",w,{once:!0}):w(),()=>{L&&document.removeEventListener("DOMContentLoaded",w),document.removeEventListener("visibilitychange",b),f?.disconnect(),k?.disconnect();for(let h of i.values())clearTimeout(h);i.clear(),l.clear()}}function je(e,t={}){if(typeof window>"u"||typeof document>"u")return()=>{};let{selector:n="[data-hp-exposure]",visibleRatio:r=.5,dwellMs:o=1e3,root:s=document}=t;return z({selector:n,root:s,visibleRatio:r,dwellMs:o,onViewable(i){let c=i.dataset?.hpExposure?.trim();return c&&e.surfaceViewed({surface:c}),!0}})}var Pt=e=>{try{return new URL(e,document.baseURI).toString()}catch{return e}};function qe(e,t={}){if(typeof window>"u"||typeof document>"u")return()=>{};let{selector:n="[data-hp-list]",tileSelector:r="[data-hp-product]",visibleRatio:o=.5,dwellMs:s=1e3,root:i=document}=t,c=new Set,l=A=>A.closest?.(n)??null,d=A=>{let k=A.dataset.hpList?.trim();return k?{list_id:k,category:A.dataset.hpListCategory?.trim()||null}:null},m=A=>Array.from(A.querySelectorAll(r)).filter(k=>l(k)===A),f=(A,k)=>{try{let w=JSON.parse(A.dataset.hpProduct??"");return!w||typeof w!="object"||!w.product_id?null:{...w,position:w.position??k+1,...w.url?{url:Pt(w.url)}:{},...w.image_url?{image_url:Pt(w.image_url)}:{}}}catch{return null}},g=z({selector:`${n} ${r}`,root:i,visibleRatio:o,dwellMs:s,onViewable(A){let k=l(A);if(!k||k.dataset.hpListView==="manual")return!0;let w=d(k);if(!w||c.has(w.list_id))return!0;let L=new Set,h=m(k).map(f).filter(x=>!x||L.has(x.product_id)?!1:(L.add(x.product_id),!0));return h.length===0||(c.add(w.list_id),e.productListViewed({...w,products:h})),!0}}),b=A=>{let w=A.target?.closest?.("a[href]")?.closest(r);if(!w)return;let L=l(w),h=f(w,L?m(L).indexOf(w):0);h&&e.productClicked({list_id:L?d(L)?.list_id??null:null,products:[h]})};return document.addEventListener("click",b,!0),()=>{document.removeEventListener("click",b,!0),g()}}function T(e,...t){let n=e.dataset??{};for(let r of t){let o=n[r];if(typeof o=="string"&&o.trim()!=="")return o.trim()}return null}function Se(e,...t){let n=T(e,...t);if(n===null)return null;let r=Number(n);return Number.isFinite(r)?Math.trunc(r):null}function nr(e,t){let n=T(e,"hpPromotion");return n?{promotion_id:n,name:T(e,"hpPromotionName"),creative:T(e,"hpPromotionCreative"),position:T(e,"hpPromotionSlot")??String(t+1)}:null}function Be(e,t={}){if(typeof window>"u"||typeof document>"u")return()=>{};let{selector:n="[data-hp-promotion]",resolve:r=nr,visibleRatio:o=.5,dwellMs:s=1e3,root:i=document}=t,c=new Set,l=f=>{let g=Array.from(i.querySelectorAll(n)),b=g.indexOf(f);try{return r(f,b<0?g.length:b)}catch{return null}},d=z({selector:n,root:i,visibleRatio:o,dwellMs:s,onViewable(f){let g=l(f);return!g||c.has(g.promotion_id)?!1:(c.add(g.promotion_id),e.promotionViewed(g),!0)}}),m=f=>{let g=f.target;if(!g||typeof g.closest!="function")return;let b=g.closest(n),A=g.closest("a[href]");if(!b||!A||!b.contains(A)&&!A.contains(b))return;let k=l(b);k&&e.promotionClicked({...k,href:A.getAttribute("href")})};return document.addEventListener("click",m,!0),()=>{document.removeEventListener("click",m,!0),d()}}function rr(e){let t=T(e,"hpRecoRequest"),n=T(e,"hpRecoPlacement"),r=T(e,"hpRecoStrategy");if(!t||!n||!r)return null;let o=T(e,"hpRecoHoldout");return{recommendation_request_id:t,placement:n,strategy:r,recipe:T(e,"hpRecoRecipe"),algorithm_version:T(e,"hpRecoAlgorithmVersion"),experiment:T(e,"hpRecoExperiment"),variant:T(e,"hpRecoVariant"),holdout:o===null?null:o==="true",assignment_bucket:Se(e,"hpRecoBucket"),assignment_unit_kind:T(e,"hpRecoUnitKind"),candidate_set_fingerprint:T(e,"hpRecoFingerprint"),candidate_set_size:Se(e,"hpRecoCandidates"),ranked_by:T(e,"hpRecoRankedBy"),served_at:T(e,"hpRecoServedAt")}}function or(e,t){let n=T(e,"hpRecoProduct");if(!n)return null;let r=T(e,"hpRecoPrice");return{product_id:n,sku:T(e,"hpRecoSku"),name:T(e,"hpRecoName"),brand:T(e,"hpRecoBrand"),price:r===null||Number.isNaN(Number(r))?null:Number(r),position:Se(e,"hpRecoPosition")??t+1}}function Fe(e,t={}){if(typeof window>"u"||typeof document>"u")return()=>{};let{selector:n="[data-hp-reco-request]",tileSelector:r="[data-hp-reco-product]",resolveRail:o=rr,resolveTile:s=or,visibleRatio:i=.5,dwellMs:c=1e3,root:l=document}=t,d=new Set,m=new Map,f=new Map,g=(h,...x)=>{try{return h(...x)}catch{return null}},b=h=>typeof h.closest=="function"?h.closest(n):null,A=h=>Array.from(h.querySelectorAll(r)),k=(h,x)=>{if(f.delete(x.recommendation_request_id),d.has(x.recommendation_request_id))return;let p=A(h).map((S,P)=>g(s,S,P)).filter(S=>S?.product_id);if(p.length===0)return;d.add(x.recommendation_request_id);let _=[...m.get(x.recommendation_request_id)?.values()??[]].sort((S,P)=>S.position-P.position);e.recommendationsViewed({...x,products:p,viewable:_})},w=z({selector:`${n} ${r}`,root:l,visibleRatio:i,dwellMs:c,onViewable(h){let x=b(h);if(!x)return!0;let p=g(o,x);if(!p)return!0;let _=p.recommendation_request_id,S=g(s,h,A(x).indexOf(h));if(S?.product_id){m.has(_)||m.set(_,new Map);let P=m.get(_);P.has(S.position)||P.set(S.position,{position:S.position,product_id:S.product_id,viewable_at:new Date().toISOString()})}return!d.has(_)&&!f.has(_)&&f.set(_,setTimeout(()=>k(x,p),0)),!0}}),L=h=>{let x=h.target;if(!x||typeof x.closest!="function")return;let p=x.closest(r);if(!p)return;let _=x.closest("[data-hp-reco-interaction]"),S=x.closest("a[href]"),P=S&&(p.contains(S)||S.contains(p));if(!(_&&p.contains(_))&&!P)return;let M=b(p);if(!M)return;let B=g(o,M);if(!B)return;let y=g(s,p,A(M).indexOf(p));if(!y?.product_id)return;let V=B.recommendation_request_id;d.has(V)||(clearTimeout(f.get(V)),k(M,B)),e.productClicked({recommendation_request_id:B.recommendation_request_id,interaction:T(_??p,"hpRecoInteraction")??"navigate",products:[y]})};return document.addEventListener("click",L,!0),()=>{document.removeEventListener("click",L,!0);for(let h of f.values())clearTimeout(h);f.clear(),w()}}var ne=new Set(["home","list","product","search","basket","checkout","order_confirmation","login","register","account","info","blog","other"]),sr=new Set(["products"]),Lt=e=>e==null;function He(e){if(Array.isArray(e))return e.map(He);if(e===null||typeof e!="object")return e;let t={};for(let[n,r]of Object.entries(e))Lt(r)||(t[n]=He(r));return t}function Ke(e){if(!e)return e;let t=He(e);for(let n of sr)n in e&&Lt(e[n])&&(t[n]=e[n]);return t}function Ot(){try{let e=performance.getEntriesByType("navigation")[0];return e?["navigate","reload","back_forward","prerender"].includes(e.type)?e.type:"navigate":null}catch{return null}}var ir=new Set(["","unknown","undefined","null","none","nil","nan","anonymous","guest","0","-1"]);function q(e){if(e==null)return null;let t=String(e).trim();return ir.has(t.toLowerCase())?null:t}function re(){return typeof crypto<"u"&&crypto.randomUUID?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,e=>{let t=Math.random()*16|0;return(e==="x"?t:t&3|8).toString(16)})}function R(e){if(typeof document>"u")return null;try{let t=document.cookie.split(";").map(n=>n.trim()).find(n=>n.startsWith(`${e}=`));return t&&decodeURIComponent(t.slice(e.length+1))||null}catch{return null}}var ar={gclid:"gclid",fbclid:"fbclid",ttclid:"ttclid"},cr=/[a-z0-9._%+-]+@[a-z0-9-]+(\.[a-z0-9-]+)*\.[a-z]{2,}/i,ur=/\d(?:[ -]?\d){12,}/,lr=new Set(["gclid","fbclid","ttclid","msclkid"]);function Tt(e,t=""){if(typeof e!="string"||e==="")return!1;let n=[e];try{let o=decodeURIComponent(e.replace(/\+/g," "));o!==e&&n.push(o)}catch{}let r=!lr.has(String(t).toLowerCase());return n.some(o=>cr.test(o)||r&&ur.test(o))}function dr(e){if(!e)return null;let t=new URLSearchParams(e),n={},r={utm_source:"source",utm_medium:"medium",utm_campaign:"name",utm_term:"term",utm_content:"content"};for(let[o,s]of[...Object.entries(r),...Object.entries(ar)]){let i=t.get(o);i&&!Tt(i,o)&&(n[s]=i)}return Object.keys(n).length?n:null}var fr=["email","e-mail","password","pass","pwd","token","auth","code","uid","customer_id","postcode","zip","phone","applyCode"],pr=["email","password","passwd","pwd","token","secret","auth","session","phone","postcode","zip","address","birth","card","cvv","iban","ssn","customerid","userid"],mr=["tel","uid","dob"],hr=["utm_source","utm_medium","utm_campaign","utm_term","utm_content","gclid","fbclid","ttclid","msclkid"],gr=new Set(fr.map(e=>e.toLowerCase()));function yr(e){let t=String(e).toLowerCase();if(gr.has(t))return!0;let n=t.replace(/[-_.]/g,"");return pr.some(o=>n.includes(o))?!0:String(e).replace(/([a-z0-9])([A-Z])/g,"$1 $2").toLowerCase().split(/[^a-z0-9]+/).some(o=>mr.includes(o))}function Ge(e){return(e??[]).map(t=>{try{return new RegExp(`^${t}$`,"i")}catch{return{test:n=>n.toLowerCase()===String(t).toLowerCase()}}})}function Rt(e,t={}){if(!e)return null;try{let n=new URLSearchParams(e),r=t.queryParamAllowlist?Ge([...t.queryParamAllowlist,...hr]):null,o=Ge(t.queryParamBlocklist),s=Ge(t.sensitiveParamAllowlist);for(let c of new Set(n.keys()))(yr(c)&&!s.some(d=>d.test(c))||n.getAll(c).some(d=>Tt(d,c))||(r?!r.some(d=>d.test(c)):o.some(d=>d.test(c))))&&n.delete(c);let i=n.toString();return i?`?${i}`:""}catch{return""}}function wr(e,t,n={}){if(!e)return null;try{let r=new URL(e);return t&&r.origin===t?r.origin+r.pathname+(Rt(r.search,n)??""):r.origin+r.pathname}catch{return null}}function ze(e){if(typeof e!="string")return null;let t=e.trim().toUpperCase();return/^[A-Z]{3}$/.test(t)?t:null}function Nt(e){if(typeof e!="string")return null;let t=e.trim().toUpperCase();return/^[A-Z]{2}$/.test(t)?t:null}function Sr(e){if(typeof e!="string")return null;let t=e.trim().replace(/_/g,"-");if(!/^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/.test(t))return null;try{return Intl.getCanonicalLocales(t)[0]}catch{return null}}function $(e,t){if(typeof e=="function")try{let n=e();if(n!=null)return n}catch{}return t}function _r(e){return{property_id:String(e.propertyId),channel:e.channel??null,market:e.market??null,environment:e.environment??null}}function br(e){let t=typeof window<"u"?window.location:null,n=t?Rt(t.search,e):null;return{url:t?t.origin+t.pathname+(n??""):e.pageUrl??null,path:t?t.pathname:e.pagePath??null,search:n??null,referrer:typeof document<"u"?wr(document.referrer,t?.origin,e):null,title:typeof document<"u"&&document.title||null,type:e.pageType??null}}function Dt(e,t={}){let n=typeof window<"u"?window.location:null,r={library:{name:e.libraryName,version:e.libraryVersion},spec:{version:"1.8"},source:_r(e),session_id:$(e.getSessionId,null),render_id:$(e.getRenderId,null),identity_trust:q($(e.getAnonymousId,null))?"device":"none",page:br(e),campaign:n?dr(n.search):null,device:{user_agent:typeof navigator<"u"?navigator.userAgent:null,type:null,viewport_width:typeof window<"u"?window.innerWidth:null,viewport_height:typeof window<"u"?window.innerHeight:null},currency:ze($(e.getCurrency,e.currency)),locale:Sr($(e.getLocale,e.locale)),timezone:(()=>{try{return Intl.DateTimeFormat().resolvedOptions().timeZone}catch{return null}})(),country:Nt(e.country),ship_to_country:Nt($(e.getShipToCountry,e.shipToCountry)),ip:null,bot:null,consent:e.getConsent?.()??null,experiments:$(e.getExperiments,null),audiences:$(e.getAudiences,null),decisions:$(e.getDecisions,null),recommendations:$(e.getRecommendations,null),extensions:$(e.getExtensions,null)};return Mt(r,t)}function Mt(e,t){if(!t)return e;let n={...e};for(let[r,o]of Object.entries(t))o&&typeof o=="object"&&!Array.isArray(o)&&e[r]&&typeof e[r]=="object"&&!Array.isArray(e[r])?n[r]=Mt(e[r],o):o!==void 0&&(n[r]=o);return n}var Z=Object.freeze({consent:"thg_hp_consent",retry:"thg_hp_retry",sessionSignals:"thg_hp_session_signals",identified:"thg_hp_identified",previousPage:"thg_hp_prev_page",decisions:"thg_hp_decisions",settings:"thg_hp_settings",held:"thg_hp_held",carried:"thg_hp_carried",recommendationContext:"thg_hp_recommendation_context",anonymousId:"thg_hp_anonymous_id",session:"thg_hp_session",preview:"thg_hp_preview"}),xr={retry:"cdp.retry",sessionSignals:"cdp_session_signals",identified:"cdp_identified",previousPage:"cdp_prev_page_message_id"},We={...Z},$t=!1;function _e(e){let t={...Z};for(let[n,r]of Object.entries(e??{})){if(!(n in Z))throw new Error(`storageNames: unknown kind "${n}"`);if(typeof r!="string"||!/^[\w.-]+$/.test(r))throw new Error(`storageNames.${n} must be a non-empty cookie-safe name`);t[n]=r}We=t}function N(e){return We[e]}var Ir="_",Ut=e=>e==null||e===""?Ir:String(e);function j(e,t,n){let r=`${e}:${Ut(t)}`;return n==null||n===""?r:`${r}:${n}`}function Vt(e,t){return`${e}:${Ut(t)}:`}function be(){if(!$t){$t=!0;for(let e of[()=>localStorage,()=>sessionStorage]){let t;try{if(t=e(),!t)continue;for(let n of Object.keys(t))for(let[r,o]of Object.entries(xr)){if(n!==o&&!n.startsWith(`${o}:`))continue;let s=We[r]+n.slice(o.length);t.getItem(s)===null&&t.setItem(s,t.getItem(n)),t.removeItem(n)}}catch{}}}}var Xe=1800*1e3,xe=()=>{try{return typeof sessionStorage>"u"?null:sessionStorage}catch{return null}},Je=()=>{try{return typeof localStorage>"u"?null:localStorage}catch{return null}};function jt(e,t=()=>Date.now()){return{read(){try{let n=xe()?.getItem(e),r=n?JSON.parse(n):null;return!r||t()-r.savedAt>Xe?(r&&xe()?.removeItem(e),{items:[],anonymousId:null}):{items:Array.isArray(r.items)?r.items:[],anonymousId:typeof r.anonymousId=="string"?r.anonymousId:null}}catch{return{items:[],anonymousId:null}}},write(n,r=null){try{n.length===0?xe()?.removeItem(e):xe()?.setItem(e,JSON.stringify({savedAt:t(),items:n,anonymousId:r}))}catch{}}}}var Ar=400*24*60*60;function W(e,t,n,r){if(typeof document>"u")return!1;try{let o=typeof location<"u"&&location.protocol==="https:"?"; Secure":"",s=r?`; Domain=${r}`:"",i=n===null?"":`; Max-Age=${n}`;return document.cookie=`${e}=${encodeURIComponent(t)}; Path=/${i}; SameSite=Lax`+s+o,!0}catch{return!1}}function vr({name:e,domain:t=null,legacyKey:n,read:r,mint:o,mayStore:s,carried:i=null}){let c=null;try{c=Je()?.getItem(n)||null}catch{c=null}let l=r(e)??c??i,d=!1,m=()=>{if(!(d||l===null||!s())&&(d=W(e,l,Ar,t),!(!d||c===null)))try{Je()?.removeItem(n),c=null}catch{}};return{get(){return l??(l=o()),m(),l},peek:()=>l,persist:m,forget(){W(e,"",0,t),d=!1;try{Je()?.removeItem(n)}catch{}c=null}}}function qt(e,{carried:t,mayStore:n}){let r=e.anonymousIdSource;if(r?.source==="site_cookie"&&r.cookie){let i=r.cookie;return e.getAnonymousId=()=>R(i),null}if(r?.source!=="hp_cookie")return null;let o=N("anonymousId"),s=vr({name:o,domain:e.consentCookieDomain??null,legacyKey:j(o,e.propertyId),read:R,mint:re,mayStore:n,carried:t});return e.getAnonymousId=()=>s.get(),s}var kr=200;function Bt(e,t,n=()=>Date.now()){let r=()=>j(N("carried"),e),o=!1;function s(){try{let i=JSON.parse(sessionStorage.getItem(r())??"null");return!i||!(n()-i.savedAt<=Xe)?[]:Array.isArray(i.calls)?i.calls:[]}catch{return[]}}return{take(){let i=s();try{sessionStorage.removeItem(r())}catch{}return i},save(i){try{return o||!t()?!1:(sessionStorage.setItem(r(),JSON.stringify({savedAt:n(),calls:[...s(),...i].slice(-kr)})),!0)}catch{return!1}},close(){o=!0}}}var ks=Z.consent,Ie=Object.freeze({visitor:"visitor",host:"host"}),Ft=()=>({basis:Ie.host}),Er="hp-consent",Q=["analytics","marketing","personalisation","ad_user_data","ad_personalization"],Ht={analytics:"a",marketing:"m",personalisation:"p",ad_user_data:"aud",ad_personalization:"adp"},K=()=>typeof window<"u",Kt=()=>K()&&typeof window.cookieStore?.addEventListener=="function",Cr=e=>e===!0?!0:e===!1?!1:null;function oe(e){if(!e||typeof e!="object")return null;let t={};for(let n of Q)t[n]=Cr(e[n]);return t}function Ze(e,t){return!e||!t?e===t:Q.every(n=>e[n]===t[n])}function Qe(e){if(!e)return"";let t=[];for(let n of Q){let r=e[n];(r===!0||r===!1)&&t.push(`${Ht[n]}=${r?1:0}`)}return t.join("&")}function Gt(e){return{[Er]:Qe(oe(e))}}function me(e){let t=oe({});if(e==null)return t;let n=String(e).trim();if(n.length>=2&&n.startsWith('"')&&n.endsWith('"')&&(n=n.slice(1,-1)),n.includes("%"))try{n=decodeURIComponent(n.replace(/\+/g," "))}catch{return t}let r=Object.fromEntries(Object.entries(Ht).map(([s,i])=>[i,s])),o=new Set;for(let s of n.split("&")){let i=s.indexOf("=");if(i<=0)continue;let c=r[s.slice(0,i).trim()],l={1:!0,0:!1}[s.slice(i+1).trim()]??null;!c||l===null||o.has(c)||(t[c]===null?t[c]=l:t[c]!==l&&(t[c]=null,o.add(c)))}return t}function et(e,t){if(!t)return e;let n=r=>{if(!r)return r;let o={...r};for(let s of Q)s!=="analytics"&&t[s]!==!0&&(o[s]=null);return o};return{name:e.name,basis:e.basis,read:()=>n(e.read()),subscribe:r=>e.subscribe(o=>r(n(o))),get observesChanges(){return e.observesChanges}}}function tt(e,t={}){return{...e,ad_user_data:t.ad_user_data??e.marketing,ad_personalization:t.ad_personalization??e.marketing}}function Ae(e,t){let n=!0;return{notify:()=>{if(n)try{let o=e();o&&t(o)}catch{}},stop:()=>n=!1}}var Pr={analytics:["analytics","measure_content_performance","audience_measurement"],marketing:["marketing","targeting","advertising","select_personalized_ads","create_ads_profile"],personalisation:["personalisation","personalization","select_personalized_content","create_content_profile"]};function Lr(e,t){if(!t?.length)return null;let n=!1;for(let[r,o]of Object.entries(e)){let s=String(o?.id??r).toLowerCase();if(t.some(c=>{let l=String(c).toLowerCase();return s===l||s.startsWith(`${l}-`)})&&(n=!0,o?.enabled===!0))return!0}return n?!1:null}function zt(e={}){let t={...Pr,...e.purposes};function n(){let r=K()?window.Didomi:void 0;if(!r?.getCurrentUserStatus||r.shouldUserStatusBeCollected?.()!==!1||r.isUserStatusPartial?.()===!0)return null;let o=r.getCurrentUserStatus()?.purposes;if(!o||typeof o!="object")return null;let s=i=>Lr(o,t[i]);return tt({analytics:s("analytics"),marketing:s("marketing"),personalisation:s("personalisation")},{ad_user_data:s("ad_user_data"),ad_personalization:s("ad_personalization")})}return{name:"didomi",read:n,observesChanges:!0,subscribe(r){if(!K())return()=>{};let o=Ae(n,r),s={event:"consent.changed",listener:o.notify};return window.didomiEventListeners=window.didomiEventListeners||[],window.didomiEventListeners.push(s),window.didomiOnReady=window.didomiOnReady||[],window.didomiOnReady.push(i=>{o.notify(),(i??window.Didomi)?.on?.("consent.changed",o.notify)}),window.Didomi?.on?.("consent.changed",o.notify),o.stop}}}function Ye(e){let t={};if(!e)return t;for(let n of String(e).split("&")){let r=n.indexOf("=");if(r<=0)continue;let o=n.slice(r+1).replace(/\+/g," ");try{o=decodeURIComponent(o)}catch{}t[n.slice(0,r)]=o}return t}function Wt(e={}){let t={analytics:"C0002",personalisation:"C0003",marketing:"C0004",...e.groups},n=e.impliedConsent===!0;function r(){let i=K()?window.OnetrustActiveGroups:void 0;if(typeof i=="string"&&i.length){let d=new Set(i.split(",").map(m=>m.trim()).filter(Boolean));return m=>d.has(m)}let c=Ye(R("OptanonConsent"));if(!c.groups)return null;let l={};for(let d of c.groups.split(",")){let[m,f]=d.split(":");m?.trim()&&(l[m.trim()]=f==="1"?!0:f==="0"?!1:null)}return d=>l[d]??null}function o(){if(R("OptanonAlertBoxClosed"))return!0;try{if(K()&&window.OneTrust?.IsAlertBoxClosed?.()===!0)return!0}catch{}return Number.parseInt(Ye(R("OptanonConsent")).interactionCount,10)>0?!0:n&&K()&&(!!window.OneTrust||typeof window.OnetrustActiveGroups=="string")}function s(){if(!o())return null;let i=r();if(!i)return null;let c=f=>t[f]?i(t[f]):null,l=tt({analytics:c("analytics"),marketing:c("marketing"),personalisation:c("personalisation")},{ad_user_data:c("ad_user_data"),ad_personalization:c("ad_personalization")}),d=Ye(R("OptanonConsent"));return(typeof navigator<"u"&&navigator.globalPrivacyControl===!0||d.isGpcEnabled==="1")&&(l.marketing=!1,l.ad_user_data=!1,l.ad_personalization=!1),l}return{name:"onetrust",read:s,observesChanges:!0,subscribe(i){if(!K())return()=>{};let c=Ae(s,i),l=!1,d=()=>{l||!window.OneTrust?.OnConsentChanged||(l=!0,window.OneTrust.OnConsentChanged(c.notify))};window.addEventListener("OneTrustGroupsUpdated",c.notify);let m=window.OptanonWrapper;return window.OptanonWrapper=function(...g){try{typeof m=="function"&&m.apply(this,g)}finally{d(),c.notify()}},d(),()=>{c.stop(),window.removeEventListener("OneTrustGroupsUpdated",c.notify)}}}}function Or(e){let t=String(e).split(",").map(n=>n.trim()).filter(Boolean);return t.length?new Set(t):null}function Jt(e){let{name:t,categories:n,parse:r=Or,refusedValue:o,source:s="native",changeEvent:i="thg_hp_consent_change"}=e??{};if(typeof t!="string"||t==="")throw new Error("cookieConsent: `name` (the cookie your banner writes) is required");if(!n||typeof n.analytics!="string")throw new Error("cookieConsent: `categories.analytics` (the category id that grants analytics) is required");function c(){let l=R(t);if(l==null)return null;let d;if(o!==void 0&&l===o)d=new Set;else try{d=r(l)}catch{d=null}if(!(d instanceof Set))return null;let m=f=>n[f]?d.has(n[f]):null;return tt({analytics:m("analytics"),marketing:m("marketing"),personalisation:m("personalisation")},{ad_user_data:m("ad_user_data"),ad_personalization:m("ad_personalization")})}return{name:s,read:c,get observesChanges(){return Kt()},subscribe(l){if(!K())return()=>{};let d=Ae(c,l),m=f=>{[...f.changed??[],...f.deleted??[]].some(b=>b?.name===t)&&d.notify()};return window.addEventListener(i,d.notify),window.cookieStore?.addEventListener?.("change",m),()=>{d.stop(),window.removeEventListener(i,d.notify),window.cookieStore?.removeEventListener?.("change",m)}}}}function Xt(e={}){let t=e.name??N("consent"),n=()=>{let r=R(t);if(r==null)return null;let o=me(r);return o.analytics==null?null:o};return{name:"forwarded",read:n,get observesChanges(){return Kt()},subscribe(r){if(!K())return()=>{};let o=Ae(n,r),s=i=>{[...i.changed??[],...i.deleted??[]].some(l=>l?.name===t)&&o.notify()};return window.cookieStore?.addEventListener?.("change",s),()=>{o.stop(),window.cookieStore?.removeEventListener?.("change",s)}}}}function Yt(e){let t=oe(e);return{name:"static",basis:Ie.host,read:()=>t?{...t}:null,observesChanges:!0,subscribe:()=>()=>{}}}function Zt(e){if(e==null)return null;let t=String(e).split("/").map(n=>n.trim()).filter(Boolean).join("/");return t===""?null:t}function Nr(e){let t=e.item_category;if(t!=null&&String(t).includes("/"))return Zt(t);let n=[t,e.item_category2,e.item_category3,e.item_category4,e.item_category5].map(r=>r==null?"":String(r).trim()).filter(Boolean);return Zt(n.join("/"))}var Tr=e=>{if(e==null||typeof e=="object")return null;let t=String(e).trim();return t===""?null:t};function Rr(...e){for(let t of e){if(typeof t=="number"&&Number.isFinite(t))return String(t);if(typeof t=="string"&&t.trim()!=="")return t.trim()}return null}var ve=e=>Rr(e.item_group_id,e.item_id);function Dr(e,t=ve){if(!e)return null;let n=c=>c==null||c===""||Number.isNaN(Number(c))?null:Number(c),r=c=>{let l=n(c);return l==null?null:Math.round(l*1e6)/1e6},o=c=>{let l=r(c);return l!=null&&l>=0?l:null},s=c=>{let l=n(c);return Number.isInteger(l)&&l>=0?l:null},i=Tr;return{product_id:i(t(e)),sku:i(e.item_id),name:i(e.item_name),brand:i(e.item_brand),variant:i(e.item_variant)??i(e.variant),category:Nr(e),price:r(e.price),discount:o(e.discount),quantity:s(e.quantity),coupon:i(e.coupon),position:s(e.index)!=null?s(e.index)+1:null,url:i(e.url)??i(e.item_url)??null,image_url:i(e.image_url)??i(e.image_link)??null,displayed_sku:i(e.displayed_sku)??i(e.displayed_variant_sku)}}function nt(e,t=ve){return(e??[]).map(n=>Dr(n,t)).filter(n=>n?.product_id)}var Mr=1e3,$r={shipping:2,payment:3},Ur=new Set(["password","google","apple","facebook","magic_link","sso","passkey","other"]),D=e=>{if(e==null||typeof e=="object")return null;let t=String(e).trim();return t===""?null:t},Vr=e=>{if(e==null||e===""||typeof e=="object")return null;let t=Number(e);return Number.isFinite(t)&&t>=0?t:null},ke=e=>e!==null&&typeof e=="object";function jr(e){return Array.isArray(e.items)?e.items.filter(ke):null}function Qt(e){let t=D(e)?.toLowerCase().replace(/[\s-]+/g,"_");return t?Ur.has(t)?t:t==="email"||t==="email_password"?"password":"other":null}function qr({ecommerce:e,items:t}){let n=s=>({promotion_id:D(s.promotion_id),name:D(s.promotion_name),creative:D(s.creative_name),position:D(s.creative_slot)}),r=new Set,o=[];for(let s of[n(e),...t.map(n)])!s.promotion_id||r.has(s.promotion_id)||(r.add(s.promotion_id),o.push(s));return o}var en=({ecommerce:e,items:t})=>D(e.item_list_id)??D(e.item_list_name)??D(t[0]?.item_list_id)??D(t[0]?.item_list_name),ee=(e,t)=>t.products?.length?{event:e,properties:t}:null,Br=new Set(["product","products","item","items"]);function Fr({ecommerce:e}){let t=D(e.item_id),n=D(e.content_type)?.toLowerCase();return!t||n&&!Br.has(n)?null:{event:"Product Shared",properties:{share_via:D(e.method),products:[{product_id:t}]}}}function Hr(e){let t=o=>ee("Product List Viewed",{list_id:en(o),products:o.products}),n=o=>{let s=D(o.ecommerce.search_term);return s?{event:"Products Searched",properties:{query:s,...o.products.length?{products:o.products}:{}}}:null},r=o=>s=>s.promotions.map(i=>({event:o,properties:i}));return{view_item:o=>ee("Product Viewed",{currency:o.currency,value:o.value,products:o.products.slice(0,1)}),view_item_list:t,view_search_results:o=>n(o)??t(o),search:n,select_item:o=>ee("Product Clicked",{list_id:en(o),products:o.products.slice(0,1)}),add_to_cart:o=>ee("Product Added",{cart_id:D(o.ecommerce.cart_id),products:o.products}),remove_from_cart:o=>ee("Product Removed",{cart_id:D(o.ecommerce.cart_id),products:o.products}),view_cart:o=>({event:"Cart Viewed",properties:{cart_id:D(o.ecommerce.cart_id),value:o.value,currency:o.currency,coupon:D(o.ecommerce.coupon),...o.itemsListed?{products:o.products}:{}}}),begin_checkout:o=>ee("Checkout Started",{value:o.value,currency:o.currency,coupon:D(o.ecommerce.coupon),products:o.products}),add_shipping_info:o=>({event:"Checkout Step Completed",properties:{step:e.shipping,step_name:"shipping",shipping_method:D(o.ecommerce.shipping_tier)}}),add_payment_info:o=>({event:"Payment Info Entered",properties:{step:e.payment,step_name:"payment",payment_method:D(o.ecommerce.payment_type)}}),view_promotion:r("Promotion Viewed"),select_promotion:r("Promotion Clicked"),add_to_wishlist:o=>ee("Product Added to Wishlist",{products:o.products}),share:Fr,login:o=>({event:"Signed In",properties:{method:Qt(o.ecommerce.method)}}),sign_up:o=>({event:"Signed Up",properties:{method:Qt(o.ecommerce.method)}})}}var Kr=e=>e.event;function Gr(e,t){if(!ke(e))return null;if(typeof e.length=="number"&&e[0]==="event"&&typeof e[1]=="string"){let r=ke(e[2])?e[2]:{};return{name:e[1],entry:r,ecommerce:r}}let n=D(t(e));return n?{name:n,entry:e,ecommerce:ke(e.ecommerce)?e.ecommerce:e}:null}function rt(e){return Array.isArray(e)?`[${e.map(rt).join(",")}]`:e&&typeof e=="object"?`{${Object.keys(e).sort().map(t=>`${JSON.stringify(t)}:${rt(e[t])}`).join(",")}}`:JSON.stringify(e??null)}function tn(e,t={}){if(!e||typeof e.track!="function")throw new Error("connectDataLayer: analytics must have a track(event, properties, options) method");let n=t.dataLayer??(typeof window<"u"?window.dataLayer=window.dataLayer||[]:null);if(!Array.isArray(n))return()=>{};let r=Hr({...$r,...t.steps}),o=t.map??{},s=t.eventName??Kr,i=t.productId??ve,c=new Set(["purchase","refund",...t.ignore??[]]),l=t.dedupeMs??Mr,d=new Map,m=!0;function f(p,_,S){if(l<=0)return!1;let P=`${p}|${S??""}|${rt(_)}`,M=Date.now();for(let[B,y]of d)M-y>l&&d.delete(B);return d.has(P)?!0:(d.set(P,M),!1)}let g=null,b=new Set;function A(p,_){if(p!=="Product Viewed")return!1;let S=typeof location>"u"?"":location.pathname;S!==g&&(g=S,b.clear());let P=_.products?.[0]?.product_id;return P?b.has(P)?!0:(b.add(P),!1):!1}function k({event:p,properties:_={}},S){if(!p||f(p,_,S)||A(p,_))return;let P=S?{context:{currency:S}}:void 0;if(p==="Signed Out"&&typeof e.signedOut=="function"){e.signedOut(P);return}e.track(p,_,P)}function w(p){let _=t.transform?t.transform(p):p,S=Gr(_,s);if(!S||c.has(S.name))return;let P=Object.hasOwn(o,S.name),M=P?o[S.name]:r[S.name];if(typeof M!="function")return;let B=jr(S.ecommerce),y=B??[],V={...S,items:y,itemsListed:B!==null,products:nt(y,i),currency:ze(S.ecommerce.currency),value:Vr(S.ecommerce.value)};V.promotions=qr(V);let le=P?M(_,{...V,toProducts:F=>nt(F,i)}):M(V);for(let F of[le].flat())F&&k(F,V.currency)}function L(p){if(m)try{w(p)}catch(_){try{t.onError?.(_,p)}catch{}}}for(let p of n.slice())L(p);let h=n.push;function x(...p){let _=h.apply(this,p);for(let S of p)L(S);return _}return n.push=x,()=>{m=!1,n.push===x&&(n.push=h)}}var zr=4320*60*60,Wr=100,Jr=1e3,Xr=2e3,nn=500,Ee=e=>(e?.unref?.(),e);function Yr(e,t){if(t?.analytics===!0&&e.analytics===!1)return"withdrawn";let n=Q.map(r=>e[r]).filter(r=>r!==null);return n.length&&n.every(Boolean)?"accept_all":n.length&&n.every(r=>!r)?"reject_all":"custom"}function Zr(e,t,n=N("consent")){if(!(typeof document>"u"))try{let r=typeof location<"u"&&location.protocol==="https:"?"; Secure":"",o=t?`; Domain=${t}`:"";document.cookie=`${n}=${Qe(e)}; Path=/; Max-Age=${zr}; SameSite=Lax`+o+r}catch{}}function Qr(e){let t=me(R(e));return Q.some(n=>t[n]!==null)?t:null}function rn(e){let{adapter:t,timeoutMs:n=5e3,cookieDomain:r=null,cookieName:o=N("consent"),release:s,announce:i}=e,c=e.discard??(()=>{}),l="pending",d=null,m=null,f=null,g=null,b=!1,A=null,k=[],w=a=>{l=a,e.onStatus?.(a)};function L(){try{return oe(t.read())}catch{return null}}function h(){m&&clearInterval(m),f&&clearTimeout(f),m=null,f=null}let x=()=>typeof window<"u"&&typeof document<"u",p=()=>{try{return typeof t.subscribe=="function"&&t.observesChanges===!0}catch{return!1}};function _(){g&&clearInterval(g),g=null}function S(){b||g||d===null||p()||x()&&document.visibilityState==="hidden"||(g=Ee(setInterval(()=>V(L()),Xr)))}let P=()=>l==="pending"||l==="unknown",M=e.store??null,B=()=>{let a=k.splice(0);M?.write([]);for(let u of a)s(u)},y=()=>{let a=k.splice(0);M?.write([]);for(let u of a)c(u)};function V(a){let u=oe(a);if(!u||u.analytics===null)return;let v=d===null,C=v?Qr(o):d;if(!v&&Ze(C,u))return;h(),d=u,S(),Zr(u,r,o);let E=!Ze(C,u);if(u.analytics){w("granted"),B(),E&&i({action:Yr(u,C),state:u,withdrawal:!1});return}y(),C?.analytics===!0&&i({action:"withdrawn",state:u,withdrawal:!0}),w("denied")}function le(){f=null,l==="pending"&&(w("unknown"),m&&clearInterval(m),m=Ee(setInterval(()=>V(L()),Jr)))}function F(a){P()&&(M||a?.type!=="visibilitychange"&&!a?.persisted&&y())}let de=a=>{document.visibilityState==="hidden"?(_(),F(a)):S()},ge=a=>{_(),F(a)},fe=()=>S();return{get status(){return l},get state(){return d?{...d}:null},stamp(){return d?{...d,source:t.name??null,basis:t.basis??Ie.visitor}:null},admits(){return l==="granted"},hold(a){return!P()||k.length>=nn?(c(a),!1):(k.push(a),M?.write(k),!0)},onHidden:F,start(){b=!1;try{A=t.subscribe?.(a=>V(a))??null}catch(a){e.debug&&console.warn("[hp] consent adapter subscribe failed",a)}x()&&(window.addEventListener("pagehide",ge),window.addEventListener("pageshow",fe),document.addEventListener("visibilitychange",de)),M&&k.length===0&&k.push(...M.read().slice(-nn)),V(L()),!d&&(m=Ee(setInterval(()=>V(L()),Wr)),f=Ee(setTimeout(le,n)))},stop(){b=!0,h(),_(),typeof A=="function"&&A(),x()&&(window.removeEventListener("pagehide",ge),window.removeEventListener("pageshow",fe),document.removeEventListener("visibilitychange",de))}}}var eo=["recommendation","search_rank","banner","offer","layout","copy","price","other"],Ce=e=>typeof e=="string"&&e!=="";function on(e){return(Array.isArray(e?.decisions)?e.decisions:[]).filter(t=>Ce(t?.slot)&&Ce(t.decision_id)&&t.holdout!==!0).map(({decision_id:t,slot:n,arm:r,kind:o})=>({decision_id:t,slot:n,...Ce(r)&&{arm:r},...eo.includes(o)&&{kind:o}}))}function sn({key:e,mayStore:t}){let n,r=()=>{if(n)return n;try{let s=JSON.parse(sessionStorage.getItem(e));n=Array.isArray(s)?s.filter(i=>Ce(i?.slot)):[]}catch{n=[]}return n},o=()=>{try{n?.length&&t()&&sessionStorage.setItem(e,JSON.stringify(n))}catch{}};return{persist:o,current:()=>r().length?n.map(s=>({...s})):null,remember(s){if(!s.length)return;let i=new Set(s.map(c=>c.slot));n=[...r().filter(c=>!i.has(c.slot)),...s],o()},forget(){n=[];try{sessionStorage.removeItem(e)}catch{}}}}function to(e){let t=2166136261,n=String(e);for(let r=0;r<n.length;r+=1)t^=n.charCodeAt(r),t=t+((t<<1)+(t<<4)+(t<<7)+(t<<8)+(t<<24))>>>0;return t^=t>>>16,t=Math.imul(t,2246822507)>>>0,t^=t>>>13,t=Math.imul(t,3266489909)>>>0,t^=t>>>16,t>>>0}function an(e,t,n=1e4){return to(`${t}\0${e}`)%n}function ot(e){let t=o=>Math.min(Math.max(Number(o)||0,0),1),n=t(e.share),r=1-n;return{holdout_share:n,personalised_share:e.ramp==null?r:Math.min(t(e.ramp),r)}}function st(e,t){if(!e||typeof e.salt!="string")return{holdout:!1,bucket:null,group:"personalised"};let n=e.ramp!=null;if(t==null||t==="")return{holdout:!1,bucket:null,group:n?"excluded":"personalised"};let{holdout_share:r,personalised_share:o}=ot(e),s=an(t,e.salt),i=l=>Math.round(l*1e4),c=r>0&&s<i(r)?"holdout":!n||s<i(r+o)?"personalised":"excluded";return{holdout:c==="holdout",bucket:s,group:c}}function cn(e,t){return typeof e!="string"||e===""||t==null||t===""?null:an(t,`bandit-vs-default:${e}`)<1e4/2?"default":"bandit"}var no="hp_preview",ro=256;function un(e,t=null){if(typeof location>"u")return R(e);let n=null;try{n=new URLSearchParams(location.search).get(no)}catch{n=null}if(n===null)return R(e);let r=n.trim();return r===""||r.length>ro?(W(e,"",0,t),null):(W(e,r,null,t),r)}var oo=40,so=128,io=5,ao=60,dn=e=>typeof e=="string"&&e!==""&&e.length<=so&&!e.includes(","),ln=e=>Array.isArray(e)?e.filter(dn).slice(0,oo):[],co=e=>Array.isArray(e)?e.filter(t=>typeof t=="string").map(t=>t.replaceAll(","," ").trim()).filter(t=>t!=="").slice(0,io):[],uo=e=>Object.entries(e&&typeof e=="object"?e:{}).filter(([t,n])=>dn(t)&&Number.isFinite(n)&&n>=1).sort(([,t],[,n])=>n-t).slice(0,ao).map(([t,n])=>`${t}:${Math.floor(n)}`);function it(e){let t={},n={viewed:ln(e?.recentProducts),basket:ln(e?.basketProducts),q:co(e?.recentSearches),impressed:uo(e?.impressions)};for(let[r,o]of Object.entries(n))o.length>0&&(t[r]=o.join(","));return t}var lo=1800*1e3;function fn(e,t=Date.now()){try{localStorage.setItem(N("recommendationContext"),JSON.stringify({context:e,until:t+lo}))}catch{}}function pn(){try{localStorage.removeItem(N("recommendationContext"))}catch{}}function at(e){return e.trim().toLowerCase().replace(/\s*\/\s*/g,"/").replace(/^\/+|\/+$/g,"")}var fo=2,po=12;function mo(e,t=fo){return e.split("/").slice(0,t).join("/")}function se(e,t){if(typeof t!="string"||!t.trim())return null;let n=mo(at(t));return n?(e.unshift(n),e.length=Math.min(e.length,po),n):null}var te=()=>N("sessionSignals"),ie=null,ct=()=>ie??te();function mn(){try{return ie?localStorage:sessionStorage}catch{return}}function hn(e,t){let n=t==null?null:String(t),r=e?j(te(),n,e):null;if(r===ie)return;let o=ie===null?ho():null;if(ie=r,!!ie)try{let s=ct(),i=Vt(te(),n);for(let c of Object.keys(localStorage))c.startsWith(i)&&c!==s&&localStorage.removeItem(c);o&&!localStorage.getItem(s)&&(localStorage.setItem(s,JSON.stringify(o)),sessionStorage.removeItem(te()))}catch{}}function ho(){try{let e=sessionStorage.getItem(te());if(!e)return null;let t=JSON.parse(e);return t&&(t.productViews>0||t.listViews>0||t.recentProducts?.length||t.recentBrowse?.length)?t:null}catch{return null}}function Pe(){return{productViews:0,listViews:0,cartAdds:0,searches:0,recentProducts:[],recentCategories:[],recentBrowse:[],basketProducts:[],recentSearches:[],impressions:{},browsingCategory:null,startedAt:0}}function ut(){let e=mn();if(!e)return Pe();try{let t=e.getItem(ct());return t?{...Pe(),...JSON.parse(t)}:{...Pe(),startedAt:Date.now()}}catch{return{...Pe(),startedAt:Date.now()}}}function gn(e){let t=mn();if(t)try{t.setItem(ct(),JSON.stringify(e))}catch{}}function yn(){return ut()}function wn(){for(let e of[()=>sessionStorage,()=>localStorage])try{let t=e();t.removeItem(te());for(let n of Object.keys(t))n.startsWith(`${te()}:`)&&t.removeItem(n)}catch{}}var Sn=20,_n=200,go=new Set(["Product Viewed","Product Clicked","Product Added","Product Added to Wishlist"]),yo=new Set(["Product Clicked","Product Added to Wishlist"]);function bn(e,t){let n=ut();n.startedAt||(n.startedAt=Date.now());let r=Array.isArray(t?.products)?t.products:[];switch(e){case"Product Viewed":n.productViews+=1,se(n.recentBrowse,n.browsingCategory);break;case"Product List Viewed":n.listViews+=1;{let i=t?.list_id;typeof i=="string"&&i.includes("/")&&!i.startsWith("cdp-")&&(n.browsingCategory=se(n.recentBrowse,i)??n.browsingCategory)}break;case"Product List Filtered":{let i=t?.change,c=typeof t?.list_id=="string"?t.list_id:"",l=typeof i?.value=="string"?i.value:"";l&&i?.action!=="removed"&&(n.browsingCategory=se(n.recentBrowse,`${c}/${l}`)??n.browsingCategory);break}case"Product Added":{n.cartAdds+=1,se(n.recentBrowse,n.browsingCategory);for(let i of r){let c=i.product_id;typeof c!="string"||!c||(n.basketProducts=[c,...n.basketProducts.filter(l=>l!==c)].slice(0,Sn))}break}case"Product Removed":{for(let i of r){let c=i.product_id;typeof c=="string"&&c&&(n.basketProducts=n.basketProducts.filter(l=>l!==c))}break}case"Products Searched":{n.searches+=1;let i=t?.query;if(typeof i=="string"&&i.trim()){let c=i.trim().toLowerCase().slice(0,64);n.recentSearches=[c,...n.recentSearches.filter(l=>l!==c)].slice(0,5)}break}case"Product Clicked":case"Product Added to Wishlist":break;default:return n}let o=go.has(e);if(o)for(let i of r)typeof i.product_id=="string"&&i.product_id&&delete n.impressions[i.product_id];else{for(let c of r){let l=c.product_id;typeof l!="string"||!l||(n.impressions[l]=(n.impressions[l]??0)+1)}let i=Object.keys(n.impressions);if(i.length>_n){let c={};for(let l of i.sort((d,m)=>n.impressions[m]-n.impressions[d]).slice(0,_n))c[l]=n.impressions[l];n.impressions=c}}let s=new Set;for(let i of r){let c=i.product_id;if(o&&typeof c=="string"&&c&&(n.recentProducts=[c,...n.recentProducts.filter(l=>l!==c)].slice(0,Sn)),!yo.has(e)&&typeof i.category=="string"&&i.category){let l=at(i.category);l&&!s.has(l)&&(s.add(l),se(n.recentCategories,l))}}return gn(n),n}var xn=1800*1e3;function In({name:e,domain:t=null,mayStore:n,now:r=()=>Date.now(),mint:o=re}){let s=null,i=0,c=()=>{s!==null&&n()&&W(e,s,xn/1e3,t)};return{get(){let l=r(),d=R(e);return d?s=d:(s===null||l-i>=xn)&&(s=o()),i=l,c(),s},persist:c,forget(){W(e,"",0,t),s=null}}}var G=e=>e!==null&&typeof e=="object"&&!Array.isArray(e),vn=e=>Array.isArray(e)&&e.every(t=>typeof t=="string"),lt=Object.freeze({enabled:!1});function kn(e,t){let n=e?.track;return G(n)?(Object.hasOwn(n,t)?n[t]:n.__default)?.enabled!==!1:!0}function wo(e,t){return`${ae(e)}/v1/projects/${encodeURIComponent(t)}/settings`}var So="https://hp.uat.thgingenuity.com";function ae(e){return e==null||e===""?So:En(e)}function En(e){if(typeof e!="string")return null;let t=e.trim().replace(/\/+$/,"");return/^https?:\/\/[^/\s]+$/i.test(t)?t:/^[a-z0-9.-]+(:\d+)?$/i.test(t)?`https://${t}`:null}function _o(e){if(!G(e))return null;let t=Object.fromEntries(Object.entries(e).filter(([,n])=>typeof n=="boolean"));return Object.keys(t).length?t:null}var An=e=>typeof e=="number"&&e>=0&&e<=1;function bo(e){if(!G(e))return null;let t=Object.fromEntries(Object.entries(e).filter(([,n])=>G(n)&&An(n.share)&&(n.ramp==null||An(n.ramp))&&typeof n.salt=="string"&&n.salt!=="").map(([n,r])=>[n,{share:r.share,ramp:r.ramp??null,salt:r.salt,...vn(r.readoutSlots)&&r.readoutSlots.length>0&&{readoutSlots:r.readoutSlots}}]));return Object.keys(t).length?t:null}var xo=/^[A-Za-z0-9!#$%&'*+.^_`|~-]{1,128}$/;function Io(e){return G(e)?e.source==="hp_cookie"?{source:"hp_cookie"}:e.source!=="site_cookie"?null:e.cookie==null?{source:"site_cookie",cookie:null}:typeof e.cookie=="string"&&xo.test(e.cookie)?{source:"site_cookie",cookie:e.cookie}:null:null}function dt(e){let t=G(e)?e.hp:null;return!G(t)||t.enabled!==!0?lt:{enabled:!0,propertyId:typeof t.propertyId=="string"&&t.propertyId!==""?t.propertyId:null,environment:typeof t.environment=="string"?t.environment:null,consentPurposes:_o(t.consent?.purposes),queryParamAllowlist:vn(t.queryParamAllowlist)?t.queryParamAllowlist:null,anonymousId:Io(t.anonymousId),apiHost:En(t.apiHost),holdouts:bo(t.holdouts),plan:G(e.plan)&&G(e.plan.track)?{track:e.plan.track}:null}}async function Cn(e,t,n=globalThis.fetch){try{let r=await n(wo(e,t),{credentials:"omit",headers:{accept:"application/json"}});return r.ok?dt(await r.json()):null}catch{return null}}var Le=e=>j(N("settings"),e);function Pn(e){try{let t=localStorage.getItem(Le(e));if(!t)return null;let n=JSON.parse(t);return n?.enabled===!0?n:null}catch{return null}}function Ln(e,t){try{t?.enabled===!0?localStorage.setItem(Le(e),JSON.stringify(t)):localStorage.removeItem(Le(e))}catch{}}function ft(e){try{localStorage.removeItem(Le(e))}catch{}}var Ao={flushAt:20,flushIntervalMs:1e4,maxQueue:500,maxRetryAgeMs:1440*60*1e3,maxRetries:5,maxPersistedBatches:20,maxPersistedBytes:1024*1024,storageKey:Z.retry,beaconSafeBytes:56*1024,maxBatchEvents:100,maxBatchBytes:500*1024},pt=()=>Date.now(),ce=e=>typeof TextEncoder<"u"?new TextEncoder().encode(e).length:Buffer.byteLength(e),mt=()=>typeof window<"u"&&typeof document<"u";function vo(){try{return!mt()||!window.localStorage?null:(window.localStorage.getItem("thg_hp_probe"),window.localStorage)}catch{return null}}var I,ht,On,Oe,gt,yt,Nn,he,wt,Ne,Tn,St,Rn,Te,Re=class{constructor(t){It(this,I);this.o={...Ao,...t},this.queue=[],this.timer=null,this.storage=vo(),this.inflight=null,this.sending=new Set,this.replayAllowed=!this.o.deferReplay,this.draining=null,mt()&&(window.addEventListener("pagehide",()=>this.flush({beacon:!0})),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&this.flush()}),O(this,I,Te).call(this))}replayPersisted(){return this.replayAllowed=!0,O(this,I,Te).call(this)}clearPersisted(){if(this.replayAllowed=!1,!!this.storage)try{this.storage.removeItem(this.o.storageKey)}catch{}}enqueue(t){this.queue.push(t),this.queue.length>this.o.maxQueue&&this.queue.splice(0,this.queue.length-this.o.maxQueue),this.queue.length>=this.o.flushAt?this.flush():O(this,I,ht).call(this)}flush(t={}){if(O(this,I,On).call(this),this.queue.length===0)return Promise.resolve();if(t.beacon&&mt()&&typeof navigator<"u"&&navigator.sendBeacon)return O(this,I,Nn).call(this),Promise.resolve();if(this.inflight)return this.inflight;let n=O(this,I,yt).call(this,this.queue,this.o.maxBatchEvents,this.o.maxBatchBytes).map(r=>{let o=O(this,I,St).call(this,r);return o&&this.sending.add(o),{batch:r,id:o}});return this.queue=[],this.inflight=(async()=>{for(let{batch:r,id:o}of n)await O(this,I,he).call(this,r,0,o)})().finally(()=>{this.inflight=null,this.queue.length>=this.o.flushAt?this.flush():this.queue.length&&O(this,I,ht).call(this)}),this.inflight}};I=new WeakSet,ht=function(){this.timer||(this.timer=setTimeout(()=>this.flush(),this.o.flushIntervalMs),this.timer?.unref?.())},On=function(){this.timer&&(clearTimeout(this.timer),this.timer=null)},Oe=function(t){return JSON.stringify({batch:t,writeKey:this.o.writeKey,sentAt:new Date().toISOString()})},gt=function(){return`${this.o.apiHost.replace(/\/$/,"")}/v1/batch`},yt=function(t,n,r){let o=ce(O(this,I,Oe).call(this,[])),s=[],i=[],c=o;for(let l of t){let d=ce(JSON.stringify(l))+1;i.length&&(i.length>=n||c+d>r)&&(s.push(i),i=[],c=o),i.push(l),c+=d}return i.length&&s.push(i),s},Nn=function(){let t=this.queue;this.queue=[];let n=O(this,I,gt).call(this);for(let r of O(this,I,yt).call(this,t,this.o.maxBatchEvents,this.o.beaconSafeBytes)){let o=O(this,I,Oe).call(this,r);if(ce(o)<=this.o.beaconSafeBytes){let s=!1;try{s=navigator.sendBeacon(n,new Blob([o],{type:"text/plain"}))}catch{s=!1}if(s)continue}O(this,I,he).call(this,r,0,O(this,I,St).call(this,r))}},he=async function(t,n,r=null){r&&this.sending.add(r);let o=O(this,I,Oe).call(this,t);try{let s=await fetch(O(this,I,gt).call(this),{method:"POST",headers:{"Content-Type":"text/plain"},body:o,keepalive:ce(o)<=this.o.beaconSafeBytes});if(s.ok)return O(this,I,wt).call(this,r),O(this,I,Te).call(this),!0;if(!(s.status===429||s.status>=500))return O(this,I,wt).call(this,r),this.o.onError?.(`CDP ingest rejected batch (${s.status}); dropping ${t.length} events`),!0;throw new Error(`retryable ${s.status}`)}catch(s){if(n>=this.o.maxRetries){r&&this.sending.delete(r);let c=r?"kept for the next page":"dropped";return this.o.onError?.(`CDP ingest unreachable after ${n} attempts; ${c}: ${t.length} events`,s),!1}let i=Math.min(1e3*2**n,3e4)*(.5+Math.random());return await new Promise(c=>setTimeout(c,i)),O(this,I,he).call(this,t,n+1,r)}},wt=function(t){t&&(this.sending.delete(t),O(this,I,Rn).call(this,t))},Ne=function(){let t=JSON.parse(this.storage.getItem(this.o.storageKey)||"[]");return Array.isArray(t)?t:[]},Tn=function(t,n){let r=i=>ce(JSON.stringify(i))+1,o=[...t,n].slice(-this.o.maxPersistedBatches),s=o.reduce((i,c)=>i+r(c),2);for(;o.length>1&&s>this.o.maxPersistedBytes;)s-=r(o[0]),o=o.slice(1);for(;o.length;)try{return this.storage.setItem(this.o.storageKey,JSON.stringify(o)),!0}catch{o=o.slice(1)}return!1},St=function(t){if(!this.storage||this.o.mayPersist&&!this.o.mayPersist())return null;try{let n={at:pt(),id:`${pt()}-${Math.random().toString(36).slice(2)}`,batch:t};return ce(JSON.stringify(n))+3>this.o.maxPersistedBytes?null:O(this,I,Tn).call(this,O(this,I,Ne).call(this),n)?n.id:null}catch{return null}},Rn=function(t){if(!(!t||!this.storage))try{let n=O(this,I,Ne).call(this).filter(r=>r.id!==t);n.length?this.storage.setItem(this.o.storageKey,JSON.stringify(n)):this.storage.removeItem(this.o.storageKey)}catch{}},Te=function(){if(!this.storage||!this.replayAllowed)return this.draining??Promise.resolve();if(this.draining)return this.draining;let t;try{let r=O(this,I,Ne).call(this);t=r.filter(o=>Array.isArray(o?.batch)&&pt()-o.at<this.o.maxRetryAgeMs).map(o=>o.id?o:{...o,id:`${o.at}-${Math.random().toString(36).slice(2)}`}),(t.length!==r.length||t.some((o,s)=>o!==r[s]))&&(t.length?this.storage.setItem(this.o.storageKey,JSON.stringify(t)):this.storage.removeItem(this.o.storageKey))}catch{return Promise.resolve()}let n=t.filter(r=>!this.sending.has(r.id));return n.length?(this.draining=(async()=>{for(let r of n)if(!this.replayAllowed||!await O(this,I,he).call(this,r.batch,0,r.id))return})().finally(()=>{this.draining=null}),this.draining):Promise.resolve()};var Dn="3.17.1",Mn="@thg-agentic/hyper-personalisation-web";var ko=new Set(["audiences","decisions","recommendations"]);var Eo=Symbol.for("@thg-agentic/hyper-personalisation-web.replay");function Co(e){let t={...e??{}},n=Array.isArray(t.products)?t.products:null;if(t.item_count==null&&n){let r=n.map(o=>o?.quantity);r.every(Number.isInteger)&&(t.item_count=r.reduce((o,s)=>o+s,0))}return t}var Po="e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";function Lo(e){if(/^[0-9a-fA-F]{64}$/.test(e))return e.toLowerCase();if(!/^[A-Za-z0-9+/_-]{43}=?$/.test(e))return null;try{let t=atob(e.replace(/-/g,"+").replace(/_/g,"/").padEnd(44,"="));return t.length!==32?null:Array.from(t,n=>n.charCodeAt(0).toString(16).padStart(2,"0")).join("")}catch{return null}}function De(e){if(typeof e!="string")return null;let t=Lo(e.trim());return t===Po?null:t}var $n=!1;function Oo(e){!e||$n||($n=!0,console.warn("[hp] mintAnonymousId is ignored since 3.6: where device ids come from is set per source in the console (the site\u2019s own cookie, or Hyper-Personalisation\u2019s)."))}function Un(e){return e&&typeof e.context=="object"&&e.context!==null?e.context:e}function Vn(e){let t=ae(e?.apiHost);if(!t)throw new Error("createAnalytics: apiHost, when given, must be a host or an http(s) origin");if(!e?.propertyId)throw new Error("createAnalytics: propertyId is required");let n={libraryName:Mn,libraryVersion:Dn,writeKey:"web",queryParamBlocklist:[],debug:!1,validate:null,consentTimeoutMs:5e3,...e,apiHost:t};_e(n.storageNames),be();let r=!!n.consent,o=null;n.getConsent=()=>o?o.stamp():Ft();let s=()=>!r||o?.status==="granted",i=r&&n.holdAcrossPages?jt(j(N("held"),n.propertyId)):null,c=i?.read()??{items:[],anonymousId:null};n.mintAnonymousId!==void 0&&Oo(n.debug);let l=qt(n,{carried:c.anonymousId,mayStore:s}),d=n.getSessionId==null?In({name:N("session"),domain:n.consentCookieDomain??null,mayStore:s}):null;d&&(n.getSessionId=()=>d.get());let m=un(N("preview"),n.consentCookieDomain??null),f=n.transport??new Re({apiHost:n.apiHost,writeKey:n.writeKey,storageKey:j(N("retry"),n.propertyId),mayPersist:r?()=>o?.status==="granted":void 0,deferReplay:r,onError:(a,u)=>n.debug&&console.warn("[hp]",a,u??"")}),g=j(N("previousPage"),n.propertyId),b=null,A=()=>{if(b)return b;try{return sessionStorage.getItem(g)||null}catch{return null}},k=a=>{if(b=a||null,!(r&&o.status!=="granted"))try{sessionStorage.setItem(g,a)}catch{}},w=sn({key:j(N("decisions"),n.propertyId),mayStore:s});n.getDecisions??(n.getDecisions=w.current);let L=q($(n.getUserId,null)),h=null;function x(a,u,v=!1){let C=h;h=null;let E={messageId:C?.messageId??re(),timestamp:C?.timestamp??new Date().toISOString(),anonymousId:q($(n.getAnonymousId,null)),userId:L??null,...a,context:Dt(n,Un(u))};if(E.properties&&(E.properties=Ke(E.properties)),n.beforeSend){let H;try{H=n.beforeSend(E)}catch(Jn){return console.warn("[hp] beforeSend failed; the event was not sent",Jn),null}if(!H)return null;let{type:ye,event:bt,messageId:zn,timestamp:Wn}=E;E={...H,type:ye,messageId:zn,timestamp:Wn,...bt?{event:bt}:{}}}if(n.validate){let H=n.validate(E,{producer:"web"});if(!H.valid){let ye=`[hp] invalid ${E.event??E.type}:
|
|
2
|
+
${H.errors.join(`
|
|
3
|
+
`)}`;if(n.throwOnInvalid)throw new Error(ye);console.error(ye)}}return n.debug&&console.debug("[hp]",E.event??E.type,E),B(E,v),E}function p(a){if(a.type==="track"&&M(a),n.onEvent)try{n.onEvent(a)}catch(u){console.warn("[hp] onEvent observer failed",u)}}let _=()=>r&&o?.state?.personalisation===!0,S=()=>hn($(n.getSessionId,null),n.propertyId),P=!1;function M(a){try{_()?(S(),fn(it(bn(a.event,a.properties))),P=!0):(pn(),P&&wn(),P=!1)}catch(u){n.debug&&console.warn("[hp] the visit was not recorded",u)}}function B(a,u){!r||u||o.admits()?(p(a),f.enqueue(a)):o.hold(a)}let y=(a,u,v)=>kn(n.plan,a)?x({type:"track",event:a,properties:u??{}},v):null,V=new Set,le=(a,u,v)=>a==="audiences"?{audiences:u?null:v.audiences??[]}:a==="decisions"?{decisions:(v.decisions??[]).map(C=>({slot:C.slot,decision_id:C.decision_id??null,arm:C.arm??null,...C.variant&&{variant:C.variant}}))}:{recommendation_request_id:v.recommendation_request_id??null};r&&(o=rn({adapter:n.consent,timeoutMs:n.consentTimeoutMs,cookieDomain:n.consentCookieDomain,debug:n.debug,store:i&&{read:()=>c.items,write:a=>i.write(a,l?.peek()??null)},release(a){a.context&&(a.context.consent=o.stamp()),p(a),f.enqueue(a)},onStatus(a){if(a==="granted"&&(l?.persist(),d?.persist(),b&&k(b),w.persist(),f.replayPersisted?.()),a==="denied"){f.clearPersisted?.(),l?.forget(),d?.forget();try{sessionStorage.removeItem(g)}catch{}w.forget()}try{n.onConsentStatus?.(a)}catch(u){console.warn("[hp] onConsentStatus observer failed",u)}},announce({action:a,state:u,withdrawal:v}){x({type:"track",event:"Consent Updated",properties:{action:a,state:u}},void 0,v),v&&f.flush()}}),o.start());let F=()=>n.holdout?ot(n.holdout):null,de=()=>st(n.holdout,q($(n.getAnonymousId,null))).group;function ge(a,u){if(!Array.isArray(a?.decisions))return a;let v=n.holdout?.readoutSlots??[],C=q($(n.getAnonymousId,null));return{...a,decisions:a.decisions.map(E=>{let H=E?.variant==="bandit"||E?.variant==="default"?E.variant:!u&&E?.holdout!==!0&&v.includes(E?.slot)?cn(E.slot,C):null;return H?H==="default"?{...E,variant:H,decision_id:null}:{...E,variant:H}:E})}}function fe(a,u){if(m)return null;let v=a?.exposure,C=v?.holdout===!0,E=ge(a,C);return w.remember(on(E)),!v?.token||V.has(v.token)?null:(V.add(v.token),y("Personalisation Exposed",{exposure_token:v.token,surface:v.surface,holdout:C,holdout_share:v.holdout_share??F()?.holdout_share??null,personalised_share:v.personalised_share??F()?.personalised_share??null,...le(v.surface,C,E)},u))}return{track:y,page(a={},u){let v=n.pageType??null;a.type&&(ne.has(a.type)?(v=a.type,n.pageType=a.type):n.debug&&console.warn(`[hp] ignoring unknown page.type "${a.type}". Allowed: ${[...ne].join(", ")}`));let C=x({type:"page",properties:{name:a.name??null,navigation_trigger:a.navigation_trigger??Ot(),previous_message_id:A()}},{page:{type:v},...Un(u)??{}});return C&&k(C.messageId),C},identify(a,u){let{userId:v,...C}=a&&typeof a=="object"?a:{};if(v!==void 0&&(L=q(v)),typeof C.email_sha256=="string"){let E=De(C.email_sha256);E?C.email_sha256=E:delete C.email_sha256}return x({type:"identify",traits:C},u)},reset(){L=null,k("")},productViewed:(a,u)=>y("Product Viewed",a,u),productListViewed:(a,u)=>y("Product List Viewed",a,u),recommendationsViewed:(a,u)=>y("Recommendations Viewed",a,u),personalisationExposed:(a,u)=>y("Personalisation Exposed",a,u),recordExposure:fe,surfaceViewed({surface:a}={},u){if(!ko.has(a)||!n.holdout||a!=="decisions"&&!_())return null;let v=q($(n.getAnonymousId,null)),C=st(n.holdout,v).group;if(C==="excluded")return null;let E=Ct({sessionId:we($(n.getSessionId,null),v),surface:a});return E?fe({exposure:{token:E,surface:a,holdout:C==="holdout"}},u):null},inHoldout:()=>de()!=="personalised",personalisationGroup:de,productClicked:(a,u)=>y("Product Clicked",a,u),productListFiltered:(a,u)=>y("Product List Filtered",a,u),productsSearched:(a,u)=>y("Products Searched",a,u),productAdded:(a,u)=>y("Product Added",a,u),productRemoved:(a,u)=>y("Product Removed",a,u),cartUpdated:(a,u)=>y("Cart Updated",Co(a),u),identityLinked:(a,u)=>y("Identity Linked",a,u),slotEngaged:(a,u)=>m?null:y("Slot Engaged",a,u),productVariantSelected:(a,u)=>y("Product Variant Selected",a,u),cartViewed:(a,u)=>y("Cart Viewed",a,u),productAddedToWishlist:(a,u)=>y("Product Added to Wishlist",a,u),productRemovedFromWishlist:(a,u)=>y("Product Removed from Wishlist",a,u),wishlistProductAddedToCart:(a,u)=>y("Wishlist Product Added to Cart",a,u),productShared:(a,u)=>y("Product Shared",a,u),cartShared:(a,u)=>y("Cart Shared",a,u),couponEntered:(a,u)=>y("Coupon Entered",a,u),couponApplied:(a,u)=>y("Coupon Applied",a,u),couponDenied:(a,u)=>y("Coupon Denied",a,u),couponRemoved:(a,u)=>y("Coupon Removed",a,u),checkoutStarted:(a,u)=>y("Checkout Started",a,u),promotionViewed:(a,u)=>y("Promotion Viewed",a,u),promotionClicked:(a,u)=>y("Promotion Clicked",a,u),checkoutStepViewed:(a,u)=>y("Checkout Step Viewed",a,u),checkoutStepCompleted:(a,u)=>y("Checkout Step Completed",a,u),paymentInfoEntered:(a,u)=>y("Payment Info Entered",a,u),checkoutFailed:(a,u)=>y("Checkout Failed",a,u),signedIn:(a,u)=>y("Signed In",a,u),signedUp:(a,u)=>y("Signed Up",a,u),signedOut(a){let u=y("Signed Out",{},a);return L=null,u},newsletterSubscribed:(a,u)=>y("Newsletter Subscribed",a,u),newsletterUnsubscribed:(a,u)=>y("Newsletter Unsubscribed",a,u),backInStockSubscribed:(a,u)=>y("Back In Stock Subscribed",a,u),productReviewed:(a,u)=>y("Product Reviewed",a,u),consentUpdated:(a,u)=>y("Consent Updated",a,u),flush:a=>f.flush(a),recommendationContext(){if(!_())return{};try{return S(),it(yn())}catch{return{}}},get consent(){return o?o.state:null},servingHeaders:()=>Gt(o?.state??null),get lastPageMessageId(){return A()},get anonymousId(){return q($(n.getAnonymousId,null))},[Eo](a,u,v){if(typeof this[a]=="function"){h=typeof v?.messageId=="string"&&typeof v?.timestamp=="string"?{messageId:v.messageId,timestamp:v.timestamp}:null;try{this[a](...Array.isArray(u)?u:[])}finally{h=null}}}}}var _t=Symbol.for("@thg-agentic/hyper-personalisation-web.bootstrap"),No=Symbol.for("@thg-agentic/hyper-personalisation-web.drain"),To=Symbol.for("@thg-agentic/hyper-personalisation-web.carry"),Ro=Symbol.for("@thg-agentic/hyper-personalisation-web.replay"),Do=Symbol.for("@thg-agentic/hyper-personalisation-web.assignment"),Mo=Symbol.for("@thg-agentic/hyper-personalisation-web.recommendation-context"),Ue=e=>e?()=>R(e):()=>null,Bn={didomi:e=>zt({purposes:e.purposes}),onetrust:e=>Wt({groups:e.groups,impliedConsent:e.impliedConsent}),cookie:e=>Jt({name:e.name,categories:e.categories,refusedValue:e.refusedValue,source:e.source,changeEvent:e.changeEvent}),forwarded:e=>Xt({name:e.name}),static:e=>{if(!e.state||typeof e.state!="object")throw new Error('consent.state is required for provider "static"');return Yt(e.state)}},jn=Object.keys(Bn),ue=e=>e!==null&&typeof e=="object"&&!Array.isArray(e),Me=e=>typeof e=="string"&&e!=="",$o=e=>Array.isArray(e)&&e.every(t=>typeof t=="string"),Fn=e=>ue(e)&&typeof e.read=="function";function Uo(e,t={}){let n=[];if(!ue(e))return["config must be { writeKey, apiHost? }"];Me(e.writeKey)||n.push("writeKey is required"),ae(e.apiHost)||n.push("apiHost, when given, must be a host or an http(s) origin"),t.propertyId!==void 0&&!Me(t.propertyId)&&n.push("propertyId must be a string");let{consent:r}=t;if(!Fn(r)&&!(ue(r)&&jn.includes(r.provider))&&n.push(`consent must be a consent adapter or { provider: ${jn.join(" | ")} }`),t.autocapture!==void 0&&t.autocapture!==!1&&!ue(t.autocapture)&&n.push("autocapture must be false or { promotions, productLists, recommendations }"),t.beforeSend!==void 0&&typeof t.beforeSend!="function"&&n.push("beforeSend must be a function"),t.identityCookies!==void 0)if(!ue(t.identityCookies))n.push("identityCookies must be an object");else for(let o of["anonymousId","sessionId"]){let s=t.identityCookies[o];s!==void 0&&!Me(s)&&n.push(`identityCookies.${o} must be a cookie name`)}for(let o of["getCurrency","getShipToCountry","getLocale"])t[o]!==void 0&&typeof t[o]!="function"&&n.push(`${o} must be a function`);return t.dataLayer!==void 0&&(ue(t.dataLayer)?(t.dataLayer.name!==void 0&&!Me(t.dataLayer.name)&&n.push("dataLayer.name must be a string"),t.dataLayer.ignore!==void 0&&!$o(t.dataLayer.ignore)&&n.push("dataLayer.ignore must be an array of strings")):n.push("dataLayer must be an object")),n}function Vo(e,t,n){if(Fn(e))return et(e,t.consentPurposes);let{provider:r,...o}=e,s=n?.consent?.[r]??Bn[r];return et(s(o),t.consentPurposes)}var Hn=(e,t)=>`${e??""}|${t??""}`;function jo(e,t){let n=q(t?.userId),r=De(t?.emailSha256);if(!n&&!r)return null;try{let o=j(N("identified"),e);if(sessionStorage.getItem(o)===Hn(n,r))return null}catch{}return{...n?{userId:n}:{},...r?{email_sha256:r}:{}}}async function qo({writeKey:e,apiHost:t},n){if(n.settings!==void 0)return{settings:dt(n.settings),revalidated:null};let r=Cn(t,e),o=Pn(e);if(o)return{settings:o,revalidated:r};let s=await r,i=s??lt;return{settings:i,revalidated:Promise.resolve(i),unreachable:s===null}}function Kn(e,t={}){let n=typeof window<"u"?window:null;if(n?.[_t])return n[_t];let r=Bt(e?.writeKey,()=>{let{analytics:s}=me(R(N("consent")));return s===!0||s===null&&t.holdAcrossPages===!0}),o=Bo(e,t,r);if(n){let s=t.extend?.getSessionId??Ue(t.identityCookies?.sessionId),i=Ue(t.identityCookies?.anonymousId);n[Do]=({slot:c,arm:l}={})=>Et({sessionId:we(s(),i()),slot:c,arm:l}),n[_t]=o,n[To]=r,n[No]?.(o)}return o}function qn(e,t){if(t===!1)return()=>{};let n={promotions:!0,productLists:!0,recommendations:!0,exposures:!0,...t},r=s=>s&&typeof s=="object"?s:{},o=[];try{n.promotions&&o.push(Be(e,r(n.promotions))),n.productLists&&o.push(qe(e,r(n.productLists))),n.recommendations&&o.push(Fe(e,r(n.recommendations))),n.exposures&&o.push(je(e,r(n.exposures)))}catch(s){console.warn("[hp] autocapture not started:",s?.message??s)}return()=>{for(let s of o)s()}}function $e(e){return e.close(),null}async function Bo(e,t,n){try{let r=Uo(e,t);if(r.length)return console.warn(`[hp] not started, the configuration is invalid: ${r.join("; ")}`),$e(n);_e(t.storageNames),be();let{writeKey:o}=e,[{settings:s,revalidated:i,unreachable:c},l]=await Promise.all([qo(e,t),Promise.resolve(t.identity).catch(()=>null)]),d=i?s:null,m=!1,f=()=>m&&d&&Ln(o,d);if(i?.then(p=>{p&&(d=p,p.enabled?f():ft(o))}),c)return null;if(!s.enabled)return t.debug&&console.info("[hp] off: the settings for this write key do not enable it"),$e(n);let g=s.propertyId??t.propertyId;if(!g)return console.warn("[hp] not started: this write key covers several properties, so propertyId is required"),$e(n);let{preset:b}=t,A=jo(g,l),k=q(l?.userId),w=Vn({apiHost:s.apiHost??ae(e.apiHost),writeKey:o,propertyId:g,pageType:ne.has(t.pageType)?t.pageType:void 0,environment:s.environment,queryParamAllowlist:s.queryParamAllowlist??void 0,plan:s.plan,holdout:s.holdouts?.[g],getCurrency:t.getCurrency,getShipToCountry:t.getShipToCountry,getLocale:t.getLocale,consent:Vo(t.consent,s,b),consentCookieDomain:t.consentCookieDomain,storageNames:t.storageNames,holdAcrossPages:t.holdAcrossPages===!0,mintAnonymousId:t.mintAnonymousId,debug:t.debug===!0,anonymousIdSource:s.anonymousId??void 0,getAnonymousId:Ue(t.identityCookies?.anonymousId),getSessionId:t.identityCookies?.sessionId?Ue(t.identityCookies.sessionId):void 0,getUserId:()=>k,beforeSend:t.beforeSend,...t.extend,onConsentStatus(p){if(p==="granted"&&(m=!0,f()),p==="denied"){ft(o);try{sessionStorage.removeItem(j(N("identified"),g))}catch{}}t.extend?.onConsentStatus?.(p)},onEvent(p){if(p.type==="identify"){let _=Hn(q(p.userId),De(p.traits?.email_sha256));try{sessionStorage.setItem(j(N("identified"),g),_)}catch{}}t.extend?.onEvent?.(p)}});typeof window<"u"&&(window[Mo]=()=>w.recommendationContext());for(let[p,_,S]of n.take())try{w[Ro](p,_,S)}catch(P){console.warn(`[hp] carried ${p} failed`,P)}w.page(t.pageType?{type:t.pageType}:{});let L=qn(w,t.autocapture),h=w.page;w.page=(...p)=>{let _=h(...p);return L(),L=qn(w,t.autocapture),_};let x=typeof window<"u"?window:null;if(t.dataLayer&&x){let p=t.dataLayer.name??"dataLayer";x[p]=x[p]||[],tn(w,{...b,dataLayer:x[p],ignore:[...b?.ignore??[],...t.dataLayer.ignore??[]],onError:_=>t.debug&&console.warn("[hp] dataLayer mapping failed",_)})}return A&&w.identify(A),w}catch(r){return console.warn("[hp] not started:",r?.message??r),$e(n)}}function Gn(e){e.initialized=!0,Kn(e._config,e._options).then(t=>{let n=e.splice(0);if(!t){for(let r of e.methods??[])e[r]=()=>e;return}e.client=t;for(let[r,o]of Object.entries(t))typeof o=="function"&&(e[r]=o);for(let[r,...o]of n)try{t[r]?.(...o)}catch(s){console.warn(`[hp] queued ${r} failed`,s)}})}var U=window.hp=window.hp||[];U.servingHeaders??(U.servingHeaders=()=>({}));U.inHoldout??(U.inHoldout=()=>!0);U.personalisationGroup??(U.personalisationGroup=()=>"excluded");U.recommendationContext??(U.recommendationContext=()=>({}));U.initialized||(U._config?Gn(U):U.load=(e,t)=>{U._config=e,U._options=t??{},Gn(U)});})();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(()=>{(()=>{let e=window.hp=window.hp||[];if(!(e.initialized||e.invoked)){e.invoked=!0,e.methods=["track","page","identify","reset","flush","productViewed","productListViewed","recommendationsViewed","personalisationExposed","recordExposure","surfaceViewed","productClicked","productListFiltered","productsSearched","productAdded","productRemoved","cartUpdated","identityLinked","slotEngaged","productVariantSelected","cartViewed","productAddedToWishlist","productRemovedFromWishlist","wishlistProductAddedToCart","productShared","cartShared","couponEntered","couponApplied","couponDenied","couponRemoved","checkoutStarted","promotionViewed","promotionClicked","checkoutStepViewed","checkoutStepCompleted","paymentInfoEntered","checkoutFailed","signedIn","signedUp","signedOut","newsletterSubscribed","newsletterUnsubscribed","backInStockSubscribed","productReviewed","consentUpdated"],e.factory=t=>(...o)=>(e.push([t,...o]),e);for(let t of e.methods)e[t]=e.factory(t);e.servingHeaders=()=>({}),e.inHoldout=()=>!0,e.personalisationGroup=()=>"excluded",e.recommendationContext=()=>({}),e.load=(t,o,r)=>{e._config=t,e._options=o||{};let d=document.createElement("script");d.async=!0,d.src=r;let i=document.getElementsByTagName("script")[0];i.parentNode.insertBefore(d,i)},e.SNIPPET_VERSION="1",e.load({writeKey:"hp_pk_live_REPLACE_ME",apiHost:"https://api.example.com"},{consent:{provider:"onetrust"},identityCookies:{sessionId:"session_id"}},"https://cdn.jsdelivr.net/npm/@thg-agentic/hyper-personalisation-web@3/dist/hp.min.js")}})();})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,86 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thg-agentic/hyper-personalisation-web",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "3.17.1",
|
|
4
|
+
"description": "Browser SDK for the THG Hyper-Personalisation event specification: event tracking from a write key and remote settings, strict CMP-agnostic consent, a GA4 dataLayer bridge and in-session audience evaluation. Dependency-free ESM on the Segment wire protocol, a CDN script (dist/hp.min.js), and an optional @segment/analytics-next plugin path.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"types": "./types/index.d.ts",
|
|
8
|
+
"unpkg": "./dist/hp.min.js",
|
|
9
|
+
"jsdelivr": "./dist/hp.min.js",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./types/index.d.ts",
|
|
13
|
+
"default": "./src/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./consent": {
|
|
16
|
+
"types": "./types/consent.d.ts",
|
|
17
|
+
"default": "./src/consent.js"
|
|
18
|
+
},
|
|
19
|
+
"./datalayer": {
|
|
20
|
+
"types": "./types/datalayer.d.ts",
|
|
21
|
+
"default": "./src/datalayer.js"
|
|
22
|
+
},
|
|
23
|
+
"./segment": {
|
|
24
|
+
"types": "./types/segment.d.ts",
|
|
25
|
+
"default": "./src/segment.js"
|
|
26
|
+
},
|
|
27
|
+
"./bootstrap": {
|
|
28
|
+
"types": "./types/bootstrap.d.ts",
|
|
29
|
+
"default": "./src/bootstrap.js"
|
|
30
|
+
},
|
|
31
|
+
"./hp": {
|
|
32
|
+
"types": "./types/hp.d.ts",
|
|
33
|
+
"default": "./src/hp.js"
|
|
34
|
+
},
|
|
35
|
+
"./auto": {
|
|
36
|
+
"types": "./types/auto.d.ts",
|
|
37
|
+
"default": "./src/auto.js"
|
|
38
|
+
},
|
|
39
|
+
"./session": "./src/session.js",
|
|
40
|
+
"./experiments": "./src/experiments.js",
|
|
41
|
+
"./conformance": "./src/conformance.js"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"src/",
|
|
45
|
+
"types/**/*.d.ts",
|
|
46
|
+
"dist/hp.min.js",
|
|
47
|
+
"dist/snippet.min.js"
|
|
48
|
+
],
|
|
49
|
+
"sideEffects": false,
|
|
50
|
+
"license": "MIT",
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "git+https://github.com/THG-Commerce/hyper-personalisation.git",
|
|
54
|
+
"directory": "sdks/web"
|
|
55
|
+
},
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"access": "public"
|
|
58
|
+
},
|
|
59
|
+
"//peerDependencies": [
|
|
60
|
+
"OPTIONAL PEER, NOT A DEPENDENCY. The default delivery path is src/transport.js, which imports",
|
|
61
|
+
"nothing. Only the opt-in ./segment entry point runs inside analytics-next, and it never imports",
|
|
62
|
+
"it: the host passes cdpPlugins() to its own AnalyticsBrowser, so there is exactly one copy."
|
|
63
|
+
],
|
|
64
|
+
"peerDependencies": {
|
|
65
|
+
"@segment/analytics-next": "^1.84.1"
|
|
66
|
+
},
|
|
67
|
+
"peerDependenciesMeta": {
|
|
68
|
+
"@segment/analytics-next": {
|
|
69
|
+
"optional": true
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"devDependencies": {
|
|
73
|
+
"@segment/analytics-next": "1.84.3",
|
|
74
|
+
"esbuild": "^0.28.2",
|
|
75
|
+
"jsdom": "^30.1.1",
|
|
76
|
+
"typescript": "^7.0.2",
|
|
77
|
+
"@thg-agentic/hyper-personalisation-spec": "1.8.1"
|
|
78
|
+
},
|
|
79
|
+
"scripts": {
|
|
80
|
+
"test": "node --test tests/*.test.mjs",
|
|
81
|
+
"build": "node scripts/build-cdn.mjs",
|
|
82
|
+
"build:types": "node scripts/build-types.mjs",
|
|
83
|
+
"pack": "pnpm pack --pack-destination dist",
|
|
84
|
+
"check:types": "tsc -p types/tsconfig.json"
|
|
85
|
+
}
|
|
6
86
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `decision_id` of a server-side assignment (`POST /v1/assignments`), derived rather than
|
|
3
|
+
* minted, so a page cached per arm, not per visitor, can still name the decision it showed.
|
|
4
|
+
*
|
|
5
|
+
* decision_id = UUIDv5(ASSIGNMENT_DECISION_NAMESPACE, `${session_id}:${slot}:${arm}`)
|
|
6
|
+
*
|
|
7
|
+
* `session_id` is the session HP assigned under: the renderer's session id, or, with none, the
|
|
8
|
+
* visitor id. HP logs the assignment's `Decision Made` under this id, and the page reports
|
|
9
|
+
* `Slot Engaged` with the same one, so the reward joins on `decision_id` as for any decision.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { sha1 } from './hash.js';
|
|
13
|
+
|
|
14
|
+
/** HP's fixed namespace for assignment decision ids. Never changes: every id ever logged uses it. */
|
|
15
|
+
export const ASSIGNMENT_DECISION_NAMESPACE =
|
|
16
|
+
'e28e8ada-5b76-4209-bba9-79b823a0af75';
|
|
17
|
+
|
|
18
|
+
const hex = (bytes) =>
|
|
19
|
+
Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
20
|
+
|
|
21
|
+
/** A name-based UUID, version 5 (RFC 9562 §5.5). */
|
|
22
|
+
export function uuidV5(namespace, name) {
|
|
23
|
+
const space = Uint8Array.from(
|
|
24
|
+
namespace.replace(/-/g, '').match(/../g),
|
|
25
|
+
(pair) => Number.parseInt(pair, 16),
|
|
26
|
+
);
|
|
27
|
+
const text = new TextEncoder().encode(name);
|
|
28
|
+
const input = new Uint8Array(16 + text.length);
|
|
29
|
+
input.set(space);
|
|
30
|
+
input.set(text, 16);
|
|
31
|
+
const bytes = sha1(input).slice(0, 16);
|
|
32
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x50;
|
|
33
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
34
|
+
const h = hex(bytes);
|
|
35
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A value HP takes as given: a non-empty string within its length limit. */
|
|
39
|
+
const given = (value, max) =>
|
|
40
|
+
typeof value === 'string' && value !== '' && value.length <= max
|
|
41
|
+
? value
|
|
42
|
+
: null;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The session HP assigns under, from what the renderer forwarded: the session id (at most 64
|
|
46
|
+
* characters), else the visitor id (at most 256). Null with neither: HP assigns nothing then.
|
|
47
|
+
*/
|
|
48
|
+
export function assignmentSession(sessionId, visitorId) {
|
|
49
|
+
return given(sessionId, 64) ?? given(visitorId, 256);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The `decision_id` HP logged for assigning `arm` to `slot` in `sessionId`. Null when any of the
|
|
54
|
+
* three is missing or empty.
|
|
55
|
+
*/
|
|
56
|
+
export function assignmentDecisionId({ sessionId, slot, arm } = {}) {
|
|
57
|
+
const session = given(sessionId, 256);
|
|
58
|
+
if (!session || !given(slot, 256) || !given(arm, 256)) return null;
|
|
59
|
+
return uuidV5(ASSIGNMENT_DECISION_NAMESPACE, `${session}:${slot}:${arm}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** HP's fixed namespace for the exposure tokens a page derives itself. Never changes. */
|
|
63
|
+
export const SURFACE_EXPOSURE_NAMESPACE =
|
|
64
|
+
'4b1f2c7e-9a3d-4e58-b6c1-2d7f0a9e8c53';
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The token of the property holdout's exposure for `surface` in `sessionId`, for a surface the
|
|
68
|
+
* page saw rendered without its serving answer (a product rail the site's server fetched): one per session
|
|
69
|
+
* per surface, which is the grain the readout counts. Null without a session or a surface.
|
|
70
|
+
*/
|
|
71
|
+
export function surfaceExposureToken({ sessionId, surface } = {}) {
|
|
72
|
+
const session = given(sessionId, 256);
|
|
73
|
+
if (!session || !given(surface, 64)) return null;
|
|
74
|
+
return uuidV5(SURFACE_EXPOSURE_NAMESPACE, `${session}:${surface}`);
|
|
75
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Read the first non-empty dataset value among several candidate keys. */
|
|
2
|
+
export function firstAttr(el, ...keys) {
|
|
3
|
+
const data = el.dataset ?? {};
|
|
4
|
+
for (const key of keys) {
|
|
5
|
+
const value = data[key];
|
|
6
|
+
if (typeof value === 'string' && value.trim() !== '') return value.trim();
|
|
7
|
+
}
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function intAttr(el, ...keys) {
|
|
12
|
+
const raw = firstAttr(el, ...keys);
|
|
13
|
+
if (raw === null) return null;
|
|
14
|
+
const n = Number(raw);
|
|
15
|
+
return Number.isFinite(n) ? Math.trunc(n) : null;
|
|
16
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_DWELL_MS,
|
|
3
|
+
DEFAULT_VISIBLE_RATIO,
|
|
4
|
+
watchViewability,
|
|
5
|
+
} from './viewability.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Personalised surfaces described in markup: an element `data-hp-exposure="<surface>"`
|
|
9
|
+
* (`recommendations`, `decisions` or `audiences`) records the property holdout's exposure for that
|
|
10
|
+
* surface once it is viewable, through `surfaceViewed`. Mark the container the site renders for
|
|
11
|
+
* both groups, HP's content for a treated visitor and the default for a held-out one, so the two
|
|
12
|
+
* are counted on the same trigger. `surfaceViewed` decides the group and records one exposure per
|
|
13
|
+
* session per surface, so marking several elements, or the same one on every page, counts once.
|
|
14
|
+
*
|
|
15
|
+
* @param {{ surfaceViewed(fields: { surface: string }): unknown }} analytics
|
|
16
|
+
* @param {object} [options]
|
|
17
|
+
* @param {string} [options.selector]
|
|
18
|
+
* @param {number} [options.visibleRatio]
|
|
19
|
+
* @param {number} [options.dwellMs]
|
|
20
|
+
* @param {Document|Element} [options.root]
|
|
21
|
+
* @returns {() => void} Teardown, for a SPA route change.
|
|
22
|
+
*/
|
|
23
|
+
export function trackExposures(analytics, options = {}) {
|
|
24
|
+
if (typeof window === 'undefined' || typeof document === 'undefined')
|
|
25
|
+
return () => {};
|
|
26
|
+
|
|
27
|
+
const {
|
|
28
|
+
selector = '[data-hp-exposure]',
|
|
29
|
+
visibleRatio = DEFAULT_VISIBLE_RATIO,
|
|
30
|
+
dwellMs = DEFAULT_DWELL_MS,
|
|
31
|
+
root = document,
|
|
32
|
+
} = options;
|
|
33
|
+
|
|
34
|
+
return watchViewability({
|
|
35
|
+
selector,
|
|
36
|
+
root,
|
|
37
|
+
visibleRatio,
|
|
38
|
+
dwellMs,
|
|
39
|
+
onViewable(el) {
|
|
40
|
+
const surface = el.dataset?.hpExposure?.trim();
|
|
41
|
+
if (surface) analytics.surfaceViewed({ surface });
|
|
42
|
+
return true;
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_DWELL_MS,
|
|
3
|
+
DEFAULT_VISIBLE_RATIO,
|
|
4
|
+
watchViewability,
|
|
5
|
+
} from './viewability.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Product lists described in markup: a container `data-hp-list="<list id>"`, optionally
|
|
9
|
+
* `data-hp-list-category`, whose tiles carry `data-hp-product`, the spec product as JSON.
|
|
10
|
+
*
|
|
11
|
+
* - Product List Viewed, once per list id per page, when one of its tiles meets the viewability
|
|
12
|
+
* standard, carrying every tile in the list.
|
|
13
|
+
* - Product Clicked, for a click on a link inside a tile. A button inside one (quick buy, a
|
|
14
|
+
* wishlist heart, a colour swatch) is not a product click.
|
|
15
|
+
*
|
|
16
|
+
* A list the page reports itself, because only the page knows its pagination and filters, is
|
|
17
|
+
* marked `data-hp-list-view="manual"`: its clicks are still captured, its views are not.
|
|
18
|
+
*
|
|
19
|
+
* @param {object} analytics An SDK instance from createAnalytics.
|
|
20
|
+
* @param {object} [options]
|
|
21
|
+
* @param {string} [options.selector] Which elements are lists.
|
|
22
|
+
* @param {string} [options.tileSelector] Which elements inside a list are product tiles.
|
|
23
|
+
* @param {number} [options.visibleRatio]
|
|
24
|
+
* @param {number} [options.dwellMs]
|
|
25
|
+
* @param {Document|Element} [options.root]
|
|
26
|
+
* @returns {() => void} Teardown.
|
|
27
|
+
*/
|
|
28
|
+
const absolute = (value) => {
|
|
29
|
+
try {
|
|
30
|
+
return new URL(value, document.baseURI).toString();
|
|
31
|
+
} catch {
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function trackProductLists(analytics, options = {}) {
|
|
37
|
+
if (typeof window === 'undefined' || typeof document === 'undefined')
|
|
38
|
+
return () => {};
|
|
39
|
+
|
|
40
|
+
const {
|
|
41
|
+
selector = '[data-hp-list]',
|
|
42
|
+
tileSelector = '[data-hp-product]',
|
|
43
|
+
visibleRatio = DEFAULT_VISIBLE_RATIO,
|
|
44
|
+
dwellMs = DEFAULT_DWELL_MS,
|
|
45
|
+
root = document,
|
|
46
|
+
} = options;
|
|
47
|
+
|
|
48
|
+
/** List ids already reported: a re-rendered carousel replaces nodes, not the list. */
|
|
49
|
+
const reported = new Set();
|
|
50
|
+
|
|
51
|
+
const listOf = (el) => el.closest?.(selector) ?? null;
|
|
52
|
+
|
|
53
|
+
const listFields = (list) => {
|
|
54
|
+
const id = list.dataset.hpList?.trim();
|
|
55
|
+
if (!id) return null;
|
|
56
|
+
return {
|
|
57
|
+
list_id: id,
|
|
58
|
+
category: list.dataset.hpListCategory?.trim() || null,
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// A tile's own list only: a carousel nested in a list does not lend it its tiles.
|
|
63
|
+
const tilesOf = (list) =>
|
|
64
|
+
Array.from(list.querySelectorAll(tileSelector)).filter(
|
|
65
|
+
(tile) => listOf(tile) === list,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const productOf = (tile, index) => {
|
|
69
|
+
try {
|
|
70
|
+
const product = JSON.parse(tile.dataset.hpProduct ?? '');
|
|
71
|
+
if (!product || typeof product !== 'object' || !product.product_id)
|
|
72
|
+
return null;
|
|
73
|
+
return {
|
|
74
|
+
...product,
|
|
75
|
+
position: product.position ?? index + 1,
|
|
76
|
+
// Markup rendered on a server may not know its own origin; the page does.
|
|
77
|
+
...(product.url ? { url: absolute(product.url) } : {}),
|
|
78
|
+
...(product.image_url
|
|
79
|
+
? { image_url: absolute(product.image_url) }
|
|
80
|
+
: {}),
|
|
81
|
+
};
|
|
82
|
+
} catch {
|
|
83
|
+
// One malformed tile must not stop the rest of the list reporting.
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const stopWatching = watchViewability({
|
|
89
|
+
selector: `${selector} ${tileSelector}`,
|
|
90
|
+
root,
|
|
91
|
+
visibleRatio,
|
|
92
|
+
dwellMs,
|
|
93
|
+
onViewable(tile) {
|
|
94
|
+
const list = listOf(tile);
|
|
95
|
+
if (!list || list.dataset.hpListView === 'manual') return true;
|
|
96
|
+
const fields = listFields(list);
|
|
97
|
+
if (!fields || reported.has(fields.list_id)) return true;
|
|
98
|
+
// A responsive page often renders a list twice, one layout hidden: a product is listed once.
|
|
99
|
+
const seen = new Set();
|
|
100
|
+
const products = tilesOf(list)
|
|
101
|
+
.map(productOf)
|
|
102
|
+
.filter((product) => {
|
|
103
|
+
if (!product || seen.has(product.product_id)) return false;
|
|
104
|
+
seen.add(product.product_id);
|
|
105
|
+
return true;
|
|
106
|
+
});
|
|
107
|
+
// An empty list would fail validation at the ingest.
|
|
108
|
+
if (products.length === 0) return true;
|
|
109
|
+
reported.add(fields.list_id);
|
|
110
|
+
analytics.productListViewed({ ...fields, products });
|
|
111
|
+
return true;
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Capture phase: tile components often stopPropagation, which would hide them from a bubble
|
|
116
|
+
// listener.
|
|
117
|
+
const onClick = (event) => {
|
|
118
|
+
const link = event.target?.closest?.('a[href]');
|
|
119
|
+
const tile = link?.closest(tileSelector);
|
|
120
|
+
if (!tile) return;
|
|
121
|
+
const list = listOf(tile);
|
|
122
|
+
const product = productOf(tile, list ? tilesOf(list).indexOf(tile) : 0);
|
|
123
|
+
if (!product) return;
|
|
124
|
+
analytics.productClicked({
|
|
125
|
+
list_id: list ? (listFields(list)?.list_id ?? null) : null,
|
|
126
|
+
products: [product],
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
document.addEventListener('click', onClick, true);
|
|
131
|
+
|
|
132
|
+
return () => {
|
|
133
|
+
document.removeEventListener('click', onClick, true);
|
|
134
|
+
stopWatching();
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { firstAttr } from './attributes.js';
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_DWELL_MS,
|
|
4
|
+
DEFAULT_VISIBLE_RATIO,
|
|
5
|
+
watchViewability,
|
|
6
|
+
} from './viewability.js';
|
|
7
|
+
|
|
8
|
+
/** Read a promotion off its `data-hp-promotion*` attributes. */
|
|
9
|
+
function defaultResolve(el, index) {
|
|
10
|
+
const promotionId = firstAttr(el, 'hpPromotion');
|
|
11
|
+
if (!promotionId) return null;
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
promotion_id: promotionId,
|
|
15
|
+
name: firstAttr(el, 'hpPromotionName'),
|
|
16
|
+
creative: firstAttr(el, 'hpPromotionCreative'),
|
|
17
|
+
// Segment's `position` (GA4 calls it creative_slot): a named slot, or the 1-based rank down
|
|
18
|
+
// the page as a string.
|
|
19
|
+
position: firstAttr(el, 'hpPromotionSlot') ?? String(index + 1),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Watch promotional slots and report impressions and clicks.
|
|
25
|
+
*
|
|
26
|
+
* @param {object} analytics An SDK instance from createAnalytics.
|
|
27
|
+
* @param {object} [options]
|
|
28
|
+
* @param {string} [options.selector] Which elements are promotional slots.
|
|
29
|
+
* @param {(el: Element, index: number) => object|null} [options.resolve] Element -> spec fields.
|
|
30
|
+
* Returning null skips the element, which is how a slot with no stable id opts out.
|
|
31
|
+
* @param {number} [options.visibleRatio] Fraction on screen that counts as visible.
|
|
32
|
+
* @param {number} [options.dwellMs] How long it must stay visible.
|
|
33
|
+
* @param {Document|Element} [options.root] Where to look. Defaults to the document.
|
|
34
|
+
* @returns {() => void} Teardown, for a SPA route change.
|
|
35
|
+
*/
|
|
36
|
+
export function trackPromotions(analytics, options = {}) {
|
|
37
|
+
if (typeof window === 'undefined' || typeof document === 'undefined')
|
|
38
|
+
return () => {};
|
|
39
|
+
|
|
40
|
+
const {
|
|
41
|
+
selector = '[data-hp-promotion]',
|
|
42
|
+
resolve = defaultResolve,
|
|
43
|
+
visibleRatio = DEFAULT_VISIBLE_RATIO,
|
|
44
|
+
dwellMs = DEFAULT_DWELL_MS,
|
|
45
|
+
root = document,
|
|
46
|
+
} = options;
|
|
47
|
+
|
|
48
|
+
// Keyed by promotion id, not element: a re-rendered carousel replaces the node, not the banner.
|
|
49
|
+
const seen = new Set();
|
|
50
|
+
|
|
51
|
+
const fieldsFor = (el) => {
|
|
52
|
+
// Ranked among the slots in the document now, in document order.
|
|
53
|
+
const all = Array.from(root.querySelectorAll(selector));
|
|
54
|
+
const index = all.indexOf(el);
|
|
55
|
+
try {
|
|
56
|
+
return resolve(el, index < 0 ? all.length : index);
|
|
57
|
+
} catch {
|
|
58
|
+
// A resolver that throws on one odd slot must not stop the rest of the page reporting.
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const stopWatching = watchViewability({
|
|
64
|
+
selector,
|
|
65
|
+
root,
|
|
66
|
+
visibleRatio,
|
|
67
|
+
dwellMs,
|
|
68
|
+
onViewable(el) {
|
|
69
|
+
const fields = fieldsFor(el);
|
|
70
|
+
if (!fields || seen.has(fields.promotion_id)) return false;
|
|
71
|
+
seen.add(fields.promotion_id);
|
|
72
|
+
analytics.promotionViewed(fields);
|
|
73
|
+
return true;
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Capture phase: banner components often stopPropagation, which would hide them from a bubble
|
|
78
|
+
// listener. Only a link is a click-through: a slider's arrows, a tab or a video's play button
|
|
79
|
+
// inside the slot are not.
|
|
80
|
+
const onClick = (event) => {
|
|
81
|
+
const target = event.target;
|
|
82
|
+
if (!target || typeof target.closest !== 'function') return;
|
|
83
|
+
const el = target.closest(selector);
|
|
84
|
+
const anchor = target.closest('a[href]');
|
|
85
|
+
if (!el || !anchor) return;
|
|
86
|
+
// The link must belong to the slot: inside it, or wrapping it.
|
|
87
|
+
if (!el.contains(anchor) && !anchor.contains(el)) return;
|
|
88
|
+
|
|
89
|
+
const fields = fieldsFor(el);
|
|
90
|
+
if (!fields) return;
|
|
91
|
+
|
|
92
|
+
analytics.promotionClicked({
|
|
93
|
+
...fields,
|
|
94
|
+
href: anchor.getAttribute('href'),
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
document.addEventListener('click', onClick, true);
|
|
99
|
+
|
|
100
|
+
return () => {
|
|
101
|
+
document.removeEventListener('click', onClick, true);
|
|
102
|
+
stopWatching();
|
|
103
|
+
};
|
|
104
|
+
}
|