@tokenoftrust/storefront-runner 2.0.41 → 2.0.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/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 +7 -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/image-dependencies.ts +1438 -0
- 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/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 +30 -2
- 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.mjs +9 -0
- package/packages/public-runtime/src/integration/native-ga4-sandbox-entry.ts +27 -3
- package/packages/public-runtime/src/integration/native-ga4-sandbox.ts +13 -0
- package/packages/public-runtime/src/integration/native-ga4.ts +11 -0
- package/packages/public-runtime/src/integration/platform-adapters.ts +5 -1
- 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 +19 -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
|
@@ -17,6 +17,14 @@ export * from "./product.js";
|
|
|
17
17
|
export * from "./tenant.js";
|
|
18
18
|
export * from "./publication-space.js";
|
|
19
19
|
export * from "./blog-visibility.mjs";
|
|
20
|
+
// Per-tenant build-time embed defaults (tenant-embed-defaults.mjs) are deliberately
|
|
21
|
+
// NOT re-exported here: this barrel is inside the v84 clean-clone ATTESTED source
|
|
22
|
+
// graph, so an `export *` widens that graph's membership and takes a human-review
|
|
23
|
+
// gate (`repin-v84-clean-clone.mjs` refuses a membership change, correctly). The
|
|
24
|
+
// module is reached by its own subpath export instead —
|
|
25
|
+
// `@tot/public-runtime/tenant-embed-defaults` — which keeps the attestation surface
|
|
26
|
+
// unchanged. Adding a convenience re-export here would cost an attestation review
|
|
27
|
+
// for nothing.
|
|
20
28
|
export * from "./publication-routing.js";
|
|
21
29
|
export * from "./publication-version.js";
|
|
22
30
|
export * from "./publication-artifact.js";
|
|
@@ -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));
|
|
@@ -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";
|
|
@@ -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
|
/**
|
|
@@ -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;
|
|
@@ -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 {
|
|
@@ -20,6 +20,15 @@ export function resolveNativeGa4SandboxUrl(parentHref) {
|
|
|
20
20
|
if (parent.hostname === "tokenoftrust.store" || parent.hostname.endsWith(".tokenoftrust.store")) {
|
|
21
21
|
return new URL(NATIVE_GA4_SANDBOX_ASSET_URL, NATIVE_GA4_SANDBOX_ORIGIN).href;
|
|
22
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
|
+
}
|
|
23
32
|
const sandbox = new URL(NATIVE_GA4_SANDBOX_ASSET_URL, parent);
|
|
24
33
|
sandbox.hostname = parent.hostname.startsWith("www.")
|
|
25
34
|
? parent.hostname.slice("www.".length)
|
|
@@ -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;
|
|
@@ -86,6 +101,7 @@ function runPort(port: MessagePort, win: NativeGa4SandboxRuntimeWindow, doc: Doc
|
|
|
86
101
|
let disposed = false;
|
|
87
102
|
let canonicalOrigin = "";
|
|
88
103
|
let currentDestinations: string[] = [];
|
|
104
|
+
let governedProperties: Record<string, string> = {};
|
|
89
105
|
let lastPageLocation = "";
|
|
90
106
|
let tagLoaded = false;
|
|
91
107
|
|
|
@@ -98,13 +114,15 @@ function runPort(port: MessagePort, win: NativeGa4SandboxRuntimeWindow, doc: Doc
|
|
|
98
114
|
|
|
99
115
|
const configure = (message: Record<string, unknown>): void => {
|
|
100
116
|
if (
|
|
101
|
-
!exactKeys(message, ["schemaVersion", "type", "destinationIds", "canonicalOrigin", "analyticsConsent"]) ||
|
|
117
|
+
!exactKeys(message, ["schemaVersion", "type", "destinationIds", "canonicalOrigin", "analyticsConsent", "governedProperties"]) ||
|
|
102
118
|
message["schemaVersion"] !== 1 || message["type"] !== "configure" ||
|
|
103
119
|
message["analyticsConsent"] !== "granted" ||
|
|
104
|
-
!validIds(message["destinationIds"]) || !validOrigin(message["canonicalOrigin"])
|
|
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"];
|
|
125
|
+
governedProperties = { ...message["governedProperties"] };
|
|
108
126
|
currentDestinations = [...ids];
|
|
109
127
|
lastPageLocation = "";
|
|
110
128
|
active = true;
|
|
@@ -152,6 +170,7 @@ function runPort(port: MessagePort, win: NativeGa4SandboxRuntimeWindow, doc: Doc
|
|
|
152
170
|
const pageLocation = `${canonicalOrigin}${message["path"]}`;
|
|
153
171
|
lastPageLocation = pageLocation;
|
|
154
172
|
const params: Record<string, unknown> = {
|
|
173
|
+
...governedProperties,
|
|
155
174
|
page_location: pageLocation,
|
|
156
175
|
page_path: message["path"],
|
|
157
176
|
};
|
|
@@ -175,7 +194,12 @@ function runPort(port: MessagePort, win: NativeGa4SandboxRuntimeWindow, doc: Doc
|
|
|
175
194
|
message["pageLocation"] !== lastPageLocation || !lastPageLocation
|
|
176
195
|
) return;
|
|
177
196
|
for (const id of currentDestinations) {
|
|
178
|
-
gtag("event", message["name"], {
|
|
197
|
+
gtag("event", message["name"], {
|
|
198
|
+
...governedProperties,
|
|
199
|
+
send_to: id,
|
|
200
|
+
page_location: lastPageLocation,
|
|
201
|
+
transport_type: "beacon",
|
|
202
|
+
});
|
|
179
203
|
}
|
|
180
204
|
}
|
|
181
205
|
};
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
/** Closed parent/iframe protocol for the reviewed GA4 isolated-origin sandbox. */
|
|
2
|
+
import type { GovernedEventProperties } from "./governed-event-properties.js";
|
|
3
|
+
import { resolveNativeGa4SandboxUrl } from "./native-ga4-sandbox-asset.mjs";
|
|
4
|
+
|
|
2
5
|
export const NATIVE_GA4_SANDBOX_PROTOCOL = "tot.ga4-sandbox/1" as const;
|
|
3
6
|
export const NATIVE_GA4_SANDBOX_MAX_DESTINATIONS = 4 as const;
|
|
4
7
|
|
|
@@ -8,6 +11,12 @@ export interface NativeGa4SandboxConfig {
|
|
|
8
11
|
canonicalOrigin: string;
|
|
9
12
|
/** The parent may configure GA4 only after the platform consent gate grants analytics. */
|
|
10
13
|
analyticsConsent: "granted";
|
|
14
|
+
/**
|
|
15
|
+
* Governed labelling attached to every event this sandbox sends. Configured
|
|
16
|
+
* once rather than per command: all three are constant for the document's
|
|
17
|
+
* life, so a per-event copy would only widen the hostile boundary.
|
|
18
|
+
*/
|
|
19
|
+
governedProperties: GovernedEventProperties;
|
|
11
20
|
}
|
|
12
21
|
|
|
13
22
|
export interface NativeGa4PageViewCommand {
|
|
@@ -97,6 +106,10 @@ export function createNativeGa4SandboxTransport(
|
|
|
97
106
|
if (!host.parentOrigin || sandboxOrigin === host.parentOrigin) {
|
|
98
107
|
throw new Error("ga4_sandbox_origin_not_isolated");
|
|
99
108
|
}
|
|
109
|
+
const expectedSandboxOrigin = new URL(resolveNativeGa4SandboxUrl(host.parentOrigin)).origin;
|
|
110
|
+
if (sandboxOrigin !== expectedSandboxOrigin) {
|
|
111
|
+
throw new Error("ga4_sandbox_origin_not_same_site");
|
|
112
|
+
}
|
|
100
113
|
const frame = host.createFrame();
|
|
101
114
|
frame.setAttribute("sandbox", "allow-scripts allow-same-origin");
|
|
102
115
|
frame.setAttribute("referrerpolicy", "no-referrer");
|
|
@@ -17,6 +17,10 @@ import {
|
|
|
17
17
|
} from "./platform-adapters.js";
|
|
18
18
|
import { INTEGRATION_ENVIRONMENTS, type IntegrationEnvironment } from "./environment.js";
|
|
19
19
|
import { resemblesManagedAnalyticsProtectedValue } from "./managed-analytics-value-safety.js";
|
|
20
|
+
import {
|
|
21
|
+
buildGovernedEventProperties,
|
|
22
|
+
type GovernedEventProperties,
|
|
23
|
+
} from "./governed-event-properties.js";
|
|
20
24
|
import type {
|
|
21
25
|
NativeGa4SandboxCommand,
|
|
22
26
|
NativeGa4SandboxConfig,
|
|
@@ -72,6 +76,7 @@ interface ActiveGa4Config {
|
|
|
72
76
|
environment: IntegrationEnvironment;
|
|
73
77
|
siteSurface: IntegrationLifecycleContext["environment"]["siteSurface"];
|
|
74
78
|
canonicalOrigin: string;
|
|
79
|
+
governedProperties: GovernedEventProperties;
|
|
75
80
|
}
|
|
76
81
|
|
|
77
82
|
function observeSafely(observer: NativeGa4Observer | undefined, code: NativeGa4DiagnosticCode): void {
|
|
@@ -101,11 +106,16 @@ function configuredFor(ctx: IntegrationLifecycleContext): ActiveGa4Config | null
|
|
|
101
106
|
destinationIds.length === 0 || typeof canonicalHost !== "string" || canonicalHost.length === 0 ||
|
|
102
107
|
canonicalHost.includes("/") || canonicalHost.includes("?") || canonicalHost.includes("#")
|
|
103
108
|
) return null;
|
|
109
|
+
// Unlabellable traffic is not collected: without a provable tenant and stage
|
|
110
|
+
// every number downstream is unattributable, so this joins the other gates.
|
|
111
|
+
const governedProperties = buildGovernedEventProperties(ctx.environment, ctx.tenantPublicId);
|
|
112
|
+
if (!governedProperties) return null;
|
|
104
113
|
return {
|
|
105
114
|
destinationIds,
|
|
106
115
|
environment: ctx.environment.environment,
|
|
107
116
|
siteSurface: ctx.environment.siteSurface,
|
|
108
117
|
canonicalOrigin: `https://${canonicalHost}`,
|
|
118
|
+
governedProperties,
|
|
109
119
|
};
|
|
110
120
|
}
|
|
111
121
|
|
|
@@ -228,6 +238,7 @@ export function createNativeGa4Module(options: NativeGa4ModuleOptions): Integrat
|
|
|
228
238
|
destinationIds: [...next.destinationIds],
|
|
229
239
|
canonicalOrigin: next.canonicalOrigin,
|
|
230
240
|
analyticsConsent: "granted" as const,
|
|
241
|
+
governedProperties: { ...next.governedProperties },
|
|
231
242
|
};
|
|
232
243
|
runTransportEffect(transport?.configure ? () => transport.configure!(configuration) : undefined);
|
|
233
244
|
} catch {
|
|
@@ -490,7 +490,11 @@ export function buildPlatformAdapterDeclaration(
|
|
|
490
490
|
if (def.adapterId === GOOGLE_MEASUREMENT_ADAPTER.adapterId) {
|
|
491
491
|
const canonicalHost = envResult.value.canonicalHost.toLowerCase();
|
|
492
492
|
const apexHost = canonicalHost.startsWith("www.") ? canonicalHost.slice(4) : canonicalHost;
|
|
493
|
-
cspSources["frame-src"] = [
|
|
493
|
+
cspSources["frame-src"] = [
|
|
494
|
+
`https://${apexHost}`,
|
|
495
|
+
`https://analytics-frame.${apexHost}`,
|
|
496
|
+
`https://www.${apexHost}`,
|
|
497
|
+
];
|
|
494
498
|
}
|
|
495
499
|
|
|
496
500
|
// Public config: one flat, primitive, PUBLIC id per configured stage. These are
|