@tokenoftrust/storefront-runner 2.0.40 → 2.0.42
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/apps/storefront/integrations/lifecycle-host-asset.mjs +20 -6
- package/apps/storefront/package.json +1 -0
- package/apps/storefront/platform-reconcile-tenants.json +3 -0
- package/apps/storefront/public/favicon.ico +0 -0
- package/apps/storefront/public/platform/google-measurement/v2/entry.mjs +1 -1
- package/apps/storefront/public/shared/commerce-chrome.css +147 -16
- package/apps/storefront/src/components/AsyncStylesheet.astro +26 -0
- package/apps/storefront/src/components/Breadcrumbs.astro +8 -1
- package/apps/storefront/src/components/Img.astro +6 -5
- package/apps/storefront/src/components/ProductCard.astro +6 -4
- package/apps/storefront/src/components/Seo.astro +2 -1
- package/apps/storefront/src/components/chrome/SiteFooter.astro +45 -11
- package/apps/storefront/src/components/chrome/SiteHeader.astro +83 -13
- package/apps/storefront/src/components/commerce/RatingStars.astro +1 -1
- package/apps/storefront/src/components/compliance/ComplianceNotice.astro +2 -2
- package/apps/storefront/src/components/content/PromoTiles.astro +107 -0
- package/apps/storefront/src/components/content/TrustStrip.astro +30 -14
- package/apps/storefront/src/components/home/Hero.astro +156 -4
- package/apps/storefront/src/components/islands/AgeGate.tsx +135 -26
- package/apps/storefront/src/components/islands/ImageGallery.tsx +6 -9
- package/apps/storefront/src/components/islands/IsolatedAgeGate.tsx +194 -46
- package/apps/storefront/src/components/islands/SearchTypeahead.tsx +10 -4
- package/apps/storefront/src/components/marketing/Faq.astro +9 -2
- package/apps/storefront/src/config/devTenantSeed.ts +1 -1
- package/apps/storefront/src/config/kvTenantResolver.ts +7 -1
- package/apps/storefront/src/config/publicationSpaceConfig.ts +12 -1
- package/apps/storefront/src/config/publicationSpaceRegistry.ts +5 -0
- package/apps/storefront/src/config/tenants.ts +22 -0
- package/apps/storefront/src/layouts/Layout.astro +69 -44
- package/apps/storefront/src/lib/activity/changeActorAttribution.ts +24 -8
- package/apps/storefront/src/lib/activity/recordActivity.ts +15 -0
- package/apps/storefront/src/lib/asyncCss.ts +23 -0
- package/apps/storefront/src/lib/auth/loginGate.ts +22 -11
- package/apps/storefront/src/lib/basePath.ts +70 -9
- package/apps/storefront/src/lib/chrome/managedCart.ts +50 -0
- package/apps/storefront/src/lib/chrome/model.ts +4 -0
- package/apps/storefront/src/lib/chrome/newsletterAgeAttestation.ts +9 -0
- package/apps/storefront/src/lib/content/trustStrip.ts +38 -3
- package/apps/storefront/src/lib/cspStaticHashes.ts +4 -1
- package/apps/storefront/src/lib/diag/capture.ts +161 -0
- package/apps/storefront/src/lib/diag/consoleLine.ts +76 -0
- package/apps/storefront/src/lib/diag/debugConsole.ts +118 -0
- package/apps/storefront/src/lib/diag/debugMode.ts +163 -0
- package/apps/storefront/src/lib/diag/debugWindow.ts +111 -0
- package/apps/storefront/src/lib/diag/reference.ts +148 -0
- package/apps/storefront/src/lib/diag/referenceStore.ts +113 -0
- package/apps/storefront/src/lib/diag/report.ts +109 -0
- package/apps/storefront/src/lib/diag/session.ts +93 -0
- package/apps/storefront/src/lib/featuredProductsPresentation.ts +5 -0
- package/apps/storefront/src/lib/heroSlideshow.ts +41 -0
- package/apps/storefront/src/lib/homeVisualParity.ts +107 -0
- package/apps/storefront/src/lib/imageAttrs.ts +9 -6
- package/apps/storefront/src/lib/integration/nonceStamp.ts +23 -1
- package/apps/storefront/src/lib/integration/renderIntegrationState.ts +36 -0
- package/apps/storefront/src/lib/integration/tenantAnalyticsDeclarations.ts +17 -12
- package/apps/storefront/src/lib/jsonScript.ts +13 -0
- package/apps/storefront/src/lib/pinnedRouteRewriteBoundary.ts +31 -0
- package/apps/storefront/src/lib/previewReload.ts +53 -9
- package/apps/storefront/src/lib/rawChrome.ts +148 -19
- package/apps/storefront/src/lib/rawMarketingHtml.ts +120 -7
- package/apps/storefront/src/lib/storyblok/content-model.ts +46 -3
- package/apps/storefront/src/lib/styleGuideThemes.ts +107 -0
- package/apps/storefront/src/lib/tenantRouting.ts +15 -4
- package/apps/storefront/src/lib/tot/syncState.ts +21 -0
- package/apps/storefront/src/lib/tracking/publishSafetyNet.ts +29 -3
- package/apps/storefront/src/middleware/controlPlane.ts +9 -5
- package/apps/storefront/src/middleware/index.ts +13 -1
- package/apps/storefront/src/middleware/renderChain.ts +54 -11
- package/apps/storefront/src/middleware/routeBranch.ts +58 -12
- package/apps/storefront/src/middleware/staticBundle.ts +37 -3
- package/apps/storefront/src/pages/[...slug].astro +6 -2
- package/apps/storefront/src/pages/api/request-access.ts +5 -4
- package/apps/storefront/src/pages/blog/[slug].astro +11 -1
- package/apps/storefront/src/pages/cart.ts +23 -0
- package/apps/storefront/src/pages/index.astro +64 -85
- package/apps/storefront/src/pages/products/[handle].astro +8 -6
- package/apps/storefront/src/pages/sitemap.xml.ts +8 -2
- package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +14 -25
- package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +10 -17
- package/apps/storefront/src/pages/style-guide/[tenant]/guide/[theme].astro +9 -16
- package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +9 -15
- package/apps/storefront/src/pages/the-build/[slug].astro +7 -1
- package/apps/storefront/src/styles/fonts.css +7 -0
- package/apps/storefront/src/styles/global.css +397 -3
- package/package.json +4 -1
- package/packages/cli/src/visual-parity.mjs +321 -0
- package/packages/public-runtime/package.json +2 -0
- package/packages/public-runtime/schemas/publication-space/v1/manifest.schema.json +1 -1
- package/packages/public-runtime/schemas/publication-space/v1/normalized-mount.schema.json +1 -1
- package/packages/public-runtime/src/chrome.ts +44 -7
- package/packages/public-runtime/src/csp.ts +12 -1
- package/packages/public-runtime/src/extension-contract-values.d.mts +19 -0
- package/packages/public-runtime/src/extension-contract-values.mjs +62 -0
- package/packages/public-runtime/src/extension-contract.ts +20 -6
- package/packages/public-runtime/src/index.ts +8 -0
- package/packages/public-runtime/src/integration/consent-broker.ts +10 -7
- package/packages/public-runtime/src/integration/consent-config.ts +9 -0
- package/packages/public-runtime/src/integration/contact-sales-action-source.ts +56 -10
- package/packages/public-runtime/src/integration/csp-compiler.ts +6 -3
- package/packages/public-runtime/src/integration/governed-event-properties.ts +62 -0
- package/packages/public-runtime/src/integration/index.ts +1 -0
- package/packages/public-runtime/src/integration/lifecycle-boot.ts +34 -6
- package/packages/public-runtime/src/integration/lifecycle-host-asset.d.mts +2 -0
- package/packages/public-runtime/src/integration/lifecycle-host-asset.mjs +17 -3
- package/packages/public-runtime/src/integration/native-amplitude.ts +12 -3
- package/packages/public-runtime/src/integration/native-ga4-sandbox-asset.d.mts +2 -0
- package/packages/public-runtime/src/integration/native-ga4-sandbox-asset.mjs +35 -1
- package/packages/public-runtime/src/integration/native-ga4-sandbox-entry.ts +45 -12
- package/packages/public-runtime/src/integration/native-ga4-sandbox.ts +28 -7
- package/packages/public-runtime/src/integration/native-ga4.ts +18 -2
- package/packages/public-runtime/src/integration/platform-adapters.ts +14 -3
- package/packages/public-runtime/src/integration/render-phases.ts +28 -2
- package/packages/public-runtime/src/publication-redirects.d.mts +45 -0
- package/packages/public-runtime/src/publication-redirects.mjs +379 -0
- package/packages/public-runtime/src/publication-space.ts +24 -3
- package/packages/public-runtime/src/static-bundle.ts +145 -26
- package/packages/public-runtime/src/tenant-embed-defaults.d.mts +3 -0
- package/packages/public-runtime/src/tenant-embed-defaults.mjs +40 -0
- package/packages/public-runtime/src/tenant.ts +59 -1
- package/pnpm-lock.runner.yaml +12 -0
- package/pnpm-workspace.yaml +4 -0
- package/scripts/dev/git-status.mjs +1 -1
- package/scripts/dev/publish.mjs +6 -0
- package/scripts/lib/tenant-asset-copy.mjs +4 -2
- package/scripts/tenant/materialize-tenant-content.mjs +1 -0
- package/scripts/tenant/validate.mjs +171 -8
- package/apps/storefront/src/lib/cfImage.ts +0 -55
- package/apps/storefront/src/workers/pruneRollupWorker.ts +0 -120
- package/tenants/home/public/fonts/inter-latin-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-ext-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-ext-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-ext-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-700-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-800-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-700-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-800-normal.woff2 +0 -0
- package/tenants/home/public/js/storefront.js +0 -129
- package/tenants/home/public/logo-wordmark.png +0 -0
- package/tenants/home/public/pages/storefront.css +0 -235
- package/tenants/home/theme.json +0 -6
|
@@ -475,10 +475,10 @@ export class ConsentBroker {
|
|
|
475
475
|
/**
|
|
476
476
|
* Viewer-owned analytics consent for the public storefront document.
|
|
477
477
|
*
|
|
478
|
-
* The tenant configuration
|
|
479
|
-
*
|
|
480
|
-
*
|
|
481
|
-
*
|
|
478
|
+
* The tenant configuration supplies the analytics default and a stored viewer choice
|
|
479
|
+
* overrides it. Global Privacy Control always denies analytics. The same synchronous
|
|
480
|
+
* early boot runs in both document mounts before any integration or measurement loader
|
|
481
|
+
* can execute.
|
|
482
482
|
*/
|
|
483
483
|
|
|
484
484
|
export const VIEWER_ANALYTICS_CONSENT_STORAGE_KEY = "tot.viewer-analytics-consent.v1";
|
|
@@ -492,7 +492,7 @@ export type ViewerAnalyticsConsentDecision = "granted" | "denied";
|
|
|
492
492
|
export interface ViewerAnalyticsConsentState {
|
|
493
493
|
schemaVersion: 1;
|
|
494
494
|
decision: ViewerAnalyticsConsentDecision;
|
|
495
|
-
source: "default-deny" | "stored" | "global-privacy-control";
|
|
495
|
+
source: "tenant-default" | "default-deny" | "stored" | "global-privacy-control";
|
|
496
496
|
canGrant: boolean;
|
|
497
497
|
expiresAt: number | null;
|
|
498
498
|
retentionDays: typeof VIEWER_ANALYTICS_CONSENT_RETENTION_DAYS;
|
|
@@ -591,6 +591,7 @@ export function applyEarlyViewerAnalyticsConsent(
|
|
|
591
591
|
// approval. Only the current authenticated server-rendered snapshot may grant.
|
|
592
592
|
const contextComplete = tenantPublicId.length > 0 && environment.length > 0 && siteSurface.length > 0;
|
|
593
593
|
const tenantApprovedAnalytics = contextComplete && rawApproved.includes("analytics");
|
|
594
|
+
const tenantDefaultAnalytics = tenantApprovedAnalytics && rawEligible.includes("analytics");
|
|
594
595
|
const globalPrivacyControl = win.navigator?.globalPrivacyControl === true;
|
|
595
596
|
|
|
596
597
|
let stored: { decision: ViewerAnalyticsConsentDecision; expiresAt: number } | null = null;
|
|
@@ -631,12 +632,14 @@ export function applyEarlyViewerAnalyticsConsent(
|
|
|
631
632
|
const decision: ViewerAnalyticsConsentDecision =
|
|
632
633
|
globalPrivacyControl || !tenantApprovedAnalytics
|
|
633
634
|
? "denied"
|
|
634
|
-
: stored?.decision ?? "denied";
|
|
635
|
+
: stored?.decision ?? (tenantDefaultAnalytics ? "granted" : "denied");
|
|
635
636
|
const source: ViewerAnalyticsConsentState["source"] = globalPrivacyControl
|
|
636
637
|
? "global-privacy-control"
|
|
637
638
|
: stored
|
|
638
639
|
? "stored"
|
|
639
|
-
:
|
|
640
|
+
: tenantDefaultAnalytics
|
|
641
|
+
? "tenant-default"
|
|
642
|
+
: "default-deny";
|
|
640
643
|
|
|
641
644
|
const eligible = canonicalPurposes.filter(
|
|
642
645
|
(purpose) =>
|
|
@@ -258,3 +258,12 @@ export function unmetConsentDependencies(
|
|
|
258
258
|
export function emptyConsentConfig(): TenantConsentConfig {
|
|
259
259
|
return { schemaVersion: CONSENT_CONFIG_SCHEMA_VERSION, purposes: [], cmp: { mode: "none" } };
|
|
260
260
|
}
|
|
261
|
+
|
|
262
|
+
/** Platform default when a tenant configured analytics and saved no consent override. */
|
|
263
|
+
export function defaultAnalyticsConsentConfig(): TenantConsentConfig {
|
|
264
|
+
return {
|
|
265
|
+
schemaVersion: CONSENT_CONFIG_SCHEMA_VERSION,
|
|
266
|
+
purposes: [{ purpose: "analytics", legalBasis: "legitimate-interest", defaultState: "granted" }],
|
|
267
|
+
cmp: { mode: "none" },
|
|
268
|
+
};
|
|
269
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The platform-owned browser binding for
|
|
2
|
+
* The platform-owned browser binding for reviewed contact-sales CTA identities.
|
|
3
3
|
*
|
|
4
4
|
* This is deliberately not a general interaction adapter. Both DOM identifiers and
|
|
5
5
|
* the emitted semantic name are fixed below; the source reads no labels, URLs,
|
|
@@ -10,19 +10,41 @@ import { managedAnalyticsOpaqueRevision } from "./managed-analytics-value-safety
|
|
|
10
10
|
|
|
11
11
|
/** The only reviewed CTA identities that may raise this marketing action. */
|
|
12
12
|
export const CONTACT_SALES_ACTION_SOURCES = [
|
|
13
|
-
{
|
|
14
|
-
{
|
|
13
|
+
{ selector: '[data-cta-id="cta-book-demo"]', logicalId: "contact-sales-book-demo" },
|
|
14
|
+
{ selector: '[data-cta-id="book-demo"]', logicalId: "contact-sales-book-demo" },
|
|
15
|
+
{ selector: '[data-cta-id="contact-sales"]', logicalId: "contact-sales-lower" },
|
|
16
|
+
{ selector: '[data-tot-action="contact-sales"]', logicalId: "contact-sales-action" },
|
|
17
|
+
{ selector: '[data-tot-action="nav-contact-sales"]', logicalId: "contact-sales-nav" },
|
|
15
18
|
] as const;
|
|
16
19
|
|
|
17
|
-
const CONTACT_SALES_ACTION_NAME = "user_clicked_contact_sales_mktg" as const;
|
|
20
|
+
export const CONTACT_SALES_ACTION_NAME = "user_clicked_contact_sales_mktg" as const;
|
|
21
|
+
/** The reviewed action allowlist paired with this fixed platform source. */
|
|
22
|
+
export const CONTACT_SALES_ACTION_REGISTRY = {
|
|
23
|
+
schemaVersion: 1,
|
|
24
|
+
actions: [{ name: CONTACT_SALES_ACTION_NAME }],
|
|
25
|
+
} as const;
|
|
18
26
|
const CORRELATION_TTL_MS = 60 * 60 * 1000;
|
|
27
|
+
export const CONTACT_SALES_NAVIGATION_DEFER_MS = 100;
|
|
28
|
+
|
|
29
|
+
export interface ContactSalesClickEvent {
|
|
30
|
+
altKey?: boolean;
|
|
31
|
+
button?: number;
|
|
32
|
+
ctrlKey?: boolean;
|
|
33
|
+
defaultPrevented?: boolean;
|
|
34
|
+
metaKey?: boolean;
|
|
35
|
+
preventDefault?(): void;
|
|
36
|
+
shiftKey?: boolean;
|
|
37
|
+
}
|
|
19
38
|
|
|
20
39
|
export interface ContactSalesActionElement {
|
|
21
|
-
|
|
22
|
-
|
|
40
|
+
download?: string;
|
|
41
|
+
href?: string;
|
|
42
|
+
target?: string;
|
|
43
|
+
addEventListener(type: "click", listener: (event: ContactSalesClickEvent) => void): void;
|
|
44
|
+
removeEventListener(type: "click", listener: (event: ContactSalesClickEvent) => void): void;
|
|
23
45
|
}
|
|
24
46
|
|
|
25
|
-
/** The narrow DOM seam used only to find the
|
|
47
|
+
/** The narrow DOM seam used only to find the fixed platform CTA identities. */
|
|
26
48
|
export interface ContactSalesActionDocument {
|
|
27
49
|
querySelectorAll(selector: string): ArrayLike<ContactSalesActionElement>;
|
|
28
50
|
}
|
|
@@ -31,10 +53,23 @@ export interface ContactSalesActionSourceOptions {
|
|
|
31
53
|
document?: ContactSalesActionDocument;
|
|
32
54
|
planRevision: string;
|
|
33
55
|
now?: () => number;
|
|
56
|
+
navigate?: (href: string) => void;
|
|
57
|
+
schedule?: (callback: () => void, delayMs: number) => void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function deferredNavigationHref(node: ContactSalesActionElement, event: ContactSalesClickEvent): string | undefined {
|
|
61
|
+
if (
|
|
62
|
+
event.defaultPrevented || (event.button !== undefined && event.button !== 0) ||
|
|
63
|
+
event.altKey || event.ctrlKey || event.metaKey || event.shiftKey ||
|
|
64
|
+
typeof event.preventDefault !== "function" || typeof node.href !== "string" || !node.href ||
|
|
65
|
+
(typeof node.target === "string" && node.target !== "" && node.target !== "_self") ||
|
|
66
|
+
(typeof node.download === "string" && node.download !== "")
|
|
67
|
+
) return undefined;
|
|
68
|
+
return node.href;
|
|
34
69
|
}
|
|
35
70
|
|
|
36
71
|
/**
|
|
37
|
-
* Bind
|
|
72
|
+
* Bind the fixed reviewed DOM identities to a typed source for the managed-analytics
|
|
38
73
|
* bridge. A keyboard activation of an anchor/button produces its native `click`, so
|
|
39
74
|
* it follows this same semantic path without a separate key listener.
|
|
40
75
|
*/
|
|
@@ -43,6 +78,7 @@ export function createContactSalesActionSource(
|
|
|
43
78
|
): PlatformManagedActionSource {
|
|
44
79
|
const document = options.document;
|
|
45
80
|
const now = options.now ?? Date.now;
|
|
81
|
+
const schedule = options.schedule ?? ((callback, delayMs) => { setTimeout(callback, delayMs); });
|
|
46
82
|
const revisionId = managedAnalyticsOpaqueRevision(options.planRevision);
|
|
47
83
|
let sequence = 0;
|
|
48
84
|
|
|
@@ -50,10 +86,15 @@ export function createContactSalesActionSource(
|
|
|
50
86
|
subscribe(listener) {
|
|
51
87
|
if (!document || typeof document.querySelectorAll !== "function") return () => {};
|
|
52
88
|
const removals: Array<() => void> = [];
|
|
89
|
+
const bound = new Set<ContactSalesActionElement>();
|
|
53
90
|
for (const source of CONTACT_SALES_ACTION_SOURCES) {
|
|
54
|
-
const nodes = document.querySelectorAll(
|
|
91
|
+
const nodes = document.querySelectorAll(source.selector);
|
|
55
92
|
for (const node of Array.from(nodes)) {
|
|
56
|
-
|
|
93
|
+
if (bound.has(node)) continue;
|
|
94
|
+
bound.add(node);
|
|
95
|
+
const emit = (event: ContactSalesClickEvent = {}): void => {
|
|
96
|
+
const navigationHref = options.navigate ? deferredNavigationHref(node, event) : undefined;
|
|
97
|
+
if (navigationHref) event.preventDefault?.();
|
|
57
98
|
sequence += 1;
|
|
58
99
|
const at = now();
|
|
59
100
|
const activation: PlatformManagedActionActivation = {
|
|
@@ -66,6 +107,11 @@ export function createContactSalesActionSource(
|
|
|
66
107
|
sequence,
|
|
67
108
|
};
|
|
68
109
|
listener(activation);
|
|
110
|
+
// A full-document navigation destroys the isolated GA4 iframe. Give its
|
|
111
|
+
// MessagePort one bounded turn to receive the reviewed action first.
|
|
112
|
+
if (navigationHref) {
|
|
113
|
+
schedule(() => options.navigate?.(navigationHref), CONTACT_SALES_NAVIGATION_DEFER_MS);
|
|
114
|
+
}
|
|
69
115
|
};
|
|
70
116
|
node.addEventListener("click", emit);
|
|
71
117
|
removals.push(() => node.removeEventListener("click", emit));
|
|
@@ -63,9 +63,12 @@ export const INTEGRATION_CSP_PROFILES: Readonly<Record<string, CspSourceSet>> =
|
|
|
63
63
|
"connect-src": ["https://cdn.usefathom.com"],
|
|
64
64
|
},
|
|
65
65
|
|
|
66
|
-
// Google code and network authority live only in the
|
|
67
|
-
//
|
|
68
|
-
|
|
66
|
+
// Google code and network authority live only in the isolated-origin sandbox. The
|
|
67
|
+
// hosted preview uses the dedicated tokenoftrust.store sibling. Custom-domain
|
|
68
|
+
// publication aliases are added from the validated canonical host at declaration time.
|
|
69
|
+
"google-measurement": {
|
|
70
|
+
"frame-src": ["https://analytics-frame.tokenoftrust.store"],
|
|
71
|
+
},
|
|
69
72
|
|
|
70
73
|
// Meta pixel (fbevents.js). The loader is served from connect.facebook.net; the
|
|
71
74
|
// pixel's collect endpoint is the facebook.com tracking origin. The tenant
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The governed event-property set every reviewed analytics adapter attaches to
|
|
3
|
+
* every event it sends.
|
|
4
|
+
*
|
|
5
|
+
* These three values answer "whose traffic, on which surface, from which stage"
|
|
6
|
+
* — the question a merchant has to answer before any storefront number is worth
|
|
7
|
+
* reading. They are DERIVED from the renderer's own trusted context
|
|
8
|
+
* (`BrowserEnvironment` + the server-stamped tenant identity), never declared by
|
|
9
|
+
* a tenant and never read from the browser. That is the whole point: a tenant
|
|
10
|
+
* that could type its own `tenant` or `environment` could mislabel its traffic,
|
|
11
|
+
* so the adapter configuration contract bans those keys outright
|
|
12
|
+
* (`managed-analytics-api-v1.schemas.ts` FORBIDDEN_KEY_PATTERN, `secrets.ts`
|
|
13
|
+
* findForbiddenFields) and this module supplies them instead.
|
|
14
|
+
*
|
|
15
|
+
* They carry no person, device, account, or network signal, so they add nothing
|
|
16
|
+
* to the anonymous-collection posture the adapters already hold.
|
|
17
|
+
*/
|
|
18
|
+
import {
|
|
19
|
+
validateBrowserEnvironment,
|
|
20
|
+
type BrowserEnvironment,
|
|
21
|
+
type IntegrationEnvironment,
|
|
22
|
+
type IntegrationSiteSurface,
|
|
23
|
+
} from "./environment.js";
|
|
24
|
+
|
|
25
|
+
/** The exact, closed key set. Adding a key here is a reviewed platform change. */
|
|
26
|
+
export const GOVERNED_EVENT_PROPERTY_KEYS = Object.freeze([
|
|
27
|
+
"tenant",
|
|
28
|
+
"environment",
|
|
29
|
+
"site_surface",
|
|
30
|
+
] as const);
|
|
31
|
+
|
|
32
|
+
export interface GovernedEventProperties {
|
|
33
|
+
/** The tenant's public id, e.g. `tokenoftrust.com`. Server-stamped. */
|
|
34
|
+
tenant: string;
|
|
35
|
+
/** The four-stage publication stage: `local` | `candidate` | `staged` | `live`. */
|
|
36
|
+
environment: IntegrationEnvironment;
|
|
37
|
+
/** The document surface: `storefront` | `checkout` | `admin` | `marketing`. */
|
|
38
|
+
site_surface: IntegrationSiteSurface;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Build the governed property set, or null when the context cannot be trusted.
|
|
43
|
+
*
|
|
44
|
+
* Fail-closed is deliberate and matches every other adapter gate: an adapter
|
|
45
|
+
* that cannot prove which tenant and stage an event belongs to must not label
|
|
46
|
+
* it, and a caller that gets null drops the event rather than sending it
|
|
47
|
+
* unlabelled. Mislabelled traffic is worse than absent traffic — it silently
|
|
48
|
+
* corrupts every report built on it.
|
|
49
|
+
*/
|
|
50
|
+
export function buildGovernedEventProperties(
|
|
51
|
+
environment: BrowserEnvironment,
|
|
52
|
+
tenantPublicId: string,
|
|
53
|
+
): GovernedEventProperties | null {
|
|
54
|
+
const validated = validateBrowserEnvironment(environment);
|
|
55
|
+
if (!validated.ok) return null;
|
|
56
|
+
if (typeof tenantPublicId !== "string" || tenantPublicId.length === 0) return null;
|
|
57
|
+
return Object.freeze({
|
|
58
|
+
tenant: tenantPublicId,
|
|
59
|
+
environment: validated.value.environment,
|
|
60
|
+
site_surface: validated.value.siteSurface,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -59,6 +59,7 @@ export * from "./platform-adapters.js";
|
|
|
59
59
|
export * from "./native-amplitude.js";
|
|
60
60
|
export * from "./native-ga4.js";
|
|
61
61
|
export * from "./native-ga4-sandbox.js";
|
|
62
|
+
export * from "./native-ga4-sandbox-asset.mjs";
|
|
62
63
|
export * from "./platform-adapter-runtime.js";
|
|
63
64
|
export * from "./render-phases.js";
|
|
64
65
|
export * from "./interim-bridge.js";
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* constructs bundled platform code with host-owned capabilities. Every other entry goes
|
|
17
17
|
* through the authority-tier loader: it loads ONLY an `elevated-parent` entry (whose
|
|
18
18
|
* honest, review-only contract IS arbitrary parent-document JavaScript) and REFUSES every
|
|
19
|
-
* other tier. A reviewed registry entry may construct its own
|
|
19
|
+
* other tier. A reviewed registry entry may construct its own isolated-origin iframe and
|
|
20
20
|
* private typed MessagePort; arbitrary `sandbox`-tier entry modules remain refused by
|
|
21
21
|
* the parent-realm loader, so their isolation is never silently downgraded. Loading is
|
|
22
22
|
* also consent-gated: the binding reads
|
|
@@ -42,7 +42,7 @@ import type { NavigationCompletedEvent, NavigationKind } from "./events.js";
|
|
|
42
42
|
import { CommerceEventBroker } from "./commerce-broker.js";
|
|
43
43
|
import { validateBrowserEnvironment, BROWSER_ENVIRONMENT_GLOBAL, type BrowserEnvironment } from "./environment.js";
|
|
44
44
|
import { makePlatformAdapterRuntimeLoader } from "./platform-adapter-runtime.js";
|
|
45
|
-
import {
|
|
45
|
+
import { resolveNativeGa4SandboxUrl, type NativeGa4Transport } from "./native-ga4.js";
|
|
46
46
|
import {
|
|
47
47
|
createBrowserNativeGa4SandboxHost,
|
|
48
48
|
createNativeGa4SandboxTransport,
|
|
@@ -51,6 +51,7 @@ import { ManagedAnalyticsEventBrokerBridge } from "./managed-analytics-bridge.js
|
|
|
51
51
|
import { minimizePageAttribution, type ManagedPageViewInput } from "./managed-analytics.js";
|
|
52
52
|
import { managedAnalyticsOpaqueRevision } from "./managed-analytics-value-safety.js";
|
|
53
53
|
import {
|
|
54
|
+
CONTACT_SALES_ACTION_REGISTRY,
|
|
54
55
|
createContactSalesActionSource,
|
|
55
56
|
type ContactSalesActionDocument,
|
|
56
57
|
} from "./contact-sales-action-source.js";
|
|
@@ -68,7 +69,7 @@ export const LIFECYCLE_NAVIGATION_SINK_GLOBAL = "__TOT_INTEGRATION_NAVIGATION__"
|
|
|
68
69
|
|
|
69
70
|
/** The narrow slice of `window` the binding depends on — structural, for edge-safe typing. */
|
|
70
71
|
export interface LifecycleWindow {
|
|
71
|
-
location: { pathname: string; search: string; hash: string; href: string };
|
|
72
|
+
location: { pathname: string; search: string; hash: string; href: string; assign?: (href: string) => void };
|
|
72
73
|
addEventListener(type: string, listener: (event: unknown) => void): void;
|
|
73
74
|
fetch?: typeof fetch;
|
|
74
75
|
/**
|
|
@@ -118,7 +119,7 @@ export interface BootLifecycleOptions {
|
|
|
118
119
|
fetchCapability?: typeof fetch;
|
|
119
120
|
/** Entry import capability; injectable for tests and non-browser hosts. */
|
|
120
121
|
importModule?: (url: string) => Promise<unknown>;
|
|
121
|
-
/** Reviewed
|
|
122
|
+
/** Reviewed isolated-origin GA4 transport; injectable for DOM-free tests. */
|
|
122
123
|
nativeGa4Transport?: NativeGa4Transport;
|
|
123
124
|
}
|
|
124
125
|
|
|
@@ -207,6 +208,7 @@ function makeDocumentCommerceBroker(
|
|
|
207
208
|
},
|
|
208
209
|
source: `//${descriptor.tenantPublicId}`,
|
|
209
210
|
consentSnapshot: getConsentSnapshot,
|
|
211
|
+
tenantActionRegistry: CONTACT_SALES_ACTION_REGISTRY,
|
|
210
212
|
});
|
|
211
213
|
}
|
|
212
214
|
|
|
@@ -242,7 +244,7 @@ function documentPageView(
|
|
|
242
244
|
const id = `pageview-${managedAnalyticsOpaqueRevision(descriptor.planRevision)}`;
|
|
243
245
|
return {
|
|
244
246
|
logicalId: id,
|
|
245
|
-
path: win.location.pathname,
|
|
247
|
+
path: canonicalDocumentPath(win.location.pathname, descriptor.tenantPublicId),
|
|
246
248
|
correlation: {
|
|
247
249
|
id,
|
|
248
250
|
expiresAt: new Date(now() + PAGE_VIEW_CORRELATION_TTL_MS).toISOString(),
|
|
@@ -251,6 +253,28 @@ function documentPageView(
|
|
|
251
253
|
};
|
|
252
254
|
}
|
|
253
255
|
|
|
256
|
+
/**
|
|
257
|
+
* Project immutable preview wrapper routes back to the tenant path they render.
|
|
258
|
+
* The wrapper contains a commit SHA, which is both non-canonical and deliberately
|
|
259
|
+
* rejected by the analytics value-safety boundary. Public/live routes pass through.
|
|
260
|
+
*/
|
|
261
|
+
export function canonicalDocumentPath(pathname: string, tenantPublicId: string): string {
|
|
262
|
+
const segments = pathname.split("/");
|
|
263
|
+
const previewIndex = segments[1] === tenantPublicId && segments[2] === "preview"
|
|
264
|
+
? 2
|
|
265
|
+
: segments[1] === "preview"
|
|
266
|
+
? 1
|
|
267
|
+
: -1;
|
|
268
|
+
if (
|
|
269
|
+
previewIndex !== -1 &&
|
|
270
|
+
(segments[previewIndex + 1] === "staged" || segments[previewIndex + 1] === "rev") &&
|
|
271
|
+
/^[0-9a-f]{40,64}$/i.test(segments[previewIndex + 2] ?? "")
|
|
272
|
+
) {
|
|
273
|
+
return `/${segments.slice(previewIndex + 3).join("/")}`;
|
|
274
|
+
}
|
|
275
|
+
return pathname;
|
|
276
|
+
}
|
|
277
|
+
|
|
254
278
|
function readDescriptor(win: LifecycleWindow): IntegrationBootDescriptor | null {
|
|
255
279
|
const descriptor = win[INTEGRATION_PLAN_GLOBAL];
|
|
256
280
|
if (!descriptor || typeof descriptor !== "object") return null;
|
|
@@ -326,7 +350,7 @@ export function bootIntegrationLifecycle(
|
|
|
326
350
|
}
|
|
327
351
|
return createNativeGa4SandboxTransport(
|
|
328
352
|
createBrowserNativeGa4SandboxHost(document, MessageChannel),
|
|
329
|
-
|
|
353
|
+
resolveNativeGa4SandboxUrl(win.location.href),
|
|
330
354
|
);
|
|
331
355
|
},
|
|
332
356
|
});
|
|
@@ -364,6 +388,10 @@ export function bootIntegrationLifecycle(
|
|
|
364
388
|
: undefined,
|
|
365
389
|
planRevision: descriptor.planRevision,
|
|
366
390
|
now: options.now ?? Date.now,
|
|
391
|
+
navigate: (href) => {
|
|
392
|
+
if (typeof win.location.assign === "function") win.location.assign(href);
|
|
393
|
+
else win.location.href = href;
|
|
394
|
+
},
|
|
367
395
|
}),
|
|
368
396
|
}),
|
|
369
397
|
pageView: () => documentPageView(win, descriptor, options.now ?? Date.now),
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
/** Types for the dual-consumer (`.ts` engine + `.mjs` build plugin) host-asset path. */
|
|
2
2
|
export declare const LIFECYCLE_HOST_ASSET_PATH: string;
|
|
3
3
|
export declare const LIFECYCLE_HOST_ASSET_URL: string;
|
|
4
|
+
export declare const LIFECYCLE_HOST_COMPATIBILITY_ASSET_PATHS: readonly string[];
|
|
5
|
+
export declare const LIFECYCLE_HOST_COMPATIBILITY_ASSET_URLS: readonly string[];
|
|
@@ -3,15 +3,29 @@
|
|
|
3
3
|
*
|
|
4
4
|
* It lives in a `.mjs` module because it has TWO consumers that cannot share a `.ts`
|
|
5
5
|
* one: the render engine (`render-phases.ts`) that emits the `<script src>`, and the
|
|
6
|
-
* build
|
|
6
|
+
* build integration (`apps/storefront/integrations/lifecycle-host-asset.mjs`) that produces
|
|
7
7
|
* the file at that exact path. One source of truth, so the tag and the artifact cannot
|
|
8
8
|
* disagree — a mismatch would leave every static tenant page with plan data and no host,
|
|
9
9
|
* which fails silently in exactly the way it is not allowed to.
|
|
10
10
|
*
|
|
11
|
-
* The path is version-pinned (`/
|
|
11
|
+
* The path is version-pinned (`/v3/`) like a platform adapter entry, so a future host
|
|
12
12
|
* with a different boot contract ships alongside rather than replacing this one.
|
|
13
13
|
*/
|
|
14
|
-
export const LIFECYCLE_HOST_ASSET_PATH = "platform/lifecycle/
|
|
14
|
+
export const LIFECYCLE_HOST_ASSET_PATH = "platform/lifecycle/v3/boot.mjs";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Earlier immutable URLs that must keep serving the current host bytes during a
|
|
18
|
+
* version transition. Cached HTML may still reference one of these paths after a
|
|
19
|
+
* pointer flip, so dropping it would turn a cache refresh into a lifecycle 404.
|
|
20
|
+
*/
|
|
21
|
+
export const LIFECYCLE_HOST_COMPATIBILITY_ASSET_PATHS = Object.freeze([
|
|
22
|
+
"platform/lifecycle/v2/boot.mjs",
|
|
23
|
+
]);
|
|
15
24
|
|
|
16
25
|
/** The root-absolute, same-origin URL the served markup references. */
|
|
17
26
|
export const LIFECYCLE_HOST_ASSET_URL = `/${LIFECYCLE_HOST_ASSET_PATH}`;
|
|
27
|
+
|
|
28
|
+
/** Root-absolute compatibility URLs emitted alongside the current URL. */
|
|
29
|
+
export const LIFECYCLE_HOST_COMPATIBILITY_ASSET_URLS = Object.freeze(
|
|
30
|
+
LIFECYCLE_HOST_COMPATIBILITY_ASSET_PATHS.map((path) => `/${path}`),
|
|
31
|
+
);
|
|
@@ -20,6 +20,10 @@ import {
|
|
|
20
20
|
type IntegrationEnvironment,
|
|
21
21
|
} from "./environment.js";
|
|
22
22
|
import { resemblesManagedAnalyticsPii } from "./managed-analytics-value-safety.js";
|
|
23
|
+
import {
|
|
24
|
+
buildGovernedEventProperties,
|
|
25
|
+
type GovernedEventProperties,
|
|
26
|
+
} from "./governed-event-properties.js";
|
|
23
27
|
import type {
|
|
24
28
|
IntegrationEventSubscribe,
|
|
25
29
|
IntegrationLifecycleContext,
|
|
@@ -47,7 +51,7 @@ export interface AmplitudeHttpEvent {
|
|
|
47
51
|
event_type: "page_view" | (typeof APPROVED_MARKETING_ACTION_NAMES)[number];
|
|
48
52
|
device_id: typeof ANONYMOUS_DEVICE_ID;
|
|
49
53
|
insert_id: string;
|
|
50
|
-
event_properties: { page_path
|
|
54
|
+
event_properties: GovernedEventProperties & { page_path?: string };
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
export const AMPLITUDE_DEDUPE_CAPACITY = 512 as const;
|
|
@@ -127,6 +131,11 @@ export function mapCommerceEventToAmplitude(
|
|
|
127
131
|
) return null;
|
|
128
132
|
if (!event.consent.eligiblePurposes.includes("analytics")) return null;
|
|
129
133
|
|
|
134
|
+
// Derived from the same trusted context the equality check above just proved
|
|
135
|
+
// this event agrees with, so labelling can never disagree with admission.
|
|
136
|
+
const governed = buildGovernedEventProperties(environmentResult.value, tenantPublicId);
|
|
137
|
+
if (!governed) return null;
|
|
138
|
+
|
|
130
139
|
const base = {
|
|
131
140
|
device_id: ANONYMOUS_DEVICE_ID,
|
|
132
141
|
insert_id: eventDedupeKey(event),
|
|
@@ -137,12 +146,12 @@ export function mapCommerceEventToAmplitude(
|
|
|
137
146
|
return {
|
|
138
147
|
...base,
|
|
139
148
|
event_type: name as (typeof APPROVED_MARKETING_ACTION_NAMES)[number],
|
|
140
|
-
event_properties: {},
|
|
149
|
+
event_properties: { ...governed },
|
|
141
150
|
};
|
|
142
151
|
}
|
|
143
152
|
const path = event.data["path"];
|
|
144
153
|
if (!isSafePagePath(path)) return null;
|
|
145
|
-
return { ...base, event_type: "page_view", event_properties: { page_path: path } };
|
|
154
|
+
return { ...base, event_type: "page_view", event_properties: { ...governed, page_path: path } };
|
|
146
155
|
}
|
|
147
156
|
|
|
148
157
|
export interface RegisterAmplitudeHttpDestinationInput {
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export const NATIVE_GA4_SANDBOX_ASSET_PATH: "platform/google-measurement/v2/sandbox.html";
|
|
2
2
|
export const NATIVE_GA4_SANDBOX_ASSET_URL: "/platform/google-measurement/v2/sandbox.html";
|
|
3
|
+
export const NATIVE_GA4_SANDBOX_ORIGIN: "https://analytics-frame.tokenoftrust.store";
|
|
4
|
+
export function resolveNativeGa4SandboxUrl(parentHref: string): string;
|
|
@@ -1,3 +1,37 @@
|
|
|
1
|
-
/** Stable provider-neutral artifact path for the reviewed GA4
|
|
1
|
+
/** Stable provider-neutral artifact path for the reviewed GA4 transport frame. */
|
|
2
2
|
export const NATIVE_GA4_SANDBOX_ASSET_PATH = "platform/google-measurement/v2/sandbox.html";
|
|
3
3
|
export const NATIVE_GA4_SANDBOX_ASSET_URL = `/${NATIVE_GA4_SANDBOX_ASSET_PATH}`;
|
|
4
|
+
export const NATIVE_GA4_SANDBOX_ORIGIN = "https://analytics-frame.tokenoftrust.store";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Keep Google tag execution on a real, isolated origin. An opaque sandbox origin makes
|
|
8
|
+
* gtag.js load successfully but silently suppresses every collection request. Production
|
|
9
|
+
* uses a dedicated no-auth hostname for hosted surfaces and the tenant's paired apex/www
|
|
10
|
+
* publication alias for custom domains. Local development uses an analytics.localhost
|
|
11
|
+
* sibling while retaining the current dev-server port.
|
|
12
|
+
*/
|
|
13
|
+
export function resolveNativeGa4SandboxUrl(parentHref) {
|
|
14
|
+
const parent = new URL(parentHref);
|
|
15
|
+
if (parent.hostname === "localhost" || parent.hostname.endsWith(".localhost")) {
|
|
16
|
+
const sandbox = new URL(NATIVE_GA4_SANDBOX_ASSET_URL, parent);
|
|
17
|
+
sandbox.hostname = "analytics.localhost";
|
|
18
|
+
return sandbox.href;
|
|
19
|
+
}
|
|
20
|
+
if (parent.hostname === "tokenoftrust.store" || parent.hostname.endsWith(".tokenoftrust.store")) {
|
|
21
|
+
return new URL(NATIVE_GA4_SANDBOX_ASSET_URL, NATIVE_GA4_SANDBOX_ORIGIN).href;
|
|
22
|
+
}
|
|
23
|
+
// A static publication verification alias is deliberately not the tenant's
|
|
24
|
+
// canonical apex/www pair. Its frame must still share the tenant's site: gtag.js
|
|
25
|
+
// loads but suppresses collection from a third-party frame. The corresponding
|
|
26
|
+
// analytics-frame hostname is provisioned as a Cloudflare-for-SaaS custom host.
|
|
27
|
+
if (parent.hostname.startsWith("published.")) {
|
|
28
|
+
const sandbox = new URL(NATIVE_GA4_SANDBOX_ASSET_URL, parent);
|
|
29
|
+
sandbox.hostname = `analytics-frame.${parent.hostname.slice("published.".length)}`;
|
|
30
|
+
return sandbox.href;
|
|
31
|
+
}
|
|
32
|
+
const sandbox = new URL(NATIVE_GA4_SANDBOX_ASSET_URL, parent);
|
|
33
|
+
sandbox.hostname = parent.hostname.startsWith("www.")
|
|
34
|
+
? parent.hostname.slice("www.".length)
|
|
35
|
+
: `www.${parent.hostname}`;
|
|
36
|
+
return sandbox.href;
|
|
37
|
+
}
|
|
@@ -9,6 +9,7 @@ import { resemblesManagedAnalyticsProtectedValue } from "./managed-analytics-val
|
|
|
9
9
|
|
|
10
10
|
const MEASUREMENT_ID = /^G-[A-Z0-9]{4,20}$/;
|
|
11
11
|
const CAMPAIGN_VALUE = /^[A-Za-z0-9][A-Za-z0-9._~-]{0,127}$/;
|
|
12
|
+
const GOVERNED_VALUE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
12
13
|
const APPROVED_ACTIONS: ReadonlySet<string> = new Set(APPROVED_MARKETING_ACTION_NAMES);
|
|
13
14
|
|
|
14
15
|
function plainRecord(value: unknown): Record<string, unknown> | null {
|
|
@@ -44,6 +45,20 @@ function validIds(value: unknown): value is string[] {
|
|
|
44
45
|
value.every((id) => typeof id === "string" && MEASUREMENT_ID.test(id)) && new Set(value).size === value.length;
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Governed labelling arrives across a hostile boundary, so it is re-validated
|
|
50
|
+
* here rather than trusted: the exact three keys, each a bare token. `tenant`
|
|
51
|
+
* is a public host id and the other two are closed enums the parent derives, so
|
|
52
|
+
* nothing here can smuggle a value gtag would treat as anything but a label.
|
|
53
|
+
*/
|
|
54
|
+
function validGovernedProperties(value: unknown): value is Record<string, string> {
|
|
55
|
+
const record = plainRecord(value);
|
|
56
|
+
if (!record || !exactKeys(record, ["tenant", "environment", "site_surface"])) return false;
|
|
57
|
+
return Object.values(record).every((item) =>
|
|
58
|
+
typeof item === "string" && GOVERNED_VALUE.test(item) &&
|
|
59
|
+
!resemblesManagedAnalyticsProtectedValue(item));
|
|
60
|
+
}
|
|
61
|
+
|
|
47
62
|
function validCampaign(value: unknown): value is Record<string, string> {
|
|
48
63
|
const record = plainRecord(value);
|
|
49
64
|
if (!record || !exactKeys(record, ["source", "medium", "campaign", "term", "content"], [])) return false;
|
|
@@ -64,11 +79,11 @@ export function startNativeGa4Sandbox(win: NativeGa4SandboxRuntimeWindow, doc: D
|
|
|
64
79
|
const bind = (event: MessageEvent): void => {
|
|
65
80
|
try {
|
|
66
81
|
const data = plainRecord(event.data);
|
|
67
|
-
const ownOrigin = new URL(win.location.href).origin;
|
|
68
82
|
if (
|
|
69
|
-
bound || event.source !== win.parent || event.
|
|
70
|
-
!data || !exactKeys(data, ["protocol", "type"]) ||
|
|
83
|
+
bound || event.source !== win.parent || event.ports.length !== 1 ||
|
|
84
|
+
!data || !exactKeys(data, ["protocol", "type", "parentOrigin"]) ||
|
|
71
85
|
data["protocol"] !== NATIVE_GA4_SANDBOX_PROTOCOL || data["type"] !== "bind"
|
|
86
|
+
|| !validOrigin(data["parentOrigin"]) || event.origin !== data["parentOrigin"]
|
|
72
87
|
) return;
|
|
73
88
|
bound = true;
|
|
74
89
|
win.removeEventListener("message", bind);
|
|
@@ -85,9 +100,9 @@ function runPort(port: MessagePort, win: NativeGa4SandboxRuntimeWindow, doc: Doc
|
|
|
85
100
|
let active = false;
|
|
86
101
|
let disposed = false;
|
|
87
102
|
let canonicalOrigin = "";
|
|
88
|
-
let
|
|
103
|
+
let currentDestinations: string[] = [];
|
|
104
|
+
let governedProperties: Record<string, string> = {};
|
|
89
105
|
let lastPageLocation = "";
|
|
90
|
-
let generation = 0;
|
|
91
106
|
let tagLoaded = false;
|
|
92
107
|
|
|
93
108
|
const dataLayer: unknown[] = [];
|
|
@@ -99,13 +114,16 @@ function runPort(port: MessagePort, win: NativeGa4SandboxRuntimeWindow, doc: Doc
|
|
|
99
114
|
|
|
100
115
|
const configure = (message: Record<string, unknown>): void => {
|
|
101
116
|
if (
|
|
102
|
-
!exactKeys(message, ["schemaVersion", "type", "destinationIds", "canonicalOrigin"]) ||
|
|
117
|
+
!exactKeys(message, ["schemaVersion", "type", "destinationIds", "canonicalOrigin", "analyticsConsent", "governedProperties"]) ||
|
|
103
118
|
message["schemaVersion"] !== 1 || message["type"] !== "configure" ||
|
|
104
|
-
|
|
119
|
+
message["analyticsConsent"] !== "granted" ||
|
|
120
|
+
!validIds(message["destinationIds"]) || !validOrigin(message["canonicalOrigin"]) ||
|
|
121
|
+
!validGovernedProperties(message["governedProperties"])
|
|
105
122
|
) return;
|
|
106
123
|
const ids = message["destinationIds"];
|
|
107
124
|
canonicalOrigin = message["canonicalOrigin"];
|
|
108
|
-
|
|
125
|
+
governedProperties = { ...message["governedProperties"] };
|
|
126
|
+
currentDestinations = [...ids];
|
|
109
127
|
lastPageLocation = "";
|
|
110
128
|
active = true;
|
|
111
129
|
|
|
@@ -119,9 +137,13 @@ function runPort(port: MessagePort, win: NativeGa4SandboxRuntimeWindow, doc: Doc
|
|
|
119
137
|
});
|
|
120
138
|
gtag("js", new Date());
|
|
121
139
|
}
|
|
140
|
+
// This sandbox is created and configured only after the parent lifecycle's
|
|
141
|
+
// platform consent gate grants analytics. Mirror that granted state into Google's
|
|
142
|
+
// consent mode; leaving its default denied suppresses the explicit page_view that
|
|
143
|
+
// follows even though the platform correctly admitted it.
|
|
144
|
+
gtag("consent", "update", { analytics_storage: message["analyticsConsent"] });
|
|
122
145
|
for (const id of ids) {
|
|
123
146
|
gtag("config", id, {
|
|
124
|
-
groups: currentGroup,
|
|
125
147
|
send_page_view: false,
|
|
126
148
|
allow_google_signals: false,
|
|
127
149
|
allow_ad_personalization_signals: false,
|
|
@@ -148,7 +170,7 @@ function runPort(port: MessagePort, win: NativeGa4SandboxRuntimeWindow, doc: Doc
|
|
|
148
170
|
const pageLocation = `${canonicalOrigin}${message["path"]}`;
|
|
149
171
|
lastPageLocation = pageLocation;
|
|
150
172
|
const params: Record<string, unknown> = {
|
|
151
|
-
|
|
173
|
+
...governedProperties,
|
|
152
174
|
page_location: pageLocation,
|
|
153
175
|
page_path: message["path"],
|
|
154
176
|
};
|
|
@@ -160,7 +182,9 @@ function runPort(port: MessagePort, win: NativeGa4SandboxRuntimeWindow, doc: Doc
|
|
|
160
182
|
if (campaign[key] !== undefined) params[`campaign_${ga4Key}`] = campaign[key];
|
|
161
183
|
}
|
|
162
184
|
}
|
|
163
|
-
|
|
185
|
+
for (const id of currentDestinations) {
|
|
186
|
+
gtag("event", "page_view", { ...params, send_to: id });
|
|
187
|
+
}
|
|
164
188
|
return;
|
|
165
189
|
}
|
|
166
190
|
if (message["type"] === "action") {
|
|
@@ -169,7 +193,14 @@ function runPort(port: MessagePort, win: NativeGa4SandboxRuntimeWindow, doc: Doc
|
|
|
169
193
|
typeof message["name"] !== "string" || !APPROVED_ACTIONS.has(message["name"]) ||
|
|
170
194
|
message["pageLocation"] !== lastPageLocation || !lastPageLocation
|
|
171
195
|
) return;
|
|
172
|
-
|
|
196
|
+
for (const id of currentDestinations) {
|
|
197
|
+
gtag("event", message["name"], {
|
|
198
|
+
...governedProperties,
|
|
199
|
+
send_to: id,
|
|
200
|
+
page_location: lastPageLocation,
|
|
201
|
+
transport_type: "beacon",
|
|
202
|
+
});
|
|
203
|
+
}
|
|
173
204
|
}
|
|
174
205
|
};
|
|
175
206
|
|
|
@@ -181,6 +212,7 @@ function runPort(port: MessagePort, win: NativeGa4SandboxRuntimeWindow, doc: Doc
|
|
|
181
212
|
if (message["type"] === "disable") {
|
|
182
213
|
if (exactKeys(message, ["schemaVersion", "type"]) && message["schemaVersion"] === 1) {
|
|
183
214
|
active = false;
|
|
215
|
+
currentDestinations = [];
|
|
184
216
|
lastPageLocation = "";
|
|
185
217
|
}
|
|
186
218
|
return;
|
|
@@ -189,6 +221,7 @@ function runPort(port: MessagePort, win: NativeGa4SandboxRuntimeWindow, doc: Doc
|
|
|
189
221
|
if (exactKeys(message, ["schemaVersion", "type"]) && message["schemaVersion"] === 1) {
|
|
190
222
|
active = false;
|
|
191
223
|
disposed = true;
|
|
224
|
+
currentDestinations = [];
|
|
192
225
|
lastPageLocation = "";
|
|
193
226
|
port.close();
|
|
194
227
|
}
|