@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.
- package/CHANGELOG.md +15 -11
- package/README.md +4 -4
- package/dist/astro/index.d.ts +1 -1
- package/dist/astro/index.js +1 -1
- package/dist/astro/page.d.ts +6 -0
- package/dist/astro/page.js +3 -2
- package/dist/audience-data-models.d.ts +286 -0
- package/dist/audience-data-models.js +46 -0
- package/dist/cli/compile-registry.d.mts +6 -0
- package/dist/cli/compile-registry.mjs +774 -0
- package/dist/cli/materialize-template.d.mts +19 -0
- package/dist/cli/materialize-template.mjs +128 -0
- package/dist/cli/validate-content.d.mts +32 -0
- package/dist/cli/validate-content.mjs +464 -0
- package/dist/cli/verify-next-cdn-routes.d.mts +2 -0
- package/dist/cli/verify-next-cdn-routes.mjs +47 -0
- package/dist/collect/aci.js +1 -0
- package/dist/collect/aci.js.sha256 +1 -0
- package/dist/collect/assignments.d.ts +65 -0
- package/dist/collect/assignments.js +138 -0
- package/dist/collect/batcher.d.ts +23 -0
- package/dist/collect/batcher.js +28 -6
- package/dist/collect/consent/envelope.d.ts +9 -3
- package/dist/collect/consent/envelope.js +58 -40
- package/dist/collect/declarative.d.ts +1 -1
- package/dist/collect/declarative.js +7 -1
- package/dist/collect/index.js +39 -6
- package/dist/collect/linkclicks.js +4 -2
- package/dist/collect/pageviews.d.ts +15 -3
- package/dist/collect/pageviews.js +28 -13
- package/dist/collect/transport.d.ts +4 -0
- package/dist/collect/transport.js +37 -2
- package/dist/consent/portable-envelope.d.ts +9 -0
- package/dist/consent/portable-envelope.js +110 -0
- package/dist/consent-banner/index.d.ts +33 -0
- package/dist/consent-banner/index.js +100 -0
- package/dist/content/assets.d.ts +1 -0
- package/dist/content/assets.js +1 -0
- package/dist/content/personalization.d.ts +35 -0
- package/dist/content/personalization.js +57 -0
- package/dist/content/resolve-slots.js +1 -0
- package/dist/content/types.d.ts +9 -1
- package/dist/edge/personalization-core.d.ts +11 -0
- package/dist/edge/personalization-core.js +265 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/next/assignments-script.d.ts +14 -0
- package/dist/next/assignments-script.js +17 -0
- package/dist/next/collect-route.d.ts +19 -0
- package/dist/next/collect-route.js +422 -0
- package/dist/next/envelope-digest.d.ts +21 -0
- package/dist/next/envelope-digest.js +58 -0
- package/dist/next/index.d.ts +1 -0
- package/dist/next/index.js +1 -0
- package/dist/next/middleware.d.ts +31 -3
- package/dist/next/middleware.js +254 -30
- package/dist/next/page.js +2 -1
- package/dist/next/personalization-host.d.ts +127 -0
- package/dist/next/personalization-host.js +257 -0
- package/dist/next/server.d.ts +1 -0
- package/dist/next/server.js +9 -6
- package/dist/next/vercel-collect-sdk-route.d.ts +27 -0
- package/dist/next/vercel-collect-sdk-route.js +205 -0
- package/dist/next/vercel-personalization.d.ts +4 -0
- package/dist/next/vercel-personalization.js +4 -0
- package/dist/next/vercel-s3-personalization.d.ts +79 -0
- package/dist/next/vercel-s3-personalization.js +574 -0
- package/dist/providers/s3.d.ts +20 -5
- package/dist/providers/s3.js +93 -23
- package/dist/react/VideoPlayer.d.ts +6 -6
- package/dist/react/index.d.ts +6 -6
- package/dist/testing/conformance/checks.js +75 -4
- package/dist/types/block-ref.d.ts +2 -0
- package/dist/types/block-ref.js +1 -0
- package/dist/types/page.d.ts +1 -1
- package/package.json +44 -15
- package/src/astro/ConsentBanner.astro +62 -0
- package/src/cli/compile-registry.mjs +24 -10
- package/src/cli/validate-content.mjs +52 -13
- package/src/cli/verify-next-cdn-routes.mjs +5 -4
- package/src/consent-banner/styles.css +110 -0
- package/src/types/block-ref.test.ts +45 -0
- package/src/types/block-ref.ts +2 -0
- package/src/types/page.ts +1 -1
- package/templates/astro/starter-content/src/cms/contracts/components/index.ts +2 -0
- package/templates/astro/starter-content/src/cms/contracts/components/personalizationChoice.contract.ts +17 -0
- package/templates/astro/starter-content/src/cms/registry.ts +3 -0
- package/templates/astro/starter-content/src/components/sections/PersonalizationChoice.astro +41 -0
- package/templates/astro/starter-content/src/fixtures/atomicBlocks.ts +7 -6
- package/templates/astro/starter-content/src/fixtures/composition.ts +3 -2
- package/templates/astro/starter-content/src/fixtures/sections.ts +2 -1
- package/templates/astro/starter-content/src/render/RenderPage.astro +3 -2
- package/templates/astro/template/src/layouts/SiteLayout.astro +3 -1
- package/templates/astro/template/src/pages/[...slug].astro +1 -0
- package/templates/common/.agents/skills/authoring/SKILL.md +11 -3
- package/templates/common/.agents/skills/authoring/references/block-slots.md +3 -0
- package/templates/common/.content/audiences/preference-blue/_index.json +11 -0
- package/templates/common/.content/audiences/preference-red/_index.json +11 -0
- package/templates/common/.content/overlays/audience/preference-blue/pages/home/_index.json +13 -0
- package/templates/common/.content/overlays/audience/preference-red/pages/home/_index.json +13 -0
- package/templates/common/.content/pages/home/_index.json +124 -4
- package/templates/common/.content/segments/preference-blue/_index.json +20 -0
- package/templates/common/.content/segments/preference-red/_index.json +20 -0
- package/templates/common/contracts/components/index.ts +21 -1
- package/templates/common/contracts/components/personalizationChoice.contract.ts +17 -0
- package/templates/common/starter-content/.content/pages/about/_index.json +3 -0
- package/templates/common/starter-content/.content/pages/home/_index.json +9 -0
- package/templates/nextjs/starter-content/src/cms/contracts/components/index.ts +3 -1
- package/templates/nextjs/starter-content/src/cms/contracts/components/personalizationChoice.contract.ts +17 -0
- package/templates/nextjs/starter-content/src/cms/registry.ts +3 -0
- package/templates/nextjs/starter-content/src/components/sections/PersonalizationChoice.tsx +53 -0
- package/templates/nextjs/starter-content/src/middleware.ts +41 -0
- package/templates/nextjs/template/src/app/collect/route.ts +18 -0
- package/templates/nextjs/template/src/middleware.ts +36 -2
- package/templates/nextjs/template/src/middleware.ts.tmpl +31 -1
- package/templates/astro/template/.aci.yaml +0 -28
- package/templates/common/.content/.gradial-dam/blobs/sha256/7f/bf/7fbfc7fe230ff12688c496f660119e41d9725c4504d82e310c15665c73c09292 +0 -0
- package/templates/common/.content/.gradial-dam/blobs/sha256/a0/f5/a0f506689de9b15a7055f70103a3f02cd3ba105771ae2de6754df2ede1210d3a +0 -0
- package/templates/common/.content/.gradial-dam/blobs/sha256/dd/b7/ddb7e9642be08317bcd4b2bd26b58ba8d0d34dd7239e03e684a4f3634fa3845b +0 -0
- package/templates/common/.content/.gradial-dam/blobs/sha256/fb/32/fb3260e8286ea8231ea1c77b9c0be92d6edd9026565731eb14cfdcfd6b1b75b3 +0 -0
- package/templates/common/.content/config/robots.txt +0 -2
- package/templates/common/.content/config/site.json +0 -197
- package/templates/common/.content/dam/folders/seed/ast_site_about_hero/asset.json +0 -9
- package/templates/common/.content/dam/folders/seed/ast_site_about_hero/editorial.json +0 -14
- package/templates/common/.content/dam/folders/seed/ast_site_about_hero/governance.json +0 -23
- package/templates/common/.content/dam/folders/seed/ast_site_about_hero/versions/v0001.json +0 -37
- package/templates/common/.content/dam/folders/seed/ast_site_home_hero/asset.json +0 -9
- package/templates/common/.content/dam/folders/seed/ast_site_home_hero/editorial.json +0 -14
- package/templates/common/.content/dam/folders/seed/ast_site_home_hero/governance.json +0 -23
- package/templates/common/.content/dam/folders/seed/ast_site_home_hero/versions/v0001.json +0 -37
- package/templates/common/.content/fragments/navbar/_index.json +0 -25
- package/templates/common/.content/fragments/site-footer/_index.json +0 -62
- package/templates/common/.content/pages/about/_index.json +0 -51
- 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;
|
package/dist/collect/batcher.js
CHANGED
|
@@ -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),
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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 = '{"
|
|
19
|
+
const ENVELOPE_PREFIX = '{"envelopeId":"';
|
|
20
|
+
const EVENTS_PREFIX = '","events":[';
|
|
10
21
|
const ENVELOPE_SUFFIX = "]}";
|
|
11
|
-
const
|
|
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.
|
|
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):
|
|
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):
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
let cookie = `${CONSENT_COOKIE}=${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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;
|