@gradial/aci 0.1.41 → 0.1.43

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.
Files changed (134) hide show
  1. package/CHANGELOG.md +15 -11
  2. package/README.md +4 -4
  3. package/dist/astro/index.d.ts +1 -1
  4. package/dist/astro/index.js +1 -1
  5. package/dist/astro/page.d.ts +6 -0
  6. package/dist/astro/page.js +3 -2
  7. package/dist/audience-data-models.d.ts +286 -0
  8. package/dist/audience-data-models.js +46 -0
  9. package/dist/cli/compile-registry.d.mts +6 -0
  10. package/dist/cli/compile-registry.mjs +774 -0
  11. package/dist/cli/materialize-template.d.mts +19 -0
  12. package/dist/cli/materialize-template.mjs +128 -0
  13. package/dist/cli/validate-content.d.mts +32 -0
  14. package/dist/cli/validate-content.mjs +464 -0
  15. package/dist/cli/verify-next-cdn-routes.d.mts +2 -0
  16. package/dist/cli/verify-next-cdn-routes.mjs +47 -0
  17. package/dist/collect/aci.js +1 -0
  18. package/dist/collect/aci.js.sha256 +1 -0
  19. package/dist/collect/assignments.d.ts +65 -0
  20. package/dist/collect/assignments.js +138 -0
  21. package/dist/collect/batcher.d.ts +23 -0
  22. package/dist/collect/batcher.js +28 -6
  23. package/dist/collect/consent/envelope.d.ts +9 -3
  24. package/dist/collect/consent/envelope.js +58 -40
  25. package/dist/collect/declarative.d.ts +1 -1
  26. package/dist/collect/declarative.js +7 -1
  27. package/dist/collect/index.js +39 -6
  28. package/dist/collect/linkclicks.js +4 -2
  29. package/dist/collect/pageviews.d.ts +15 -3
  30. package/dist/collect/pageviews.js +28 -13
  31. package/dist/collect/transport.d.ts +4 -0
  32. package/dist/collect/transport.js +37 -2
  33. package/dist/consent/portable-envelope.d.ts +9 -0
  34. package/dist/consent/portable-envelope.js +110 -0
  35. package/dist/consent-banner/index.d.ts +33 -0
  36. package/dist/consent-banner/index.js +100 -0
  37. package/dist/content/assets.d.ts +1 -0
  38. package/dist/content/assets.js +1 -0
  39. package/dist/content/personalization.d.ts +35 -0
  40. package/dist/content/personalization.js +57 -0
  41. package/dist/content/resolve-slots.js +1 -0
  42. package/dist/content/types.d.ts +9 -1
  43. package/dist/edge/personalization-core.d.ts +11 -0
  44. package/dist/edge/personalization-core.js +265 -0
  45. package/dist/index.d.ts +1 -0
  46. package/dist/index.js +1 -0
  47. package/dist/next/assignments-script.d.ts +14 -0
  48. package/dist/next/assignments-script.js +17 -0
  49. package/dist/next/collect-route.d.ts +19 -0
  50. package/dist/next/collect-route.js +422 -0
  51. package/dist/next/envelope-digest.d.ts +21 -0
  52. package/dist/next/envelope-digest.js +58 -0
  53. package/dist/next/index.d.ts +1 -0
  54. package/dist/next/index.js +1 -0
  55. package/dist/next/middleware.d.ts +31 -3
  56. package/dist/next/middleware.js +254 -30
  57. package/dist/next/page.js +2 -1
  58. package/dist/next/personalization-host.d.ts +127 -0
  59. package/dist/next/personalization-host.js +257 -0
  60. package/dist/next/server.d.ts +1 -0
  61. package/dist/next/server.js +9 -6
  62. package/dist/next/vercel-collect-sdk-route.d.ts +27 -0
  63. package/dist/next/vercel-collect-sdk-route.js +205 -0
  64. package/dist/next/vercel-personalization.d.ts +4 -0
  65. package/dist/next/vercel-personalization.js +4 -0
  66. package/dist/next/vercel-s3-personalization.d.ts +79 -0
  67. package/dist/next/vercel-s3-personalization.js +574 -0
  68. package/dist/providers/s3.d.ts +20 -5
  69. package/dist/providers/s3.js +93 -23
  70. package/dist/react/VideoPlayer.d.ts +6 -6
  71. package/dist/react/index.d.ts +6 -6
  72. package/dist/testing/conformance/checks.js +75 -4
  73. package/dist/types/block-ref.d.ts +2 -0
  74. package/dist/types/block-ref.js +1 -0
  75. package/dist/types/page.d.ts +1 -1
  76. package/package.json +44 -15
  77. package/src/astro/ConsentBanner.astro +62 -0
  78. package/src/cli/compile-registry.mjs +24 -10
  79. package/src/cli/validate-content.mjs +52 -13
  80. package/src/cli/verify-next-cdn-routes.mjs +5 -4
  81. package/src/consent-banner/styles.css +110 -0
  82. package/src/types/block-ref.test.ts +45 -0
  83. package/src/types/block-ref.ts +2 -0
  84. package/src/types/page.ts +1 -1
  85. package/templates/astro/starter-content/src/cms/contracts/components/index.ts +2 -0
  86. package/templates/astro/starter-content/src/cms/contracts/components/personalizationChoice.contract.ts +17 -0
  87. package/templates/astro/starter-content/src/cms/registry.ts +3 -0
  88. package/templates/astro/starter-content/src/components/sections/PersonalizationChoice.astro +41 -0
  89. package/templates/astro/starter-content/src/fixtures/atomicBlocks.ts +7 -6
  90. package/templates/astro/starter-content/src/fixtures/composition.ts +3 -2
  91. package/templates/astro/starter-content/src/fixtures/sections.ts +2 -1
  92. package/templates/astro/starter-content/src/render/RenderPage.astro +3 -2
  93. package/templates/astro/template/src/layouts/SiteLayout.astro +3 -1
  94. package/templates/astro/template/src/pages/[...slug].astro +1 -0
  95. package/templates/common/.agents/skills/authoring/SKILL.md +11 -3
  96. package/templates/common/.agents/skills/authoring/references/block-slots.md +3 -0
  97. package/templates/common/.content/audiences/preference-blue/_index.json +11 -0
  98. package/templates/common/.content/audiences/preference-red/_index.json +11 -0
  99. package/templates/common/.content/overlays/audience/preference-blue/pages/home/_index.json +13 -0
  100. package/templates/common/.content/overlays/audience/preference-red/pages/home/_index.json +13 -0
  101. package/templates/common/.content/pages/home/_index.json +124 -4
  102. package/templates/common/.content/segments/preference-blue/_index.json +20 -0
  103. package/templates/common/.content/segments/preference-red/_index.json +20 -0
  104. package/templates/common/contracts/components/index.ts +21 -1
  105. package/templates/common/contracts/components/personalizationChoice.contract.ts +17 -0
  106. package/templates/common/starter-content/.content/pages/about/_index.json +3 -0
  107. package/templates/common/starter-content/.content/pages/home/_index.json +9 -0
  108. package/templates/nextjs/starter-content/src/cms/contracts/components/index.ts +3 -1
  109. package/templates/nextjs/starter-content/src/cms/contracts/components/personalizationChoice.contract.ts +17 -0
  110. package/templates/nextjs/starter-content/src/cms/registry.ts +3 -0
  111. package/templates/nextjs/starter-content/src/components/sections/PersonalizationChoice.tsx +53 -0
  112. package/templates/nextjs/starter-content/src/middleware.ts +41 -0
  113. package/templates/nextjs/template/src/app/collect/route.ts +18 -0
  114. package/templates/nextjs/template/src/middleware.ts +36 -2
  115. package/templates/nextjs/template/src/middleware.ts.tmpl +31 -1
  116. package/templates/astro/template/.aci.yaml +0 -28
  117. package/templates/common/.content/.gradial-dam/blobs/sha256/7f/bf/7fbfc7fe230ff12688c496f660119e41d9725c4504d82e310c15665c73c09292 +0 -0
  118. package/templates/common/.content/.gradial-dam/blobs/sha256/a0/f5/a0f506689de9b15a7055f70103a3f02cd3ba105771ae2de6754df2ede1210d3a +0 -0
  119. package/templates/common/.content/.gradial-dam/blobs/sha256/dd/b7/ddb7e9642be08317bcd4b2bd26b58ba8d0d34dd7239e03e684a4f3634fa3845b +0 -0
  120. package/templates/common/.content/.gradial-dam/blobs/sha256/fb/32/fb3260e8286ea8231ea1c77b9c0be92d6edd9026565731eb14cfdcfd6b1b75b3 +0 -0
  121. package/templates/common/.content/config/robots.txt +0 -2
  122. package/templates/common/.content/config/site.json +0 -197
  123. package/templates/common/.content/dam/folders/seed/ast_site_about_hero/asset.json +0 -9
  124. package/templates/common/.content/dam/folders/seed/ast_site_about_hero/editorial.json +0 -14
  125. package/templates/common/.content/dam/folders/seed/ast_site_about_hero/governance.json +0 -23
  126. package/templates/common/.content/dam/folders/seed/ast_site_about_hero/versions/v0001.json +0 -37
  127. package/templates/common/.content/dam/folders/seed/ast_site_home_hero/asset.json +0 -9
  128. package/templates/common/.content/dam/folders/seed/ast_site_home_hero/editorial.json +0 -14
  129. package/templates/common/.content/dam/folders/seed/ast_site_home_hero/governance.json +0 -23
  130. package/templates/common/.content/dam/folders/seed/ast_site_home_hero/versions/v0001.json +0 -37
  131. package/templates/common/.content/fragments/navbar/_index.json +0 -25
  132. package/templates/common/.content/fragments/site-footer/_index.json +0 -62
  133. package/templates/common/.content/pages/about/_index.json +0 -51
  134. package/templates/nextjs/template/.aci.yaml +0 -26
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ const projectRoot = process.argv[2] || '.';
5
+ const checks = [];
6
+ const missing = [];
7
+ function checkFile(label, relativePath, validate, message) {
8
+ const fullPath = path.join(projectRoot, relativePath);
9
+ if (!fs.existsSync(fullPath)) {
10
+ missing.push(`✗ ${label}: ${relativePath} not found`);
11
+ return;
12
+ }
13
+ const content = fs.readFileSync(fullPath, 'utf-8');
14
+ if (!validate(content)) {
15
+ checks.push(`✗ ${label}: ${message}`);
16
+ return;
17
+ }
18
+ checks.push(`✓ ${label}`);
19
+ }
20
+ function checkMissing(label, relativePath, message) {
21
+ const fullPath = path.join(projectRoot, relativePath);
22
+ if (fs.existsSync(fullPath)) {
23
+ checks.push(`✗ ${label}: ${relativePath} should be removed. ${message}`);
24
+ return;
25
+ }
26
+ checks.push(`✓ ${label}`);
27
+ }
28
+ checkFile('middleware is configured', 'src/middleware.ts', (content) => (content.includes('@gradial/aci/next/middleware') || content.includes('createGradialMiddleware'))
29
+ && !content.includes('|__r'), 'Expected src/middleware.ts to use @gradial/aci/next/middleware and not exclude __r; middleware blocks direct /__r requests.');
30
+ checkFile('catch-all page route exists', 'src/app/[[...slug]]/page.tsx', (content) => content.includes('createPage') || content.includes('getRenderInput'), 'Expected catch-all page to use createPage() from the SDK.');
31
+ checkMissing('legacy required catch-all is removed', 'src/app/[...slug]/page.tsx', 'Use src/app/[[...slug]]/page.tsx (optional catch-all). The required [...slug] form breaks file-provider local dev where "/" arrives with an empty slug.');
32
+ checkFile('canonical asset route exists', 'src/app/gradial/assets/[...path]/route.ts', (content) => content.includes('@gradial/aci/next/asset-route'), 'Expected the canonical asset route to use @gradial/aci/next/asset-route.');
33
+ checkMissing('legacy release-scoped asset route is removed', 'src/app/gradial/assets/[releaseId]/[...path]/route.ts', 'Release-scoped asset URLs are produced by withAci() rewrites into the canonical src/app/gradial/assets/[...path]/route.ts; do not add a [releaseId] route.');
34
+ checkMissing('no separate __r route folder', 'src/app/__r/[releaseId]/routes/[[...slug]]/page.tsx', 'The root catch-all handles release-scoped rewrites. Do not create a separate __r route folder.');
35
+ checkFile('next.config uses withAci', 'next.config.mjs', (content) => content.includes('withAci'), 'Expected next.config.mjs to use withAci() from @gradial/aci/next.');
36
+ for (const line of checks) {
37
+ console.log(line);
38
+ }
39
+ for (const line of missing) {
40
+ console.log(line);
41
+ }
42
+ const failed = [...checks.filter((line) => line.startsWith('✗')), ...missing];
43
+ if (failed.length > 0) {
44
+ console.error(`\n${failed.length} check(s) failed`);
45
+ process.exit(1);
46
+ }
47
+ console.log('\nAll route checks passed');
@@ -0,0 +1 @@
1
+ "use strict";(()=>{var Ne=Object.defineProperty;var Le=(t,e,n)=>e in t?Ne(t,e,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[e]=n;var y=(t,e,n)=>Le(t,typeof e!="symbol"?e+"":e,n);var x="data-aci-event",Oe="data-aci-on",H="data-aci-prop-",De="data-aci-refresh",q="custom.";function D(t){if(typeof t!="string")return;let e=t.trim().toLowerCase();if(!(e===""||/[\s/]/.test(e)))return e.startsWith(q)?e:q+e}function Re(t){let e;for(let n of Array.from(t.attributes)){if(!n.name.startsWith(H))continue;let r=n.name.slice(H.length);r!==""&&((e!=null?e:e={})[r]=n.value)}return e}function U(t,e){let n=D(t.getAttribute(x));if(n===void 0){e==null||e("capture",{reason:"malformed_declaration",attr:x});return}return{eventName:n,properties:Re(t)}}function X(t){var e,n;return(n=(e=t.getAttribute(Oe))==null?void 0:e.trim().toLowerCase())!=null?n:"click"}function $(t,e,n){let{document:r,debug:o}=e;r.addEventListener("click",f=>{var l,p;try{let u=f.target;if((l=u==null?void 0:u.closest)==null?void 0:l.call(u,`[${De}]`)){n==null||n();return}let m=(p=u==null?void 0:u.closest)==null?void 0:p.call(u,`[${x}]`);if(!m||X(m)!=="click")return;let g=U(m,o);g&&t(g)}catch(u){o==null||o("capture",{reason:"click_handler_failed",error:String(u)})}});let i=e.intersectionObserver;if(typeof i!="function")return;let s=Array.from(r.querySelectorAll(`[${x}]`)).filter(f=>X(f)==="visible");if(s.length===0)return;let a=new WeakSet,c=new i((f,l)=>{try{for(let p of f){if(!p.isIntersecting||p.intersectionRatio<.5||a.has(p.target))continue;a.add(p.target),l.unobserve(p.target);let u=U(p.target,o);u&&t(u)}}catch(p){o==null||o("capture",{reason:"visible_handler_failed",error:String(p)})}},{threshold:.5});for(let f of s)c.observe(f)}function K(t){return typeof t=="object"&&t!==null&&!Array.isArray(t)}function Y(t,e,n,r){let o=(...a)=>{try{let[c,...f]=a;if(c==="track"){let l=D(f[0]);if(l===void 0){r==null||r("capture",{reason:"malformed_track_name",name:f[0]});return}let p=f[1];if(p!==void 0&&!K(p)){r==null||r("capture",{reason:"malformed_track_properties",name:l});return}n.track(l,p);return}if(c==="consent"){if(e.preset!=="manual"){r==null||r("consent",{reason:"preset_authoritative",preset:e.preset});return}let l=f[0];if(!K(l)){r==null||r("consent",{reason:"malformed_consent_envelope"});return}n.consent({p:l.p,src:l.src});return}r==null||r("capture",{reason:"unknown_command",command:c})}catch(c){r==null||r("capture",{reason:"command_failed",error:String(c)})}},i=t.aci,s=i==null?void 0:i.q;if(t.aci=o,s!=null)for(let a of Array.from(s)){let c=Array.from(a);o(...c)}}var Be="experiment.exposure",Fe='script[type="application/json"][data-aci-assignments]';function Ve(t){if(typeof t!="object"||t===null)return null;let e=t,n=e.experimentId,r=e.armId,o=e.allocationVersion;return typeof n!="string"||n===""||typeof r!="string"||r===""||typeof o!="string"?null:{experimentId:n,armId:r,allocationVersion:o}}function Me(t,e){try{let n=t.querySelector(Fe);if(!n)return[];let r=n.textContent;if(r==null||r.trim()==="")return[];let o=JSON.parse(r);if(typeof o!="object"||o===null)return e==null||e("capture",{reason:"assignments_malformed"}),[];let i=o;if(i.v!==1||!Array.isArray(i.experiments))return e==null||e("capture",{reason:"assignments_malformed"}),[];let s=[];for(let a of i.experiments){let c=Ve(a);c&&s.push(c)}return s}catch(n){return e==null||e("capture",{reason:"assignments_parse_failed",error:String(n)}),[]}}function J(t,e){if(!e||t.experiment!==void 0)return t;let n=e.getActiveAssignments()[0];if(!n)return t;let r={experimentId:n.experimentId,armId:n.armId,allocationVersion:n.allocationVersion};return{...t,experiment:r}}function Q(t){let e=[],n=new Set,r,o=()=>{e=Me(t.document,t.debug);for(let i of e)n.has(i.experimentId)||(n.add(i.experimentId),r==null||r({eventName:Be,url:t.window.location.href,experiment:{experimentId:i.experimentId,armId:i.armId,allocationVersion:i.allocationVersion}}))};return{getActiveAssignments:()=>e.map(i=>({...i})),start(i){r=i,o()},rearm(){n.clear(),o()}}}var Z="0123456789ABCDEFGHJKMNPQRSTVWXYZ";function We(t){globalThis.crypto.getRandomValues(t)}function S(t,e){let n=t!=null?t:Date.now(),r="";for(let c=0;c<10;c++)r=Z[n%32]+r,n=Math.floor(n/32);let o=new Uint8Array(10);(e!=null?e:We)(o);let i="",s=0,a=0;for(let c of o)for(s=s<<8|c,a+=8;a>=5;)i+=Z[s>>>a-5&31],a-=5;return r+i}var Ge=60*1024,je=100,ze=5e3,ee='{"envelopeId":"',te='","events":[',ne="]}",He=26,A=ee.length+He+te.length+ne.length,qe=new TextEncoder,Ue=t=>qe.encode(t).length,T=class{constructor(e){y(this,"options");y(this,"maxBytes");y(this,"queue",[]);y(this,"queuedBytes",0);y(this,"flushing",!1);y(this,"intervalId");var n;this.options=e,this.maxBytes=(n=e.maxBatchBytes)!=null?n:Ge}get pending(){return this.queue.length}push(e){let n=JSON.stringify(e),r=Ue(n);this.queue.push({json:n,bytes:r,eventId:e.eventId}),this.queuedBytes+=r,!this.flushing&&this.envelopeBytes()>=this.maxBytes&&this.flush()}flush(){var e,n;if(!this.flushing){this.flushing=!0;try{(n=(e=this.options).onTick)==null||n.call(e),this.drain()}finally{this.flushing=!1}}}start(){var o,i,s,a;let e=(o=this.options.timers)!=null?o:{setInterval:(c,f)=>setInterval(c,f),clearInterval:c=>clearInterval(c)};this.intervalId=e.setInterval(()=>this.flush(),(i=this.options.flushIntervalMs)!=null?i:ze);let n=(s=this.options.documentTarget)!=null?s:typeof document!="undefined"?document:void 0;n==null||n.addEventListener("visibilitychange",()=>{n.visibilityState==="hidden"&&this.flush()});let r=(a=this.options.windowTarget)!=null?a:typeof window!="undefined"?window:void 0;r==null||r.addEventListener("pagehide",()=>this.flush()),this.flush()}stop(){if(this.intervalId!==void 0){let e=this.options.timers;e?e.clearInterval(this.intervalId):clearInterval(this.intervalId),this.intervalId=void 0}}envelopeBytes(){let e=this.queue.length>1?this.queue.length-1:0;return A+this.queuedBytes+e}drain(){var e,n,r;for(;this.queue.length>0;){let o=this.queue[0];if(A+o.bytes>this.maxBytes){this.queue.shift(),this.queuedBytes-=o.bytes,(n=(e=this.options).debug)==null||n.call(e,"flush",{reason:"oversize_event",eventId:o.eventId,bytes:o.bytes});continue}let i=[],s=A;for(;this.queue.length>0;){let c=this.queue[0];if(A+c.bytes>this.maxBytes||i.length>=je)break;let f=s+c.bytes+(i.length>0?1:0);if(f>this.maxBytes)break;i.push(c.json),s=f,this.queue.shift(),this.queuedBytes-=c.bytes}if(i.length===0)continue;let a=((r=this.options.createEnvelopeId)!=null?r:S)();this.options.send(ee+a+te+i.join(",")+ne)}}};var Xe=new Set(["c15t","onetrust","cookiebot","trustarc","manual"]);function P(t,e){var o;let n=(o=t==null?void 0:t.getAttribute)==null?void 0:o.call(t,e);if(n==null)return;let r=n.trim();return r===""?void 0:r}function re(t){var r;let e=(r=P(t,"data-aci-consent"))==null?void 0:r.toLowerCase(),n=e!==void 0&&Xe.has(e)?e:"manual";return{site:P(t,"data-aci-site"),preset:n,categories:P(t,"data-aci-consent-categories"),debug:P(t,"data-aci-debug")==="1"}}function oe(t,e,n){if(!t)return()=>{};let r=e!=null?e:(i=>console.debug("[aci]",i)),o=n!=null?n:Date.now;return(i,s)=>{try{r({t:o(),kind:i,detail:s})}catch{}}}var R=new TextEncoder,$e=2048,Ke=32,Ye=128,Je=512,Qe=2147483647,Ze=/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))?(Z|[+-]\d{2}:\d{2})$/,et=/^[\t\n\v\f\r ]+|[\t\n\v\f\r ]+$/g,tt=new Set(["true","all","granted","yes","personalization","personalized"]);function _(t,e=!0){let n=w(t);if(!n)return null;if(e)try{n=decodeURIComponent(n)}catch{}if(n=w(n),!n||R.encode(n).byteLength>$e)return null;if(tt.has(n.toLowerCase()))return{version:0,purposes:["personalization"],grantedAt:""};try{let r=JSON.parse(n),o=typeof r.t=="string"?r.t:"";return!Number.isInteger(r.v)||r.v<0||r.v>Qe||!Array.isArray(r.p)||r.p.length>Ke||!r.p.every(i=>typeof i=="string")||r.p.some(i=>R.encode(i).byteLength>Ye)||r.t!==void 0&&r.t!==null&&(typeof r.t!="string"||r.t===""||!nt(r.t))||r.src!==void 0&&r.src!==null&&typeof r.src!="string"||typeof r.src=="string"&&R.encode(r.src).byteLength>Je?null:{version:r.v,purposes:r.p,grantedAt:o,source:typeof r.src=="string"?r.src:void 0}}catch{return null}}function B(t){return(t==null?void 0:t.purposes.some(e=>w(e).toLowerCase()==="personalization"))||!1}function w(t){return t.replace(et,"")}function nt(t){let e=Ze.exec(t);if(!e||Number.isNaN(Date.parse(t)))return!1;let n=Number(e[1]),r=Number(e[2]),o=Number(e[3]),i=Number(e[4]),s=Number(e[5]),a=Number(e[6]);if(r<1||r>12||o<1||o>rt(n,r)||i>23||s>59||a>59)return!1;let c=e[8];return c==="Z"||Number(c.slice(1,3))<=23&&Number(c.slice(4,6))<=59}function rt(t,e){return e===2?t%4===0&&(t%100!==0||t%400===0)?29:28:[4,6,9,11].includes(e)?30:31}var ie="personalization",F="aci_consent",V=1,ot=365*24*60*60;function it(t,e){var r;let n=`${F}=${t.encoded}; Path=/; Max-Age=${ot}; SameSite=Lax`;((r=e.document.location)==null?void 0:r.protocol)==="https:"&&(n+="; Secure"),e.document.cookie=n}function I(t,e,n){var o;let r=st(t,e,n.now);return r===null?!1:(B(r.snapshot)||(o=n.gapBuffer)==null||o.purge(),it(r,n),!0)}function se(t){for(let e of t.cookie.split(";")){let n=w(e);if(n.startsWith(F+"="))return n.slice(F.length+1)}}function ae(t){return t===void 0||t===""?!1:B(_(t,!0))}function ce(t){var r;if(!Array.isArray(t))return;let e=[];for(let o of t){if(typeof o!="string")return;e.push(w(o))}let n=JSON.stringify({v:V,p:e});return(r=_(n,!1))==null?void 0:r.purposes}function ue(t){var o;if(t!==void 0&&typeof t!="string")return;let e=typeof t=="string"?w(t):"",r=JSON.stringify({v:V,p:[],src:e===""?"manual":e});return(o=_(r,!1))==null?void 0:o.source}function st(t,e,n){try{let r=JSON.stringify({v:V,p:t,t:new Date((n!=null?n:Date.now)()).toISOString(),src:e}),o=_(r,!1);return o===null?null:{encoded:encodeURIComponent(r),snapshot:o}}catch{return null}}var at="cmp-",ct=/^[a-z0-9._-]+$/,ut={c15t:"measurement,marketing",onetrust:"c0002,c0004",cookiebot:"statistics,marketing",trustarc:"functional,advertising"};function M(t,e){let n=e!=null?e:ut[t];if(n===void 0)return null;let r=n.split(",").map(o=>o.trim().toLowerCase()).filter(o=>o!=="");if(r.length===0)return null;for(let o of r)if(!ct.test(o))return null;return r}function ft(t,e){return e.filter(r=>r in t.categories).some(r=>t.categories[r]===!0)?[ie]:[]}function k(t,e,n){var r,o,i,s;try{let a=M(e,n.categories);if(a===null){(r=n.debug)==null||r.call(n,"consent",{reason:"unparseable_mapping",preset:e});return}if(a.filter(l=>l in t.categories).length===0){(o=n.debug)==null||o.call(n,"consent",{reason:"unmappable_cmp_state",preset:e});return}let f=ft(t,a);I(f,at+e,{document:n.document,gapBuffer:n.gapBuffer,now:n.now}),(i=n.debug)==null||i.call(n,"consent",{reason:"written",preset:e,purposes:f})}catch(a){(s=n.debug)==null||s.call(n,"consent",{reason:"consent_bridge_failed",preset:e,error:String(a)})}}var fe=250,lt=1e4;function C(t,e,n){var s;let r=(s=n==null?void 0:n.scheduler)!=null?s:{setTimeout:(a,c)=>setTimeout(a,c),clearTimeout:a=>clearTimeout(a)},o=Math.floor(lt/fe),i=()=>{var c,f;let a;try{a=t()}catch(l){(c=n==null?void 0:n.debug)==null||c.call(n,"consent",{reason:"cmp_read_failed",error:String(l)});return}if(a!==void 0){e(a);return}if(o-=1,o<=0){(f=n==null?void 0:n.debug)==null||f.call(n,"consent",{reason:"cmp_not_found"});return}r.setTimeout(i,fe)};i()}function b(t,e){try{e()}catch(n){t==null||t("consent",{reason:"adapter_failed",error:String(n)})}}var dt="c15t";function pt(t){if(typeof t!="object"||t===null)return;let e={};for(let[n,r]of Object.entries(t))typeof r=="boolean"&&(e[n.toLowerCase()]=r);return{categories:e}}function mt(t,e){return typeof t=="object"&&t!==null?"consents"in t?t.consents:t:e.consents}function le(t){C(()=>{let e=t.window.c15t;return typeof e=="object"&&e!==null?e:void 0},e=>{b(t.debug,()=>{var r;let n=o=>{let i=pt(o);i&&k(i,dt,t)};n(e.consents),(r=e.onConsentChange)==null||r.call(e,o=>b(t.debug,()=>n(mt(o,e))))})},t)}var gt="cookiebot",vt=["CookiebotOnConsentReady","CookiebotOnAccept","CookiebotOnDecline"];function de(t){let e=t.Cookiebot;return typeof e=="object"&&e!==null?e:void 0}function ht(t){if(typeof t!="object"||t===null)return;let e={};for(let[n,r]of Object.entries(t))typeof r=="boolean"&&(e[n.toLowerCase()]=r);return{categories:e}}function pe(t){let e=()=>{let r=de(t.window);if(!r||r.hasResponse!==!0)return;let o=ht(r.consent);o&&k(o,gt,t)},n=t.window;if(typeof n.addEventListener=="function")for(let r of vt)n.addEventListener(r,()=>b(t.debug,e));C(()=>de(t.window),()=>b(t.debug,e),t)}var me="onetrust",bt=/^c\d{4}$/;function yt(t,e){let n={};for(let r of t.split(",")){let o=r.trim().toLowerCase();o!==""&&(n[o]=!0)}for(let r of e!=null?e:[])!(r in n)&&bt.test(r)&&(n[r]=!1);return{categories:n}}function ge(t){let e=t.window,n=()=>{let o=e.OnetrustActiveGroups;if(typeof o!="string")return;let i=M(me,t.categories);k(yt(o,i),me,t)},r=e.OptanonWrapper;e.OptanonWrapper=()=>{if(typeof r=="function")try{r()}catch{}b(t.debug,n)},C(()=>{let o=e.OnetrustActiveGroups;return typeof o=="string"?o:void 0},()=>b(t.debug,n),t)}var Et="trustarc",kt={required:1,functional:2,advertising:3},ve="PrivacyManagerAPI";function he(t){let e=t.truste,n=e==null?void 0:e.cma,r=n==null?void 0:n.callApi;return typeof r=="function"?r.bind(n):void 0}function Ct(t){if(typeof t!="object"||t===null)return;let e=t.consentDecision;if(typeof e!="number"||!Number.isFinite(e)||e<1)return;let n={};for(let[r,o]of Object.entries(kt))n[r]=e>=o;return{categories:n}}function wt(t){return typeof t=="string"?t.includes(ve):typeof t=="object"&&t!==null&&ve in t}function St(t){let e=t.location;return typeof(e==null?void 0:e.hostname)=="string"?e.hostname:void 0}function be(t){let e=()=>{let r=he(t.window);if(!r)return;let o=Ct(r("getConsentDecision",St(t.window)));o&&k(o,Et,t)},n=t.window;typeof n.addEventListener=="function"&&n.addEventListener("message",r=>b(t.debug,()=>{wt(r==null?void 0:r.data)&&e()})),C(()=>he(t.window),()=>b(t.debug,e),t)}var N="aci:buf";var ye="aci_m",L=class{constructor(e){y(this,"events");y(this,"storage");y(this,"debug");this.debug=e==null?void 0:e.debug,this.storage=e==null?void 0:e.storage,this.events=this.restore()}push(e){var n;this.events.push(e),this.events.length>50&&(this.events.splice(0,this.events.length-50),(n=this.debug)==null||n.call(this,"buffer",{reason:"cap_drop_oldest"})),this.persist()}drain(){let e=this.events;return this.events=[],this.persist(),e}purge(){var e;this.events.length>0&&((e=this.debug)==null||e.call(this,"buffer",{reason:"purged",count:this.events.length})),this.events=[],this.persist()}size(){return this.events.length}restore(){var r;let e=this.storage;if(!e)return[];let n;try{n=e.getItem(N)}catch(o){return this.storage=void 0,(r=this.debug)==null||r.call(this,"buffer",{reason:"storage_unavailable",error:String(o)}),[]}if(n==null)return[];try{let o=JSON.parse(n);if(Array.isArray(o))return o}catch{}try{e.removeItem(N)}catch{this.storage=void 0}return[]}persist(){var n;let e=this.storage;if(e)try{this.events.length===0?e.removeItem(N):e.setItem(N,JSON.stringify(this.events))}catch(r){this.storage=void 0,(n=this.debug)==null||n.call(this,"buffer",{reason:"storage_unavailable",error:String(r)})}}};function Ee(t){for(let e of t.cookie.split(";")){let n=e.trim();if(n.startsWith(ye+"="))return n.slice(ye.length+1)!==""}return!1}function ke(t,e){if(e.grants()&&!e.marker()){e.gapBuffer.push(t);return}e.batcher.push(t)}function W(t,e,n,r){if(t.size()===0||!n())return;let o=t.drain();for(let i of o)e.push(i);r==null||r("buffer",{reason:"flushed",count:o.length})}var _t="rum.link_click";function Ce(t,e){var i;let{document:n,debug:r}=e,o=(i=n.defaultView)==null?void 0:i.location;n.addEventListener("click",s=>{var a,c,f;try{let l=s.target,p=(a=l==null?void 0:l.closest)==null?void 0:a.call(l,"a[href]");if(!p)return;let u=p;t({eventName:_t,url:o==null?void 0:o.href,properties:{linkText:(f=(c=u.textContent)==null?void 0:c.trim())!=null?f:"",linkHref:u.href}})}catch(l){r==null||r("capture",{reason:"link_click_failed",error:String(l)})}},{capture:!0})}var xt="rum.page_view";function we(t){let e=t.indexOf("#");return e===-1?t:t.slice(0,e)}function Se(t,e){let n=we(t.location.href),r=()=>{let s=we(t.location.href);if(s!==n){n=s;for(let a of e)a()}},o=t.history,i=s=>{let a=o[s].bind(o);o[s]=(...c)=>{let f=a(...c);return r(),f}};i("pushState"),i("replaceState"),t.addEventListener("popstate",r)}function _e(t,e){let n=()=>t({eventName:xt,url:e.location.href,referrer:e.document.referrer});return n(),n}var G="/collect";function xe(t,e){let n=e==null?void 0:e.debug,r=e!==void 0&&"navigator"in e?e.navigator:globalThis.navigator,o=e!==void 0&&"fetch"in e?e.fetch:globalThis.fetch,i;try{i=new Blob([t],{type:"text/plain"})}catch{n==null||n("flush",{reason:"no_blob"});return}if(r!=null&&r.sendBeacon)try{if(r.sendBeacon(G,i))return}catch{}if(!o){n==null||n("flush",{reason:"no_transport"});return}try{o(G,{method:"POST",body:i,keepalive:!0}).catch(s=>{n==null||n("flush",{reason:"fetch_failed",error:String(s)})})}catch(s){n==null||n("flush",{reason:"fetch_failed",error:String(s)})}}function Ae(t){var n,r;let e=t!==void 0&&"fetch"in t?t.fetch:globalThis.fetch;if(!e)return Promise.resolve(!1);try{let o=((n=t==null?void 0:t.createEnvelopeId)!=null?n:S)(),i=new Blob([`{"envelopeId":"${o}","events":[]}`],{type:"text/plain"});return e(G,{method:"POST",body:i,keepalive:!0,credentials:"same-origin"}).then(s=>{var a;return typeof s=="object"&&s!==null&&"ok"in s&&s.ok===!1?((a=t==null?void 0:t.debug)==null||a.call(t,"flush",{reason:"refresh_failed",status:"not_ok"}),!1):!0}).catch(s=>{var a;return(a=t==null?void 0:t.debug)==null||a.call(t,"flush",{reason:"refresh_failed",error:String(s)}),!1})}catch(o){return(r=t==null?void 0:t.debug)==null||r.call(t,"flush",{reason:"refresh_failed",error:String(o)}),Promise.resolve(!1)}}var At="rum.variation_view",j="data-aci-variation";function Te(t,e){var f,l,p;let{document:n,debug:r}=e,o=e.MutationObserver;if(typeof o!="function")return;let i=(f=n.defaultView)==null?void 0:f.location,s=new WeakSet,a=u=>{let h=u.getAttribute(j);h!=null&&(s.has(u)||(s.add(u),t({eventName:At,url:i==null?void 0:i.href,properties:{variantKey:h}})))},c=u=>{try{u.hasAttribute(j)&&a(u);for(let h of Array.from(u.querySelectorAll(`[${j}]`)))a(h)}catch(h){r==null||r("capture",{reason:"variation_scan_failed",error:String(h)})}};try{let u=(l=n.body)!=null?l:n.documentElement;u&&c(u)}catch(u){r==null||r("capture",{reason:"variation_initial_scan_failed",error:String(u)})}try{let u=(p=n.body)!=null?p:n.documentElement;if(!u)return;new o(m=>{try{for(let g of m)for(let d=0;d<g.addedNodes.length;d++){let v=g.addedNodes[d];if((v==null?void 0:v.nodeType)!==1)continue;let E=v.element;E&&c(E)}}catch(g){r==null||r("capture",{reason:"variation_observer_failed",error:String(g)})}}).observe(u,{subtree:!0,childList:!0})}catch(u){r==null||r("capture",{reason:"variation_observe_failed",error:String(u)})}}var Tt="rum.web_vital";function Pe(t,e){var h,m,g;let n=e.PerformanceObserver;if(typeof n!="function")return;let{performance:r,debug:o}=e,i=(m=(h=e.window)==null?void 0:h.location)==null?void 0:m.href,s=e.document,a=new Set,c=(d,v)=>{a.has(d)||(a.add(d),t({eventName:Tt,url:i,properties:{vitalName:d,vitalValue:v}}))};try{let d=r==null?void 0:r.getEntriesByType("navigation")[0];d&&typeof d.responseStart=="number"&&typeof d.requestStart=="number"&&c("TTFB",d.responseStart-d.requestStart)}catch(d){o==null||o("capture",{reason:"ttfb_failed",error:String(d)})}let f=0,l=0,p=(d,v)=>{try{new n(Ie=>{try{for(let O of Ie.getEntries())v(O)}catch(O){o==null||o("capture",{reason:`${d}_handler_failed`,error:String(O)})}}).observe({type:d,buffered:!0})}catch(E){o==null||o("capture",{reason:`${d}_observe_failed`,error:String(E)})}};p("largest-contentful-paint",d=>{c("LCP",d.startTime)}),p("first-input",d=>{c("FID",d.startTime)}),p("layout-shift",d=>{var v;d.hadRecentInput||(f+=(v=d.value)!=null?v:0)}),p("event",d=>{var E;let v=(E=d.value)!=null?E:0;v>l&&(l=v)}),p("paint",d=>{d.name==="first-contentful-paint"&&c("FCP",d.startTime)});let u=()=>{try{a.has("CLS")||c("CLS",f),a.has("INP")||c("INP",l)}catch(d){o==null||o("capture",{reason:"visibility_handler_failed",error:String(d)})}};s&&s.addEventListener("visibilitychange",()=>{s.visibilityState==="hidden"&&u()}),(g=e.window)==null||g.addEventListener("pagehide",u)}var z={c15t:le,onetrust:ge,cookiebot:pe,trustarc:be};function Pt(t){var e,n;return typeof((e=t.navigator)==null?void 0:e.sendBeacon)=="function"&&typeof t.IntersectionObserver=="function"&&typeof((n=t.crypto)==null?void 0:n.getRandomValues)=="function"}function It(t){try{return t.sessionStorage}catch{return}}function Nt(t){var o;let e=t!=null?t:typeof window!="undefined"?window:void 0;if(!e||e.__aci_loaded||(e.__aci_loaded=!0,!Pt(e)))return;let n=re(e.document.currentScript),r=oe(n.debug);try{let i=e.document,s=new L({storage:It(e),debug:r}),a=()=>{try{return ae(se(i))}catch{return!1}},c=()=>{try{return Ee(i)}catch{return!1}},f={navigator:e.navigator,fetch:typeof e.fetch=="function"?e.fetch.bind(e):void 0,debug:r},l;l=new T({send:m=>xe(m,f),debug:r,onTick:()=>W(s,l,c,r),timers:{setInterval:(m,g)=>e.setInterval(m,g),clearInterval:m=>e.clearInterval(m)},documentTarget:i,windowTarget:e});let p=Q({document:i,window:e,debug:r}),u=m=>{ke({...J(m,p),eventId:S(void 0,g=>e.crypto.getRandomValues(g))},{grants:a,marker:c,gapBuffer:s,batcher:l})};p.start(u),Y(e,n,{track:(m,g)=>u({eventName:m,properties:g}),consent:({p:m,src:g})=>{let d=ce(m);if(d===void 0){r("consent",{reason:"malformed_purposes"});return}let v=ue(g);if(v===void 0){r("consent",{reason:"malformed_source"});return}I(d,v,{document:i,gapBuffer:s})||r("consent",{reason:"oversized_envelope"})}},r);let h=_e(u,e);Se(e,[()=>p.rearm(),h]),$(u,{document:i,intersectionObserver:e.IntersectionObserver,debug:r},()=>{Ae(f).then(m=>{m&&e.location.reload()})}),Ce(u,{document:i,debug:r}),Pe(u,{performance:e.performance,PerformanceObserver:e.PerformanceObserver,window:e,document:i,debug:r}),Te(u,{document:i,MutationObserver:e.MutationObserver,debug:r}),n.preset!=="manual"&&((o=z[n.preset])==null||o.call(z,{window:e,document:i,categories:n.categories,gapBuffer:s,debug:r,scheduler:{setTimeout:(m,g)=>e.setTimeout(m,g),clearTimeout:m=>e.clearTimeout(m)}})),l.start(),W(s,l,c,r),r("init",{config:n})}catch(i){r("init",{error:String(i)})}}Nt();})();
@@ -0,0 +1 @@
1
+ 46f87bbce4c938e9540939102be1094f5cd956d0f3a5836e9af5c3b0b8c582ad
@@ -0,0 +1,65 @@
1
+ import type { PendingEvent } from "./batcher.js";
2
+ import type { DebugFn } from "./config.js";
3
+ export declare const EXPERIMENT_EXPOSURE_EVENT = "experiment.exposure";
4
+ /** One active assignment read from the page's data-aci-assignments tag. */
5
+ export interface ExperimentAssignment {
6
+ experimentId: string;
7
+ armId: string;
8
+ allocationVersion: string;
9
+ }
10
+ interface DocumentLike {
11
+ querySelector(selectors: string): {
12
+ textContent: string | null;
13
+ } | null;
14
+ }
15
+ /**
16
+ * readAssignments parses the page's data-aci-assignments tag. Absence, a
17
+ * malformed body, or an unknown version are tolerated as "no assignments"
18
+ * (debug record, no throw).
19
+ */
20
+ export declare function readAssignments(doc: DocumentLike, debug?: DebugFn): ExperimentAssignment[];
21
+ export interface AssignmentsEnv {
22
+ document: DocumentLike;
23
+ /** Only location.href is read (the exposure event's url). */
24
+ window: {
25
+ location: {
26
+ href: string;
27
+ };
28
+ };
29
+ debug?: DebugFn;
30
+ }
31
+ export interface AssignmentsHandle {
32
+ /** The assignments active for the current page view (copies). */
33
+ getActiveAssignments(): ExperimentAssignment[];
34
+ /**
35
+ * start reads the assignments tag and emits `experiment.exposure` once per
36
+ * (experiment, page view) through `emit`. Called once by init AFTER the
37
+ * emit pipeline is fully constructed — the handle is created first so
38
+ * emit's withAssignmentContext dependency exists before emit can run.
39
+ */
40
+ start(emit: (event: PendingEvent) => void): void;
41
+ /**
42
+ * rearm resets the per-page-view exposure dedupe and re-reads the tag.
43
+ * Wired by init into the shared navigation hook, ORDERED BEFORE the SPA
44
+ * page-view emitter so rum.page_view is stamped with the new page's
45
+ * context (REQ-2/3; see the module-header stale-tag limitation).
46
+ */
47
+ rearm(): void;
48
+ }
49
+ /**
50
+ * withAssignmentContext stamps the first active assignment's experiment
51
+ * context onto an event that does not already carry one (REQ-3). Events with
52
+ * explicit context (exposures, explicit conversions) are never overwritten.
53
+ */
54
+ export declare function withAssignmentContext(event: PendingEvent, handle: AssignmentsHandle | undefined): PendingEvent;
55
+ /**
56
+ * createAssignments builds the assignments collector without emitting
57
+ * anything: creation and exposure emission are split so init can construct
58
+ * the emit pipeline against the handle before any event flows (no circular
59
+ * "emit captures a not-yet-assigned handle" wiring). start() reads the tag
60
+ * and emits exposures; rearm() is the SPA-navigation reset, fanned out from
61
+ * the single navigation hook installed by init — so an "exposure per page
62
+ * view" matches a "page view" exactly.
63
+ */
64
+ export declare function createAssignments(env: AssignmentsEnv): AssignmentsHandle;
65
+ export {};
@@ -0,0 +1,138 @@
1
+ // Experiment assignments (personalization-reporting design/04/05): the
2
+ // starters render the page's build-time assignment context as a
3
+ // `<script type="application/json" data-aci-assignments>` JSON tag
4
+ // ({"v":1,"experiments":[{experimentId,armId,allocationVersion}]}). This
5
+ // collector parses it, emits `experiment.exposure` at most once per
6
+ // (experiment, page view) (REQ-2), and exposes the active assignments so the
7
+ // emit path can stamp experiment context on every event while an assignment
8
+ // is active (REQ-3). Absent or malformed tags are a silent no-op with a
9
+ // debug record; the page is never affected.
10
+ //
11
+ // KNOWN LIMITATION — SPA client-side routing reads a stale tag. The re-arm
12
+ // (AssignmentsHandle.rearm, wired to the shared navigation hook in init)
13
+ // runs synchronously when pushState/replaceState returns, but client
14
+ // routers (e.g. the Next app router) swap the page's DOM — including the
15
+ // assignments tag — AFTER pushState returns. The re-arm therefore reads the
16
+ // PREVIOUS page's tag. The design assumption is MPA / full-SSR-first
17
+ // navigation, where every page view is a fresh document load and the tag is
18
+ // always current; under pure client-side transitions, exposures and context
19
+ // stamping for the newly routed page lag one navigation behind. Accepted
20
+ // for now — do not "fix" with a MutationObserver.
21
+ export const EXPERIMENT_EXPOSURE_EVENT = "experiment.exposure";
22
+ const ASSIGNMENTS_SELECTOR = 'script[type="application/json"][data-aci-assignments]';
23
+ const ASSIGNMENTS_VERSION = 1;
24
+ function parseAssignment(value) {
25
+ if (typeof value !== "object" || value === null)
26
+ return null;
27
+ const record = value;
28
+ const experimentId = record.experimentId;
29
+ const armId = record.armId;
30
+ const allocationVersion = record.allocationVersion;
31
+ if (typeof experimentId !== "string" ||
32
+ experimentId === "" ||
33
+ typeof armId !== "string" ||
34
+ armId === "" ||
35
+ typeof allocationVersion !== "string") {
36
+ return null;
37
+ }
38
+ return { experimentId, armId, allocationVersion };
39
+ }
40
+ /**
41
+ * readAssignments parses the page's data-aci-assignments tag. Absence, a
42
+ * malformed body, or an unknown version are tolerated as "no assignments"
43
+ * (debug record, no throw).
44
+ */
45
+ export function readAssignments(doc, debug) {
46
+ try {
47
+ const tag = doc.querySelector(ASSIGNMENTS_SELECTOR);
48
+ if (!tag)
49
+ return [];
50
+ const raw = tag.textContent;
51
+ if (raw == null || raw.trim() === "")
52
+ return [];
53
+ const parsed = JSON.parse(raw);
54
+ if (typeof parsed !== "object" || parsed === null) {
55
+ debug?.("capture", { reason: "assignments_malformed" });
56
+ return [];
57
+ }
58
+ const body = parsed;
59
+ if (body.v !== ASSIGNMENTS_VERSION || !Array.isArray(body.experiments)) {
60
+ debug?.("capture", { reason: "assignments_malformed" });
61
+ return [];
62
+ }
63
+ const assignments = [];
64
+ for (const entry of body.experiments) {
65
+ const assignment = parseAssignment(entry);
66
+ if (assignment)
67
+ assignments.push(assignment);
68
+ }
69
+ return assignments;
70
+ }
71
+ catch (err) {
72
+ debug?.("capture", {
73
+ reason: "assignments_parse_failed",
74
+ error: String(err),
75
+ });
76
+ return [];
77
+ }
78
+ }
79
+ /**
80
+ * withAssignmentContext stamps the first active assignment's experiment
81
+ * context onto an event that does not already carry one (REQ-3). Events with
82
+ * explicit context (exposures, explicit conversions) are never overwritten.
83
+ */
84
+ export function withAssignmentContext(event, handle) {
85
+ if (!handle || event.experiment !== undefined)
86
+ return event;
87
+ const first = handle.getActiveAssignments()[0];
88
+ if (!first)
89
+ return event;
90
+ const experiment = {
91
+ experimentId: first.experimentId,
92
+ armId: first.armId,
93
+ allocationVersion: first.allocationVersion,
94
+ };
95
+ return { ...event, experiment };
96
+ }
97
+ /**
98
+ * createAssignments builds the assignments collector without emitting
99
+ * anything: creation and exposure emission are split so init can construct
100
+ * the emit pipeline against the handle before any event flows (no circular
101
+ * "emit captures a not-yet-assigned handle" wiring). start() reads the tag
102
+ * and emits exposures; rearm() is the SPA-navigation reset, fanned out from
103
+ * the single navigation hook installed by init — so an "exposure per page
104
+ * view" matches a "page view" exactly.
105
+ */
106
+ export function createAssignments(env) {
107
+ let active = [];
108
+ const exposed = new Set();
109
+ let emit;
110
+ const arm = () => {
111
+ active = readAssignments(env.document, env.debug);
112
+ for (const assignment of active) {
113
+ if (exposed.has(assignment.experimentId))
114
+ continue;
115
+ exposed.add(assignment.experimentId);
116
+ emit?.({
117
+ eventName: EXPERIMENT_EXPOSURE_EVENT,
118
+ url: env.window.location.href,
119
+ experiment: {
120
+ experimentId: assignment.experimentId,
121
+ armId: assignment.armId,
122
+ allocationVersion: assignment.allocationVersion,
123
+ },
124
+ });
125
+ }
126
+ };
127
+ return {
128
+ getActiveAssignments: () => active.map((assignment) => ({ ...assignment })),
129
+ start(emitFn) {
130
+ emit = emitFn;
131
+ arm();
132
+ },
133
+ rearm() {
134
+ exposed.clear();
135
+ arm();
136
+ },
137
+ };
138
+ }
@@ -1,4 +1,16 @@
1
1
  import type { DebugFn } from "./config.js";
2
+ /**
3
+ * Structured experiment context (personalization-reporting REQ-1/REQ-3):
4
+ * populated from the page's data-aci-assignments tag so conversions are
5
+ * attributable to (experiment, arm) at ingest. Explicit conversions may add
6
+ * goalId.
7
+ */
8
+ export interface ExperimentContext {
9
+ experimentId: string;
10
+ armId: string;
11
+ allocationVersion: string;
12
+ goalId?: string;
13
+ }
2
14
  /** One collect event on the wire (personalization-data-plane design/04). */
3
15
  export interface CollectEvent {
4
16
  eventId: string;
@@ -7,11 +19,20 @@ export interface CollectEvent {
7
19
  properties?: Record<string, unknown>;
8
20
  url?: string;
9
21
  referrer?: string;
22
+ experiment?: ExperimentContext;
10
23
  }
11
24
  /** An event at capture time, before the eventId is stamped. */
12
25
  export type PendingEvent = Omit<CollectEvent, "eventId">;
13
26
  /** Planning threshold: full serialized envelope, UTF-8 bytes. */
14
27
  export declare const MAX_BATCH_BYTES: number;
28
+ /**
29
+ * Hard event-count cap per envelope. The Next collect-route imports this
30
+ * constant and rejects larger envelopes (the Go handler enforces only the
31
+ * 64KiB body cap today), so a byte-capped-only batch of small events (~400
32
+ * fit in 60KiB) would 400 on that host and silently lose the whole flush.
33
+ * The client caps at the strictest host.
34
+ */
35
+ export declare const MAX_BATCH_EVENTS = 100;
15
36
  /** Flush interval (build-time constant, design/04 config table). */
16
37
  export declare const FLUSH_INTERVAL_MS = 5000;
17
38
  export interface BatcherTimers {
@@ -24,6 +45,8 @@ interface ListenerTarget {
24
45
  export interface BatcherOptions {
25
46
  /** Transport for one serialized batch envelope. */
26
47
  send: (body: string) => void;
48
+ /** Injectable only so tests can prove one ID is minted per sealed batch. */
49
+ createEnvelopeId?: () => string;
27
50
  debug?: DebugFn;
28
51
  /** Runs at the start of every flush tick (gap-buffer marker re-check). */
29
52
  onTick?: () => void;
@@ -1,14 +1,29 @@
1
1
  // Batcher (design/05): queue events, split so no request exceeds the 60 KiB
2
- // planning threshold (64 KiB wire ceiling), flush on start, size, interval,
3
- // visibilitychange and pagehide. Byte accounting is incremental — a running
4
- // per-event size computed at enqueue — so a flush stays O(n).
2
+ // planning threshold (64 KiB wire ceiling), mint one envelope ULID per sealed
3
+ // batch, and flush on start, size, interval, visibilitychange and pagehide.
4
+ // Byte accounting is incremental — a running per-event size computed at
5
+ // enqueue — so a flush stays O(n).
6
+ import { ulid } from "./ulid.js";
5
7
  /** Planning threshold: full serialized envelope, UTF-8 bytes. */
6
8
  export const MAX_BATCH_BYTES = 60 * 1024;
9
+ /**
10
+ * Hard event-count cap per envelope. The Next collect-route imports this
11
+ * constant and rejects larger envelopes (the Go handler enforces only the
12
+ * 64KiB body cap today), so a byte-capped-only batch of small events (~400
13
+ * fit in 60KiB) would 400 on that host and silently lose the whole flush.
14
+ * The client caps at the strictest host.
15
+ */
16
+ export const MAX_BATCH_EVENTS = 100;
7
17
  /** Flush interval (build-time constant, design/04 config table). */
8
18
  export const FLUSH_INTERVAL_MS = 5000;
9
- const ENVELOPE_PREFIX = '{"events":[';
19
+ const ENVELOPE_PREFIX = '{"envelopeId":"';
20
+ const EVENTS_PREFIX = '","events":[';
10
21
  const ENVELOPE_SUFFIX = "]}";
11
- const ENVELOPE_OVERHEAD = ENVELOPE_PREFIX.length + ENVELOPE_SUFFIX.length;
22
+ const ENVELOPE_ID_BYTES = 26;
23
+ const ENVELOPE_OVERHEAD = ENVELOPE_PREFIX.length +
24
+ ENVELOPE_ID_BYTES +
25
+ EVENTS_PREFIX.length +
26
+ ENVELOPE_SUFFIX.length;
12
27
  const encoder = new TextEncoder();
13
28
  const utf8ByteLength = (s) => encoder.encode(s).length;
14
29
  export class Batcher {
@@ -111,6 +126,8 @@ export class Batcher {
111
126
  const next = this.queue[0];
112
127
  if (ENVELOPE_OVERHEAD + next.bytes > this.maxBytes)
113
128
  break; // oversize: outer loop drops it
129
+ if (parts.length >= MAX_BATCH_EVENTS)
130
+ break; // host-side envelope cap
114
131
  const nextSize = size + next.bytes + (parts.length > 0 ? 1 : 0);
115
132
  if (nextSize > this.maxBytes)
116
133
  break;
@@ -121,7 +138,12 @@ export class Batcher {
121
138
  }
122
139
  if (parts.length === 0)
123
140
  continue;
124
- this.options.send(ENVELOPE_PREFIX + parts.join(",") + ENVELOPE_SUFFIX);
141
+ const envelopeId = (this.options.createEnvelopeId ?? ulid)();
142
+ this.options.send(ENVELOPE_PREFIX +
143
+ envelopeId +
144
+ EVENTS_PREFIX +
145
+ parts.join(",") +
146
+ ENVELOPE_SUFFIX);
125
147
  }
126
148
  }
127
149
  }
@@ -21,16 +21,17 @@ export interface ApplyConsentDeps extends ConsentWriteDeps {
21
21
  }
22
22
  /**
23
23
  * writeConsent builds the {v:1, p, t, src} envelope, URL-encodes it and sets
24
- * the aci_consent cookie (Path=/, SameSite=Lax, Secure on https).
24
+ * the aci_consent cookie (Path=/, SameSite=Lax, Secure on https). Inputs that
25
+ * cannot produce a portable envelope are ignored.
25
26
  */
26
- export declare function writeConsent(purposes: string[], src: string, deps: ConsentWriteDeps): void;
27
+ export declare function writeConsent(purposes: string[], src: string, deps: ConsentWriteDeps): boolean;
27
28
  /**
28
29
  * applyConsent is the single write path for every consent decision — CMP
29
30
  * adapters and the manual JS API alike. Any decision where personalization
30
31
  * is absent purges the gap buffer (REQ-8), and the envelope is still
31
32
  * written: withdrawal must reach the server.
32
33
  */
33
- export declare function applyConsent(purposes: string[], src: string, deps: ApplyConsentDeps): void;
34
+ export declare function applyConsent(purposes: string[], src: string, deps: ApplyConsentDeps): boolean;
34
35
  /** readConsentCookie returns the raw (still URL-encoded) cookie value. */
35
36
  export declare function readConsentCookie(doc: {
36
37
  cookie: string;
@@ -47,3 +48,8 @@ export declare function envelopeGrantsPersonalization(raw: string | undefined):
47
48
  * and the command is ignored.
48
49
  */
49
50
  export declare function sanitizePurposes(raw: unknown): string[] | undefined;
51
+ /**
52
+ * sanitizeConsentSource preserves the manual API's default while applying the
53
+ * same portable whitespace and byte bounds as every envelope reader.
54
+ */
55
+ export declare function sanitizeConsentSource(raw: unknown): string | undefined;
@@ -2,33 +2,26 @@
2
2
  // carries URL-encoded JSON {v:1, p, t, src} — cookie values cannot carry
3
3
  // quotes. The collector and the edge URL-decode before parsing, with a
4
4
  // legacy bare-token fallback during the deprecation window.
5
+ import { parsePortableConsentEnvelope, portableConsentAllowsPersonalization, trimPortableConsentWhitespace, } from "../../consent/portable-envelope.js";
5
6
  export const PERSONALIZATION_PURPOSE = "personalization";
6
7
  export const CONSENT_COOKIE = "aci_consent";
7
8
  export const CONSENT_ENVELOPE_VERSION = 1;
8
9
  /** Consent persistence horizon; the CMP remains the source of truth. */
9
10
  export const CONSENT_MAX_AGE_S = 365 * 24 * 60 * 60;
10
- /** Mirrors the server-side grandfathered tokens (audience collect_signals). */
11
- const LEGACY_GRANT_TOKENS = new Set([
12
- "true",
13
- "all",
14
- "granted",
15
- "yes",
16
- "personalization",
17
- "personalized",
18
- ]);
19
11
  /**
20
12
  * writeConsent builds the {v:1, p, t, src} envelope, URL-encodes it and sets
21
- * the aci_consent cookie (Path=/, SameSite=Lax, Secure on https).
13
+ * the aci_consent cookie (Path=/, SameSite=Lax, Secure on https). Inputs that
14
+ * cannot produce a portable envelope are ignored.
22
15
  */
23
16
  export function writeConsent(purposes, src, deps) {
24
- const envelope = {
25
- v: CONSENT_ENVELOPE_VERSION,
26
- p: purposes,
27
- t: new Date((deps.now ?? Date.now)()).toISOString(),
28
- src,
29
- };
30
- const value = encodeURIComponent(JSON.stringify(envelope));
31
- let cookie = `${CONSENT_COOKIE}=${value}; Path=/; Max-Age=${CONSENT_MAX_AGE_S}; SameSite=Lax`;
17
+ const prepared = prepareConsentEnvelope(purposes, src, deps.now);
18
+ if (prepared === null)
19
+ return false;
20
+ writePreparedConsent(prepared, deps);
21
+ return true;
22
+ }
23
+ function writePreparedConsent(prepared, deps) {
24
+ let cookie = `${CONSENT_COOKIE}=${prepared.encoded}; Path=/; Max-Age=${CONSENT_MAX_AGE_S}; SameSite=Lax`;
32
25
  if (deps.document.location?.protocol === "https:") {
33
26
  cookie += "; Secure";
34
27
  }
@@ -41,15 +34,19 @@ export function writeConsent(purposes, src, deps) {
41
34
  * written: withdrawal must reach the server.
42
35
  */
43
36
  export function applyConsent(purposes, src, deps) {
44
- if (!purposes.includes(PERSONALIZATION_PURPOSE)) {
37
+ const prepared = prepareConsentEnvelope(purposes, src, deps.now);
38
+ if (prepared === null)
39
+ return false;
40
+ if (!portableConsentAllowsPersonalization(prepared.snapshot)) {
45
41
  deps.gapBuffer?.purge();
46
42
  }
47
- writeConsent(purposes, src, deps);
43
+ writePreparedConsent(prepared, deps);
44
+ return true;
48
45
  }
49
46
  /** readConsentCookie returns the raw (still URL-encoded) cookie value. */
50
47
  export function readConsentCookie(doc) {
51
48
  for (const part of doc.cookie.split(";")) {
52
- const trimmed = part.trim();
49
+ const trimmed = trimPortableConsentWhitespace(part);
53
50
  if (trimmed.startsWith(CONSENT_COOKIE + "=")) {
54
51
  return trimmed.slice(CONSENT_COOKIE.length + 1);
55
52
  }
@@ -64,23 +61,7 @@ export function readConsentCookie(doc) {
64
61
  export function envelopeGrantsPersonalization(raw) {
65
62
  if (raw === undefined || raw === "")
66
63
  return false;
67
- let decoded = raw;
68
- try {
69
- decoded = decodeURIComponent(raw);
70
- }
71
- catch {
72
- // Not URL-encoded; evaluate as-is.
73
- }
74
- if (LEGACY_GRANT_TOKENS.has(decoded.trim().toLowerCase()))
75
- return true;
76
- try {
77
- const envelope = JSON.parse(decoded);
78
- return (Array.isArray(envelope.p) &&
79
- envelope.p.includes(PERSONALIZATION_PURPOSE));
80
- }
81
- catch {
82
- return false;
83
- }
64
+ return portableConsentAllowsPersonalization(parsePortableConsentEnvelope(raw, true));
84
65
  }
85
66
  /**
86
67
  * sanitizePurposes validates a caller-supplied purposes value (the manual
@@ -94,7 +75,44 @@ export function sanitizePurposes(raw) {
94
75
  for (const item of raw) {
95
76
  if (typeof item !== "string")
96
77
  return undefined;
97
- purposes.push(item.trim());
78
+ purposes.push(trimPortableConsentWhitespace(item));
79
+ }
80
+ const encoded = JSON.stringify({
81
+ v: CONSENT_ENVELOPE_VERSION,
82
+ p: purposes,
83
+ });
84
+ return parsePortableConsentEnvelope(encoded, false)?.purposes;
85
+ }
86
+ /**
87
+ * sanitizeConsentSource preserves the manual API's default while applying the
88
+ * same portable whitespace and byte bounds as every envelope reader.
89
+ */
90
+ export function sanitizeConsentSource(raw) {
91
+ if (raw !== undefined && typeof raw !== "string")
92
+ return undefined;
93
+ const trimmed = typeof raw === "string" ? trimPortableConsentWhitespace(raw) : "";
94
+ const source = trimmed === "" ? "manual" : trimmed;
95
+ const encoded = JSON.stringify({
96
+ v: CONSENT_ENVELOPE_VERSION,
97
+ p: [],
98
+ src: source,
99
+ });
100
+ return parsePortableConsentEnvelope(encoded, false)?.source;
101
+ }
102
+ function prepareConsentEnvelope(purposes, src, now) {
103
+ try {
104
+ const serialized = JSON.stringify({
105
+ v: CONSENT_ENVELOPE_VERSION,
106
+ p: purposes,
107
+ t: new Date((now ?? Date.now)()).toISOString(),
108
+ src,
109
+ });
110
+ const snapshot = parsePortableConsentEnvelope(serialized, false);
111
+ return snapshot === null
112
+ ? null
113
+ : { encoded: encodeURIComponent(serialized), snapshot };
114
+ }
115
+ catch {
116
+ return null;
98
117
  }
99
- return purposes;
100
118
  }
@@ -31,5 +31,5 @@ export declare function toCustomEventName(raw: unknown): string | undefined;
31
31
  * environment has an IntersectionObserver, observes every visible-declared
32
32
  * element present at start.
33
33
  */
34
- export declare function startDeclarative(emit: (event: PendingEvent) => void, env: DeclarativeEnv): void;
34
+ export declare function startDeclarative(emit: (event: PendingEvent) => void, env: DeclarativeEnv, refresh?: () => void): void;
35
35
  export {};
@@ -6,6 +6,7 @@
6
6
  const EVENT_ATTR = "data-aci-event";
7
7
  const ON_ATTR = "data-aci-on";
8
8
  const PROP_ATTR_PREFIX = "data-aci-prop-";
9
+ const REFRESH_ATTR = "data-aci-refresh";
9
10
  const CUSTOM_PREFIX = "custom.";
10
11
  const VISIBLE_THRESHOLD = 0.5;
11
12
  /**
@@ -50,11 +51,16 @@ function trigger(el) {
50
51
  * environment has an IntersectionObserver, observes every visible-declared
51
52
  * element present at start.
52
53
  */
53
- export function startDeclarative(emit, env) {
54
+ export function startDeclarative(emit, env, refresh) {
54
55
  const { document: doc, debug } = env;
55
56
  doc.addEventListener("click", (e) => {
56
57
  try {
57
58
  const target = e.target;
59
+ const refreshTarget = target?.closest?.(`[${REFRESH_ATTR}]`);
60
+ if (refreshTarget) {
61
+ refresh?.();
62
+ return;
63
+ }
58
64
  const el = target?.closest?.(`[${EVENT_ATTR}]`);
59
65
  if (!el)
60
66
  return;