@drip-apex/sdk 0.1.0 → 0.3.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 +19283 -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 +19267 -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 +16636 -13533
- 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 +28 -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 +2071 -0
- package/src/consent-evaluation-borlabs.test.ts +41 -0
- package/src/consent-evaluation-cookiescript.test.ts +50 -0
- package/src/consent-evaluation.ts +151 -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,971 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
RECENT_MONDAY_NOON, documentListeners, windowListeners, appendedAntiFlickerStyle, configPayloadHash, flushDynamicImports, installCookieJar, installMutationObserverStub, makeClassList, makeStorage, readCookieValue, readDecodedDripContext, registerInitTestLifecycle, verifiedHandoverResponse,
|
|
4
|
+
} from "./init-test-helpers";
|
|
5
|
+
|
|
6
|
+
describe("init consent", () => {
|
|
7
|
+
registerInitTestLifecycle();
|
|
8
|
+
|
|
9
|
+
it("preserves a persisted identity when CookieScript consent is available from its cookie at init", async () => {
|
|
10
|
+
(globalThis as any).localStorage.setItem("drip_uid", "stored-visitor");
|
|
11
|
+
(globalThis as any).document.cookie = `CookieScriptConsent=${encodeURIComponent(JSON.stringify({
|
|
12
|
+
action: "accept",
|
|
13
|
+
categories: JSON.stringify(["performance"]),
|
|
14
|
+
}))}`;
|
|
15
|
+
const { getDiagnostics, init } = await import("./index");
|
|
16
|
+
const { getUserId } = await import("./hash");
|
|
17
|
+
|
|
18
|
+
init({
|
|
19
|
+
antiFlickerMode: "off",
|
|
20
|
+
consentMode: { enabled: true, provider: "cookiescript", requiredGroup: "analytics" },
|
|
21
|
+
debug: { enabled: true, restrictMode: "always" },
|
|
22
|
+
endpoint: "https://events.test",
|
|
23
|
+
experiments: [],
|
|
24
|
+
shopId: "shop_123",
|
|
25
|
+
trackingStart: "immediate",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
expect(getDiagnostics().trackingBlocked).toBe(false);
|
|
29
|
+
expect(getUserId()).toBe("stored-visitor");
|
|
30
|
+
expect((globalThis as any).localStorage.getItem("drip_uid")).toBe("stored-visitor");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("adopts an existing persisted identity when consent resolves after init", async () => {
|
|
34
|
+
const { init, setConsent } = await import("./index");
|
|
35
|
+
const { getUserId } = await import("./hash");
|
|
36
|
+
init({
|
|
37
|
+
antiFlickerMode: "off",
|
|
38
|
+
consentMode: { enabled: true },
|
|
39
|
+
endpoint: "https://events.test",
|
|
40
|
+
experiments: [],
|
|
41
|
+
shopId: "shop_123",
|
|
42
|
+
trackingStart: "immediate",
|
|
43
|
+
userId: "ephemeral-visitor",
|
|
44
|
+
});
|
|
45
|
+
(globalThis as any).localStorage.setItem("drip_uid", "stored-visitor");
|
|
46
|
+
|
|
47
|
+
setConsent(true);
|
|
48
|
+
|
|
49
|
+
expect(getUserId()).toBe("stored-visitor");
|
|
50
|
+
expect((globalThis as any).localStorage.getItem("drip_uid")).toBe("stored-visitor");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("persists the in-memory identity when first consent finds no stored identity", async () => {
|
|
54
|
+
const { init, setConsent } = await import("./index");
|
|
55
|
+
const { getUserId } = await import("./hash");
|
|
56
|
+
init({
|
|
57
|
+
antiFlickerMode: "off",
|
|
58
|
+
consentMode: { enabled: true },
|
|
59
|
+
endpoint: "https://events.test",
|
|
60
|
+
experiments: [],
|
|
61
|
+
shopId: "shop_123",
|
|
62
|
+
trackingStart: "immediate",
|
|
63
|
+
userId: "ephemeral-visitor",
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
setConsent(true);
|
|
67
|
+
|
|
68
|
+
expect(getUserId()).toBe("ephemeral-visitor");
|
|
69
|
+
expect((globalThis as any).localStorage.getItem("drip_uid")).toBe("ephemeral-visitor");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
it("stops tracking and clears consent storage when consent is revoked", async () => {
|
|
74
|
+
const { flush, init, setConsent, track } = await import("./index");
|
|
75
|
+
|
|
76
|
+
init({
|
|
77
|
+
antiFlickerMode: "off",
|
|
78
|
+
consentMode: { enabled: true },
|
|
79
|
+
endpoint: "https://events.test",
|
|
80
|
+
experiments: [],
|
|
81
|
+
hasConsent: true,
|
|
82
|
+
pageTrigger: { defaultMode: "manual" },
|
|
83
|
+
shopId: "shop_123",
|
|
84
|
+
trackingStart: "immediate",
|
|
85
|
+
});
|
|
86
|
+
(globalThis as any).localStorage.setItem("drip_cfg:shop_123:production", "{}");
|
|
87
|
+
setConsent(false);
|
|
88
|
+
track("after_revoke");
|
|
89
|
+
await flush();
|
|
90
|
+
|
|
91
|
+
expect(globalThis.fetch).not.toHaveBeenCalled();
|
|
92
|
+
expect((globalThis as any).localStorage.getItem("drip_uid")).toBeNull();
|
|
93
|
+
expect((globalThis as any).localStorage.getItem("drip_cfg:shop_123:production")).toBeNull();
|
|
94
|
+
expect((globalThis as any).sessionStorage.getItem("drip_sid")).toBeNull();
|
|
95
|
+
expect((globalThis as any).document.cookie).toContain("drip_uid=; Max-Age=0");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
it("purges replay sample and progress storage after stopping live capture on consent revoke", async () => {
|
|
100
|
+
const emptyReplaySignals = () => ({
|
|
101
|
+
clicks: [],
|
|
102
|
+
nav: [],
|
|
103
|
+
errors: [],
|
|
104
|
+
form_focus: [],
|
|
105
|
+
form_submit: [],
|
|
106
|
+
scroll_max_pct: 0,
|
|
107
|
+
});
|
|
108
|
+
const stopRecorder = vi.fn();
|
|
109
|
+
(globalThis as any).__dripReplayRecorder = {
|
|
110
|
+
version: "2.1.1",
|
|
111
|
+
start: vi.fn(() => stopRecorder),
|
|
112
|
+
compress: vi.fn(async (bytes: Uint8Array) => bytes),
|
|
113
|
+
compressSync: vi.fn((bytes: Uint8Array) => bytes),
|
|
114
|
+
takeSignals: vi.fn(emptyReplaySignals),
|
|
115
|
+
};
|
|
116
|
+
(globalThis as any).sessionStorage.setItem("drip_sid", "session_replay_consent");
|
|
117
|
+
const { init, setConsent } = await import("./index");
|
|
118
|
+
|
|
119
|
+
init({
|
|
120
|
+
antiFlickerMode: "off",
|
|
121
|
+
consentMode: { enabled: true },
|
|
122
|
+
endpoint: "https://events.test",
|
|
123
|
+
experiments: [{
|
|
124
|
+
id: "exp_replay_consent",
|
|
125
|
+
status: "running",
|
|
126
|
+
trafficAllocation: 1,
|
|
127
|
+
replayCapture: { enabled: true, sampleRate: 1 },
|
|
128
|
+
variations: [{ id: "var_replay_consent", weight: 1, mutations: [] }],
|
|
129
|
+
}],
|
|
130
|
+
hasConsent: true,
|
|
131
|
+
replayRecorderUrl: "https://events.test/drip.replay-recorder.js",
|
|
132
|
+
shopId: "shop_123",
|
|
133
|
+
trackingStart: "immediate",
|
|
134
|
+
userId: "visitor_replay_consent",
|
|
135
|
+
});
|
|
136
|
+
await flushDynamicImports();
|
|
137
|
+
|
|
138
|
+
expect((globalThis as any).sessionStorage.getItem("drip_rp:exp_replay_consent:var_replay_consent"))
|
|
139
|
+
.toBe("1");
|
|
140
|
+
expect((globalThis as any).sessionStorage.getItem("drip_replay_progress:session_replay_consent"))
|
|
141
|
+
.not.toBeNull();
|
|
142
|
+
|
|
143
|
+
setConsent(false);
|
|
144
|
+
|
|
145
|
+
expect(stopRecorder).toHaveBeenCalledTimes(1);
|
|
146
|
+
const remainingReplayKeys = Array.from(
|
|
147
|
+
{ length: (globalThis as any).sessionStorage.length },
|
|
148
|
+
(_, index) => (globalThis as any).sessionStorage.key(index),
|
|
149
|
+
).filter((key) => key?.startsWith("drip_rp:") || key?.startsWith("drip_replay_progress:"));
|
|
150
|
+
expect(remainingReplayKeys).toEqual([]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
it("does not restore replay progress when recorder startup resolves after consent revoke", async () => {
|
|
155
|
+
const emptyReplaySignals = () => ({
|
|
156
|
+
clicks: [],
|
|
157
|
+
nav: [],
|
|
158
|
+
errors: [],
|
|
159
|
+
form_focus: [],
|
|
160
|
+
form_submit: [],
|
|
161
|
+
scroll_max_pct: 0,
|
|
162
|
+
});
|
|
163
|
+
const stopRecorder = vi.fn();
|
|
164
|
+
const recorder = {
|
|
165
|
+
version: "2.1.1",
|
|
166
|
+
start: vi.fn(() => stopRecorder),
|
|
167
|
+
compress: vi.fn(async (bytes: Uint8Array) => bytes),
|
|
168
|
+
compressSync: vi.fn((bytes: Uint8Array) => bytes),
|
|
169
|
+
takeSignals: vi.fn(emptyReplaySignals),
|
|
170
|
+
};
|
|
171
|
+
(globalThis as any).sessionStorage.setItem("drip_sid", "session_replay_mid_load");
|
|
172
|
+
const { init, setConsent } = await import("./index");
|
|
173
|
+
|
|
174
|
+
init({
|
|
175
|
+
antiFlickerMode: "off",
|
|
176
|
+
consentMode: { enabled: true },
|
|
177
|
+
endpoint: "https://events.test",
|
|
178
|
+
experiments: [{
|
|
179
|
+
id: "exp_replay_mid_load",
|
|
180
|
+
status: "running",
|
|
181
|
+
trafficAllocation: 1,
|
|
182
|
+
replayCapture: { enabled: true, sampleRate: 1 },
|
|
183
|
+
variations: [{ id: "var_replay_mid_load", weight: 1, mutations: [] }],
|
|
184
|
+
}],
|
|
185
|
+
hasConsent: true,
|
|
186
|
+
replayRecorderUrl: "https://events.test/drip.replay-recorder.js",
|
|
187
|
+
shopId: "shop_123",
|
|
188
|
+
trackingStart: "immediate",
|
|
189
|
+
userId: "visitor_replay_mid_load",
|
|
190
|
+
});
|
|
191
|
+
await vi.dynamicImportSettled();
|
|
192
|
+
|
|
193
|
+
const recorderScript = vi.mocked((globalThis as any).document.head.appendChild).mock.calls
|
|
194
|
+
.map(([element]: [any]) => element)
|
|
195
|
+
.find((element: any) => element.id === "drip-replay-recorder");
|
|
196
|
+
expect(recorderScript).toBeDefined();
|
|
197
|
+
|
|
198
|
+
(globalThis as any).__dripReplayRecorder = recorder;
|
|
199
|
+
recorderScript.onload();
|
|
200
|
+
await Promise.resolve();
|
|
201
|
+
setConsent(false);
|
|
202
|
+
await flushDynamicImports();
|
|
203
|
+
|
|
204
|
+
expect(stopRecorder).toHaveBeenCalledTimes(1);
|
|
205
|
+
expect((globalThis as any).sessionStorage.getItem(
|
|
206
|
+
"drip_replay_progress:session_replay_mid_load",
|
|
207
|
+
)).toBeNull();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
it("clears drip_ctx when consent is revoked", async () => {
|
|
212
|
+
let currentCookie = "";
|
|
213
|
+
const cookieWrites: string[] = [];
|
|
214
|
+
Object.defineProperty((globalThis as any).document, "cookie", {
|
|
215
|
+
configurable: true,
|
|
216
|
+
get: () => currentCookie,
|
|
217
|
+
set: (value: string) => {
|
|
218
|
+
currentCookie = value;
|
|
219
|
+
cookieWrites.push(value);
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
(globalThis as any).sessionStorage.setItem("drip_sid", "session_ctx");
|
|
223
|
+
const { init, setConsent } = await import("./index");
|
|
224
|
+
|
|
225
|
+
init({
|
|
226
|
+
antiFlickerMode: "off",
|
|
227
|
+
consentMode: { enabled: true },
|
|
228
|
+
endpoint: "https://events.test",
|
|
229
|
+
experiments: [
|
|
230
|
+
{
|
|
231
|
+
id: "exp_ctx",
|
|
232
|
+
status: "running",
|
|
233
|
+
trafficAllocation: 1,
|
|
234
|
+
variations: [{ id: "var_ctx", weight: 1, mutations: [] }],
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
hasConsent: true,
|
|
238
|
+
shopId: "shop_123",
|
|
239
|
+
trackingStart: "immediate",
|
|
240
|
+
userId: "visitor_ctx",
|
|
241
|
+
});
|
|
242
|
+
expect(cookieWrites.some((value) => value.startsWith("drip_ctx=") && value.includes("Max-Age=2592000")))
|
|
243
|
+
.toBe(true);
|
|
244
|
+
|
|
245
|
+
setConsent(false);
|
|
246
|
+
|
|
247
|
+
expect(cookieWrites.some((value) => value === "drip_ctx=; Max-Age=0; Path=/; SameSite=Lax"))
|
|
248
|
+
.toBe(true);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
it("separates variation and tracking prerequisite gates", async () => {
|
|
253
|
+
const { getDiagnostics, init } = await import("./index");
|
|
254
|
+
|
|
255
|
+
init({
|
|
256
|
+
antiFlickerMode: "off",
|
|
257
|
+
debug: { enabled: true, restrictMode: "always" },
|
|
258
|
+
endpoint: "https://events.test",
|
|
259
|
+
experiments: [
|
|
260
|
+
{
|
|
261
|
+
id: "exp_track_gate",
|
|
262
|
+
status: "running",
|
|
263
|
+
trafficAllocation: 1,
|
|
264
|
+
variations: [{ id: "var_track_gate", weight: 1, mutations: [] }],
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
projectPrerequisites: {
|
|
268
|
+
startTrackingRule: "return false;",
|
|
269
|
+
},
|
|
270
|
+
shopId: "shop_123",
|
|
271
|
+
trackingStart: "immediate",
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const diagnostics = getDiagnostics();
|
|
275
|
+
expect(diagnostics.assignments).toHaveLength(1);
|
|
276
|
+
expect(diagnostics.trackerStarted).toBe(false);
|
|
277
|
+
expect(diagnostics.trackingPrerequisiteMet).toBe(false);
|
|
278
|
+
expect(diagnostics.activation.project.gates.find((gate) => gate.id === "tracking")).toMatchObject({
|
|
279
|
+
status: "failed",
|
|
280
|
+
reason: "start_tracking_blocked",
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
it("honors Do Not Track without blocking configured rendering", async () => {
|
|
286
|
+
Object.defineProperty(globalThis, "navigator", {
|
|
287
|
+
configurable: true,
|
|
288
|
+
value: { userAgent: "Mozilla/5.0", doNotTrack: "1" },
|
|
289
|
+
});
|
|
290
|
+
(globalThis as any).localStorage.setItem("drip_uid", "stale-visitor");
|
|
291
|
+
(globalThis as any).sessionStorage.setItem("drip_sid", "stale-session");
|
|
292
|
+
const { flush, getDiagnostics, init, track } = await import("./index");
|
|
293
|
+
|
|
294
|
+
init({
|
|
295
|
+
antiFlickerMode: "off",
|
|
296
|
+
dataProtection: { respectDoNotTrack: true, storageMode: "localStorage" },
|
|
297
|
+
debug: { enabled: true, restrictMode: "always" },
|
|
298
|
+
endpoint: "https://events.test",
|
|
299
|
+
experiments: [
|
|
300
|
+
{
|
|
301
|
+
id: "exp_dnt",
|
|
302
|
+
status: "running",
|
|
303
|
+
trafficAllocation: 1,
|
|
304
|
+
variations: [{ id: "var_dnt", weight: 1, mutations: [] }],
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
shopId: "shop_123",
|
|
308
|
+
trackingStart: "immediate",
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
track("dnt_probe");
|
|
312
|
+
await flush();
|
|
313
|
+
|
|
314
|
+
const diagnostics = getDiagnostics();
|
|
315
|
+
expect(diagnostics.assignments).toHaveLength(1);
|
|
316
|
+
expect(diagnostics.trackingDisabledByDnt).toBe(true);
|
|
317
|
+
expect(diagnostics.trackingBlocked).toBe(true);
|
|
318
|
+
expect(diagnostics.trackerStarted).toBe(false);
|
|
319
|
+
expect(globalThis.fetch).not.toHaveBeenCalled();
|
|
320
|
+
expect((globalThis as any).localStorage.getItem("drip_uid")).toBeNull();
|
|
321
|
+
expect((globalThis as any).sessionStorage.getItem("drip_sid")).toBeNull();
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
it("returns no order attribution while tracking is blocked", async () => {
|
|
326
|
+
Object.defineProperty(globalThis, "navigator", {
|
|
327
|
+
configurable: true,
|
|
328
|
+
value: { userAgent: "Mozilla/5.0", doNotTrack: "1" },
|
|
329
|
+
});
|
|
330
|
+
const { getRuntimeValue, init } = await import("./index");
|
|
331
|
+
|
|
332
|
+
init({
|
|
333
|
+
antiFlickerMode: "off",
|
|
334
|
+
dataProtection: { respectDoNotTrack: true, storageMode: "localStorage" },
|
|
335
|
+
endpoint: "https://events.test",
|
|
336
|
+
experiments: [
|
|
337
|
+
{
|
|
338
|
+
id: "exp_dnt_attr",
|
|
339
|
+
status: "running",
|
|
340
|
+
trafficAllocation: 1,
|
|
341
|
+
variations: [{ id: "var_dnt_attr", weight: 1, mutations: [] }],
|
|
342
|
+
},
|
|
343
|
+
],
|
|
344
|
+
shopId: "shop_123",
|
|
345
|
+
trackingStart: "immediate",
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
expect(getRuntimeValue("orderAttribution")).toEqual({
|
|
349
|
+
visitorId: "",
|
|
350
|
+
assignments: [],
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
it("redacts consent diagnostics unless SDK debug mode is enabled", async () => {
|
|
356
|
+
const { getDiagnostics, init } = await import("./index");
|
|
357
|
+
|
|
358
|
+
init({
|
|
359
|
+
antiFlickerMode: "off",
|
|
360
|
+
consentMode: { enabled: true },
|
|
361
|
+
endpoint: "https://events.test",
|
|
362
|
+
experiments: [],
|
|
363
|
+
hasConsent: false,
|
|
364
|
+
shopId: "shop_123",
|
|
365
|
+
trackingStart: "immediate",
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const diagnostics = getDiagnostics();
|
|
369
|
+
|
|
370
|
+
expect(diagnostics.mode).toBe("public");
|
|
371
|
+
expect("consentRequired" in diagnostics).toBe(false);
|
|
372
|
+
expect("consentGranted" in diagnostics).toBe(false);
|
|
373
|
+
expect("consentModeEnabled" in diagnostics).toBe(false);
|
|
374
|
+
expect("trackingBlocked" in diagnostics).toBe(false);
|
|
375
|
+
expect("trackingPrerequisiteMet" in diagnostics).toBe(false);
|
|
376
|
+
expect("trackingDisabledByDnt" in diagnostics).toBe(false);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
it("reactivates consent and Shopify assignment sync from OneTrust-style changes", async () => {
|
|
381
|
+
const shopifyPublish = vi.fn();
|
|
382
|
+
(globalThis as any).window.Shopify = { analytics: { publish: shopifyPublish } };
|
|
383
|
+
(globalThis as any).Shopify = (globalThis as any).window.Shopify;
|
|
384
|
+
(globalThis as any).window.OnetrustActiveGroups = ",C0001,";
|
|
385
|
+
(globalThis as any).OnetrustActiveGroups = ",C0001,";
|
|
386
|
+
|
|
387
|
+
const { flush, getDiagnostics, init, track } = await import("./index");
|
|
388
|
+
|
|
389
|
+
init({
|
|
390
|
+
antiFlickerMode: "off",
|
|
391
|
+
consentMode: { enabled: true, provider: "onetrust", requiredGroup: "C0004" },
|
|
392
|
+
debug: { enabled: true, restrictMode: "always" },
|
|
393
|
+
endpoint: "https://events.test",
|
|
394
|
+
experiments: [
|
|
395
|
+
{
|
|
396
|
+
id: "exp_consent_provider",
|
|
397
|
+
status: "running",
|
|
398
|
+
trafficAllocation: 1,
|
|
399
|
+
variations: [{ id: "var_consent_provider", weight: 1, mutations: [] }],
|
|
400
|
+
},
|
|
401
|
+
],
|
|
402
|
+
shopId: "shop_123",
|
|
403
|
+
trackingStart: "immediate",
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
expect(getDiagnostics().trackingBlocked).toBe(true);
|
|
407
|
+
expect(shopifyPublish).not.toHaveBeenCalled();
|
|
408
|
+
|
|
409
|
+
(globalThis as any).window.OnetrustActiveGroups = ",C0001,C0004,";
|
|
410
|
+
(globalThis as any).OnetrustActiveGroups = ",C0001,C0004,";
|
|
411
|
+
windowListeners.OptanonConsentChanged?.forEach((handler) => handler());
|
|
412
|
+
track("after_onetrust_grant");
|
|
413
|
+
await flush();
|
|
414
|
+
|
|
415
|
+
expect(getDiagnostics().trackingBlocked).toBe(false);
|
|
416
|
+
expect(globalThis.fetch).toHaveBeenCalled();
|
|
417
|
+
expect(shopifyPublish).toHaveBeenCalledWith("drip_assignment_sync", expect.any(Object));
|
|
418
|
+
await vi.waitFor(() => {
|
|
419
|
+
expect(getDiagnostics().spa.lastShopifyAssignmentSync).toMatchObject({ status: "sent" });
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
it("reactivates consent from Usercentrics service changes", async () => {
|
|
425
|
+
let apexConsent = false;
|
|
426
|
+
const services = () => [
|
|
427
|
+
{ name: "Essential", consent: { status: true } },
|
|
428
|
+
{ name: "Apex", consent: { status: apexConsent } },
|
|
429
|
+
];
|
|
430
|
+
(globalThis as any).window.UC_UI = { getServicesBaseInfo: services };
|
|
431
|
+
(globalThis as any).UC_UI = (globalThis as any).window.UC_UI;
|
|
432
|
+
|
|
433
|
+
const { flush, getDiagnostics, init, track } = await import("./index");
|
|
434
|
+
|
|
435
|
+
init({
|
|
436
|
+
antiFlickerMode: "off",
|
|
437
|
+
consentMode: { enabled: true, provider: "usercentrics", requiredGroup: "Apex" },
|
|
438
|
+
debug: { enabled: true, restrictMode: "always" },
|
|
439
|
+
endpoint: "https://events.test",
|
|
440
|
+
experiments: [
|
|
441
|
+
{
|
|
442
|
+
id: "exp_usercentrics",
|
|
443
|
+
status: "running",
|
|
444
|
+
trafficAllocation: 1,
|
|
445
|
+
variations: [{ id: "var_usercentrics", weight: 1, mutations: [] }],
|
|
446
|
+
},
|
|
447
|
+
],
|
|
448
|
+
shopId: "shop_123",
|
|
449
|
+
trackingStart: "immediate",
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
expect(getDiagnostics().trackingBlocked).toBe(true);
|
|
453
|
+
|
|
454
|
+
apexConsent = true;
|
|
455
|
+
windowListeners.UC_UI_CMP_EVENT?.forEach((handler) => handler());
|
|
456
|
+
track("after_usercentrics_grant");
|
|
457
|
+
await flush();
|
|
458
|
+
|
|
459
|
+
expect(getDiagnostics().trackingBlocked).toBe(false);
|
|
460
|
+
expect(globalThis.fetch).toHaveBeenCalled();
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
it("reactivates consent from Cookiebot category changes", async () => {
|
|
465
|
+
(globalThis as any).window.Cookiebot = {
|
|
466
|
+
consent: {
|
|
467
|
+
necessary: true,
|
|
468
|
+
preferences: false,
|
|
469
|
+
statistics: false,
|
|
470
|
+
marketing: false,
|
|
471
|
+
},
|
|
472
|
+
};
|
|
473
|
+
(globalThis as any).Cookiebot = (globalThis as any).window.Cookiebot;
|
|
474
|
+
|
|
475
|
+
const { flush, getDiagnostics, init, track } = await import("./index");
|
|
476
|
+
|
|
477
|
+
init({
|
|
478
|
+
antiFlickerMode: "off",
|
|
479
|
+
consentMode: { enabled: true, provider: "cookiebot", requiredGroup: "statistics" },
|
|
480
|
+
debug: { enabled: true, restrictMode: "always" },
|
|
481
|
+
endpoint: "https://events.test",
|
|
482
|
+
experiments: [
|
|
483
|
+
{
|
|
484
|
+
id: "exp_cookiebot",
|
|
485
|
+
status: "running",
|
|
486
|
+
trafficAllocation: 1,
|
|
487
|
+
variations: [{ id: "var_cookiebot", weight: 1, mutations: [] }],
|
|
488
|
+
},
|
|
489
|
+
],
|
|
490
|
+
shopId: "shop_123",
|
|
491
|
+
trackingStart: "immediate",
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
expect(getDiagnostics().trackingBlocked).toBe(true);
|
|
495
|
+
|
|
496
|
+
(globalThis as any).window.Cookiebot.consent.statistics = true;
|
|
497
|
+
windowListeners.CookiebotOnAccept?.forEach((handler) => handler());
|
|
498
|
+
track("after_cookiebot_grant");
|
|
499
|
+
await flush();
|
|
500
|
+
|
|
501
|
+
expect(getDiagnostics().trackingBlocked).toBe(false);
|
|
502
|
+
expect(globalThis.fetch).toHaveBeenCalled();
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
it("grants configured consent from accepted CCM19 embeddings", async () => {
|
|
507
|
+
(globalThis as any).window.CCM = {
|
|
508
|
+
acceptedEmbeddings: [{ id: "x", name: "Apex" }],
|
|
509
|
+
};
|
|
510
|
+
(globalThis as any).CCM = (globalThis as any).window.CCM;
|
|
511
|
+
|
|
512
|
+
const { flush, getDiagnostics, init, track } = await import("./index");
|
|
513
|
+
|
|
514
|
+
init({
|
|
515
|
+
antiFlickerMode: "off",
|
|
516
|
+
consentMode: { enabled: true, provider: "ccm19", requiredGroup: "apex" },
|
|
517
|
+
debug: { enabled: true, restrictMode: "always" },
|
|
518
|
+
endpoint: "https://events.test",
|
|
519
|
+
experiments: [
|
|
520
|
+
{
|
|
521
|
+
id: "exp_ccm19",
|
|
522
|
+
status: "running",
|
|
523
|
+
trafficAllocation: 1,
|
|
524
|
+
variations: [{ id: "var_ccm19", weight: 1, mutations: [] }],
|
|
525
|
+
},
|
|
526
|
+
],
|
|
527
|
+
shopId: "shop_123",
|
|
528
|
+
trackingStart: "immediate",
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
expect(getDiagnostics().trackingBlocked).toBe(false);
|
|
532
|
+
track("after_ccm19_grant");
|
|
533
|
+
await flush();
|
|
534
|
+
expect(globalThis.fetch).toHaveBeenCalled();
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
it("falls back to the stored consent cookie when CCM19 embeddings are unavailable", async () => {
|
|
539
|
+
(globalThis as any).document.cookie = "drip_consent=1";
|
|
540
|
+
(globalThis as any).window.CCM = {};
|
|
541
|
+
(globalThis as any).CCM = (globalThis as any).window.CCM;
|
|
542
|
+
|
|
543
|
+
const { getDiagnostics, init } = await import("./index");
|
|
544
|
+
|
|
545
|
+
init({
|
|
546
|
+
antiFlickerMode: "off",
|
|
547
|
+
consentMode: { enabled: true, provider: "ccm19", requiredGroup: "Apex" },
|
|
548
|
+
debug: { enabled: true, restrictMode: "always" },
|
|
549
|
+
endpoint: "https://events.test",
|
|
550
|
+
experiments: [],
|
|
551
|
+
shopId: "shop_123",
|
|
552
|
+
trackingStart: "immediate",
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
expect(getDiagnostics().trackingBlocked).toBe(false);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
it("reactivates consent from CCM19 embedding changes", async () => {
|
|
560
|
+
(globalThis as any).window.CCM = {
|
|
561
|
+
acceptedEmbeddings: [],
|
|
562
|
+
};
|
|
563
|
+
(globalThis as any).CCM = (globalThis as any).window.CCM;
|
|
564
|
+
|
|
565
|
+
const { flush, getDiagnostics, init, track } = await import("./index");
|
|
566
|
+
|
|
567
|
+
init({
|
|
568
|
+
antiFlickerMode: "off",
|
|
569
|
+
consentMode: { enabled: true, provider: "ccm19", requiredGroup: "apex-embedding" },
|
|
570
|
+
debug: { enabled: true, restrictMode: "always" },
|
|
571
|
+
endpoint: "https://events.test",
|
|
572
|
+
experiments: [
|
|
573
|
+
{
|
|
574
|
+
id: "exp_ccm19_event",
|
|
575
|
+
status: "running",
|
|
576
|
+
trafficAllocation: 1,
|
|
577
|
+
variations: [{ id: "var_ccm19_event", weight: 1, mutations: [] }],
|
|
578
|
+
},
|
|
579
|
+
],
|
|
580
|
+
shopId: "shop_123",
|
|
581
|
+
trackingStart: "immediate",
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
expect(getDiagnostics().trackingBlocked).toBe(true);
|
|
585
|
+
|
|
586
|
+
(globalThis as any).window.CCM.acceptedEmbeddings = [{ id: "apex-embedding", name: "Apex Embed" }];
|
|
587
|
+
windowListeners.ccm19EmbeddingAccepted?.forEach((handler) => handler());
|
|
588
|
+
track("after_ccm19_event_grant");
|
|
589
|
+
await flush();
|
|
590
|
+
|
|
591
|
+
expect(getDiagnostics().trackingBlocked).toBe(false);
|
|
592
|
+
expect(globalThis.fetch).toHaveBeenCalled();
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
it("renders variants without storage or tracking until Pandectes grants analytics consent", async () => {
|
|
597
|
+
let granted = false;
|
|
598
|
+
(globalThis as any).window.Pandectes = { analyticsProcessingAllowed: () => granted };
|
|
599
|
+
(globalThis as any).Pandectes = (globalThis as any).window.Pandectes;
|
|
600
|
+
const { flush, getAssignments, getDiagnostics, init, track } = await import("./index");
|
|
601
|
+
|
|
602
|
+
init({
|
|
603
|
+
antiFlickerMode: "off",
|
|
604
|
+
consentMode: { enabled: true, provider: "pandectes" },
|
|
605
|
+
debug: { enabled: true, restrictMode: "always" },
|
|
606
|
+
endpoint: "https://events.test",
|
|
607
|
+
experiments: [{ id: "exp_pandectes", status: "running", trafficAllocation: 1, variations: [{ id: "var_pandectes", weight: 1, mutations: [] }] }],
|
|
608
|
+
shopId: "shop_123",
|
|
609
|
+
trackingStart: "immediate",
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
expect(getAssignments()).toHaveLength(1);
|
|
613
|
+
expect((globalThis as any).localStorage.length).toBe(0);
|
|
614
|
+
track("denied");
|
|
615
|
+
await flush();
|
|
616
|
+
expect(globalThis.fetch).not.toHaveBeenCalled();
|
|
617
|
+
|
|
618
|
+
granted = true;
|
|
619
|
+
windowListeners.PandectesInitialized?.forEach((handler) => handler());
|
|
620
|
+
track("granted");
|
|
621
|
+
await flush();
|
|
622
|
+
expect(getDiagnostics().trackingBlocked).toBe(false);
|
|
623
|
+
expect((globalThis as any).localStorage.length).toBeGreaterThan(0);
|
|
624
|
+
expect(globalThis.fetch).toHaveBeenCalled();
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
it("reactivates Pandectes consent from document visitorConsentCollected", async () => {
|
|
629
|
+
let granted = false;
|
|
630
|
+
(globalThis as any).window.Pandectes = { analyticsProcessingAllowed: () => granted };
|
|
631
|
+
const { getDiagnostics, init } = await import("./index");
|
|
632
|
+
init({ antiFlickerMode: "off", consentMode: { enabled: true, provider: "pandectes" }, debug: { enabled: true, restrictMode: "always" }, endpoint: "https://events.test", experiments: [], shopId: "shop_123", trackingStart: "immediate" });
|
|
633
|
+
expect(getDiagnostics().trackingBlocked).toBe(true);
|
|
634
|
+
granted = true;
|
|
635
|
+
documentListeners.visitorConsentCollected?.forEach((handler) => handler());
|
|
636
|
+
expect(getDiagnostics().trackingBlocked).toBe(false);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
it("falls back to Shopify Customer Privacy for Pandectes consent", async () => {
|
|
641
|
+
(globalThis as any).window.Shopify = { customerPrivacy: { analyticsProcessingAllowed: () => true } };
|
|
642
|
+
const { getDiagnostics, init } = await import("./index");
|
|
643
|
+
init({ antiFlickerMode: "off", consentMode: { enabled: true, provider: "pandectes" }, debug: { enabled: true, restrictMode: "always" }, endpoint: "https://events.test", experiments: [], shopId: "shop_123", trackingStart: "immediate" });
|
|
644
|
+
expect(getDiagnostics().trackingBlocked).toBe(false);
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
it("routes Pandectes marketing consent to marketingAllowed", async () => {
|
|
649
|
+
const analyticsProcessingAllowed = vi.fn(() => false);
|
|
650
|
+
const marketingAllowed = vi.fn(() => true);
|
|
651
|
+
(globalThis as any).window.Pandectes = { analyticsProcessingAllowed, marketingAllowed };
|
|
652
|
+
const { getDiagnostics, init } = await import("./index");
|
|
653
|
+
init({ antiFlickerMode: "off", consentMode: { enabled: true, provider: "pandectes", requiredGroup: " marketing " }, debug: { enabled: true, restrictMode: "always" }, endpoint: "https://events.test", experiments: [], shopId: "shop_123", trackingStart: "immediate" });
|
|
654
|
+
expect(getDiagnostics().trackingBlocked).toBe(false);
|
|
655
|
+
expect(marketingAllowed).toHaveBeenCalled();
|
|
656
|
+
expect(analyticsProcessingAllowed).not.toHaveBeenCalled();
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
it.each([
|
|
661
|
+
["analytics", "analyticsProcessingAllowed"],
|
|
662
|
+
["statistics", "analyticsProcessingAllowed"],
|
|
663
|
+
["statistic", "analyticsProcessingAllowed"],
|
|
664
|
+
["stats", "analyticsProcessingAllowed"],
|
|
665
|
+
["marketing", "marketingAllowed"],
|
|
666
|
+
["preferences", "preferencesProcessingAllowed"],
|
|
667
|
+
["preference", "preferencesProcessingAllowed"],
|
|
668
|
+
["sale_of_data", "saleOfDataAllowed"],
|
|
669
|
+
["saleofdata", "saleOfDataAllowed"],
|
|
670
|
+
["sale-of-data", "saleOfDataAllowed"],
|
|
671
|
+
["sale", "saleOfDataAllowed"],
|
|
672
|
+
])("maps Pandectes purpose %s to %s", async (requiredGroup, expectedMethod) => {
|
|
673
|
+
const methods = {
|
|
674
|
+
analyticsProcessingAllowed: vi.fn(() => true),
|
|
675
|
+
marketingAllowed: vi.fn(() => true),
|
|
676
|
+
preferencesProcessingAllowed: vi.fn(() => true),
|
|
677
|
+
saleOfDataAllowed: vi.fn(() => true),
|
|
678
|
+
};
|
|
679
|
+
(globalThis as any).window.Pandectes = methods;
|
|
680
|
+
const { getDiagnostics, init } = await import("./index");
|
|
681
|
+
init({ antiFlickerMode: "off", consentMode: { enabled: true, provider: "pandectes", requiredGroup }, debug: { enabled: true, restrictMode: "always" }, endpoint: "https://events.test", experiments: [], shopId: "shop_123", trackingStart: "immediate" });
|
|
682
|
+
|
|
683
|
+
expect(getDiagnostics().trackingBlocked).toBe(false);
|
|
684
|
+
expect(methods[expectedMethod as keyof typeof methods]).toHaveBeenCalled();
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
it("denies unknown Pandectes purposes without falling back to analytics", async () => {
|
|
689
|
+
const analyticsProcessingAllowed = vi.fn(() => true);
|
|
690
|
+
(globalThis as any).window.Pandectes = { analyticsProcessingAllowed };
|
|
691
|
+
const { getDiagnostics, init } = await import("./index");
|
|
692
|
+
init({ antiFlickerMode: "off", consentMode: { enabled: true, provider: "pandectes", requiredGroup: "markting" }, debug: { enabled: true, restrictMode: "always" }, endpoint: "https://events.test", experiments: [], shopId: "shop_123", trackingStart: "immediate" });
|
|
693
|
+
|
|
694
|
+
expect(getDiagnostics().trackingBlocked).toBe(true);
|
|
695
|
+
expect(analyticsProcessingAllowed).not.toHaveBeenCalled();
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
it("keeps polling when Pandectes is absent or throws and grants later without an event", async () => {
|
|
700
|
+
(globalThis as any).window.Pandectes = { analyticsProcessingAllowed: () => { throw new Error("not ready"); } };
|
|
701
|
+
const { getDiagnostics, init } = await import("./index");
|
|
702
|
+
init({ antiFlickerMode: "off", consentMode: { enabled: true, provider: "pandectes" }, debug: { enabled: true, restrictMode: "always" }, endpoint: "https://events.test", experiments: [], shopId: "shop_123", trackingStart: "immediate" });
|
|
703
|
+
expect(getDiagnostics().trackingBlocked).toBe(true);
|
|
704
|
+
delete (globalThis as any).window.Pandectes;
|
|
705
|
+
await vi.advanceTimersByTimeAsync(1_000);
|
|
706
|
+
expect(getDiagnostics().trackingBlocked).toBe(true);
|
|
707
|
+
(globalThis as any).window.Pandectes = { analyticsProcessingAllowed: () => true };
|
|
708
|
+
await vi.advanceTimersByTimeAsync(1_000);
|
|
709
|
+
expect(getDiagnostics().trackingBlocked).toBe(false);
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
it("renders variants without storage or tracking until CookieScript grants performance consent", async () => {
|
|
714
|
+
(globalThis as any).document.cookie = "drip_consent=1";
|
|
715
|
+
const currentState = vi.fn(() => ({ action: "accept", categories: ["strict"] }));
|
|
716
|
+
(globalThis as any).window.CookieScript = {
|
|
717
|
+
instance: {
|
|
718
|
+
currentState,
|
|
719
|
+
},
|
|
720
|
+
};
|
|
721
|
+
const { flush, getAssignments, getDiagnostics, init, track } = await import("./index");
|
|
722
|
+
|
|
723
|
+
init({
|
|
724
|
+
antiFlickerMode: "off",
|
|
725
|
+
consentMode: { enabled: true, provider: "cookiescript" },
|
|
726
|
+
debug: { enabled: true, restrictMode: "always" },
|
|
727
|
+
endpoint: "https://events.test",
|
|
728
|
+
experiments: [{
|
|
729
|
+
id: "exp_cookiescript",
|
|
730
|
+
status: "running",
|
|
731
|
+
trafficAllocation: 1,
|
|
732
|
+
variations: [{ id: "var_cookiescript", weight: 1, mutations: [] }],
|
|
733
|
+
}],
|
|
734
|
+
shopId: "shop_123",
|
|
735
|
+
trackingStart: "immediate",
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
expect(getAssignments()).toHaveLength(1);
|
|
739
|
+
expect(currentState).toHaveBeenCalled();
|
|
740
|
+
expect(getDiagnostics().trackingBlocked).toBe(true);
|
|
741
|
+
expect((globalThis as any).localStorage.length).toBe(0);
|
|
742
|
+
track("cookiescript_performance_denied");
|
|
743
|
+
await flush();
|
|
744
|
+
expect(globalThis.fetch).not.toHaveBeenCalled();
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
it("starts storage and tracking when CookieScriptAccept grants performance consent", async () => {
|
|
749
|
+
let currentState = { action: "accept", categories: ["strict"] };
|
|
750
|
+
(globalThis as any).window.CookieScript = {
|
|
751
|
+
instance: {
|
|
752
|
+
currentState: () => currentState,
|
|
753
|
+
},
|
|
754
|
+
};
|
|
755
|
+
const { flush, getDiagnostics, init, track } = await import("./index");
|
|
756
|
+
|
|
757
|
+
init({
|
|
758
|
+
antiFlickerMode: "off",
|
|
759
|
+
consentMode: { enabled: true, provider: "cookiescript" },
|
|
760
|
+
debug: { enabled: true, restrictMode: "always" },
|
|
761
|
+
endpoint: "https://events.test",
|
|
762
|
+
experiments: [{
|
|
763
|
+
id: "exp_cookiescript_event",
|
|
764
|
+
status: "running",
|
|
765
|
+
trafficAllocation: 1,
|
|
766
|
+
variations: [{ id: "var_cookiescript_event", weight: 1, mutations: [] }],
|
|
767
|
+
}],
|
|
768
|
+
shopId: "shop_123",
|
|
769
|
+
trackingStart: "immediate",
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
expect(getDiagnostics().trackingBlocked).toBe(true);
|
|
773
|
+
currentState = { action: "accept", categories: ["strict", "performance"] };
|
|
774
|
+
windowListeners.CookieScriptAccept?.forEach((handler) => handler());
|
|
775
|
+
track("cookiescript_performance_granted");
|
|
776
|
+
await flush();
|
|
777
|
+
|
|
778
|
+
expect(getDiagnostics().trackingBlocked).toBe(false);
|
|
779
|
+
expect((globalThis as any).localStorage.length).toBeGreaterThan(0);
|
|
780
|
+
expect(globalThis.fetch).toHaveBeenCalled();
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
it("reactivates rejected CookieScript consent from CookieScriptAcceptAll", async () => {
|
|
785
|
+
let currentState: { action: string; categories?: string[] } = { action: "reject" };
|
|
786
|
+
(globalThis as any).window.CookieScript = {
|
|
787
|
+
instance: {
|
|
788
|
+
currentState: () => currentState,
|
|
789
|
+
},
|
|
790
|
+
};
|
|
791
|
+
const { getDiagnostics, init } = await import("./index");
|
|
792
|
+
|
|
793
|
+
init({
|
|
794
|
+
antiFlickerMode: "off",
|
|
795
|
+
consentMode: { enabled: true, provider: "cookiescript" },
|
|
796
|
+
debug: { enabled: true, restrictMode: "always" },
|
|
797
|
+
endpoint: "https://events.test",
|
|
798
|
+
experiments: [],
|
|
799
|
+
shopId: "shop_123",
|
|
800
|
+
trackingStart: "immediate",
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
expect(getDiagnostics().trackingBlocked).toBe(true);
|
|
804
|
+
currentState = { action: "accept", categories: ["performance"] };
|
|
805
|
+
windowListeners.CookieScriptAcceptAll?.forEach((handler) => handler());
|
|
806
|
+
expect(getDiagnostics().trackingBlocked).toBe(false);
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
it("ignores the cached consent cookie when CookieScript is absent and grants on a later poll", async () => {
|
|
811
|
+
(globalThis as any).document.cookie = "drip_consent=1";
|
|
812
|
+
const { getDiagnostics, init } = await import("./index");
|
|
813
|
+
|
|
814
|
+
init({
|
|
815
|
+
antiFlickerMode: "off",
|
|
816
|
+
consentMode: { enabled: true, provider: "cookiescript" },
|
|
817
|
+
debug: { enabled: true, restrictMode: "always" },
|
|
818
|
+
endpoint: "https://events.test",
|
|
819
|
+
experiments: [],
|
|
820
|
+
shopId: "shop_123",
|
|
821
|
+
trackingStart: "immediate",
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
expect(getDiagnostics().trackingBlocked).toBe(true);
|
|
825
|
+
(globalThis as any).window.CookieScript = {
|
|
826
|
+
instance: {
|
|
827
|
+
currentState: () => ({
|
|
828
|
+
action: "accept",
|
|
829
|
+
categories: ["performance"],
|
|
830
|
+
}),
|
|
831
|
+
},
|
|
832
|
+
};
|
|
833
|
+
await vi.advanceTimersByTimeAsync(1_000);
|
|
834
|
+
expect(getDiagnostics().trackingBlocked).toBe(false);
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
it("ignores the cached consent cookie when CookieScript throws and grants on a later poll", async () => {
|
|
839
|
+
(globalThis as any).document.cookie = "drip_consent=1";
|
|
840
|
+
(globalThis as any).window.CookieScript = {
|
|
841
|
+
instance: {
|
|
842
|
+
currentState: () => { throw new Error("not ready"); },
|
|
843
|
+
},
|
|
844
|
+
};
|
|
845
|
+
const { getDiagnostics, init } = await import("./index");
|
|
846
|
+
|
|
847
|
+
init({
|
|
848
|
+
antiFlickerMode: "off",
|
|
849
|
+
consentMode: { enabled: true, provider: "cookiescript" },
|
|
850
|
+
debug: { enabled: true, restrictMode: "always" },
|
|
851
|
+
endpoint: "https://events.test",
|
|
852
|
+
experiments: [],
|
|
853
|
+
shopId: "shop_123",
|
|
854
|
+
trackingStart: "immediate",
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
expect(getDiagnostics().trackingBlocked).toBe(true);
|
|
858
|
+
|
|
859
|
+
(globalThis as any).window.CookieScript.instance.currentState = () => ({
|
|
860
|
+
action: "accept",
|
|
861
|
+
categories: ["performance"],
|
|
862
|
+
});
|
|
863
|
+
await vi.advanceTimersByTimeAsync(1_000);
|
|
864
|
+
expect(getDiagnostics().trackingBlocked).toBe(false);
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
it("routes CookieScript required groups to aliased and custom categories", async () => {
|
|
869
|
+
const trackingBlockedFor = async (requiredGroup: string, categories: string[]) => {
|
|
870
|
+
vi.clearAllTimers();
|
|
871
|
+
vi.resetModules();
|
|
872
|
+
delete (globalThis as any).__drip;
|
|
873
|
+
(globalThis as any).window.CookieScript = {
|
|
874
|
+
instance: {
|
|
875
|
+
currentState: () => ({ action: "accept", categories }),
|
|
876
|
+
},
|
|
877
|
+
};
|
|
878
|
+
const { getDiagnostics, init } = await import("./index");
|
|
879
|
+
|
|
880
|
+
init({
|
|
881
|
+
antiFlickerMode: "off",
|
|
882
|
+
consentMode: { enabled: true, provider: "cookiescript", requiredGroup },
|
|
883
|
+
debug: { enabled: true, restrictMode: "always" },
|
|
884
|
+
endpoint: "https://events.test",
|
|
885
|
+
experiments: [],
|
|
886
|
+
shopId: "shop_123",
|
|
887
|
+
trackingStart: "immediate",
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
return getDiagnostics().trackingBlocked;
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
expect(await trackingBlockedFor("marketing", ["targeting"])).toBe(false);
|
|
894
|
+
expect(await trackingBlockedFor("marketing", ["performance"])).toBe(true);
|
|
895
|
+
expect(await trackingBlockedFor("myCustomCat", ["mycustomcat"])).toBe(false);
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
it("keeps unanswered CookieScript consent blocked and polls for a later grant", async () => {
|
|
900
|
+
let currentState: { action?: string; categories?: string[] } = { categories: [] };
|
|
901
|
+
const currentStateReader = vi.fn(() => currentState);
|
|
902
|
+
(globalThis as any).window.CookieScript = {
|
|
903
|
+
instance: {
|
|
904
|
+
currentState: currentStateReader,
|
|
905
|
+
},
|
|
906
|
+
};
|
|
907
|
+
const { getDiagnostics, init } = await import("./index");
|
|
908
|
+
|
|
909
|
+
init({
|
|
910
|
+
antiFlickerMode: "off",
|
|
911
|
+
consentMode: { enabled: true, provider: "cookiescript" },
|
|
912
|
+
debug: { enabled: true, restrictMode: "always" },
|
|
913
|
+
endpoint: "https://events.test",
|
|
914
|
+
experiments: [],
|
|
915
|
+
shopId: "shop_123",
|
|
916
|
+
trackingStart: "immediate",
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
expect(currentStateReader).toHaveBeenCalled();
|
|
920
|
+
expect(getDiagnostics().trackingBlocked).toBe(true);
|
|
921
|
+
|
|
922
|
+
const callsBeforePoll = currentStateReader.mock.calls.length;
|
|
923
|
+
currentState = { action: "accept", categories: ["performance"] };
|
|
924
|
+
await vi.advanceTimersByTimeAsync(1_000);
|
|
925
|
+
|
|
926
|
+
expect(currentStateReader.mock.calls.length).toBeGreaterThan(callsBeforePoll);
|
|
927
|
+
expect(getDiagnostics().trackingBlocked).toBe(false);
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
it("polls custom consent getters so tracking starts after consent without navigation", async () => {
|
|
932
|
+
const { flush, getDiagnostics, init } = await import("./index");
|
|
933
|
+
|
|
934
|
+
init({
|
|
935
|
+
antiFlickerMode: "off",
|
|
936
|
+
consentMode: {
|
|
937
|
+
enabled: true,
|
|
938
|
+
provider: "custom",
|
|
939
|
+
getter: "dr_cookiebanner_options.dr_acceptedCategories",
|
|
940
|
+
requiredGroup: "statistics",
|
|
941
|
+
},
|
|
942
|
+
debug: { enabled: true, restrictMode: "always" },
|
|
943
|
+
endpoint: "https://events.test",
|
|
944
|
+
experiments: [
|
|
945
|
+
{
|
|
946
|
+
id: "exp_custom_poll",
|
|
947
|
+
status: "running",
|
|
948
|
+
trafficAllocation: 1,
|
|
949
|
+
variations: [{ id: "var_custom_poll", weight: 1, mutations: [] }],
|
|
950
|
+
},
|
|
951
|
+
],
|
|
952
|
+
shopId: "shop_123",
|
|
953
|
+
trackingStart: "immediate",
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
expect(getDiagnostics().trackingBlocked).toBe(true);
|
|
957
|
+
expect(globalThis.fetch).not.toHaveBeenCalled();
|
|
958
|
+
|
|
959
|
+
(globalThis as any).dr_cookiebanner_options = { dr_acceptedCategories: ["essential", "statistics"] };
|
|
960
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
961
|
+
await flush();
|
|
962
|
+
|
|
963
|
+
expect(getDiagnostics().trackingBlocked).toBe(false);
|
|
964
|
+
const eventsCall = vi.mocked(globalThis.fetch).mock.calls.find((call) => call[0] === "https://events.test/v1/events");
|
|
965
|
+
expect(eventsCall).toBeDefined();
|
|
966
|
+
const body = JSON.parse(String(eventsCall?.[1]?.body));
|
|
967
|
+
expect(body.events.map((event: { type: string }) => event.type)).toEqual(
|
|
968
|
+
expect.arrayContaining(["experiment_viewed", "pageview"]),
|
|
969
|
+
);
|
|
970
|
+
});
|
|
971
|
+
});
|