@gradial/aci 0.1.52-rc.3 → 0.1.52-rc.34-e2e.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +80 -0
- package/README.md +86 -5
- package/dist/astro/error-page.js +1 -0
- package/dist/astro/index.js +4 -0
- package/dist/astro/page.d.ts +5 -2
- package/dist/astro/page.js +35 -23
- package/dist/astro/preview.js +2 -17
- package/dist/audience-data-models.d.ts +308 -194
- package/dist/audience-data-models.js +314 -51
- package/dist/cli/compile-registry.d.mts +5 -1
- package/dist/cli/compile-registry.mjs +31 -46
- package/dist/cli/mintlify-render.mjs +8 -0
- package/dist/cli/validate-content.d.mts +31 -1
- package/dist/cli/validate-content.mjs +50 -12
- package/dist/collect/aci.js +1 -1
- package/dist/collect/aci.js.sha256 +1 -1
- package/dist/collect/api.d.ts +6 -7
- package/dist/collect/api.js +21 -8
- package/dist/collect/assignments.d.ts +7 -9
- package/dist/collect/assignments.js +23 -32
- package/dist/collect/batcher.d.ts +9 -7
- package/dist/collect/batcher.js +6 -5
- 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 +21 -10
- package/dist/collect/index.js +67 -20
- 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/index.d.ts +2 -1
- package/dist/content/index.js +4 -1
- package/dist/content/provider.d.ts +16 -2
- package/dist/content/provider.js +5 -1
- package/dist/content/resolve-slots.d.ts +33 -4
- package/dist/content/resolve-slots.js +127 -19
- package/dist/content/types.d.ts +7 -1
- package/dist/edge/expression-capabilities.v1.d.ts +106 -0
- package/dist/edge/expression-capabilities.v1.js +165 -0
- package/dist/index.d.ts +1 -2
- package/dist/index.js +0 -1
- package/dist/internal/domain/edge/compose.d.ts +24 -0
- package/dist/internal/domain/edge/compose.js +89 -0
- package/dist/internal/domain/edge/identity.d.ts +5 -0
- package/dist/internal/domain/edge/identity.js +50 -14
- package/dist/internal/domain/edge/index.d.ts +2 -0
- package/dist/internal/domain/edge/index.js +2 -0
- package/dist/internal/domain/edge/mimic_token.d.ts +14 -0
- package/dist/internal/domain/edge/mimic_token.js +125 -0
- package/dist/internal/domain/edge/personalization-core.d.ts +61 -22
- package/dist/internal/domain/edge/personalization-core.js +789 -198
- 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 +6 -32
- package/dist/internal/domain/edge/record/codec.js +86 -168
- package/dist/internal/domain/edge/resolve.js +76 -39
- package/dist/internal/domain/edge/testdata/flat_only_validator_v1.d.ts +10 -0
- package/dist/internal/domain/edge/testdata/flat_only_validator_v1.js +253 -0
- package/dist/internal/domain/edge/types.d.ts +93 -41
- package/dist/internal/domain/edge/types.js +3 -3
- 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 +24 -0
- package/dist/internal/platform/collectionstore/errors.js +48 -0
- package/dist/mimic/core.d.ts +2 -0
- package/dist/mimic/core.js +31 -0
- package/dist/mintlify/blocks.d.ts +6 -0
- package/dist/mintlify/blocks.js +201 -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 +14 -0
- package/dist/mintlify/materialize.js +254 -0
- package/dist/mintlify/types.d.ts +74 -0
- package/dist/mintlify/types.js +1 -0
- package/dist/mintlify/validate.d.ts +7 -0
- package/dist/mintlify/validate.js +256 -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-pages.d.ts +4 -0
- package/dist/next/error-pages.js +4 -0
- package/dist/next/error-seat-payload.d.ts +2 -1
- package/dist/next/error-seat-payload.js +14 -24
- package/dist/next/error-seat.js +7 -2
- package/dist/next/hosted-page-target.d.ts +7 -0
- package/dist/next/hosted-page-target.js +39 -0
- package/dist/next/index.d.ts +1 -1
- package/dist/next/index.js +3 -1
- package/dist/next/page.d.ts +29 -6
- package/dist/next/page.js +47 -25
- package/dist/next/release-context.d.ts +29 -0
- package/dist/next/release-context.js +55 -0
- package/dist/next/render.d.ts +1 -1
- package/dist/next/render.js +15 -5
- package/dist/next/server.d.ts +8 -0
- package/dist/next/server.js +44 -3
- package/dist/next/vercel-edge-projection-v1.d.ts +0 -9
- package/dist/next/vercel-edge-projection-v1.js +9 -539
- package/dist/preview/core.d.ts +1 -0
- package/dist/preview/core.js +27 -0
- package/dist/react/VideoPlayer.d.ts +3 -3
- package/dist/react/VideoPlayer.js +2 -2
- package/dist/react/index.d.ts +4 -4
- package/dist/react/index.js +2 -3
- 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 +15 -8
- package/dist/testing/conformance/root-identity.d.ts +31 -0
- package/dist/testing/conformance/root-identity.js +361 -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 +5 -2
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +4 -3
- package/dist/types/block-ref.d.ts +2 -0
- package/dist/types/block-ref.js +3 -2
- package/dist/types/component.d.ts +9 -0
- package/dist/types/config.d.ts +0 -1
- package/dist/types/image.d.ts +54 -17
- package/dist/types/image.js +102 -18
- package/dist/types/index.d.ts +0 -1
- package/dist/types/index.js +0 -1
- package/dist/types/media.d.ts +14 -6
- package/dist/types/page.d.ts +0 -1
- package/dist/types/video.d.ts +7 -3
- package/migrations/conformance-registry.md +22 -0
- package/package.json +21 -7
- package/src/cli/compile-registry.mjs +30 -42
- package/src/cli/mintlify-render.mjs +9 -0
- package/src/cli/validate-content.mjs +52 -13
- package/src/types/block-ref.test.ts +60 -0
- package/src/types/block-ref.ts +4 -2
- package/src/types/component.ts +10 -0
- package/src/types/config.ts +0 -1
- package/src/types/image.ts +133 -29
- package/src/types/index.ts +0 -1
- package/src/types/page.ts +0 -1
- package/src/types/video.ts +1 -1
- 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/atomicBlocks.ts +7 -6
- package/templates/astro/starter-content/src/fixtures/composition.ts +3 -2
- package/templates/astro/starter-content/src/fixtures/sections.ts +3 -3
- package/templates/astro/starter-content/src/stories/CompositionStoryFrame.astro +7 -2
- package/templates/astro/template/.aci.yaml.tmpl +0 -1
- package/templates/astro/template/package.json.tmpl +5 -5
- package/templates/astro/template/src/layouts/SiteLayout.astro +2 -2
- package/templates/common/.agents/skills/aci/SKILL.md +58 -1
- package/templates/common/.agents/skills/authoring/SKILL.md +203 -43
- package/templates/common/.agents/skills/authoring/references/block-registry.md +19 -3
- package/templates/common/.agents/skills/authoring/references/block-slots.md +3 -0
- package/templates/common/.agents/skills/authoring/references/contracts.md +9 -30
- package/templates/common/.agents/skills/authoring/references/repo-structure.md +5 -4
- package/templates/common/.agents/skills/authoring/references/sdk-server-functions.md +3 -3
- package/templates/common/.agents/skills/dam-delivery/SKILL.md +217 -0
- package/templates/common/.agents/skills/dam-delivery/references/permalink-grammar.md +167 -0
- package/templates/common/.agents/skills/migration/references/canon-migration.md +18 -67
- package/templates/common/.agents/skills/migration/references/selector-map.md +1 -1
- package/templates/common/.content/fragments/navbar/_index.json +24 -0
- package/templates/common/.content/fragments/site-footer/_index.json +61 -0
- package/templates/common/.content/pages/404/_index.json +4 -2
- package/templates/common/.content/pages/{home/_index.json → _index.json} +10 -1
- package/templates/common/.content/pages/error/_index.json +4 -2
- package/templates/common/.content/schemas/site.schema.json +11 -1
- package/templates/common/.content/templates/error.json +24 -0
- package/templates/common/.content/templates/home.json +28 -0
- package/templates/common/.content/templates/site.json +8 -0
- package/templates/common/.content/templates/standard.json +24 -0
- package/templates/common/.nvmrc +1 -1
- package/templates/common/starter-content/.content/.rules-baseline.json +8 -0
- package/templates/common/starter-content/.content/config/site.json +1 -1
- package/templates/common/starter-content/.content/content-rules/no-public-resource-paths.json +6 -0
- package/templates/common/starter-content/.content/dam/README.md +74 -0
- 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 → 000000000000000000000000101}/governance.json +2 -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/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000102}/governance.json +2 -1
- package/templates/common/starter-content/.content/fragments/navbar/_index.json +23 -20
- package/templates/common/starter-content/.content/fragments/site-footer/_index.json +59 -56
- package/templates/common/starter-content/.content/pages/404/_index.json +2 -2
- package/templates/common/starter-content/.content/pages/{home/_index.json → _index.json} +23 -3
- package/templates/common/starter-content/.content/pages/about/_index.json +5 -2
- package/templates/common/starter-content/.content/pages/error/_index.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/button.json +55 -0
- package/templates/common/starter-content/.content/patterns/components/container.json +67 -0
- package/templates/common/starter-content/.content/patterns/components/feature_card.json +30 -0
- package/templates/common/starter-content/.content/patterns/components/feature_grid.json +59 -0
- package/templates/common/starter-content/.content/patterns/components/home_hero.json +151 -0
- package/templates/common/starter-content/.content/patterns/components/personalization_choice.json +42 -0
- package/templates/common/starter-content/.content/patterns/components/site-footer.json +203 -0
- package/templates/common/starter-content/.content/patterns/components/site-navigation.json +115 -0
- package/templates/common/starter-content/.content/patterns/components/stats_bar.json +45 -0
- 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/nextjs/template/.aci.yaml +9 -0
- package/templates/nextjs/template/.aci.yaml.tmpl +0 -1
- package/templates/nextjs/template/package.json.tmpl +4 -3
- package/templates/nextjs/template/src/app/_components/c15t-bridge.ts +30 -0
- package/templates/nextjs/template/src/app/_components/c15t-consent-ui.tsx +37 -0
- package/templates/nextjs/template/src/app/_components/consent-manager.css +63 -0
- package/templates/nextjs/template/src/app/_components/consent-manager.tsx +76 -0
- package/templates/nextjs/template/src/app/fonts/inter/OFL.txt +93 -0
- package/templates/nextjs/template/src/app/fonts/inter/inter-latin.woff2 +0 -0
- package/templates/nextjs/template/src/app/fonts/space-grotesk/OFL.txt +93 -0
- package/templates/nextjs/template/src/app/fonts/space-grotesk/space-grotesk-latin.woff2 +0 -0
- package/templates/nextjs/template/src/app/layout.tsx +12 -7
- package/templates/nextjs/template/tests/conformance.test.ts.tmpl +1 -0
- package/dist/define-layout.d.ts +0 -6
- package/dist/define-layout.js +0 -14
- package/dist/edge/personalization-core.d.ts +0 -11
- package/dist/edge/personalization-core.js +0 -269
- package/dist/next/collect-route.d.ts +0 -19
- package/dist/next/collect-route.js +0 -459
- package/dist/next/envelope-digest.d.ts +0 -21
- package/dist/next/envelope-digest.js +0 -58
- package/dist/next/middleware.d.ts +0 -40
- package/dist/next/middleware.js +0 -475
- package/dist/next/personalization-host.d.ts +0 -134
- package/dist/next/personalization-host.js +0 -270
- package/dist/next/root-layout.d.ts +0 -4
- package/dist/next/root-layout.js +0 -5
- package/dist/next/vercel-collect-sdk-route.d.ts +0 -27
- package/dist/next/vercel-collect-sdk-route.js +0 -181
- package/dist/next/vercel-personalization.d.ts +0 -4
- package/dist/next/vercel-personalization.js +0 -4
- package/dist/next/vercel-s3-personalization.d.ts +0 -79
- package/dist/next/vercel-s3-personalization.js +0 -570
- package/dist/types/layout.d.ts +0 -24
- package/src/types/layout.ts +0 -29
- package/templates/astro/starter-content/src/render/RenderPage.astro +0 -67
- package/templates/astro/template/src/cms/contracts/layouts/index.ts +0 -18
- package/templates/nextjs/starter-content/src/components/elements/AtomicBlock.tsx +0 -13
- package/templates/nextjs/template/src/cms/contracts/layouts/index.ts +0 -16
- /package/dist/{types/layout.js → cli/mintlify-render.d.mts} +0 -0
- /package/templates/common/.content/overlays/audience/preference-blue/pages/{home/_index.json → _index.json} +0 -0
- /package/templates/common/.content/overlays/audience/preference-red/pages/{home/_index.json → _index.json} +0 -0
- /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_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_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
|
/**
|
|
@@ -4,20 +4,23 @@
|
|
|
4
4
|
// response. Bounded at 50 (drop-oldest), persisted per-tab in
|
|
5
5
|
// sessionStorage["aci:buf"], purged on withdrawal, flushed through the
|
|
6
6
|
// normal batcher when the marker appears (checked at init and on every
|
|
7
|
-
// flush tick). Flushed events keep their eventIds.
|
|
7
|
+
// flush tick). Flushed events keep their eventIds. Storage is authoritative:
|
|
8
|
+
// a BFCache-retained document must observe another document's drain/purge.
|
|
8
9
|
export const GAP_BUFFER_KEY = "aci:buf";
|
|
9
10
|
export const GAP_BUFFER_CAP = 50;
|
|
10
11
|
const MINT_MARKER_COOKIE = "aci_m";
|
|
11
12
|
export class GapBuffer {
|
|
12
|
-
events
|
|
13
|
+
// Only retain events in memory when storage is unavailable. A successful
|
|
14
|
+
// storage operation must never leave a second, stale owner of that queue.
|
|
15
|
+
events = [];
|
|
13
16
|
storage;
|
|
14
17
|
debug;
|
|
15
18
|
constructor(env) {
|
|
16
19
|
this.debug = env?.debug;
|
|
17
20
|
this.storage = env?.storage;
|
|
18
|
-
this.events = this.restore();
|
|
19
21
|
}
|
|
20
22
|
push(event) {
|
|
23
|
+
this.events = this.restore();
|
|
21
24
|
this.events.push(event);
|
|
22
25
|
if (this.events.length > GAP_BUFFER_CAP) {
|
|
23
26
|
this.events.splice(0, this.events.length - GAP_BUFFER_CAP);
|
|
@@ -27,13 +30,14 @@ export class GapBuffer {
|
|
|
27
30
|
}
|
|
28
31
|
/** drain returns and clears all buffered events, preserving order and ids. */
|
|
29
32
|
drain() {
|
|
30
|
-
const drained = this.
|
|
33
|
+
const drained = this.restore();
|
|
31
34
|
this.events = [];
|
|
32
35
|
this.persist();
|
|
33
36
|
return drained;
|
|
34
37
|
}
|
|
35
38
|
/** purge empties the buffer (consent withdrawal). */
|
|
36
39
|
purge() {
|
|
40
|
+
this.events = this.restore();
|
|
37
41
|
if (this.events.length > 0) {
|
|
38
42
|
this.debug?.("buffer", { reason: "purged", count: this.events.length });
|
|
39
43
|
}
|
|
@@ -41,12 +45,12 @@ export class GapBuffer {
|
|
|
41
45
|
this.persist();
|
|
42
46
|
}
|
|
43
47
|
size() {
|
|
44
|
-
return this.
|
|
48
|
+
return this.restore().length;
|
|
45
49
|
}
|
|
46
50
|
restore() {
|
|
47
51
|
const storage = this.storage;
|
|
48
52
|
if (!storage)
|
|
49
|
-
return
|
|
53
|
+
return this.events;
|
|
50
54
|
let raw;
|
|
51
55
|
try {
|
|
52
56
|
raw = storage.getItem(GAP_BUFFER_KEY);
|
|
@@ -86,6 +90,7 @@ export class GapBuffer {
|
|
|
86
90
|
else {
|
|
87
91
|
storage.setItem(GAP_BUFFER_KEY, JSON.stringify(this.events));
|
|
88
92
|
}
|
|
93
|
+
this.events = [];
|
|
89
94
|
}
|
|
90
95
|
catch (err) {
|
|
91
96
|
this.storage = undefined;
|
|
@@ -104,12 +109,18 @@ export function hasMintMarker(doc) {
|
|
|
104
109
|
return false;
|
|
105
110
|
}
|
|
106
111
|
/**
|
|
107
|
-
* routeEvent implements the emit routing (design/05): events captured
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
112
|
+
* routeEvent implements the emit routing (design/05): events captured while
|
|
113
|
+
* a CMP's consent decision is pending (and no identity exists) are held in
|
|
114
|
+
* page memory until the decision; events captured in the consent gap —
|
|
115
|
+
* envelope grants personalization AND the marker is absent — are buffered;
|
|
116
|
+
* everything else goes straight to the batcher (the collector enforces
|
|
117
|
+
* consent server-side).
|
|
111
118
|
*/
|
|
112
119
|
export function routeEvent(event, deps) {
|
|
120
|
+
if (deps.consentHold && deps.consentPending?.()) {
|
|
121
|
+
deps.consentHold.push(event);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
113
124
|
if (deps.grants() && !deps.marker()) {
|
|
114
125
|
deps.gapBuffer.push(event);
|
|
115
126
|
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);
|
|
116
|
-
|
|
117
|
-
|
|
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
|
+
}));
|
|
165
|
+
installCommandQueue(w, {
|
|
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);
|
package/dist/content/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export * from './routes.js';
|
|
|
3
3
|
export * from './types.js';
|
|
4
4
|
export * from './personalization.js';
|
|
5
5
|
export * from './cache.js';
|
|
6
|
-
export
|
|
6
|
+
export { FragmentResolutionError, MAX_EXPANDED_BLOCKS, MAX_FRAGMENT_DEPTH, fragmentToBlock, preserveFragmentScope, resolveFragmentNodes, resolveSlots, resolveSlotsSync, } from './resolve-slots.js';
|
|
7
|
+
export type { FragmentBlock, FragmentData, FragmentLoader } from './resolve-slots.js';
|
|
7
8
|
export * from './default-error-page.js';
|
|
8
9
|
export * from './error-pages.js';
|
package/dist/content/index.js
CHANGED
|
@@ -3,6 +3,9 @@ export * from './routes.js';
|
|
|
3
3
|
export * from './types.js';
|
|
4
4
|
export * from './personalization.js';
|
|
5
5
|
export * from './cache.js';
|
|
6
|
-
export
|
|
6
|
+
// Explicit list, not `export *`: fragmentRootScope is runtime-only provenance
|
|
7
|
+
// shared with mintlify/blocks.ts via a direct resolve-slots import and must
|
|
8
|
+
// never leak into the public '@gradial/aci/content' surface.
|
|
9
|
+
export { FragmentResolutionError, MAX_EXPANDED_BLOCKS, MAX_FRAGMENT_DEPTH, fragmentToBlock, preserveFragmentScope, resolveFragmentNodes, resolveSlots, resolveSlotsSync, } from './resolve-slots.js';
|
|
7
10
|
export * from './default-error-page.js';
|
|
8
11
|
export * from './error-pages.js';
|
|
@@ -2,6 +2,7 @@ import type { KernelPage, KernelRouteMetadata, KernelSiteConfig, RenderInput } f
|
|
|
2
2
|
export interface RouteEntry {
|
|
3
3
|
slug: string;
|
|
4
4
|
path: string;
|
|
5
|
+
variantKey: string;
|
|
5
6
|
payloadRef: string;
|
|
6
7
|
digest: string;
|
|
7
8
|
fragmentRefs?: string[];
|
|
@@ -19,8 +20,21 @@ export interface FragmentEntry {
|
|
|
19
20
|
digest: string;
|
|
20
21
|
fragmentRefs?: string[];
|
|
21
22
|
}
|
|
23
|
+
export interface LayoutVariantEntry {
|
|
24
|
+
payloadRef: string;
|
|
25
|
+
digest: string;
|
|
26
|
+
slots: LayoutSlot[];
|
|
27
|
+
defaults?: Record<string, FragmentRef[]>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Derived template render plan in the compiled release wire format.
|
|
31
|
+
* This is never an authorable layout: page templates own regions and defaults.
|
|
32
|
+
* The LayoutEntry name and layouts wire key remain for retained release readers.
|
|
33
|
+
*/
|
|
22
34
|
export interface LayoutEntry {
|
|
23
35
|
name: string;
|
|
36
|
+
digest?: string;
|
|
37
|
+
variants?: Record<string, LayoutVariantEntry>;
|
|
24
38
|
payloadRef: string;
|
|
25
39
|
slots: LayoutSlot[];
|
|
26
40
|
defaults?: Record<string, FragmentRef[]>;
|
|
@@ -32,7 +46,6 @@ export interface LayoutSlot {
|
|
|
32
46
|
export interface FragmentRef {
|
|
33
47
|
kind: string;
|
|
34
48
|
fragmentId: string;
|
|
35
|
-
inline: boolean;
|
|
36
49
|
}
|
|
37
50
|
export interface CompiledManifest {
|
|
38
51
|
manifestVersion: string;
|
|
@@ -59,8 +72,9 @@ export declare class LayoutNotFoundError extends Error {
|
|
|
59
72
|
constructor(name: string, cause?: unknown);
|
|
60
73
|
}
|
|
61
74
|
export interface RenderInputOptions {
|
|
75
|
+
variantKey: string;
|
|
62
76
|
domain?: string;
|
|
63
77
|
locale?: string;
|
|
64
78
|
}
|
|
65
|
-
export declare function loadRenderInput(provider: ContentProvider, route
|
|
79
|
+
export declare function loadRenderInput(provider: ContentProvider, route: string | undefined, options: RenderInputOptions): Promise<RenderInput>;
|
|
66
80
|
export declare function routeMetadataForContent(siteConfig: KernelSiteConfig, page: KernelPage | null): KernelRouteMetadata;
|