@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.
Files changed (245) hide show
  1. package/README.md +5 -0
  2. package/dist/bundle-report.json +584 -226
  3. package/dist/define.cjs +123 -0
  4. package/dist/define.cjs.map +7 -0
  5. package/dist/define.mjs +102 -0
  6. package/dist/define.mjs.map +7 -0
  7. package/dist/drip.cjs +19262 -0
  8. package/dist/drip.cjs.map +7 -0
  9. package/dist/drip.debug.js +1 -1
  10. package/dist/drip.debug.js.map +1 -1
  11. package/dist/drip.heatmap.js +1 -1
  12. package/dist/drip.heatmap.js.map +1 -1
  13. package/dist/drip.js +1 -1
  14. package/dist/drip.js.map +1 -1
  15. package/dist/drip.live.js +1 -1
  16. package/dist/drip.live.js.map +1 -1
  17. package/dist/drip.mjs +19246 -0
  18. package/dist/drip.mjs.map +7 -0
  19. package/dist/drip.replay-recorder.js +2 -0
  20. package/dist/drip.replay-recorder.js.map +1 -0
  21. package/dist/editor.js +147 -142
  22. package/dist/editor.js.map +4 -4
  23. package/dist/flags.cjs +1788 -0
  24. package/dist/flags.cjs.map +7 -0
  25. package/dist/flags.mjs +1765 -0
  26. package/dist/flags.mjs.map +7 -0
  27. package/dist/index.mjs +12 -0
  28. package/dist/index.mjs.map +2 -2
  29. package/dist/metafiles/define.cjs.meta.json +28 -0
  30. package/dist/metafiles/define.mjs.meta.json +32 -0
  31. package/dist/metafiles/drip.cjs.meta.json +3193 -0
  32. package/dist/metafiles/drip.debug.meta.json +2859 -574
  33. package/dist/metafiles/drip.heatmap.meta.json +2732 -460
  34. package/dist/metafiles/drip.live.meta.json +2732 -460
  35. package/dist/metafiles/drip.meta.json +2859 -574
  36. package/dist/metafiles/drip.mjs.meta.json +3235 -0
  37. package/dist/metafiles/drip.replay-recorder.meta.json +82 -0
  38. package/dist/metafiles/editor.meta.json +67 -27
  39. package/dist/metafiles/flags.cjs.meta.json +220 -0
  40. package/dist/metafiles/flags.mjs.meta.json +225 -0
  41. package/dist/metafiles/index.meta.json +2717 -491
  42. package/dist/metafiles/index.mjs.meta.json +14 -4
  43. package/dist/react/index.js +16621 -13539
  44. package/dist/react/index.js.map +4 -4
  45. package/dist/sri.json +20 -15
  46. package/dist/types/activation-runtime.d.ts +91 -0
  47. package/dist/types/analytics-runtime.d.ts +166 -0
  48. package/dist/types/antiflicker.d.ts +9 -3
  49. package/dist/types/assignment-cookie-sync.d.ts +31 -0
  50. package/dist/types/assignment-runtime.d.ts +257 -0
  51. package/dist/types/assignment-store.d.ts +4 -8
  52. package/dist/types/bucket.d.ts +1 -2
  53. package/dist/types/compact-store.d.ts +1 -0
  54. package/dist/types/config-runtime.d.ts +313 -0
  55. package/dist/types/consent-evaluation.d.ts +27 -0
  56. package/dist/types/css.d.ts +5 -3
  57. package/dist/types/debug-loader.d.ts +2 -0
  58. package/dist/types/decide-assignments.d.ts +31 -0
  59. package/dist/types/delivery-timing.d.ts +8 -0
  60. package/dist/types/early-interaction-capture.d.ts +79 -0
  61. package/dist/types/early-reassert.d.ts +32 -0
  62. package/dist/types/edge-runtime.d.ts +11 -0
  63. package/dist/types/editor/api.d.ts +3 -0
  64. package/dist/types/editor/controller.d.ts +3 -1
  65. package/dist/types/editor/publication-result.d.ts +11 -0
  66. package/dist/types/editor/types.d.ts +2 -1
  67. package/dist/types/editor/view-kit.d.ts +1 -1
  68. package/dist/types/endpoint.d.ts +8 -0
  69. package/dist/types/exclusion-groups.d.ts +4 -7
  70. package/dist/types/flags.d.ts +2 -0
  71. package/dist/types/goal-eligibility.d.ts +4 -0
  72. package/dist/types/goals.d.ts +10 -1
  73. package/dist/types/guards.d.ts +7 -7
  74. package/dist/types/handover.d.ts +12 -1
  75. package/dist/types/hash.d.ts +8 -4
  76. package/dist/types/hydration.d.ts +2 -1
  77. package/dist/types/index.d.ts +24 -128
  78. package/dist/types/init-test-helpers.d.ts +48 -0
  79. package/dist/types/inline-bootstrap.d.ts +26 -0
  80. package/dist/types/js.d.ts +3 -1
  81. package/dist/types/money.d.ts +12 -0
  82. package/dist/types/mutate.d.ts +45 -6
  83. package/dist/types/performance.d.ts +2 -0
  84. package/dist/types/persistence-decoders.d.ts +12 -0
  85. package/dist/types/public-api.d.ts +209 -0
  86. package/dist/types/qa-assistant-state.d.ts +34 -0
  87. package/dist/types/react/index.d.ts +28 -3
  88. package/dist/types/reassert-sentinel.d.ts +6 -0
  89. package/dist/types/redirect.d.ts +48 -0
  90. package/dist/types/replay-recorder-entry.d.ts +1 -0
  91. package/dist/types/replay-signal-state.d.ts +6 -0
  92. package/dist/types/replay-signals.d.ts +13 -0
  93. package/dist/types/replay.d.ts +110 -0
  94. package/dist/types/router.d.ts +12 -2
  95. package/dist/types/runtime-state.d.ts +166 -0
  96. package/dist/types/sanitize.d.ts +1 -0
  97. package/dist/types/shadow.d.ts +3 -0
  98. package/dist/types/shopify-cart.d.ts +25 -1
  99. package/dist/types/storefront-vitals.d.ts +24 -0
  100. package/dist/types/targeting-diagnostics.d.ts +1 -0
  101. package/dist/types/targeting.d.ts +16 -2
  102. package/dist/types/track.d.ts +15 -1
  103. package/dist/types/types.d.ts +115 -9
  104. package/dist/types/wait.d.ts +14 -1
  105. package/dist/types/weekday-schedule.d.ts +3 -0
  106. package/package.json +17 -8
  107. package/src/activation-runtime.ts +2737 -0
  108. package/src/analytics-runtime.ts +2175 -0
  109. package/src/antiflicker.test.ts +216 -44
  110. package/src/antiflicker.ts +103 -28
  111. package/src/artifact-concurrency.test.ts +1091 -0
  112. package/src/assignment-cookie-sync.test.ts +202 -0
  113. package/src/assignment-cookie-sync.ts +189 -0
  114. package/src/assignment-runtime.ts +3204 -0
  115. package/src/assignment-store.test.ts +56 -0
  116. package/src/assignment-store.ts +7 -38
  117. package/src/bucket.ts +3 -18
  118. package/src/commerce-analytics-lifecycle.test.ts +75 -15
  119. package/src/commerce-config-lifecycle.test.ts +81 -1
  120. package/src/commerce-price-preserve.test.ts +133 -0
  121. package/src/commerce-price.test.ts +97 -0
  122. package/src/commerce-price.ts +74 -20
  123. package/src/commerce-shipping.test.ts +53 -0
  124. package/src/commerce-shipping.ts +5 -11
  125. package/src/compact-store.ts +15 -0
  126. package/src/config-fetch.test.ts +480 -9
  127. package/src/config-runtime.ts +2070 -0
  128. package/src/consent-evaluation-cookiescript.test.ts +50 -0
  129. package/src/consent-evaluation.ts +138 -0
  130. package/src/css.test.ts +30 -0
  131. package/src/css.ts +120 -6
  132. package/src/cwv.test.ts +60 -77
  133. package/src/debug-loader.test.ts +214 -0
  134. package/src/debug-loader.ts +41 -0
  135. package/src/decide-assignments.test.ts +196 -0
  136. package/src/decide-assignments.ts +204 -0
  137. package/src/delivery-timing.test.ts +104 -0
  138. package/src/delivery-timing.ts +89 -0
  139. package/src/early-interaction-capture.test.ts +451 -0
  140. package/src/early-interaction-capture.ts +230 -0
  141. package/src/early-reassert.test.ts +207 -0
  142. package/src/early-reassert.ts +152 -0
  143. package/src/edge-adoption.test.ts +435 -0
  144. package/src/edge-runtime.ts +30 -0
  145. package/src/editor/api.test.ts +109 -0
  146. package/src/editor/api.ts +34 -3
  147. package/src/editor/controller.test.ts +297 -14
  148. package/src/editor/controller.ts +69 -5
  149. package/src/editor/index.test.ts +2 -0
  150. package/src/editor/index.ts +1 -0
  151. package/src/editor/publication-result.ts +58 -0
  152. package/src/editor/types.ts +10 -1
  153. package/src/editor/view-kit.ts +2 -1
  154. package/src/editor/view.test.ts +108 -0
  155. package/src/editor/view.ts +49 -7
  156. package/src/endpoint.test.ts +15 -0
  157. package/src/endpoint.ts +10 -0
  158. package/src/exclusion-groups.ts +8 -18
  159. package/src/flags.test.ts +88 -1
  160. package/src/flags.ts +29 -29
  161. package/src/global-runtime-reconciler.test.ts +271 -0
  162. package/src/goal-eligibility.test.ts +29 -0
  163. package/src/goal-eligibility.ts +17 -0
  164. package/src/goals.test.ts +144 -1
  165. package/src/goals.ts +97 -18
  166. package/src/guards.test.ts +99 -1
  167. package/src/guards.ts +34 -15
  168. package/src/handover-cache-order.test.ts +6 -3
  169. package/src/handover.test.ts +154 -8
  170. package/src/handover.ts +122 -12
  171. package/src/hash.ts +29 -54
  172. package/src/holdout.test.ts +26 -0
  173. package/src/hydration.test.ts +73 -2
  174. package/src/hydration.ts +18 -9
  175. package/src/index.ts +130 -7287
  176. package/src/init-analytics.test.ts +1694 -0
  177. package/src/init-anti-flicker.test.ts +1211 -0
  178. package/src/init-cart-attributes.test.ts +2003 -0
  179. package/src/init-config-resolution.test.ts +999 -0
  180. package/src/init-consent.test.ts +971 -0
  181. package/src/init-empty.test.ts +11 -5406
  182. package/src/init-integrations.test.ts +846 -0
  183. package/src/init-page-activation.test.ts +5064 -0
  184. package/src/init-qa-loading.test.ts +740 -0
  185. package/src/init-redirects.test.ts +2156 -0
  186. package/src/init-test-helpers.ts +287 -0
  187. package/src/inline-bootstrap.test.ts +184 -0
  188. package/src/inline-bootstrap.ts +73 -0
  189. package/src/js.test.ts +51 -1
  190. package/src/js.ts +86 -2
  191. package/src/money.test.ts +55 -1
  192. package/src/money.ts +104 -2
  193. package/src/money.web-parity.contract.test.ts +85 -0
  194. package/src/mutate.test.ts +474 -10
  195. package/src/mutate.ts +739 -230
  196. package/src/page-trigger.test.ts +218 -8
  197. package/src/performance.ts +10 -0
  198. package/src/persistence-decoders.ts +18 -0
  199. package/src/public-api.ts +1828 -0
  200. package/src/qa-assistant-state.test.ts +249 -3
  201. package/src/qa-assistant-state.ts +146 -6
  202. package/src/qa-assistant-view.test.ts +220 -2
  203. package/src/qa-assistant-view.ts +102 -5
  204. package/src/react/index.test.tsx +37 -0
  205. package/src/react/index.tsx +19 -3
  206. package/src/reassert-sentinel.test.ts +155 -1
  207. package/src/reassert-sentinel.ts +168 -8
  208. package/src/redirect.test.ts +541 -0
  209. package/src/redirect.ts +572 -0
  210. package/src/replay-recorder-entry.ts +115 -0
  211. package/src/replay-signal-state.ts +100 -0
  212. package/src/replay-signals.test.ts +249 -0
  213. package/src/replay-signals.ts +151 -0
  214. package/src/replay.test.ts +764 -0
  215. package/src/replay.ts +514 -0
  216. package/src/router.test.ts +61 -2
  217. package/src/router.ts +50 -12
  218. package/src/runtime-state.ts +507 -0
  219. package/src/runtime.test.ts +174 -7
  220. package/src/runtime.ts +88 -14
  221. package/src/sanitize.test.ts +34 -0
  222. package/src/sanitize.ts +23 -12
  223. package/src/shadow.test.ts +187 -0
  224. package/src/shadow.ts +99 -0
  225. package/src/shopify-cart.test.ts +733 -23
  226. package/src/shopify-cart.ts +402 -49
  227. package/src/storefront-vitals.test.ts +217 -0
  228. package/src/storefront-vitals.ts +206 -0
  229. package/src/surface-signals.ts +6 -0
  230. package/src/targeting-conformance.test.ts +9 -0
  231. package/src/targeting-diagnostics.ts +38 -8
  232. package/src/targeting-parity-corpus.json +559 -0
  233. package/src/targeting-parity-corpus.test.ts +18 -0
  234. package/src/targeting.detailed.test.ts +105 -0
  235. package/src/targeting.test.ts +211 -0
  236. package/src/targeting.ts +88 -207
  237. package/src/track.test.ts +147 -0
  238. package/src/track.ts +125 -26
  239. package/src/types.ts +120 -13
  240. package/src/wait.test.ts +23 -0
  241. package/src/wait.ts +129 -13
  242. package/src/weekday-delivery.test.ts +56 -0
  243. package/src/weekday-schedule-parity-corpus.json +92 -0
  244. package/src/weekday-schedule.test.ts +69 -0
  245. package/src/weekday-schedule.ts +20 -0
@@ -0,0 +1,846 @@
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 integrations", () => {
7
+ registerInitTestLifecycle();
8
+
9
+
10
+ it("emits exposure destinations for dataLayer, Hotjar, and generic queues", async () => {
11
+ const hj = vi.fn();
12
+ (globalThis as any).window.hj = hj;
13
+ (globalThis as any).window.dataLayer = [];
14
+ (globalThis as any).window.adbq = [];
15
+ (globalThis as any).hj = hj;
16
+ (globalThis as any).adbq = (globalThis as any).window.adbq;
17
+
18
+ const { getDiagnostics, init } = await import("./index");
19
+
20
+ init({
21
+ antiFlickerMode: "off",
22
+ endpoint: "https://events.test",
23
+ experiments: [
24
+ {
25
+ id: "exp_exposure",
26
+ status: "running",
27
+ trafficAllocation: 1,
28
+ variations: [{ id: "var_exposure", weight: 1, mutations: [] }],
29
+ },
30
+ ],
31
+ exposureDestinations: {
32
+ dataLayer: { enabled: true, eventName: "apex_test_viewed" },
33
+ hotjar: { enabled: true, mode: "event", eventName: "apex_test_viewed" },
34
+ queues: [{ enabled: true, globalName: "adbq", eventName: "Viewed an experiment" }],
35
+ },
36
+ shopId: "shop_123",
37
+ trackingStart: "immediate",
38
+ });
39
+
40
+ expect((globalThis as any).window.dataLayer).toEqual(expect.arrayContaining([
41
+ expect.objectContaining({ event: "apex_test_viewed", experiment_id: "exp_exposure", variation_id: "var_exposure" }),
42
+ ]));
43
+ expect(hj).toHaveBeenCalledWith("event", "apex_test_viewed");
44
+ expect((globalThis as any).window.adbq).toEqual(expect.arrayContaining([
45
+ expect.objectContaining({ event: "Viewed an experiment", experiment_id: "exp_exposure", variation_id: "var_exposure" }),
46
+ ]));
47
+ expect(getDiagnostics().integrations.exposureDestinations.map((item) => item.status)).toContain("sent");
48
+ });
49
+
50
+
51
+ it("sets Clarity experiment tags for variants and controls", async () => {
52
+ const clarity = vi.fn();
53
+ (globalThis as any).window.clarity = clarity;
54
+
55
+ const { init } = await import("./index");
56
+
57
+ init({
58
+ antiFlickerMode: "off",
59
+ endpoint: "https://events.test",
60
+ experiments: [
61
+ {
62
+ id: "exp_clarity_variant",
63
+ name: "Homepage hero",
64
+ status: "running",
65
+ trafficAllocation: 1,
66
+ variations: [
67
+ { id: "var_clarity_control", weight: 0, mutations: [], isControl: true },
68
+ { id: "var_clarity_treatment", weight: 1, mutations: [] },
69
+ ],
70
+ },
71
+ {
72
+ id: "exp_clarity_control",
73
+ name: "Product detail",
74
+ status: "running",
75
+ trafficAllocation: 1,
76
+ variations: [
77
+ { id: "var_clarity_only_control", weight: 1, mutations: [], isControl: true },
78
+ ],
79
+ },
80
+ ],
81
+ exposureDestinations: {
82
+ clarity: { enabled: true, mode: "set" },
83
+ },
84
+ shopId: "shop_123",
85
+ trackingStart: "immediate",
86
+ });
87
+
88
+ expect(clarity).toHaveBeenCalledWith("set", "apex Homepage hero", "variant-1");
89
+ expect(clarity).toHaveBeenCalledWith("set", "apex Product detail", "control");
90
+ });
91
+
92
+
93
+ it("records a Clarity diagnostic without throwing when Clarity is absent", async () => {
94
+ const { getDiagnostics, init } = await import("./index");
95
+
96
+ expect(() => init({
97
+ antiFlickerMode: "off",
98
+ endpoint: "https://events.test",
99
+ experiments: [
100
+ {
101
+ id: "exp_clarity_absent",
102
+ status: "running",
103
+ trafficAllocation: 1,
104
+ variations: [{ id: "var_clarity_absent", weight: 1, mutations: [] }],
105
+ },
106
+ ],
107
+ exposureDestinations: {
108
+ clarity: { enabled: true },
109
+ },
110
+ shopId: "shop_123",
111
+ trackingStart: "immediate",
112
+ })).not.toThrow();
113
+
114
+ expect(getDiagnostics().integrations.exposureDestinations).toEqual(expect.arrayContaining([
115
+ expect.objectContaining({ type: "clarity", status: "failed", reason: "clarity_missing" }),
116
+ ]));
117
+ });
118
+
119
+
120
+ it("fires Clarity set and event calls while upgrading only once per page", async () => {
121
+ const clarity = vi.fn();
122
+ (globalThis as any).window.clarity = clarity;
123
+
124
+ const { init } = await import("./index");
125
+
126
+ init({
127
+ antiFlickerMode: "off",
128
+ endpoint: "https://events.test",
129
+ experiments: [
130
+ {
131
+ id: "exp_clarity_one",
132
+ name: "First test",
133
+ status: "running",
134
+ trafficAllocation: 1,
135
+ variations: [{ id: "var_clarity_one", weight: 1, mutations: [] }],
136
+ },
137
+ {
138
+ id: "exp_clarity_two",
139
+ name: "Second test",
140
+ status: "running",
141
+ trafficAllocation: 1,
142
+ variations: [{ id: "var_clarity_two", weight: 1, mutations: [] }],
143
+ },
144
+ ],
145
+ exposureDestinations: {
146
+ clarity: {
147
+ enabled: true,
148
+ mode: "both",
149
+ eventName: "apex_exposure",
150
+ upgrade: true,
151
+ },
152
+ },
153
+ shopId: "shop_123",
154
+ trackingStart: "immediate",
155
+ });
156
+
157
+ expect(clarity.mock.calls.filter(([command]) => command === "set")).toHaveLength(2);
158
+ expect(clarity.mock.calls.filter(([command]) => command === "event")).toEqual([
159
+ ["event", "apex_exposure"],
160
+ ["event", "apex_exposure"],
161
+ ]);
162
+ expect(clarity.mock.calls.filter(([command]) => command === "upgrade")).toEqual([
163
+ ["upgrade", "apex_experiment"],
164
+ ]);
165
+ });
166
+
167
+
168
+ it("sanitizes and truncates Clarity custom tag keys", async () => {
169
+ const clarity = vi.fn();
170
+ (globalThis as any).window.clarity = clarity;
171
+ const longName = `Checkout 🚀 100% ${"A".repeat(100)}`;
172
+
173
+ const { init } = await import("./index");
174
+
175
+ init({
176
+ antiFlickerMode: "off",
177
+ endpoint: "https://events.test",
178
+ experiments: [
179
+ {
180
+ id: "exp_clarity_sanitize",
181
+ name: longName,
182
+ status: "running",
183
+ trafficAllocation: 1,
184
+ variations: [{ id: "var_clarity_sanitize", weight: 1, mutations: [] }],
185
+ },
186
+ ],
187
+ exposureDestinations: {
188
+ clarity: { enabled: true },
189
+ },
190
+ shopId: "shop_123",
191
+ trackingStart: "immediate",
192
+ });
193
+
194
+ const setCall = clarity.mock.calls.find(([command]) => command === "set");
195
+ expect(setCall).toBeDefined();
196
+ expect(setCall?.[1]).toHaveLength(80);
197
+ expect(setCall?.[1]).toMatch(/^apex Checkout 100 A+$/);
198
+ expect(setCall?.[2]).toBe("variant-0");
199
+ });
200
+
201
+
202
+ it("creates the Contentsquare queue and pushes a dynamic variable", async () => {
203
+ const { init } = await import("./index");
204
+
205
+ init({
206
+ antiFlickerMode: "off",
207
+ endpoint: "https://events.test",
208
+ experiments: [
209
+ {
210
+ id: "exp_contentsquare",
211
+ name: "Collection banner",
212
+ status: "running",
213
+ trafficAllocation: 1,
214
+ variations: [{ id: "var_contentsquare", weight: 1, mutations: [] }],
215
+ },
216
+ ],
217
+ exposureDestinations: {
218
+ contentsquare: { enabled: true },
219
+ },
220
+ shopId: "shop_123",
221
+ trackingStart: "immediate",
222
+ });
223
+
224
+ expect((globalThis as any).window._uxa).toEqual([
225
+ ["trackDynamicVariable", { key: "apex Collection banner", value: "variant-0" }],
226
+ ]);
227
+ });
228
+
229
+
230
+ it("respects an existing Contentsquare queue", async () => {
231
+ const existingEntry = ["trackPageEvent", "loaded"];
232
+ const uxa = [existingEntry];
233
+ (globalThis as any).window._uxa = uxa;
234
+
235
+ const { init } = await import("./index");
236
+
237
+ init({
238
+ antiFlickerMode: "off",
239
+ endpoint: "https://events.test",
240
+ experiments: [
241
+ {
242
+ id: "exp_contentsquare_existing",
243
+ status: "running",
244
+ trafficAllocation: 1,
245
+ variations: [{ id: "var_contentsquare_existing", weight: 1, mutations: [], isControl: true }],
246
+ },
247
+ ],
248
+ exposureDestinations: {
249
+ contentsquare: { enabled: true, keyPrefix: "apex cs " },
250
+ },
251
+ shopId: "shop_123",
252
+ trackingStart: "immediate",
253
+ });
254
+
255
+ expect((globalThis as any).window._uxa).toBe(uxa);
256
+ expect(uxa).toEqual([
257
+ existingEntry,
258
+ ["trackDynamicVariable", { key: "apex cs exp_contentsquare_existing", value: "control" }],
259
+ ]);
260
+ });
261
+
262
+
263
+ it("captures PostHog exposure events with the default event name and payload", async () => {
264
+ const capture = vi.fn();
265
+ const register = vi.fn();
266
+ (globalThis as any).window.posthog = { capture, register };
267
+
268
+ const { getDiagnostics, init } = await import("./index");
269
+
270
+ init({
271
+ antiFlickerMode: "off",
272
+ endpoint: "https://events.test",
273
+ experiments: [
274
+ {
275
+ id: "exp_posthog",
276
+ status: "running",
277
+ trafficAllocation: 1,
278
+ variations: [{ id: "var_posthog", weight: 1, mutations: [] }],
279
+ },
280
+ ],
281
+ exposureDestinations: {
282
+ posthog: { enabled: true },
283
+ },
284
+ shopId: "shop_123",
285
+ trackingStart: "immediate",
286
+ });
287
+
288
+ expect(capture).toHaveBeenCalledWith("apex_experiment_viewed", expect.objectContaining({
289
+ project_id: "shop_123",
290
+ experiment_id: "exp_posthog",
291
+ variation_id: "var_posthog",
292
+ experiment_name: "exp_posthog",
293
+ variation_index: 0,
294
+ variation_name: "var_posthog",
295
+ is_control: false,
296
+ }));
297
+ expect(getDiagnostics().integrations.exposureDestinations).toEqual(expect.arrayContaining([
298
+ expect.objectContaining({ type: "posthog", status: "sent" }),
299
+ ]));
300
+ });
301
+
302
+
303
+ it("registers PostHog variant super properties by experiment", async () => {
304
+ const capture = vi.fn();
305
+ const register = vi.fn();
306
+ (globalThis as any).window.posthog = { capture, register };
307
+
308
+ const { init } = await import("./index");
309
+
310
+ init({
311
+ antiFlickerMode: "off",
312
+ endpoint: "https://events.test",
313
+ experiments: [
314
+ {
315
+ id: "exp_posthog_register",
316
+ status: "running",
317
+ trafficAllocation: 1,
318
+ variations: [{ id: "var_posthog_register", weight: 1, mutations: [] }],
319
+ },
320
+ ],
321
+ exposureDestinations: {
322
+ posthog: { enabled: true },
323
+ },
324
+ shopId: "shop_123",
325
+ trackingStart: "immediate",
326
+ });
327
+
328
+ expect(register).toHaveBeenCalledWith({ apex_exp_exp_posthog_register: "var_posthog_register" });
329
+ });
330
+
331
+
332
+ it("does not register PostHog super properties in capture mode", async () => {
333
+ const capture = vi.fn();
334
+ const register = vi.fn();
335
+ (globalThis as any).window.posthog = { capture, register };
336
+
337
+ const { init } = await import("./index");
338
+
339
+ init({
340
+ antiFlickerMode: "off",
341
+ endpoint: "https://events.test",
342
+ experiments: [
343
+ {
344
+ id: "exp_posthog_capture",
345
+ status: "running",
346
+ trafficAllocation: 1,
347
+ variations: [{ id: "var_posthog_capture", weight: 1, mutations: [] }],
348
+ },
349
+ ],
350
+ exposureDestinations: {
351
+ posthog: { enabled: true, mode: "capture" },
352
+ },
353
+ shopId: "shop_123",
354
+ trackingStart: "immediate",
355
+ });
356
+
357
+ expect(capture).toHaveBeenCalledOnce();
358
+ expect(register).not.toHaveBeenCalled();
359
+ });
360
+
361
+
362
+ it("records a PostHog diagnostic without throwing when PostHog is absent", async () => {
363
+ const { getDiagnostics, init } = await import("./index");
364
+
365
+ expect(() => init({
366
+ antiFlickerMode: "off",
367
+ endpoint: "https://events.test",
368
+ experiments: [
369
+ {
370
+ id: "exp_posthog_absent",
371
+ status: "running",
372
+ trafficAllocation: 1,
373
+ variations: [{ id: "var_posthog_absent", weight: 1, mutations: [] }],
374
+ },
375
+ ],
376
+ exposureDestinations: {
377
+ posthog: { enabled: true },
378
+ },
379
+ shopId: "shop_123",
380
+ trackingStart: "immediate",
381
+ })).not.toThrow();
382
+
383
+ expect(getDiagnostics().integrations.exposureDestinations).toEqual(expect.arrayContaining([
384
+ expect.objectContaining({ type: "posthog", status: "failed", reason: "posthog_missing" }),
385
+ ]));
386
+ });
387
+
388
+
389
+ it("emits PostHog exposure calls into stub queue objects", async () => {
390
+ const calls: Array<[string, ...unknown[]]> = [];
391
+ (globalThis as any).window.posthog = {
392
+ capture: (...args: unknown[]) => calls.push(["capture", ...args]),
393
+ register: (properties: Record<string, unknown>) => calls.push(["register", properties]),
394
+ };
395
+
396
+ const { init } = await import("./index");
397
+
398
+ init({
399
+ antiFlickerMode: "off",
400
+ endpoint: "https://events.test",
401
+ experiments: [
402
+ {
403
+ id: "exp_posthog_queue",
404
+ status: "running",
405
+ trafficAllocation: 1,
406
+ variations: [{ id: "var_posthog_queue", weight: 1, mutations: [] }],
407
+ },
408
+ ],
409
+ exposureDestinations: {
410
+ posthog: { enabled: true },
411
+ },
412
+ shopId: "shop_123",
413
+ trackingStart: "immediate",
414
+ });
415
+
416
+ expect(calls).toEqual(expect.arrayContaining([
417
+ ["capture", "apex_experiment_viewed", expect.objectContaining({
418
+ experiment_id: "exp_posthog_queue",
419
+ variation_id: "var_posthog_queue",
420
+ })],
421
+ ["register", { apex_exp_exp_posthog_queue: "var_posthog_queue" }],
422
+ ]));
423
+ });
424
+
425
+
426
+ it("stores exposed assignments and uses them for order identity beacons on non-targeted confirmation pages", async () => {
427
+ (globalThis as any).location = {
428
+ hash: "",
429
+ hostname: "plain.test",
430
+ href: "https://plain.test/products/a",
431
+ origin: "https://plain.test",
432
+ pathname: "/products/a",
433
+ search: "",
434
+ };
435
+ const { init, getRuntimeValue } = await import("./index");
436
+ const experiment = {
437
+ id: "exp_product",
438
+ status: "running" as const,
439
+ targeting: { url: [{ type: "simple" as const, pattern: "/products/*" }] },
440
+ trafficAllocation: 1,
441
+ variations: [{ id: "var_product", weight: 1, mutations: [] }],
442
+ };
443
+
444
+ init({
445
+ antiFlickerMode: "off",
446
+ endpoint: "https://events.test",
447
+ experiments: [experiment],
448
+ ingestSignature: "sig",
449
+ shopId: "11111111-1111-4111-8111-111111111111",
450
+ trackingStart: "immediate",
451
+ });
452
+
453
+ const stored = JSON.parse(String((globalThis as any).localStorage.getItem("drip_exposures")));
454
+ expect(stored).toEqual([
455
+ expect.objectContaining({ e: "exp_product", v: "var_product", t: expect.any(Number) }),
456
+ ]);
457
+ expect(getRuntimeValue("orderAttribution")).toMatchObject({
458
+ assignments: [{ experimentId: "exp_product", variationId: "var_product" }],
459
+ });
460
+
461
+ vi.mocked(globalThis.fetch).mockClear();
462
+ (globalThis as any).window.__apexOrderIdentity = {
463
+ platform: "centra",
464
+ orderId: "order-1",
465
+ orderNumber: "1001",
466
+ };
467
+ (globalThis as any).location = {
468
+ hash: "",
469
+ hostname: "plain.test",
470
+ href: "https://plain.test/checkout/thank-you?orderId=order-1",
471
+ origin: "https://plain.test",
472
+ pathname: "/checkout/thank-you",
473
+ search: "?orderId=order-1",
474
+ };
475
+ vi.mocked((globalThis as any).document.querySelector).mockImplementation((selector: string) => {
476
+ if (selector === "[data-apex-order-id]") {
477
+ return { dataset: { apexOrderId: "order-1" } };
478
+ }
479
+ return null;
480
+ });
481
+ Object.defineProperty(globalThis, "navigator", {
482
+ configurable: true,
483
+ value: { userAgent: "Mozilla/5.0", doNotTrack: "0", sendBeacon: undefined },
484
+ });
485
+
486
+ init({
487
+ antiFlickerMode: "off",
488
+ endpoint: "https://events.test",
489
+ experiments: [experiment],
490
+ ingestSignature: "sig",
491
+ shopId: "11111111-1111-4111-8111-111111111111",
492
+ trackingStart: "immediate",
493
+ });
494
+ await vi.runOnlyPendingTimersAsync();
495
+
496
+ const orderIdentityCall = vi.mocked(globalThis.fetch).mock.calls.find((call) =>
497
+ String(call[0]) === "https://events.test/v1/order-identity",
498
+ );
499
+ expect(orderIdentityCall).toBeTruthy();
500
+ const payload = JSON.parse(String(orderIdentityCall?.[1]?.body));
501
+ expect(payload).toMatchObject({
502
+ platform: "centra",
503
+ environmentKey: "production",
504
+ platformOrderId: "order-1",
505
+ orderNumber: "1001",
506
+ assignments: [{ experimentId: "exp_product", variationId: "var_product" }],
507
+ });
508
+ });
509
+
510
+
511
+ it("re-probes a cached null order identity when reInit runs on the same URL", async () => {
512
+ (globalThis as any).location = {
513
+ hash: "",
514
+ hostname: "plain.test",
515
+ href: "https://plain.test/checkout/thank-you",
516
+ origin: "https://plain.test",
517
+ pathname: "/checkout/thank-you",
518
+ search: "",
519
+ };
520
+ const { init, reInit } = await import("./index");
521
+ init({
522
+ antiFlickerMode: "off",
523
+ endpoint: "https://events.test",
524
+ experiments: [{
525
+ id: "exp_confirmation",
526
+ status: "running",
527
+ trafficAllocation: 1,
528
+ variations: [{ id: "var_confirmation", weight: 1, mutations: [] }],
529
+ }],
530
+ ingestSignature: "sig",
531
+ shopId: "11111111-1111-4111-8111-111111111111",
532
+ trackingStart: "immediate",
533
+ });
534
+ await vi.runOnlyPendingTimersAsync();
535
+ vi.mocked(globalThis.fetch).mockClear();
536
+
537
+ (globalThis as any).window.__apexOrderIdentity = {
538
+ platform: "centra",
539
+ orderId: "order-reinit",
540
+ orderNumber: "2002",
541
+ };
542
+ reInit();
543
+ await vi.runOnlyPendingTimersAsync();
544
+
545
+ const call = vi.mocked(globalThis.fetch).mock.calls.find((candidate) =>
546
+ String(candidate[0]) === "https://events.test/v1/order-identity",
547
+ );
548
+ expect(call).toBeTruthy();
549
+ expect(JSON.parse(String(call?.[1]?.body))).toMatchObject({
550
+ platform: "centra",
551
+ platformOrderId: "order-reinit",
552
+ orderNumber: "2002",
553
+ });
554
+ });
555
+
556
+
557
+ it("does not detect order identity from URL params without a confirmation signal", async () => {
558
+ (globalThis as any).location = {
559
+ hash: "",
560
+ hostname: "plain.test",
561
+ href: "https://plain.test/account/order?orderId=order-1",
562
+ origin: "https://plain.test",
563
+ pathname: "/account/order",
564
+ search: "?orderId=order-1",
565
+ };
566
+ const { init } = await import("./index");
567
+
568
+ init({
569
+ antiFlickerMode: "off",
570
+ endpoint: "https://events.test",
571
+ experiments: [
572
+ {
573
+ id: "exp_any",
574
+ status: "running",
575
+ trafficAllocation: 1,
576
+ variations: [{ id: "var_any", weight: 1, mutations: [] }],
577
+ },
578
+ ],
579
+ ingestSignature: "sig",
580
+ shopId: "11111111-1111-4111-8111-111111111111",
581
+ trackingStart: "immediate",
582
+ });
583
+ await vi.runOnlyPendingTimersAsync();
584
+
585
+ expect(vi.mocked(globalThis.fetch).mock.calls.some((call) =>
586
+ String(call[0]) === "https://events.test/v1/order-identity",
587
+ )).toBe(false);
588
+ });
589
+
590
+
591
+ it("only writes order identity dedupe after successful fetch delivery", async () => {
592
+ (globalThis as any).localStorage.setItem(
593
+ "drip_exposures",
594
+ JSON.stringify([{ e: "exp_prior", v: "var_prior", t: Date.now() }]),
595
+ );
596
+ (globalThis as any).location = {
597
+ hash: "",
598
+ hostname: "plain.test",
599
+ href: "https://plain.test/checkout/thank-you",
600
+ origin: "https://plain.test",
601
+ pathname: "/checkout/thank-you",
602
+ search: "",
603
+ };
604
+ vi.mocked((globalThis as any).document.querySelector).mockImplementation((selector: string) => {
605
+ if (selector === "[data-apex-order-id]") {
606
+ return { dataset: { apexOrderId: "order-1" } };
607
+ }
608
+ return null;
609
+ });
610
+ let orderIdentityShouldFail = true;
611
+ const orderIdentityStatuses: number[] = [];
612
+ vi.mocked(globalThis.fetch).mockImplementation(async (input: RequestInfo | URL) => {
613
+ if (String(input) === "https://events.test/v1/order-identity") {
614
+ const status = orderIdentityShouldFail ? 500 : 202;
615
+ orderIdentityStatuses.push(status);
616
+ return { status } as Response;
617
+ }
618
+ return new Response("ok", { status: 202 });
619
+ });
620
+
621
+ const { init } = await import("./index");
622
+ const config = {
623
+ antiFlickerMode: "off" as const,
624
+ endpoint: "https://events.test",
625
+ experiments: [],
626
+ ingestSignature: "sig",
627
+ shopId: "11111111-1111-4111-8111-111111111111",
628
+ trackingStart: "immediate" as const,
629
+ };
630
+
631
+ init(config);
632
+ await vi.runOnlyPendingTimersAsync();
633
+ await Promise.resolve();
634
+ const orderIdentityKeyAfterFailure = Array.from({ length: (globalThis as any).sessionStorage.length }, (_, index) =>
635
+ (globalThis as any).sessionStorage.key(index),
636
+ ).find((key) => String(key).startsWith("drip:order_identity"));
637
+ expect(orderIdentityKeyAfterFailure ? (globalThis as any).sessionStorage.getItem(orderIdentityKeyAfterFailure) : null)
638
+ .not.toBe("1");
639
+
640
+ orderIdentityShouldFail = false;
641
+ init(config);
642
+ await vi.runOnlyPendingTimersAsync();
643
+ await Promise.resolve();
644
+ expect(vi.mocked(globalThis.fetch).mock.calls.filter((call) =>
645
+ String(call[0]) === "https://events.test/v1/order-identity",
646
+ )).toHaveLength(2);
647
+ expect(vi.mocked(globalThis.fetch).mock.calls.filter((call) =>
648
+ String(call[0]) === "https://events.test/v1/order-identity",
649
+ ).length).toBeGreaterThanOrEqual(2);
650
+ });
651
+
652
+
653
+ it("does not resend order identity after a successful fetch delivery", async () => {
654
+ (globalThis as any).localStorage.setItem(
655
+ "drip_exposures",
656
+ JSON.stringify([{ e: "exp_prior", v: "var_prior", t: Date.now() }]),
657
+ );
658
+ (globalThis as any).location = {
659
+ hash: "",
660
+ hostname: "plain.test",
661
+ href: "https://plain.test/checkout/thank-you",
662
+ origin: "https://plain.test",
663
+ pathname: "/checkout/thank-you",
664
+ search: "",
665
+ };
666
+ vi.mocked((globalThis as any).document.querySelector).mockImplementation((selector: string) => {
667
+ if (selector === "[data-apex-order-id]") {
668
+ return { dataset: { apexOrderId: "order-1" } };
669
+ }
670
+ return null;
671
+ });
672
+ vi.mocked(globalThis.fetch).mockImplementation(async (input: RequestInfo | URL) =>
673
+ String(input) === "https://events.test/v1/order-identity"
674
+ ? ({ status: 202 } as Response)
675
+ : new Response("ok", { status: 202 }),
676
+ );
677
+
678
+ const { init } = await import("./index");
679
+ const config = {
680
+ antiFlickerMode: "off" as const,
681
+ endpoint: "https://events.test",
682
+ experiments: [],
683
+ ingestSignature: "sig",
684
+ shopId: "11111111-1111-4111-8111-111111111111",
685
+ trackingStart: "immediate" as const,
686
+ };
687
+
688
+ init(config);
689
+ await vi.runOnlyPendingTimersAsync();
690
+ await Promise.resolve();
691
+ init(config);
692
+ await vi.runOnlyPendingTimersAsync();
693
+
694
+ expect(vi.mocked(globalThis.fetch).mock.calls.filter((call) =>
695
+ String(call[0]) === "https://events.test/v1/order-identity",
696
+ )).toHaveLength(1);
697
+ });
698
+
699
+
700
+ it("tracks revenue from configured dataLayer purchase events", async () => {
701
+ (globalThis as any).window.dataLayer = [];
702
+
703
+ const { flush, init } = await import("./index");
704
+
705
+ init({
706
+ antiFlickerMode: "off",
707
+ dataLayerTriggers: [
708
+ {
709
+ eventName: "purchase",
710
+ reEvaluate: false,
711
+ trackRevenue: {
712
+ enabled: true,
713
+ costFrom: "cost",
714
+ profitFrom: "profit",
715
+ properties: [
716
+ { from: "liveRunId", to: "liveRunId" },
717
+ { from: "mappedRevenue", to: "revenue", valueType: "number" },
718
+ { from: "mappedSource", to: "source" },
719
+ ],
720
+ },
721
+ },
722
+ ],
723
+ endpoint: "https://events.test",
724
+ experiments: [
725
+ {
726
+ id: "exp_revenue_data_layer",
727
+ status: "running",
728
+ trafficAllocation: 1,
729
+ variations: [{ id: "var_revenue_data_layer", weight: 1, mutations: [] }],
730
+ },
731
+ ],
732
+ shopId: "shop_123",
733
+ trackingStart: "immediate",
734
+ });
735
+
736
+ (globalThis as any).window.dataLayer.push({
737
+ 0: "event",
738
+ 1: "purchase",
739
+ 2: {
740
+ value: "36",
741
+ transaction_id: "order_123",
742
+ currency: "EUR",
743
+ cost: "10",
744
+ profit: "26",
745
+ liveRunId: "run_123",
746
+ mappedRevenue: "999",
747
+ mappedSource: "untrusted_source",
748
+ },
749
+ });
750
+ await flush();
751
+
752
+ const fetchMock = vi.mocked(globalThis.fetch);
753
+ expect(fetchMock).toHaveBeenCalledTimes(1);
754
+ const body = JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body));
755
+ expect(body.events).toEqual(expect.arrayContaining([
756
+ expect.objectContaining({
757
+ type: "goal",
758
+ experimentId: "exp_revenue_data_layer",
759
+ variationId: "var_revenue_data_layer",
760
+ data: expect.objectContaining({
761
+ goalId: "__revenue__",
762
+ revenue: 36,
763
+ orderId: "order_123",
764
+ currency: "EUR",
765
+ cost: 10,
766
+ profit: 26,
767
+ liveRunId: "run_123",
768
+ source: "data_layer",
769
+ dataLayerEvent: "purchase",
770
+ }),
771
+ }),
772
+ ]));
773
+ });
774
+
775
+
776
+ it("does not let replayed dataLayer events start analytics before first assignments", async () => {
777
+ const hj = vi.fn();
778
+ (globalThis as any).window.hj = hj;
779
+ (globalThis as any).window.dataLayer = [{ event: "virtPageView", page_type: "pdp" }];
780
+ (globalThis as any).window.adbq = [];
781
+ (globalThis as any).hj = hj;
782
+ (globalThis as any).adbq = (globalThis as any).window.adbq;
783
+
784
+ const { getAssignments, getDiagnostics, getSignals, init } = await import("./index");
785
+
786
+ init({
787
+ antiFlickerMode: "off",
788
+ dataLayerTriggers: [
789
+ {
790
+ eventName: "virtPageView",
791
+ mappings: [{ from: "page_type", to: "page.type", valueType: "string" }],
792
+ trackPageview: true,
793
+ },
794
+ ],
795
+ endpoint: "https://events.test",
796
+ experiments: [
797
+ {
798
+ id: "exp_initial_data_layer",
799
+ status: "running",
800
+ targeting: {
801
+ segmentRules: [
802
+ {
803
+ id: "segment_pdp",
804
+ rules: {
805
+ version: 1,
806
+ operator: "and",
807
+ conditions: [{ signal: "page.type", operator: "equals", value: "pdp" }],
808
+ },
809
+ },
810
+ ],
811
+ },
812
+ trafficAllocation: 1,
813
+ variations: [{ id: "var_initial_data_layer", weight: 1, mutations: [] }],
814
+ },
815
+ ],
816
+ exposureDestinations: {
817
+ dataLayer: { enabled: true, eventName: "apex_test_viewed" },
818
+ hotjar: { enabled: true, mode: "event", eventName: "apex_test_viewed" },
819
+ queues: [{ enabled: true, globalName: "adbq", eventName: "Viewed an experiment" }],
820
+ },
821
+ shopId: "shop_123",
822
+ trackingStart: "immediate",
823
+ });
824
+
825
+ expect(getSignals()["page.type"]).toBe("pdp");
826
+ expect(getAssignments()).toEqual([
827
+ expect.objectContaining({ experimentId: "exp_initial_data_layer", variationId: "var_initial_data_layer" }),
828
+ ]);
829
+ expect((globalThis as any).window.dataLayer).toEqual(expect.arrayContaining([
830
+ expect.objectContaining({
831
+ event: "apex_test_viewed",
832
+ experiment_id: "exp_initial_data_layer",
833
+ variation_id: "var_initial_data_layer",
834
+ }),
835
+ ]));
836
+ expect(hj).toHaveBeenCalledWith("event", "apex_test_viewed");
837
+ expect((globalThis as any).window.adbq).toEqual(expect.arrayContaining([
838
+ expect.objectContaining({
839
+ event: "Viewed an experiment",
840
+ experiment_id: "exp_initial_data_layer",
841
+ variation_id: "var_initial_data_layer",
842
+ }),
843
+ ]));
844
+ expect(getDiagnostics().integrations.exposureDestinations.map((item) => item.status)).toContain("sent");
845
+ });
846
+ });