@drip-apex/sdk 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/bundle-report.json +584 -226
- package/dist/define.cjs +123 -0
- package/dist/define.cjs.map +7 -0
- package/dist/define.mjs +102 -0
- package/dist/define.mjs.map +7 -0
- package/dist/drip.cjs +19262 -0
- package/dist/drip.cjs.map +7 -0
- package/dist/drip.debug.js +1 -1
- package/dist/drip.debug.js.map +1 -1
- package/dist/drip.heatmap.js +1 -1
- package/dist/drip.heatmap.js.map +1 -1
- package/dist/drip.js +1 -1
- package/dist/drip.js.map +1 -1
- package/dist/drip.live.js +1 -1
- package/dist/drip.live.js.map +1 -1
- package/dist/drip.mjs +19246 -0
- package/dist/drip.mjs.map +7 -0
- package/dist/drip.replay-recorder.js +2 -0
- package/dist/drip.replay-recorder.js.map +1 -0
- package/dist/editor.js +147 -142
- package/dist/editor.js.map +4 -4
- package/dist/flags.cjs +1788 -0
- package/dist/flags.cjs.map +7 -0
- package/dist/flags.mjs +1765 -0
- package/dist/flags.mjs.map +7 -0
- package/dist/index.mjs +12 -0
- package/dist/index.mjs.map +2 -2
- package/dist/metafiles/define.cjs.meta.json +28 -0
- package/dist/metafiles/define.mjs.meta.json +32 -0
- package/dist/metafiles/drip.cjs.meta.json +3193 -0
- package/dist/metafiles/drip.debug.meta.json +2859 -574
- package/dist/metafiles/drip.heatmap.meta.json +2732 -460
- package/dist/metafiles/drip.live.meta.json +2732 -460
- package/dist/metafiles/drip.meta.json +2859 -574
- package/dist/metafiles/drip.mjs.meta.json +3235 -0
- package/dist/metafiles/drip.replay-recorder.meta.json +82 -0
- package/dist/metafiles/editor.meta.json +67 -27
- package/dist/metafiles/flags.cjs.meta.json +220 -0
- package/dist/metafiles/flags.mjs.meta.json +225 -0
- package/dist/metafiles/index.meta.json +2717 -491
- package/dist/metafiles/index.mjs.meta.json +14 -4
- package/dist/react/index.js +16621 -13539
- package/dist/react/index.js.map +4 -4
- package/dist/sri.json +20 -15
- package/dist/types/activation-runtime.d.ts +91 -0
- package/dist/types/analytics-runtime.d.ts +166 -0
- package/dist/types/antiflicker.d.ts +9 -3
- package/dist/types/assignment-cookie-sync.d.ts +31 -0
- package/dist/types/assignment-runtime.d.ts +257 -0
- package/dist/types/assignment-store.d.ts +4 -8
- package/dist/types/bucket.d.ts +1 -2
- package/dist/types/compact-store.d.ts +1 -0
- package/dist/types/config-runtime.d.ts +313 -0
- package/dist/types/consent-evaluation.d.ts +27 -0
- package/dist/types/css.d.ts +5 -3
- package/dist/types/debug-loader.d.ts +2 -0
- package/dist/types/decide-assignments.d.ts +31 -0
- package/dist/types/delivery-timing.d.ts +8 -0
- package/dist/types/early-interaction-capture.d.ts +79 -0
- package/dist/types/early-reassert.d.ts +32 -0
- package/dist/types/edge-runtime.d.ts +11 -0
- package/dist/types/editor/api.d.ts +3 -0
- package/dist/types/editor/controller.d.ts +3 -1
- package/dist/types/editor/publication-result.d.ts +11 -0
- package/dist/types/editor/types.d.ts +2 -1
- package/dist/types/editor/view-kit.d.ts +1 -1
- package/dist/types/endpoint.d.ts +8 -0
- package/dist/types/exclusion-groups.d.ts +4 -7
- package/dist/types/flags.d.ts +2 -0
- package/dist/types/goal-eligibility.d.ts +4 -0
- package/dist/types/goals.d.ts +10 -1
- package/dist/types/guards.d.ts +7 -7
- package/dist/types/handover.d.ts +12 -1
- package/dist/types/hash.d.ts +8 -4
- package/dist/types/hydration.d.ts +2 -1
- package/dist/types/index.d.ts +24 -128
- package/dist/types/init-test-helpers.d.ts +48 -0
- package/dist/types/inline-bootstrap.d.ts +26 -0
- package/dist/types/js.d.ts +3 -1
- package/dist/types/money.d.ts +12 -0
- package/dist/types/mutate.d.ts +45 -6
- package/dist/types/performance.d.ts +2 -0
- package/dist/types/persistence-decoders.d.ts +12 -0
- package/dist/types/public-api.d.ts +209 -0
- package/dist/types/qa-assistant-state.d.ts +34 -0
- package/dist/types/react/index.d.ts +28 -3
- package/dist/types/reassert-sentinel.d.ts +6 -0
- package/dist/types/redirect.d.ts +48 -0
- package/dist/types/replay-recorder-entry.d.ts +1 -0
- package/dist/types/replay-signal-state.d.ts +6 -0
- package/dist/types/replay-signals.d.ts +13 -0
- package/dist/types/replay.d.ts +110 -0
- package/dist/types/router.d.ts +12 -2
- package/dist/types/runtime-state.d.ts +166 -0
- package/dist/types/sanitize.d.ts +1 -0
- package/dist/types/shadow.d.ts +3 -0
- package/dist/types/shopify-cart.d.ts +25 -1
- package/dist/types/storefront-vitals.d.ts +24 -0
- package/dist/types/targeting-diagnostics.d.ts +1 -0
- package/dist/types/targeting.d.ts +16 -2
- package/dist/types/track.d.ts +15 -1
- package/dist/types/types.d.ts +115 -9
- package/dist/types/wait.d.ts +14 -1
- package/dist/types/weekday-schedule.d.ts +3 -0
- package/package.json +17 -8
- package/src/activation-runtime.ts +2737 -0
- package/src/analytics-runtime.ts +2175 -0
- package/src/antiflicker.test.ts +216 -44
- package/src/antiflicker.ts +103 -28
- package/src/artifact-concurrency.test.ts +1091 -0
- package/src/assignment-cookie-sync.test.ts +202 -0
- package/src/assignment-cookie-sync.ts +189 -0
- package/src/assignment-runtime.ts +3204 -0
- package/src/assignment-store.test.ts +56 -0
- package/src/assignment-store.ts +7 -38
- package/src/bucket.ts +3 -18
- package/src/commerce-analytics-lifecycle.test.ts +75 -15
- package/src/commerce-config-lifecycle.test.ts +81 -1
- package/src/commerce-price-preserve.test.ts +133 -0
- package/src/commerce-price.test.ts +97 -0
- package/src/commerce-price.ts +74 -20
- package/src/commerce-shipping.test.ts +53 -0
- package/src/commerce-shipping.ts +5 -11
- package/src/compact-store.ts +15 -0
- package/src/config-fetch.test.ts +480 -9
- package/src/config-runtime.ts +2070 -0
- package/src/consent-evaluation-cookiescript.test.ts +50 -0
- package/src/consent-evaluation.ts +138 -0
- package/src/css.test.ts +30 -0
- package/src/css.ts +120 -6
- package/src/cwv.test.ts +60 -77
- package/src/debug-loader.test.ts +214 -0
- package/src/debug-loader.ts +41 -0
- package/src/decide-assignments.test.ts +196 -0
- package/src/decide-assignments.ts +204 -0
- package/src/delivery-timing.test.ts +104 -0
- package/src/delivery-timing.ts +89 -0
- package/src/early-interaction-capture.test.ts +451 -0
- package/src/early-interaction-capture.ts +230 -0
- package/src/early-reassert.test.ts +207 -0
- package/src/early-reassert.ts +152 -0
- package/src/edge-adoption.test.ts +435 -0
- package/src/edge-runtime.ts +30 -0
- package/src/editor/api.test.ts +109 -0
- package/src/editor/api.ts +34 -3
- package/src/editor/controller.test.ts +297 -14
- package/src/editor/controller.ts +69 -5
- package/src/editor/index.test.ts +2 -0
- package/src/editor/index.ts +1 -0
- package/src/editor/publication-result.ts +58 -0
- package/src/editor/types.ts +10 -1
- package/src/editor/view-kit.ts +2 -1
- package/src/editor/view.test.ts +108 -0
- package/src/editor/view.ts +49 -7
- package/src/endpoint.test.ts +15 -0
- package/src/endpoint.ts +10 -0
- package/src/exclusion-groups.ts +8 -18
- package/src/flags.test.ts +88 -1
- package/src/flags.ts +29 -29
- package/src/global-runtime-reconciler.test.ts +271 -0
- package/src/goal-eligibility.test.ts +29 -0
- package/src/goal-eligibility.ts +17 -0
- package/src/goals.test.ts +144 -1
- package/src/goals.ts +97 -18
- package/src/guards.test.ts +99 -1
- package/src/guards.ts +34 -15
- package/src/handover-cache-order.test.ts +6 -3
- package/src/handover.test.ts +154 -8
- package/src/handover.ts +122 -12
- package/src/hash.ts +29 -54
- package/src/holdout.test.ts +26 -0
- package/src/hydration.test.ts +73 -2
- package/src/hydration.ts +18 -9
- package/src/index.ts +130 -7287
- package/src/init-analytics.test.ts +1694 -0
- package/src/init-anti-flicker.test.ts +1211 -0
- package/src/init-cart-attributes.test.ts +2003 -0
- package/src/init-config-resolution.test.ts +999 -0
- package/src/init-consent.test.ts +971 -0
- package/src/init-empty.test.ts +11 -5406
- package/src/init-integrations.test.ts +846 -0
- package/src/init-page-activation.test.ts +5064 -0
- package/src/init-qa-loading.test.ts +740 -0
- package/src/init-redirects.test.ts +2156 -0
- package/src/init-test-helpers.ts +287 -0
- package/src/inline-bootstrap.test.ts +184 -0
- package/src/inline-bootstrap.ts +73 -0
- package/src/js.test.ts +51 -1
- package/src/js.ts +86 -2
- package/src/money.test.ts +55 -1
- package/src/money.ts +104 -2
- package/src/money.web-parity.contract.test.ts +85 -0
- package/src/mutate.test.ts +474 -10
- package/src/mutate.ts +739 -230
- package/src/page-trigger.test.ts +218 -8
- package/src/performance.ts +10 -0
- package/src/persistence-decoders.ts +18 -0
- package/src/public-api.ts +1828 -0
- package/src/qa-assistant-state.test.ts +249 -3
- package/src/qa-assistant-state.ts +146 -6
- package/src/qa-assistant-view.test.ts +220 -2
- package/src/qa-assistant-view.ts +102 -5
- package/src/react/index.test.tsx +37 -0
- package/src/react/index.tsx +19 -3
- package/src/reassert-sentinel.test.ts +155 -1
- package/src/reassert-sentinel.ts +168 -8
- package/src/redirect.test.ts +541 -0
- package/src/redirect.ts +572 -0
- package/src/replay-recorder-entry.ts +115 -0
- package/src/replay-signal-state.ts +100 -0
- package/src/replay-signals.test.ts +249 -0
- package/src/replay-signals.ts +151 -0
- package/src/replay.test.ts +764 -0
- package/src/replay.ts +514 -0
- package/src/router.test.ts +61 -2
- package/src/router.ts +50 -12
- package/src/runtime-state.ts +507 -0
- package/src/runtime.test.ts +174 -7
- package/src/runtime.ts +88 -14
- package/src/sanitize.test.ts +34 -0
- package/src/sanitize.ts +23 -12
- package/src/shadow.test.ts +187 -0
- package/src/shadow.ts +99 -0
- package/src/shopify-cart.test.ts +733 -23
- package/src/shopify-cart.ts +402 -49
- package/src/storefront-vitals.test.ts +217 -0
- package/src/storefront-vitals.ts +206 -0
- package/src/surface-signals.ts +6 -0
- package/src/targeting-conformance.test.ts +9 -0
- package/src/targeting-diagnostics.ts +38 -8
- package/src/targeting-parity-corpus.json +559 -0
- package/src/targeting-parity-corpus.test.ts +18 -0
- package/src/targeting.detailed.test.ts +105 -0
- package/src/targeting.test.ts +211 -0
- package/src/targeting.ts +88 -207
- package/src/track.test.ts +147 -0
- package/src/track.ts +125 -26
- package/src/types.ts +120 -13
- package/src/wait.test.ts +23 -0
- package/src/wait.ts +129 -13
- package/src/weekday-delivery.test.ts +56 -0
- package/src/weekday-schedule-parity-corpus.json +92 -0
- package/src/weekday-schedule.test.ts +69 -0
- package/src/weekday-schedule.ts +20 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { readCookieScriptConsent } from "./consent-evaluation";
|
|
3
|
+
|
|
4
|
+
function installCookie(value: string): void {
|
|
5
|
+
Object.defineProperty(globalThis, "document", {
|
|
6
|
+
configurable: true,
|
|
7
|
+
value: { cookie: `other=1; CookieScriptConsent=${value}; trailing=1` },
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe("CookieScript consent cookie fallback", () => {
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
delete (globalThis as Record<string, unknown>).CookieScript;
|
|
14
|
+
delete (globalThis as Record<string, unknown>).document;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it.each([
|
|
18
|
+
["JSON-encoded categories", JSON.stringify(["functionality", "performance"])],
|
|
19
|
+
["array categories", ["functionality", "performance"]],
|
|
20
|
+
])("maps analytics to performance for %s", (_label, categories) => {
|
|
21
|
+
installCookie(encodeURIComponent(JSON.stringify({ action: "accept", categories })));
|
|
22
|
+
expect(readCookieScriptConsent("analytics")).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("returns false for rejection", () => {
|
|
26
|
+
installCookie(encodeURIComponent(JSON.stringify({ action: "reject" })));
|
|
27
|
+
expect(readCookieScriptConsent("analytics")).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("returns null before CookieScript records an action", () => {
|
|
31
|
+
installCookie(encodeURIComponent(JSON.stringify({ bannershown: 1 })));
|
|
32
|
+
expect(readCookieScriptConsent("analytics")).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it.each(["%E0%A4%A", encodeURIComponent("not json")])(
|
|
36
|
+
"returns null for malformed cookie %s",
|
|
37
|
+
(value) => {
|
|
38
|
+
installCookie(value);
|
|
39
|
+
expect(readCookieScriptConsent("analytics")).toBeNull();
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
it("keeps the CookieScript instance primary when it disagrees with the cookie", () => {
|
|
44
|
+
installCookie(encodeURIComponent(JSON.stringify({ action: "accept", categories: ["performance"] })));
|
|
45
|
+
(globalThis as Record<string, any>).CookieScript = {
|
|
46
|
+
instance: { currentState: () => ({ action: "reject" }) },
|
|
47
|
+
};
|
|
48
|
+
expect(readCookieScriptConsent("analytics")).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
export type SynchronousConsentInput = {
|
|
2
|
+
requireConsent?: boolean;
|
|
3
|
+
hasConsent?: boolean;
|
|
4
|
+
consentModeEnabled?: boolean;
|
|
5
|
+
configuredConsentGranted?: boolean | null;
|
|
6
|
+
consentCookieGranted: boolean;
|
|
7
|
+
respectDoNotTrack?: boolean;
|
|
8
|
+
doNotTrackValue?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type ConsentProvider = "custom" | "onetrust" | "usercentrics" | "cookiebot" | "ccm19" | "pandectes" | "cookiescript";
|
|
12
|
+
|
|
13
|
+
export function readPath(root: unknown, path?: string): unknown {
|
|
14
|
+
let current = root as Record<string, unknown> | undefined;
|
|
15
|
+
for (const part of (path ?? "").split(".").map((item) => item.trim()).filter(Boolean)) {
|
|
16
|
+
if (!current || typeof current !== "object") return undefined;
|
|
17
|
+
current = current[part] as Record<string, unknown> | undefined;
|
|
18
|
+
}
|
|
19
|
+
return current;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function tokenMatches(value: unknown, required: string): boolean {
|
|
23
|
+
return value != null && String(value).trim().toLowerCase() === required.trim().toLowerCase();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function readConsentCookie(name: string): string {
|
|
27
|
+
try {
|
|
28
|
+
const match = document.cookie.match(new RegExp("(?:^|;)\\s*" + name + "=([^;]*)"));
|
|
29
|
+
return match ? decodeURIComponent(match[1]) : "";
|
|
30
|
+
} catch { return ""; }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The SDK's synchronous native-CMP readers, shared with the pre-SDK capture fragment. */
|
|
34
|
+
export function readConfiguredConsent(provider?: ConsentProvider, requiredGroup?: string, getter?: string): boolean | null {
|
|
35
|
+
const root = globalThis as Record<string, any>;
|
|
36
|
+
if (provider === "cookiescript") return readCookieScriptConsent(requiredGroup);
|
|
37
|
+
if (provider === "usercentrics") {
|
|
38
|
+
const uc = root.UC_UI ?? root.window?.UC_UI;
|
|
39
|
+
if (typeof uc?.getServicesBaseInfo !== "function") return null;
|
|
40
|
+
let services: unknown;
|
|
41
|
+
try { services = uc.getServicesBaseInfo(); } catch { return null; }
|
|
42
|
+
if (!Array.isArray(services)) return null;
|
|
43
|
+
const required = requiredGroup?.trim() || "Apex";
|
|
44
|
+
const service = services.find((item) => item && typeof item === "object" &&
|
|
45
|
+
["name", "id", "serviceId", "templateId", "dataProcessor", "dataProcessorName"]
|
|
46
|
+
.some((key) => tokenMatches((item as Record<string, unknown>)[key], required))) as Record<string, any> | undefined;
|
|
47
|
+
return service ? service.consent?.status === true || service.consentStatus === true || service.status === true : false;
|
|
48
|
+
}
|
|
49
|
+
if (provider === "cookiebot") {
|
|
50
|
+
const consent = (root.Cookiebot ?? root.window?.Cookiebot)?.consent;
|
|
51
|
+
if (!consent || typeof consent !== "object") return null;
|
|
52
|
+
const aliases: Record<string, string> = { analytics: "statistics", stats: "statistics", statistic: "statistics", preference: "preferences" };
|
|
53
|
+
const key = aliases[(requiredGroup?.trim() || "statistics").toLowerCase()] || (requiredGroup?.trim() || "statistics").toLowerCase();
|
|
54
|
+
return typeof consent[key] === "boolean" ? consent[key] : false;
|
|
55
|
+
}
|
|
56
|
+
if (provider === "ccm19") {
|
|
57
|
+
const embeddings = (root.CCM ?? root.window?.CCM)?.acceptedEmbeddings;
|
|
58
|
+
if (!Array.isArray(embeddings)) return null;
|
|
59
|
+
const required = requiredGroup?.trim() || "Apex";
|
|
60
|
+
return embeddings.some((item) => item && typeof item === "object" &&
|
|
61
|
+
(tokenMatches((item as any).name, required) || (item as any).id === required));
|
|
62
|
+
}
|
|
63
|
+
if (provider === "pandectes") {
|
|
64
|
+
const purpose = (requiredGroup?.trim() || "analytics").toLowerCase();
|
|
65
|
+
const method = ({ analytics: "analyticsProcessingAllowed", statistics: "analyticsProcessingAllowed", statistic: "analyticsProcessingAllowed", stats: "analyticsProcessingAllowed", marketing: "marketingAllowed", preferences: "preferencesProcessingAllowed", preference: "preferencesProcessingAllowed", sale_of_data: "saleOfDataAllowed", saleofdata: "saleOfDataAllowed", "sale-of-data": "saleOfDataAllowed", sale: "saleOfDataAllowed" } as Record<string, string>)[purpose];
|
|
66
|
+
if (!method) return false;
|
|
67
|
+
for (const source of [root.Pandectes ?? root.window?.Pandectes, root.Shopify?.customerPrivacy ?? root.window?.Shopify?.customerPrivacy]) {
|
|
68
|
+
try { const value = source?.[method]?.(); if (typeof value === "boolean") return value; } catch {}
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
if (provider === "onetrust" || provider === "custom" || getter) {
|
|
73
|
+
const raw = readPath(root, getter ?? "OnetrustActiveGroups");
|
|
74
|
+
if (typeof raw === "boolean") return raw;
|
|
75
|
+
if (raw == null) return null;
|
|
76
|
+
const required = requiredGroup;
|
|
77
|
+
if (!required) return raw != null && String(raw).trim() !== "";
|
|
78
|
+
if (Array.isArray(raw)) return raw.map(String).includes(required);
|
|
79
|
+
if (typeof raw === "string") return raw.split(",").map((item) => item.trim()).filter(Boolean).includes(required);
|
|
80
|
+
return raw == null ? null : false;
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function readCookieScriptConsent(requiredGroup?: string): boolean | null {
|
|
86
|
+
const root = globalThis as Record<string, any>;
|
|
87
|
+
const requested = (requiredGroup?.trim() || "analytics").toLowerCase();
|
|
88
|
+
const aliases: Record<string, string> = {
|
|
89
|
+
analytics: "performance", statistics: "performance", statistic: "performance", stats: "performance",
|
|
90
|
+
marketing: "targeting", advertising: "targeting", ads: "targeting", functional: "functionality",
|
|
91
|
+
preferences: "functionality", preference: "functionality", necessary: "strict", essential: "strict",
|
|
92
|
+
};
|
|
93
|
+
const required = aliases[requested] || requested;
|
|
94
|
+
const instance = root.CookieScript?.instance
|
|
95
|
+
?? (root.window as Record<string, any> | undefined)?.CookieScript?.instance;
|
|
96
|
+
let currentState: unknown;
|
|
97
|
+
if (instance && typeof instance === "object" && typeof instance.currentState === "function") {
|
|
98
|
+
try { currentState = instance.currentState(); } catch {}
|
|
99
|
+
}
|
|
100
|
+
if (!currentState || typeof currentState !== "object" || Array.isArray(currentState)) {
|
|
101
|
+
try {
|
|
102
|
+
currentState = JSON.parse(readConsentCookie("CookieScriptConsent"));
|
|
103
|
+
const categories = (currentState as Record<string, unknown>).categories;
|
|
104
|
+
if (typeof categories === "string") (currentState as Record<string, unknown>).categories = JSON.parse(categories);
|
|
105
|
+
} catch { return null; }
|
|
106
|
+
}
|
|
107
|
+
if (!currentState || typeof currentState !== "object" || Array.isArray(currentState)) return null;
|
|
108
|
+
const consent = currentState as Record<string, unknown>;
|
|
109
|
+
if (consent.action === "reject") return false;
|
|
110
|
+
if (consent.action !== "accept") return null;
|
|
111
|
+
const categories = consent.categories;
|
|
112
|
+
if (!Array.isArray(categories)) return false;
|
|
113
|
+
return categories.some((item) => String(item).toLowerCase() === required);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const CONSENT_REQUIRED = 1;
|
|
117
|
+
export const CONSENT_GRANTED = 2;
|
|
118
|
+
export const DNT_BLOCKED = 4;
|
|
119
|
+
export const STORAGE_ALLOWED = 8;
|
|
120
|
+
|
|
121
|
+
export function resolveSynchronousConsentGranted(input: SynchronousConsentInput & { provider?: ConsentProvider }): boolean {
|
|
122
|
+
return Boolean(evaluateSynchronousConsentForProvider(input) & CONSENT_GRANTED);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function evaluateSynchronousConsentForProvider(input: SynchronousConsentInput & { provider?: ConsentProvider }): number {
|
|
126
|
+
const configured = input.configuredConsentGranted
|
|
127
|
+
?? (input.provider === "cookiescript" ? false : null);
|
|
128
|
+
return evaluateSynchronousConsent({ ...input, configuredConsentGranted: configured });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Pure synchronous consent/DNT bitmask shared by SDK init and the inline bootstrap. */
|
|
132
|
+
export function evaluateSynchronousConsent(input: SynchronousConsentInput): number {
|
|
133
|
+
const required = input.requireConsent === true || typeof input.hasConsent === "boolean" || input.consentModeEnabled === true;
|
|
134
|
+
const granted = !required || (typeof input.hasConsent === "boolean" ? input.hasConsent : input.configuredConsentGranted ?? input.consentCookieGranted);
|
|
135
|
+
const value = (input.doNotTrackValue ?? "").toLowerCase();
|
|
136
|
+
const dnt = input.respectDoNotTrack === true && (value === "1" || value === "yes");
|
|
137
|
+
return (required ? CONSENT_REQUIRED : 0) | (granted ? CONSENT_GRANTED : 0) | (dnt ? DNT_BLOCKED : 0) | (granted && !dnt ? STORAGE_ALLOWED : 0);
|
|
138
|
+
}
|
package/src/css.test.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { injectCss, removeAllCss, removeCssForOwner } from "./css";
|
|
4
|
+
|
|
5
|
+
describe("CSS artifact journal", () => {
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
removeAllCss();
|
|
8
|
+
document.head.innerHTML = "";
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("replaces stale content when an existing element id carries different CSS", () => {
|
|
12
|
+
injectCss(".hero{color:red}", "exp", "var", ["page-a"], "artifact-a");
|
|
13
|
+
injectCss(".hero{color:blue}", "exp", "var", ["page-a"], "artifact-a");
|
|
14
|
+
|
|
15
|
+
expect(document.getElementById("drip-css-exp-var")?.textContent).toBe(
|
|
16
|
+
".hero{color:blue}",
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("keeps a shared artifact until its final owner tears down", () => {
|
|
21
|
+
injectCss(".hero{color:red}", "exp", "var", ["page-a"], "artifact-a");
|
|
22
|
+
injectCss(".hero{color:red}", "exp", "var", ["page-b"], "artifact-a");
|
|
23
|
+
|
|
24
|
+
removeCssForOwner("page-a");
|
|
25
|
+
expect(document.getElementById("drip-css-exp-var")).not.toBeNull();
|
|
26
|
+
|
|
27
|
+
removeCssForOwner("page-b");
|
|
28
|
+
expect(document.getElementById("drip-css-exp-var")).toBeNull();
|
|
29
|
+
});
|
|
30
|
+
});
|
package/src/css.ts
CHANGED
|
@@ -1,17 +1,82 @@
|
|
|
1
1
|
const CSS_CLASS = "drip-css";
|
|
2
2
|
const CSS_LINK_CLASS = "drip-css-link";
|
|
3
|
+
type CssJournalEntry = {
|
|
4
|
+
seq: number;
|
|
5
|
+
artifactId: string;
|
|
6
|
+
epoch: number;
|
|
7
|
+
owners: Set<string>;
|
|
8
|
+
target: Element;
|
|
9
|
+
state: "applied" | "invalidated";
|
|
10
|
+
};
|
|
11
|
+
const cssJournal: CssJournalEntry[] = [];
|
|
12
|
+
let cssSequence = 0;
|
|
3
13
|
|
|
4
|
-
|
|
14
|
+
function recordArtifact(
|
|
15
|
+
element: Element,
|
|
16
|
+
artifactId: string,
|
|
17
|
+
ownerKeys: readonly string[],
|
|
18
|
+
epoch: number,
|
|
19
|
+
): void {
|
|
20
|
+
const existing = cssJournal.find(
|
|
21
|
+
(entry) => entry.target === element && entry.artifactId === artifactId,
|
|
22
|
+
);
|
|
23
|
+
if (existing) {
|
|
24
|
+
ownerKeys.forEach((ownerKey) => existing.owners.add(ownerKey));
|
|
25
|
+
existing.state = "applied";
|
|
26
|
+
existing.epoch = epoch;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
cssJournal.push({
|
|
30
|
+
seq: ++cssSequence,
|
|
31
|
+
artifactId,
|
|
32
|
+
epoch,
|
|
33
|
+
owners: new Set(ownerKeys),
|
|
34
|
+
target: element,
|
|
35
|
+
state: "applied",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function forgetElement(element: Element): void {
|
|
40
|
+
for (let index = cssJournal.length - 1; index >= 0; index -= 1) {
|
|
41
|
+
if (cssJournal[index].target === element) cssJournal.splice(index, 1);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function injectCss(
|
|
46
|
+
css: string,
|
|
47
|
+
experimentId: string,
|
|
48
|
+
variationId: string,
|
|
49
|
+
ownerKeys: readonly string[] = [],
|
|
50
|
+
artifactId = "css:" + experimentId + ":" + variationId,
|
|
51
|
+
epoch = 0,
|
|
52
|
+
): void {
|
|
5
53
|
const id = "drip-css-" + experimentId + "-" + variationId;
|
|
6
|
-
|
|
54
|
+
const existing = document.getElementById(id);
|
|
55
|
+
if (existing && existing.textContent === css) {
|
|
56
|
+
recordArtifact(existing, artifactId, ownerKeys, epoch);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (existing) {
|
|
60
|
+
forgetElement(existing);
|
|
61
|
+
existing.textContent = css;
|
|
62
|
+
recordArtifact(existing, artifactId, ownerKeys, epoch);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
7
65
|
const style = document.createElement("style");
|
|
8
66
|
style.id = id;
|
|
9
67
|
style.className = CSS_CLASS;
|
|
10
68
|
style.textContent = css;
|
|
69
|
+
recordArtifact(style, artifactId, ownerKeys, epoch);
|
|
11
70
|
(document.head || document.documentElement).appendChild(style);
|
|
12
71
|
}
|
|
13
72
|
|
|
14
|
-
export function injectExperimentCss(
|
|
73
|
+
export function injectExperimentCss(
|
|
74
|
+
css: string,
|
|
75
|
+
experimentId: string,
|
|
76
|
+
ownerKeys: readonly string[] = [],
|
|
77
|
+
artifactId = "css:experiment:" + experimentId,
|
|
78
|
+
epoch = 0,
|
|
79
|
+
): void {
|
|
15
80
|
const id = "drip-css-exp-" + experimentId;
|
|
16
81
|
let style = document.getElementById(id) as HTMLStyleElement | null;
|
|
17
82
|
if (!style) {
|
|
@@ -20,35 +85,84 @@ export function injectExperimentCss(css: string, experimentId: string): void {
|
|
|
20
85
|
style.className = CSS_CLASS;
|
|
21
86
|
(document.head || document.documentElement).appendChild(style);
|
|
22
87
|
}
|
|
88
|
+
if (style.textContent !== css) forgetElement(style);
|
|
23
89
|
style.textContent = css;
|
|
90
|
+
recordArtifact(style, artifactId, ownerKeys, epoch);
|
|
24
91
|
}
|
|
25
92
|
|
|
26
93
|
export function removeCss(experimentId: string, variationId: string): void {
|
|
27
94
|
const el = document.getElementById("drip-css-" + experimentId + "-" + variationId);
|
|
28
|
-
if (el)
|
|
95
|
+
if (el) {
|
|
96
|
+
forgetElement(el);
|
|
97
|
+
el.remove();
|
|
98
|
+
}
|
|
29
99
|
}
|
|
30
100
|
|
|
31
|
-
export function injectExternalCss(
|
|
101
|
+
export function injectExternalCss(
|
|
102
|
+
url: string,
|
|
103
|
+
experimentId: string,
|
|
104
|
+
variationId: string,
|
|
105
|
+
ownerKeys: readonly string[] = [],
|
|
106
|
+
artifactId = "css:external:" + experimentId + ":" + variationId,
|
|
107
|
+
epoch = 0,
|
|
108
|
+
): Promise<void> {
|
|
32
109
|
return new Promise((resolve, reject) => {
|
|
33
110
|
const id = "drip-css-link-" + experimentId + "-" + variationId;
|
|
34
111
|
const previous = document.getElementById(id);
|
|
35
112
|
if (previous && previous.getAttribute("href") === url) {
|
|
113
|
+
recordArtifact(previous, artifactId, ownerKeys, epoch);
|
|
36
114
|
resolve();
|
|
37
115
|
return;
|
|
38
116
|
}
|
|
39
|
-
if (previous)
|
|
117
|
+
if (previous) {
|
|
118
|
+
forgetElement(previous);
|
|
119
|
+
previous.remove();
|
|
120
|
+
}
|
|
40
121
|
const link = document.createElement("link");
|
|
41
122
|
link.id = id;
|
|
42
123
|
link.className = CSS_LINK_CLASS;
|
|
43
124
|
link.rel = "stylesheet";
|
|
44
125
|
link.href = url;
|
|
126
|
+
recordArtifact(link, artifactId, ownerKeys, epoch);
|
|
45
127
|
link.onload = () => resolve();
|
|
46
128
|
link.onerror = () => reject(new Error("Failed to load CSS artifact: " + url));
|
|
47
129
|
(document.head || document.documentElement).appendChild(link);
|
|
48
130
|
});
|
|
49
131
|
}
|
|
50
132
|
|
|
133
|
+
export function removeCssForOwner(ownerKey: string, artifactId?: string): void {
|
|
134
|
+
const removed = new Set<Element>();
|
|
135
|
+
for (let index = cssJournal.length - 1; index >= 0; index -= 1) {
|
|
136
|
+
const entry = cssJournal[index];
|
|
137
|
+
if (artifactId !== undefined && entry.artifactId !== artifactId) continue;
|
|
138
|
+
if (!entry.owners.delete(ownerKey) || entry.owners.size > 0) continue;
|
|
139
|
+
removed.add(entry.target);
|
|
140
|
+
cssJournal.splice(index, 1);
|
|
141
|
+
}
|
|
142
|
+
for (const element of removed) {
|
|
143
|
+
if (!cssJournal.some((entry) => entry.target === element)) element.remove();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function addCssOwners(
|
|
148
|
+
existingOwnerKeys: ReadonlySet<string>,
|
|
149
|
+
ownerKeys: readonly string[],
|
|
150
|
+
artifactId?: string,
|
|
151
|
+
): void {
|
|
152
|
+
if (existingOwnerKeys.size === 0 || ownerKeys.length === 0) return;
|
|
153
|
+
for (const entry of cssJournal) {
|
|
154
|
+
if (artifactId !== undefined && entry.artifactId !== artifactId) continue;
|
|
155
|
+
if (
|
|
156
|
+
!Array.from(existingOwnerKeys).some((ownerKey) =>
|
|
157
|
+
entry.owners.has(ownerKey)
|
|
158
|
+
)
|
|
159
|
+
) continue;
|
|
160
|
+
ownerKeys.forEach((ownerKey) => entry.owners.add(ownerKey));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
51
164
|
export function removeAllCss(): void {
|
|
52
165
|
document.querySelectorAll("." + CSS_CLASS).forEach((el) => el.remove());
|
|
53
166
|
document.querySelectorAll("." + CSS_LINK_CLASS).forEach((el) => el.remove());
|
|
167
|
+
cssJournal.length = 0;
|
|
54
168
|
}
|
package/src/cwv.test.ts
CHANGED
|
@@ -1,95 +1,78 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
// Coverage for the route-scoped CLS measurement: the total/after-reveal split,
|
|
4
|
+
// the hadRecentInput filter, and route-reset finalization. The full-page
|
|
5
|
+
// vitals (LCP/INP/TTFB + ratings) live in storefront-vitals and its suite.
|
|
6
|
+
describe("CWV route-scoped CLS measurement", () => {
|
|
7
|
+
let callbacks: PerformanceObserverCallback[];
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
10
|
vi.resetModules();
|
|
11
|
+
vi.useFakeTimers();
|
|
12
|
+
callbacks = [];
|
|
13
|
+
vi.stubGlobal("PerformanceObserver", class {
|
|
14
|
+
constructor(callback: PerformanceObserverCallback) { callbacks.push(callback); }
|
|
15
|
+
observe = vi.fn();
|
|
16
|
+
disconnect = vi.fn();
|
|
17
|
+
takeRecords = vi.fn(() => []);
|
|
18
|
+
});
|
|
19
|
+
vi.stubGlobal("performance", { now: () => 1_000 });
|
|
11
20
|
});
|
|
12
21
|
|
|
13
|
-
|
|
14
|
-
vi.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
expect(() => startCwvMeasurement()).not.toThrow();
|
|
18
|
-
expect(getCwvMetrics()).toEqual({ cls_total: 0, cls_after_reveal: 0 });
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
vi.useRealTimers();
|
|
24
|
+
vi.unstubAllGlobals();
|
|
19
25
|
});
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
startCwvMeasurement(true);
|
|
27
|
+
const emit = (entries: unknown[]) => callbacks.at(-1)?.(
|
|
28
|
+
{ getEntries: () => entries } as PerformanceObserverEntryList,
|
|
29
|
+
{} as PerformanceObserver,
|
|
30
|
+
);
|
|
27
31
|
|
|
28
|
-
|
|
29
|
-
await
|
|
30
|
-
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("accumulates layout shifts and splits shifts after reveal", async () => {
|
|
34
|
-
vi.useFakeTimers();
|
|
35
|
-
let callback: PerformanceObserverCallback | null = null;
|
|
36
|
-
const disconnect = vi.fn();
|
|
37
|
-
const observe = vi.fn();
|
|
38
|
-
vi.stubGlobal("PerformanceObserver", class PerformanceObserver {
|
|
39
|
-
constructor(cb: PerformanceObserverCallback) { callback = cb; }
|
|
40
|
-
observe = observe;
|
|
41
|
-
disconnect = disconnect;
|
|
42
|
-
takeRecords = vi.fn(() => []);
|
|
43
|
-
});
|
|
44
|
-
vi.spyOn(performance, "now").mockReturnValue(100);
|
|
45
|
-
const { getCwvMetrics, markCwvReveal, startCwvMeasurement, waitForCwvMetrics } = await import("./cwv");
|
|
32
|
+
it("splits CLS into total and after-reveal and ignores recent-input shifts", async () => {
|
|
33
|
+
const { markCwvReveal, resetCwvMeasurement, startCwvMeasurement, waitForCwvMetrics } = await import("./cwv");
|
|
34
|
+
resetCwvMeasurement();
|
|
46
35
|
startCwvMeasurement();
|
|
36
|
+
emit([
|
|
37
|
+
{ startTime: 500, value: 0.05, hadRecentInput: false },
|
|
38
|
+
{ startTime: 600, value: 1, hadRecentInput: true },
|
|
39
|
+
]);
|
|
47
40
|
markCwvReveal();
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
callback?.({
|
|
51
|
-
getEntries: () => [
|
|
52
|
-
{ startTime: 50, value: 0.1, hadRecentInput: false },
|
|
53
|
-
{ startTime: 150, value: 0.2, hadRecentInput: false },
|
|
54
|
-
{ startTime: 160, value: 0.4, hadRecentInput: true },
|
|
55
|
-
],
|
|
56
|
-
} as unknown as PerformanceObserverEntryList, {} as PerformanceObserver);
|
|
57
|
-
|
|
58
|
-
expect(observe).toHaveBeenCalledWith({ type: "layout-shift", buffered: true });
|
|
59
|
-
expect(getCwvMetrics()).toEqual({ cls_total: 0.3, cls_after_reveal: 0.2 });
|
|
41
|
+
emit([{ startTime: 1_500, value: 0.02, hadRecentInput: false }]);
|
|
42
|
+
const completion = waitForCwvMetrics();
|
|
60
43
|
await vi.advanceTimersByTimeAsync(5_000);
|
|
61
|
-
expect(
|
|
62
|
-
await expect(finalized).resolves.toEqual({ cls_total: 0.3, cls_after_reveal: 0.2 });
|
|
44
|
+
await expect(completion).resolves.toEqual({ cls_total: 0.07, cls_after_reveal: 0.02 });
|
|
63
45
|
});
|
|
64
46
|
|
|
65
|
-
it("finalizes the prior route
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const observes: ReturnType<typeof vi.fn>[] = [];
|
|
69
|
-
vi.stubGlobal("PerformanceObserver", class PerformanceObserver {
|
|
70
|
-
observe = vi.fn();
|
|
71
|
-
disconnect = vi.fn();
|
|
72
|
-
takeRecords = vi.fn(() => []);
|
|
73
|
-
constructor(callback: PerformanceObserverCallback) {
|
|
74
|
-
callbacks.push(callback);
|
|
75
|
-
observes.push(this.observe);
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
vi.spyOn(performance, "now").mockReturnValue(100);
|
|
79
|
-
const { getCwvMetrics, markCwvReveal, startCwvMeasurement, waitForCwvMetrics } = await import("./cwv");
|
|
80
|
-
|
|
47
|
+
it("finalizes the prior route when a new measurement starts and never reuses its values", async () => {
|
|
48
|
+
const { resetCwvMeasurement, startCwvMeasurement, getCwvMetrics, waitForCwvMetrics } = await import("./cwv");
|
|
49
|
+
resetCwvMeasurement();
|
|
81
50
|
startCwvMeasurement();
|
|
82
|
-
|
|
51
|
+
emit([{ startTime: 100, value: 0.2, hadRecentInput: false }]);
|
|
83
52
|
const firstRoute = waitForCwvMetrics();
|
|
84
|
-
callbacks[0]?.({
|
|
85
|
-
getEntries: () => [{ startTime: 150, value: 0.25, hadRecentInput: false }],
|
|
86
|
-
} as unknown as PerformanceObserverEntryList, {} as PerformanceObserver);
|
|
87
|
-
|
|
88
53
|
startCwvMeasurement();
|
|
89
|
-
|
|
90
|
-
|
|
54
|
+
await expect(firstRoute).resolves.toEqual({ cls_total: 0.2, cls_after_reveal: 0 });
|
|
55
|
+
// The fresh route starts from zero — no carry-over of the prior window.
|
|
91
56
|
expect(getCwvMetrics()).toEqual({ cls_total: 0, cls_after_reveal: 0 });
|
|
92
|
-
|
|
93
|
-
expect(
|
|
57
|
+
emit([{ startTime: 200, value: 0.1, hadRecentInput: false }]);
|
|
58
|
+
expect(getCwvMetrics()).toEqual({ cls_total: 0.1, cls_after_reveal: 0 });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("resolves null without constructing observers when measurement is disabled", async () => {
|
|
62
|
+
const { resetCwvMeasurement, startCwvMeasurement, waitForCwvMetrics } = await import("./cwv");
|
|
63
|
+
resetCwvMeasurement();
|
|
64
|
+
startCwvMeasurement(true);
|
|
65
|
+
expect(callbacks).toHaveLength(0);
|
|
66
|
+
await expect(waitForCwvMetrics()).resolves.toBeNull();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("bounds an unrevealed route with the fallback timeout", async () => {
|
|
70
|
+
const { resetCwvMeasurement, startCwvMeasurement, waitForCwvMetrics } = await import("./cwv");
|
|
71
|
+
resetCwvMeasurement();
|
|
72
|
+
startCwvMeasurement();
|
|
73
|
+
emit([{ startTime: 300, value: 0.04, hadRecentInput: false }]);
|
|
74
|
+
const completion = waitForCwvMetrics();
|
|
75
|
+
await vi.advanceTimersByTimeAsync(5_000);
|
|
76
|
+
await expect(completion).resolves.toEqual({ cls_total: 0.04, cls_after_reveal: 0 });
|
|
94
77
|
});
|
|
95
78
|
});
|