@gradial/aci 0.1.52-rc.21 → 0.1.52-rc.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -5
- package/dist/astro/index.js +4 -0
- package/dist/audience-data-models.d.ts +12 -0
- package/dist/audience-data-models.js +3 -0
- package/dist/cli/mintlify-render.d.mts +1 -0
- package/dist/cli/mintlify-render.mjs +8 -0
- package/dist/collect/aci.js +1 -1
- package/dist/collect/aci.js.sha256 +1 -1
- package/dist/collect/api.d.ts +3 -0
- package/dist/collect/api.js +19 -0
- package/dist/collect/assignments.d.ts +7 -9
- package/dist/collect/assignments.js +23 -32
- package/dist/collect/batcher.d.ts +3 -2
- package/dist/collect/consent/envelope.d.ts +7 -1
- package/dist/collect/consent/envelope.js +3 -1
- package/dist/collect/consent/mapping.d.ts +2 -0
- package/dist/collect/consent/mapping.js +1 -0
- package/dist/collect/consenthold.d.ts +38 -0
- package/dist/collect/consenthold.js +82 -0
- package/dist/collect/declarative.js +7 -5
- package/dist/collect/gapbuffer.d.ts +13 -4
- package/dist/collect/gapbuffer.js +10 -4
- package/dist/collect/index.js +66 -19
- package/dist/collect/pageviews.d.ts +11 -11
- package/dist/collect/pageviews.js +27 -22
- package/dist/consent-banner/index.d.ts +17 -0
- package/dist/consent-banner/index.js +23 -0
- package/dist/content/provider.d.ts +8 -0
- package/dist/index.d.ts +1 -1
- package/dist/internal/domain/edge/identity.d.ts +5 -0
- package/dist/internal/domain/edge/identity.js +49 -13
- package/dist/internal/domain/edge/mimic_token.d.ts +1 -0
- package/dist/internal/domain/edge/mimic_token.js +74 -7
- package/dist/internal/domain/edge/personalization-core.d.ts +8 -33
- package/dist/internal/domain/edge/personalization-core.js +151 -195
- package/dist/internal/domain/edge/personalization_index.d.ts +17 -0
- package/dist/internal/domain/edge/personalization_index.js +56 -0
- package/dist/internal/domain/edge/record/codec.d.ts +1 -30
- package/dist/internal/domain/edge/record/codec.js +1 -156
- package/dist/internal/domain/edge/resolve.js +55 -38
- package/dist/internal/domain/edge/types.d.ts +41 -32
- package/dist/internal/domain/edge/variant_key.d.ts +3 -0
- package/dist/internal/domain/edge/variant_key.js +44 -0
- package/dist/internal/platform/collectionstore/errors.d.ts +13 -0
- package/dist/internal/platform/collectionstore/errors.js +28 -0
- package/dist/mintlify/blocks.d.ts +6 -0
- package/dist/mintlify/blocks.js +198 -0
- package/dist/mintlify/config.d.ts +3 -0
- package/dist/mintlify/config.js +77 -0
- package/dist/mintlify/index.d.ts +3 -0
- package/dist/mintlify/index.js +2 -0
- package/dist/mintlify/json.d.ts +8 -0
- package/dist/mintlify/json.js +37 -0
- package/dist/mintlify/materialize.d.ts +12 -0
- package/dist/mintlify/materialize.js +159 -0
- package/dist/mintlify/types.d.ts +69 -0
- package/dist/mintlify/types.js +1 -0
- package/dist/mintlify/validate.d.ts +7 -0
- package/dist/mintlify/validate.js +252 -0
- package/dist/next/assignments-navigation.d.ts +4 -0
- package/dist/next/assignments-navigation.js +18 -0
- package/dist/next/assignments-script.d.ts +3 -2
- package/dist/next/assignments-script.js +5 -5
- package/dist/next/error-seat.js +3 -0
- package/dist/next/index.d.ts +1 -0
- package/dist/next/index.js +3 -0
- package/dist/next/render.js +8 -1
- package/dist/react/VideoPlayer.d.ts +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/registry.d.ts +4 -2
- package/dist/testing/conformance/checks.d.ts +2 -2
- package/dist/testing/conformance/checks.js +2 -0
- package/dist/testing/conformance/index.d.ts +1 -0
- package/dist/testing/conformance/index.js +1 -0
- package/dist/testing/conformance/next-stubs.d.ts +6 -0
- package/dist/testing/conformance/next-stubs.js +9 -0
- package/dist/testing/conformance/preset.d.ts +3 -2
- package/dist/testing/conformance/preset.js +12 -8
- package/dist/testing/conformance/root-identity.d.ts +31 -0
- package/dist/testing/conformance/root-identity.js +380 -0
- package/dist/testing/conformance/stubs/next-image.d.ts +7 -0
- package/dist/testing/conformance/stubs/next-image.js +5 -0
- package/dist/testing/conformance/stubs/next-link.d.ts +7 -0
- package/dist/testing/conformance/stubs/next-link.js +4 -0
- package/dist/testing/conformance/types.d.ts +2 -2
- package/dist/testing/index.js +2 -2
- package/dist/types/component.d.ts +9 -0
- package/package.json +11 -7
- package/src/cli/mintlify-render.mjs +9 -0
- package/src/types/component.ts +10 -0
- package/templates/astro/starter-content/src/components/chrome/Footer.astro +5 -3
- package/templates/astro/starter-content/src/components/chrome/Header.astro +5 -3
- package/templates/astro/starter-content/src/components/elements/Button.astro +5 -2
- package/templates/astro/starter-content/src/components/elements/ButtonBlock.astro +5 -3
- package/templates/astro/starter-content/src/components/sections/ContainerSection.astro +4 -3
- package/templates/astro/starter-content/src/components/sections/FeatureCard.astro +4 -3
- package/templates/astro/starter-content/src/components/sections/FeatureGrid.astro +4 -3
- package/templates/astro/starter-content/src/components/sections/HomeHero.astro +5 -3
- package/templates/astro/starter-content/src/components/sections/PersonalizationChoice.astro +4 -3
- package/templates/astro/starter-content/src/components/sections/StatsBar.astro +4 -3
- package/templates/astro/starter-content/src/fixtures/sections.ts +1 -1
- package/templates/astro/starter-content/src/stories/CompositionStoryFrame.astro +7 -2
- package/templates/astro/template/package.json.tmpl +4 -4
- package/templates/common/.agents/skills/aci/SKILL.md +50 -0
- package/templates/common/.agents/skills/authoring/SKILL.md +4 -3
- package/templates/common/.agents/skills/authoring/references/block-registry.md +15 -0
- package/templates/common/.agents/skills/migration/references/selector-map.md +1 -1
- package/templates/common/.content/schemas/site.schema.json +11 -1
- package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/asset.json +1 -1
- package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/asset.json +1 -1
- package/templates/common/starter-content/.content/pages/404/_index.json +1 -1
- package/templates/common/starter-content/.content/pages/{home/_index.json → _index.json} +2 -2
- package/templates/common/starter-content/.content/pages/about/_index.json +1 -1
- package/templates/common/starter-content/.content/pages/error/_index.json +1 -1
- package/templates/common/starter-content/.content/patterns/components/button.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/container.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/feature_card.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/feature_grid.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/home_hero.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/site-footer.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/site-navigation.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/stats_bar.json +2 -2
- package/templates/common/starter-content/.content/patterns/layouts/default.json +2 -2
- package/templates/common/starter-content/.content/schemas/site.schema.json +11 -1
- package/templates/nextjs/starter-content/src/components/chrome/Footer.tsx +4 -3
- package/templates/nextjs/starter-content/src/components/chrome/Header.tsx +4 -3
- package/templates/nextjs/starter-content/src/components/elements/Button.tsx +4 -3
- package/templates/nextjs/starter-content/src/components/elements/ButtonBlock.tsx +4 -3
- package/templates/nextjs/starter-content/src/components/sections/ContainerSection.tsx +4 -2
- package/templates/nextjs/starter-content/src/components/sections/FeatureCard.tsx +4 -3
- package/templates/nextjs/starter-content/src/components/sections/FeatureGrid.tsx +4 -2
- package/templates/nextjs/starter-content/src/components/sections/HomeHero.tsx +4 -3
- package/templates/nextjs/starter-content/src/components/sections/PersonalizationChoice.tsx +4 -2
- package/templates/nextjs/starter-content/src/components/sections/StatsBar.tsx +4 -3
- package/templates/astro/starter-content/src/render/RenderPage.astro +0 -67
- package/templates/common/starter-content/.content/patterns/components/personalization_choice.json +0 -42
- package/templates/nextjs/starter-content/src/components/elements/AtomicBlock.tsx +0 -13
- /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/editorial.json +0 -0
- /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000101}/governance.json +0 -0
- /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/versions/v0001.json +0 -0
- /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/editorial.json +0 -0
- /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000102}/governance.json +0 -0
- /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/versions/v0001.json +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Consent-pending hold (personalization-onboarding REQ-15, D-ONB-13): on a
|
|
2
|
+
// site whose tag names a CMP preset, a consent integration will write the
|
|
3
|
+
// aci_consent envelope — often after hydration. Until it does, and while no
|
|
4
|
+
// identity exists (no aci_m marker), the collector can only drop what the SDK
|
|
5
|
+
// sends (no identity cookie, no identity-tier consent). Rather than transmit
|
|
6
|
+
// a first view's exposure and page view to be discarded, emit holds them:
|
|
7
|
+
//
|
|
8
|
+
// - in page memory only — never sessionStorage, never transmitted;
|
|
9
|
+
// - bounded: CONSENT_HOLD_CAP events (the newest are refused, so the
|
|
10
|
+
// first-view exposure and page view survive) and CONSENT_HOLD_MAX_AGE_MS;
|
|
11
|
+
// - a granting decision releases them, in capture order with their capture-
|
|
12
|
+
// time eventIds, into the batcher so they leave in the next envelope under
|
|
13
|
+
// the just-written consent envelope (they were never enveloped before, so
|
|
14
|
+
// no eventId is reused across envelopes);
|
|
15
|
+
// - a non-granting decision, or pagehide without a decision, discards them.
|
|
16
|
+
export const CONSENT_HOLD_CAP = 50;
|
|
17
|
+
export const CONSENT_HOLD_MAX_AGE_MS = 30 * 60 * 1000;
|
|
18
|
+
export class ConsentHold {
|
|
19
|
+
events = [];
|
|
20
|
+
now;
|
|
21
|
+
debug;
|
|
22
|
+
constructor(env) {
|
|
23
|
+
this.now = env?.now ?? Date.now;
|
|
24
|
+
this.debug = env?.debug;
|
|
25
|
+
}
|
|
26
|
+
push(event) {
|
|
27
|
+
this.expire();
|
|
28
|
+
if (this.events.length >= CONSENT_HOLD_CAP) {
|
|
29
|
+
this.debug?.("buffer", { reason: "consent_hold_cap_drop_newest", eventId: event.eventId });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
this.events.push({ event, heldAt: this.now() });
|
|
33
|
+
}
|
|
34
|
+
size() {
|
|
35
|
+
return this.events.length;
|
|
36
|
+
}
|
|
37
|
+
/** release returns and clears the unexpired held events, in capture order. */
|
|
38
|
+
release() {
|
|
39
|
+
this.expire();
|
|
40
|
+
const released = this.events.map((held) => held.event);
|
|
41
|
+
this.events = [];
|
|
42
|
+
return released;
|
|
43
|
+
}
|
|
44
|
+
/** discard drops every held event; nothing is ever sent for them. */
|
|
45
|
+
discard(reason) {
|
|
46
|
+
if (this.events.length > 0) {
|
|
47
|
+
this.debug?.("buffer", { reason: `consent_hold_discarded_${reason}`, count: this.events.length });
|
|
48
|
+
}
|
|
49
|
+
this.events = [];
|
|
50
|
+
}
|
|
51
|
+
expire() {
|
|
52
|
+
const cutoff = this.now() - CONSENT_HOLD_MAX_AGE_MS;
|
|
53
|
+
let expired = 0;
|
|
54
|
+
while (expired < this.events.length && this.events[expired].heldAt < cutoff) {
|
|
55
|
+
expired += 1;
|
|
56
|
+
}
|
|
57
|
+
if (expired > 0) {
|
|
58
|
+
this.events.splice(0, expired);
|
|
59
|
+
this.debug?.("buffer", { reason: "consent_hold_expired", count: expired });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* resolveConsentHold applies a consent decision to the hold: nothing while
|
|
65
|
+
* undecided; a grant releases every held event into the batcher; any other
|
|
66
|
+
* decision discards them. Runs from the consent write path and on every
|
|
67
|
+
* batcher flush tick, so a decision written by another tab or by site code
|
|
68
|
+
* is noticed too.
|
|
69
|
+
*/
|
|
70
|
+
export function resolveConsentHold(hold, deps) {
|
|
71
|
+
if (hold.size() === 0 || !deps.decided())
|
|
72
|
+
return;
|
|
73
|
+
if (!deps.grants()) {
|
|
74
|
+
hold.discard("denied");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const events = hold.release();
|
|
78
|
+
for (const event of events) {
|
|
79
|
+
deps.batcher.push(event);
|
|
80
|
+
}
|
|
81
|
+
deps.debug?.("buffer", { reason: "consent_hold_released", count: events.length });
|
|
82
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Declarative events (design/04): elements opt in via data-aci-event, with
|
|
2
2
|
// data-aci-on=click|visible (visible = first intersection >= 50%, once per
|
|
3
|
-
// element per page view) and data-aci-prop-* string properties.
|
|
3
|
+
// element per page view) and data-aci-prop-* string properties. Every event
|
|
4
|
+
// carries the current page url so ingest can derive path and UTM. One
|
|
4
5
|
// delegated click listener + one IntersectionObserver regardless of element
|
|
5
6
|
// count. Malformed declarations are ignored silently.
|
|
6
7
|
const EVENT_ATTR = "data-aci-event";
|
|
@@ -35,13 +36,13 @@ function extractProps(el) {
|
|
|
35
36
|
}
|
|
36
37
|
return props;
|
|
37
38
|
}
|
|
38
|
-
function declaredEvent(el, debug) {
|
|
39
|
+
function declaredEvent(el, url, debug) {
|
|
39
40
|
const eventName = toCustomEventName(el.getAttribute(EVENT_ATTR));
|
|
40
41
|
if (eventName === undefined) {
|
|
41
42
|
debug?.("capture", { reason: "malformed_declaration", attr: EVENT_ATTR });
|
|
42
43
|
return undefined;
|
|
43
44
|
}
|
|
44
|
-
return { eventName, properties: extractProps(el) };
|
|
45
|
+
return { eventName, url, properties: extractProps(el) };
|
|
45
46
|
}
|
|
46
47
|
function trigger(el) {
|
|
47
48
|
return el.getAttribute(ON_ATTR)?.trim().toLowerCase() ?? "click";
|
|
@@ -53,6 +54,7 @@ function trigger(el) {
|
|
|
53
54
|
*/
|
|
54
55
|
export function startDeclarative(emit, env, refresh) {
|
|
55
56
|
const { document: doc, debug } = env;
|
|
57
|
+
const loc = doc.defaultView?.location;
|
|
56
58
|
doc.addEventListener("click", (e) => {
|
|
57
59
|
try {
|
|
58
60
|
const target = e.target;
|
|
@@ -66,7 +68,7 @@ export function startDeclarative(emit, env, refresh) {
|
|
|
66
68
|
return;
|
|
67
69
|
if (trigger(el) !== "click")
|
|
68
70
|
return; // visible or malformed trigger
|
|
69
|
-
const event = declaredEvent(el, debug);
|
|
71
|
+
const event = declaredEvent(el, loc?.href, debug);
|
|
70
72
|
if (event)
|
|
71
73
|
emit(event);
|
|
72
74
|
}
|
|
@@ -92,7 +94,7 @@ export function startDeclarative(emit, env, refresh) {
|
|
|
92
94
|
continue;
|
|
93
95
|
fired.add(entry.target);
|
|
94
96
|
obs.unobserve(entry.target); // once per element per page view
|
|
95
|
-
const event = declaredEvent(entry.target, debug);
|
|
97
|
+
const event = declaredEvent(entry.target, loc?.href, debug);
|
|
96
98
|
if (event)
|
|
97
99
|
emit(event);
|
|
98
100
|
}
|
|
@@ -39,12 +39,21 @@ export interface RouteDeps {
|
|
|
39
39
|
marker: () => boolean;
|
|
40
40
|
gapBuffer: GapBuffer;
|
|
41
41
|
batcher: EventSink;
|
|
42
|
+
/**
|
|
43
|
+
* Whether a CMP-preset site's consent decision is still pending and no
|
|
44
|
+
* identity exists (consenthold.ts). Absent means never pending.
|
|
45
|
+
*/
|
|
46
|
+
consentPending?: () => boolean;
|
|
47
|
+
/** In-memory hold for consent-pending events (consenthold.ts). */
|
|
48
|
+
consentHold?: EventSink;
|
|
42
49
|
}
|
|
43
50
|
/**
|
|
44
|
-
* routeEvent implements the emit routing (design/05): events captured
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
51
|
+
* routeEvent implements the emit routing (design/05): events captured while
|
|
52
|
+
* a CMP's consent decision is pending (and no identity exists) are held in
|
|
53
|
+
* page memory until the decision; events captured in the consent gap —
|
|
54
|
+
* envelope grants personalization AND the marker is absent — are buffered;
|
|
55
|
+
* everything else goes straight to the batcher (the collector enforces
|
|
56
|
+
* consent server-side).
|
|
48
57
|
*/
|
|
49
58
|
export declare function routeEvent(event: CollectEvent, deps: RouteDeps): void;
|
|
50
59
|
/**
|
|
@@ -104,12 +104,18 @@ export function hasMintMarker(doc) {
|
|
|
104
104
|
return false;
|
|
105
105
|
}
|
|
106
106
|
/**
|
|
107
|
-
* routeEvent implements the emit routing (design/05): events captured
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
107
|
+
* routeEvent implements the emit routing (design/05): events captured while
|
|
108
|
+
* a CMP's consent decision is pending (and no identity exists) are held in
|
|
109
|
+
* page memory until the decision; events captured in the consent gap —
|
|
110
|
+
* envelope grants personalization AND the marker is absent — are buffered;
|
|
111
|
+
* everything else goes straight to the batcher (the collector enforces
|
|
112
|
+
* consent server-side).
|
|
111
113
|
*/
|
|
112
114
|
export function routeEvent(event, deps) {
|
|
115
|
+
if (deps.consentHold && deps.consentPending?.()) {
|
|
116
|
+
deps.consentHold.push(event);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
113
119
|
if (deps.grants() && !deps.marker()) {
|
|
114
120
|
deps.gapBuffer.push(event);
|
|
115
121
|
return;
|
package/dist/collect/index.js
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
// init(); every failure is swallowed after a debug record so the page is
|
|
4
4
|
// never affected (QR-2/3).
|
|
5
5
|
import { installCommandQueue } from "./api.js";
|
|
6
|
-
import { createAssignments, withAssignmentContext } from "./assignments.js";
|
|
6
|
+
import { createAssignments, readAssignments, withAssignmentContext } from "./assignments.js";
|
|
7
7
|
import { Batcher } from "./batcher.js";
|
|
8
|
+
import { CONSENT_BANNER_DECIDED_EVENT, consentBannerPending, } from "../consent-banner/index.js";
|
|
8
9
|
import { createDebug, readTagConfig } from "./config.js";
|
|
10
|
+
import { ConsentHold, resolveConsentHold } from "./consenthold.js";
|
|
9
11
|
import { start as startC15t } from "./consent/adapters/c15t.js";
|
|
10
12
|
import { start as startCookiebot } from "./consent/adapters/cookiebot.js";
|
|
11
13
|
import { start as startOneTrust } from "./consent/adapters/onetrust.js";
|
|
@@ -74,6 +76,33 @@ export function init(win) {
|
|
|
74
76
|
return false;
|
|
75
77
|
}
|
|
76
78
|
};
|
|
79
|
+
const decided = () => {
|
|
80
|
+
try {
|
|
81
|
+
return readConsentCookie(doc) !== undefined;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
// Consent-pending hold (personalization-onboarding REQ-15): a decision
|
|
88
|
+
// is pending when a consent integration has declared it will write the
|
|
89
|
+
// envelope and has not yet. A CMP preset is that declaration; under the
|
|
90
|
+
// manual preset it is ACI's own consent banner, through its state API.
|
|
91
|
+
// Until the decision, and while no identity exists, the collector could
|
|
92
|
+
// only drop what we send, so events wait in page memory instead. A
|
|
93
|
+
// manual site with no banner keeps immediate sending.
|
|
94
|
+
const integrationPending = cfg.preset === "manual"
|
|
95
|
+
? () => {
|
|
96
|
+
try {
|
|
97
|
+
return consentBannerPending(doc);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
: () => true;
|
|
104
|
+
const consentHold = new ConsentHold({ debug });
|
|
105
|
+
const consentPending = () => !decided() && !marker() && integrationPending();
|
|
77
106
|
const transportEnv = {
|
|
78
107
|
navigator: w.navigator,
|
|
79
108
|
fetch: typeof w.fetch === "function"
|
|
@@ -85,9 +114,14 @@ export function init(win) {
|
|
|
85
114
|
batcher = new Batcher({
|
|
86
115
|
send: (body) => send(body, transportEnv),
|
|
87
116
|
debug,
|
|
88
|
-
//
|
|
89
|
-
// tab or
|
|
90
|
-
|
|
117
|
+
// Every flush tick re-checks the consent decision (so a decision
|
|
118
|
+
// written by another tab or site code releases or discards the hold)
|
|
119
|
+
// and then the marker (so a mint by another tab or navigation is
|
|
120
|
+
// noticed). Held events are older than gap-buffered ones: release first.
|
|
121
|
+
onTick: () => {
|
|
122
|
+
resolveConsentHold(consentHold, { decided, grants, batcher, debug });
|
|
123
|
+
maybeFlushGapBuffer(gapBuffer, batcher, marker, debug);
|
|
124
|
+
},
|
|
91
125
|
timers: {
|
|
92
126
|
setInterval: (fn, ms) => w.setInterval(fn, ms),
|
|
93
127
|
clearInterval: (id) => w.clearInterval(id),
|
|
@@ -95,9 +129,8 @@ export function init(win) {
|
|
|
95
129
|
documentTarget: doc,
|
|
96
130
|
windowTarget: w,
|
|
97
131
|
});
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
// one — exposures and explicit conversions carry their own. The handle
|
|
132
|
+
// Every emitted event carries all active assignments (REQ-3) unless it
|
|
133
|
+
// already supplies its explicit context — exposures and explicit conversions carry their own. The handle
|
|
101
134
|
// is created (without emitting) BEFORE emit so every dependency of emit
|
|
102
135
|
// exists before emit can run; exposures flow at start() below.
|
|
103
136
|
const assignments = createAssignments({ document: doc, window: w, debug });
|
|
@@ -105,16 +138,33 @@ export function init(win) {
|
|
|
105
138
|
routeEvent({
|
|
106
139
|
...withAssignmentContext(event, assignments),
|
|
107
140
|
eventId: ulid(undefined, (bytes) => w.crypto.getRandomValues(bytes)),
|
|
108
|
-
}, { grants, marker, gapBuffer, batcher });
|
|
141
|
+
}, { grants, marker, gapBuffer, batcher, consentHold, consentPending });
|
|
109
142
|
};
|
|
143
|
+
// A decision written through the SDK (or announced by the ACI banner)
|
|
144
|
+
// flushes at once: the tick releases (grant) or discards (anything else)
|
|
145
|
+
// the hold, and a released first view leaves before the visitor can
|
|
146
|
+
// navigate away.
|
|
147
|
+
const onDecision = () => batcher.flush();
|
|
148
|
+
doc.addEventListener(CONSENT_BANNER_DECIDED_EVENT, onDecision);
|
|
110
149
|
// Started BEFORE the command-queue drain and page views: pre-load
|
|
111
150
|
// aci('track', ...) calls and the initial rum.page_view must be stamped
|
|
112
151
|
// with the active assignment context, or queued conversions can never
|
|
113
152
|
// match a goal (review-quorum finding). Consent commands drained after
|
|
114
153
|
// this stay safe — events wait in the gap buffer until grants resolve.
|
|
115
154
|
assignments.start(emit);
|
|
155
|
+
let emitPageView = () => { };
|
|
156
|
+
const navigation = installNavigationHook(w, [
|
|
157
|
+
contexts => assignments.rearm(contexts),
|
|
158
|
+
() => emitPageView(),
|
|
159
|
+
], () => assignments.clear());
|
|
160
|
+
// Astro dispatches this nonbubbling event on document after the router commits.
|
|
161
|
+
doc.addEventListener("astro:page-load", () => navigation.complete({
|
|
162
|
+
url: w.location.href,
|
|
163
|
+
assignments: readAssignments(doc, debug),
|
|
164
|
+
}));
|
|
116
165
|
installCommandQueue(w, {
|
|
117
|
-
|
|
166
|
+
navigationComplete: completion => navigation.complete(completion),
|
|
167
|
+
track: (name, properties) => emit({ eventName: name, properties, url: w.location.href }),
|
|
118
168
|
consent: ({ p, src }) => {
|
|
119
169
|
const purposes = sanitizePurposes(p);
|
|
120
170
|
if (purposes === undefined) {
|
|
@@ -126,20 +176,12 @@ export function init(win) {
|
|
|
126
176
|
debug("consent", { reason: "malformed_source" });
|
|
127
177
|
return;
|
|
128
178
|
}
|
|
129
|
-
if (!applyConsent(purposes, source, { document: doc, gapBuffer })) {
|
|
179
|
+
if (!applyConsent(purposes, source, { document: doc, gapBuffer, onDecision })) {
|
|
130
180
|
debug("consent", { reason: "oversized_envelope" });
|
|
131
181
|
}
|
|
132
182
|
},
|
|
133
183
|
}, debug);
|
|
134
|
-
|
|
135
|
-
// The single history patch (one pushState/replaceState wrap, one dedupe
|
|
136
|
-
// state, one popstate listener). Order is load-bearing: the assignments
|
|
137
|
-
// re-arm runs BEFORE the SPA page view so rum.page_view is stamped with
|
|
138
|
-
// the new page's experiment context.
|
|
139
|
-
installNavigationHook(w, [
|
|
140
|
-
() => assignments.rearm(),
|
|
141
|
-
emitPageView,
|
|
142
|
-
]);
|
|
184
|
+
emitPageView = startPageViews(emit, w);
|
|
143
185
|
startDeclarative(emit, {
|
|
144
186
|
document: doc,
|
|
145
187
|
intersectionObserver: w.IntersectionObserver,
|
|
@@ -169,6 +211,7 @@ export function init(win) {
|
|
|
169
211
|
document: doc,
|
|
170
212
|
categories: cfg.categories,
|
|
171
213
|
gapBuffer,
|
|
214
|
+
onDecision,
|
|
172
215
|
debug,
|
|
173
216
|
scheduler: {
|
|
174
217
|
setTimeout: (fn, ms) => w.setTimeout(fn, ms),
|
|
@@ -177,6 +220,10 @@ export function init(win) {
|
|
|
177
220
|
});
|
|
178
221
|
}
|
|
179
222
|
batcher.start();
|
|
223
|
+
// Registered after batcher.start(), so the batcher's own pagehide flush
|
|
224
|
+
// has already released a hold whose decision exists; whatever remains is
|
|
225
|
+
// undecided and never leaves the page.
|
|
226
|
+
w.addEventListener("pagehide", () => consentHold.discard("unload"));
|
|
180
227
|
maybeFlushGapBuffer(gapBuffer, batcher, marker, debug);
|
|
181
228
|
debug("init", { config: cfg });
|
|
182
229
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ExperimentAssignment } from "./assignments.js";
|
|
1
2
|
import type { PendingEvent } from "./batcher.js";
|
|
2
3
|
export declare const PAGE_VIEW_EVENT = "rum.page_view";
|
|
3
4
|
interface HistoryLike {
|
|
@@ -14,17 +15,16 @@ export interface PageViewWindow {
|
|
|
14
15
|
history: HistoryLike;
|
|
15
16
|
addEventListener(type: string, fn: () => void): void;
|
|
16
17
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
export declare function installNavigationHook(win: PageViewWindow, subscribers: ReadonlyArray<() => void>): void;
|
|
18
|
+
export interface NavigationCompletion {
|
|
19
|
+
/** Absolute URL of the page the framework just committed. */
|
|
20
|
+
url: string;
|
|
21
|
+
/** Complete committed assignment set, including [] for a base page. */
|
|
22
|
+
assignments: ExperimentAssignment[];
|
|
23
|
+
}
|
|
24
|
+
/** History signals invalidate context; only framework completion arms a view. */
|
|
25
|
+
export declare function installNavigationHook(win: PageViewWindow, subscribers: ReadonlyArray<(assignments: ExperimentAssignment[]) => void>, onStart?: () => void): {
|
|
26
|
+
complete(completion: NavigationCompletion): void;
|
|
27
|
+
};
|
|
28
28
|
/**
|
|
29
29
|
* startPageViews emits the initial page view and returns the emitter for
|
|
30
30
|
* SPA route-change views — init subscribes it to the single navigation hook
|
|
@@ -1,45 +1,50 @@
|
|
|
1
1
|
// Page views (design/04): eventName "rum.page_view" with url (the full href
|
|
2
2
|
// including query) and referrer — the SDK never populates utm, ingest
|
|
3
3
|
// derives it from the URL. One event per document load plus one per
|
|
4
|
-
//
|
|
4
|
+
// completed framework route change; hash-only changes and same-URL repeats never
|
|
5
5
|
// double count.
|
|
6
6
|
export const PAGE_VIEW_EVENT = "rum.page_view";
|
|
7
7
|
function stripHash(href) {
|
|
8
8
|
const i = href.indexOf("#");
|
|
9
9
|
return i === -1 ? href : href.slice(0, i);
|
|
10
10
|
}
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
* and page views so "page view" means exactly the same thing to both
|
|
17
|
-
* (REQ-2). Subscriber order is the contract: init lists the assignments
|
|
18
|
-
* re-arm BEFORE the page-view emitter so rum.page_view is stamped with the
|
|
19
|
-
* new page's experiment context.
|
|
20
|
-
*/
|
|
21
|
-
export function installNavigationHook(win, subscribers) {
|
|
22
|
-
let last = stripHash(win.location.href);
|
|
11
|
+
/** History signals invalidate context; only framework completion arms a view. */
|
|
12
|
+
export function installNavigationHook(win, subscribers, onStart = () => { }) {
|
|
13
|
+
let observed = stripHash(win.location.href);
|
|
14
|
+
let completed = observed;
|
|
15
|
+
let pending = false;
|
|
23
16
|
const handler = () => {
|
|
24
17
|
const current = stripHash(win.location.href);
|
|
25
|
-
if (current ===
|
|
26
|
-
return;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
18
|
+
if (current === observed)
|
|
19
|
+
return;
|
|
20
|
+
observed = current;
|
|
21
|
+
pending = true;
|
|
22
|
+
onStart();
|
|
30
23
|
};
|
|
31
24
|
const history = win.history;
|
|
32
|
-
const
|
|
25
|
+
for (const method of ["pushState", "replaceState"]) {
|
|
33
26
|
const original = history[method].bind(history);
|
|
34
27
|
history[method] = (...args) => {
|
|
35
28
|
const result = original(...args);
|
|
36
29
|
handler();
|
|
37
30
|
return result;
|
|
38
31
|
};
|
|
39
|
-
}
|
|
40
|
-
wrap("pushState");
|
|
41
|
-
wrap("replaceState");
|
|
32
|
+
}
|
|
42
33
|
win.addEventListener("popstate", handler);
|
|
34
|
+
return {
|
|
35
|
+
complete({ url, assignments }) {
|
|
36
|
+
const current = stripHash(win.location.href);
|
|
37
|
+
if (stripHash(url) !== current)
|
|
38
|
+
return; // late commit for another route
|
|
39
|
+
handler();
|
|
40
|
+
if (!pending && completed === current)
|
|
41
|
+
return;
|
|
42
|
+
pending = false;
|
|
43
|
+
completed = current;
|
|
44
|
+
for (const onNavigate of subscribers)
|
|
45
|
+
onNavigate(assignments);
|
|
46
|
+
},
|
|
47
|
+
};
|
|
43
48
|
}
|
|
44
49
|
/**
|
|
45
50
|
* startPageViews emits the initial page view and returns the emitter for
|
|
@@ -16,6 +16,10 @@ export interface ConsentBannerOptions {
|
|
|
16
16
|
/** Callback fired after a consent decision is written. */
|
|
17
17
|
onDecision?: (granted: boolean) => void;
|
|
18
18
|
}
|
|
19
|
+
/** Rendered with the page by ConsentBanner.astro, before any script runs. */
|
|
20
|
+
export declare const CONSENT_BANNER_MOUNT_ATTR = "data-aci-consent-banner-mount";
|
|
21
|
+
/** Dispatched on the document right after the banner writes a decision. */
|
|
22
|
+
export declare const CONSENT_BANNER_DECIDED_EVENT = "aci:consent-banner-decided";
|
|
19
23
|
/**
|
|
20
24
|
* hasConsentCookie returns true when the aci_consent cookie exists (the user
|
|
21
25
|
* has already made a decision, regardless of whether they granted or denied).
|
|
@@ -23,6 +27,19 @@ export interface ConsentBannerOptions {
|
|
|
23
27
|
export declare function hasConsentCookie(doc: {
|
|
24
28
|
cookie: string;
|
|
25
29
|
}): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* consentBannerPending is the banner's state API for the collect SDK
|
|
32
|
+
* (personalization-onboarding REQ-15): true while this page's ACI consent
|
|
33
|
+
* banner awaits the visitor's decision, meaning the banner is declared (its
|
|
34
|
+
* mount point, which ConsentBanner.astro renders with the HTML) or mounted,
|
|
35
|
+
* and no aci_consent decision exists. Because the mount point ships in the
|
|
36
|
+
* HTML, the answer is already right for the first deferred script to run,
|
|
37
|
+
* whether the collect SDK or the banner's own script loads first.
|
|
38
|
+
*/
|
|
39
|
+
export declare function consentBannerPending(doc: {
|
|
40
|
+
cookie: string;
|
|
41
|
+
querySelector(selectors: string): unknown;
|
|
42
|
+
}): boolean;
|
|
26
43
|
/**
|
|
27
44
|
* mountConsentBanner checks for an existing consent cookie. If none exists,
|
|
28
45
|
* it injects a banner into the document body and wires up accept/reject
|
|
@@ -18,6 +18,10 @@ const DEFAULTS = {
|
|
|
18
18
|
const BANNER_ID = "aci-consent-banner";
|
|
19
19
|
const BANNER_ATTR = "data-aci-consent-banner";
|
|
20
20
|
const BANNER_HIDDEN = "aci-consent-banner--hidden";
|
|
21
|
+
/** Rendered with the page by ConsentBanner.astro, before any script runs. */
|
|
22
|
+
export const CONSENT_BANNER_MOUNT_ATTR = "data-aci-consent-banner-mount";
|
|
23
|
+
/** Dispatched on the document right after the banner writes a decision. */
|
|
24
|
+
export const CONSENT_BANNER_DECIDED_EVENT = "aci:consent-banner-decided";
|
|
21
25
|
/**
|
|
22
26
|
* hasConsentCookie returns true when the aci_consent cookie exists (the user
|
|
23
27
|
* has already made a decision, regardless of whether they granted or denied).
|
|
@@ -25,6 +29,20 @@ const BANNER_HIDDEN = "aci-consent-banner--hidden";
|
|
|
25
29
|
export function hasConsentCookie(doc) {
|
|
26
30
|
return readConsentCookie(doc) !== undefined;
|
|
27
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* consentBannerPending is the banner's state API for the collect SDK
|
|
34
|
+
* (personalization-onboarding REQ-15): true while this page's ACI consent
|
|
35
|
+
* banner awaits the visitor's decision, meaning the banner is declared (its
|
|
36
|
+
* mount point, which ConsentBanner.astro renders with the HTML) or mounted,
|
|
37
|
+
* and no aci_consent decision exists. Because the mount point ships in the
|
|
38
|
+
* HTML, the answer is already right for the first deferred script to run,
|
|
39
|
+
* whether the collect SDK or the banner's own script loads first.
|
|
40
|
+
*/
|
|
41
|
+
export function consentBannerPending(doc) {
|
|
42
|
+
if (hasConsentCookie(doc))
|
|
43
|
+
return false;
|
|
44
|
+
return doc.querySelector(`[${CONSENT_BANNER_MOUNT_ATTR}], [${BANNER_ATTR}]`) !== null;
|
|
45
|
+
}
|
|
28
46
|
/**
|
|
29
47
|
* mountConsentBanner checks for an existing consent cookie. If none exists,
|
|
30
48
|
* it injects a banner into the document body and wires up accept/reject
|
|
@@ -47,6 +65,11 @@ export function mountConsentBanner(doc, opts = {}) {
|
|
|
47
65
|
const purposes = granted ? [PERSONALIZATION_PURPOSE] : [];
|
|
48
66
|
if (!applyConsent(purposes, config.source, { document: doc }))
|
|
49
67
|
return;
|
|
68
|
+
// Tell a collect SDK on this page to act on the decision now rather than
|
|
69
|
+
// at its next flush tick (it releases or discards what it held).
|
|
70
|
+
const EventCtor = doc.defaultView?.Event;
|
|
71
|
+
if (EventCtor)
|
|
72
|
+
doc.dispatchEvent(new EventCtor(CONSENT_BANNER_DECIDED_EVENT));
|
|
50
73
|
banner.classList.add(BANNER_HIDDEN);
|
|
51
74
|
setTimeout(cleanup, 300);
|
|
52
75
|
opts.onDecision?.(granted);
|
|
@@ -19,8 +19,16 @@ export interface FragmentEntry {
|
|
|
19
19
|
digest: string;
|
|
20
20
|
fragmentRefs?: string[];
|
|
21
21
|
}
|
|
22
|
+
export interface LayoutVariantEntry {
|
|
23
|
+
payloadRef: string;
|
|
24
|
+
digest: string;
|
|
25
|
+
slots: LayoutSlot[];
|
|
26
|
+
defaults?: Record<string, FragmentRef[]>;
|
|
27
|
+
}
|
|
22
28
|
export interface LayoutEntry {
|
|
23
29
|
name: string;
|
|
30
|
+
digest?: string;
|
|
31
|
+
variants?: Record<string, LayoutVariantEntry>;
|
|
24
32
|
payloadRef: string;
|
|
25
33
|
slots: LayoutSlot[];
|
|
26
34
|
defaults?: Record<string, FragmentRef[]>;
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { createRegistry, registryLookup } from './registry.js';
|
|
|
4
4
|
export type { Registry, RegistryEntry, AnyComponent } from './registry.js';
|
|
5
5
|
export { blockRef, blockRefArray } from './types/block-ref.js';
|
|
6
6
|
export type { BlockRef, BlockRefEntry, BlockRefMeta, BlockRefUnion } from './types/block-ref.js';
|
|
7
|
-
export type { ComponentContract, ComponentContractID, ComponentContractValidator, ComponentRegistry, ComponentValidationContext, ComponentValidationIssue, InferComponentProps, } from './types/component.js';
|
|
7
|
+
export type { ComponentContract, ComponentContractID, ComponentContractValidator, ComponentRegistry, ComponentValidationContext, ComponentValidationIssue, InferComponentProps, RootIdentityProps, } from './types/component.js';
|
|
8
8
|
export { ImageSchema, imageAttrs, primaryImageSource, type Image, type ImageSource, type ImageAttrs, type ImageSlotContract, type SlotOutput, } from './types/image.js';
|
|
9
9
|
export { LinkSchema, type Link, type LinkTarget, type LinkResolverContext, resolveLink, resolveHref, assertSafeHref, normalizeLinkRel, } from './types/link.js';
|
|
10
10
|
export { HeadingSchema, InlineContentSchema, RichTextValueSchema, type Heading, type InlineContent, type RichTextValue, } from './types/rich-text.js';
|
|
@@ -2,6 +2,7 @@ import type { IdentitySigningKeys, SigningKey, CookieMutation, ConsentSnapshot,
|
|
|
2
2
|
export declare const DEFAULT_IDENTITY_TTL_SECONDS: number;
|
|
3
3
|
export declare function parseCookieHeader(value: string | null): Map<string, string>;
|
|
4
4
|
export declare function consentSignals(request: Request, cookies: Map<string, string>): ConsentSignals;
|
|
5
|
+
export declare function identityIssuancePermitted(consent: ConsentSignals, country: string, optOutCountries: readonly string[]): boolean;
|
|
5
6
|
export declare function parseConsent(raw: string, decodeCookieValue?: boolean): ConsentSnapshot | null;
|
|
6
7
|
export declare function trimConsentWhitespace(value: string): string;
|
|
7
8
|
export declare function verifyIdentityCookie(value: string, keys: IdentitySigningKeys): Promise<string | null>;
|
|
@@ -12,3 +13,7 @@ export declare function sha256Hex(value: string): Promise<string>;
|
|
|
12
13
|
export declare function verifyClaims(value: string, keys: Record<string, string>, now: Date): Promise<Record<string, unknown> | null>;
|
|
13
14
|
export declare function cookieMutation(name: string, value: string, maxAge: number, now: Date, secure: boolean, httpOnly: boolean): CookieMutation;
|
|
14
15
|
export declare function expireCookie(name: string, secure: boolean): CookieMutation;
|
|
16
|
+
export declare function verifyHMAC(value: string, signature: string, secret: string): Promise<boolean>;
|
|
17
|
+
export declare function hmacSHA256(value: string, secret: string): Promise<string>;
|
|
18
|
+
export declare function base64URLEncode(bytes: Uint8Array): string;
|
|
19
|
+
export declare function base64URLDecode(value: string): Uint8Array;
|