@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,740 @@
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 qa loading", () => {
7
+ registerInitTestLifecycle();
8
+
9
+
10
+ it.each(["?drip_qa=1", "?drip_screenshot=1"])(
11
+ "keeps forced operational assignments out of the holdout gate for %s",
12
+ async (search) => {
13
+ (globalThis as any).location.search = `${search}&drip_force=exp_operational:var_operational`;
14
+ const { init, getAssignments } = await import("./index");
15
+ const { isHoldoutVisitor } = await import("./hash");
16
+ const userId = Array.from({ length: 1000 }, (_, index) => `operational-${index}`).find((id) =>
17
+ isHoldoutVisitor({ holdout_v1_enabled: true, holdout_pct: 10 }, id),
18
+ );
19
+ expect(userId).toBeTruthy();
20
+
21
+ init({
22
+ antiFlickerMode: "off",
23
+ endpoint: "https://events.test",
24
+ experiments: [{
25
+ id: "exp_operational",
26
+ status: "running",
27
+ trafficAllocation: 1,
28
+ variations: [{ id: "var_operational", weight: 1, mutations: [] }],
29
+ }],
30
+ holdout_v1_enabled: true,
31
+ holdout_pct: 10,
32
+ shopId: "shop_123",
33
+ trackingStart: "immediate",
34
+ userId: userId!,
35
+ });
36
+
37
+ expect(getAssignments()).toEqual([
38
+ expect.objectContaining({ experimentId: "exp_operational", variationId: "var_operational" }),
39
+ ]);
40
+ },
41
+ );
42
+
43
+
44
+ it("exposes installed-site diagnostics for QA automation", async () => {
45
+ const { getDiagnostics, init } = await import("./index");
46
+
47
+ init({
48
+ antiFlickerMode: "off",
49
+ dataProtection: {
50
+ respectDoNotTrack: true,
51
+ storageMode: "localStorage",
52
+ },
53
+ debug: {
54
+ enabled: true,
55
+ restrictMode: "always",
56
+ },
57
+ endpoint: "https://events.test",
58
+ experiments: [
59
+ {
60
+ id: "exp_123",
61
+ status: "running",
62
+ trafficAllocation: 1,
63
+ variations: [
64
+ {
65
+ id: "var_123",
66
+ weight: 1,
67
+ mutations: [],
68
+ },
69
+ ],
70
+ },
71
+ ],
72
+ shopId: "shop_123",
73
+ trackingStart: "immediate",
74
+ });
75
+
76
+ expect(getDiagnostics()).toMatchObject({
77
+ mode: "debug",
78
+ initialized: true,
79
+ shopId: "shop_123",
80
+ experimentsLoaded: 1,
81
+ analyticsStarted: true,
82
+ trackerStarted: true,
83
+ configSource: "inline",
84
+ platform: "auto",
85
+ routerMode: "history",
86
+ activationMode: "observer",
87
+ consentRequired: false,
88
+ consentGranted: true,
89
+ storage: {
90
+ mode: "localStorage",
91
+ localStorageAvailable: true,
92
+ sessionStorageAvailable: true,
93
+ cookiesEnabled: true,
94
+ },
95
+ runtime: {
96
+ antiFlickerMode: "off",
97
+ trackingStart: "immediate",
98
+ pageTriggerMode: "direct",
99
+ },
100
+ });
101
+ expect(getDiagnostics().assignments).toHaveLength(1);
102
+ expect(getDiagnostics().assignments[0]).toMatchObject({
103
+ experimentId: "exp_123",
104
+ variationId: "var_123",
105
+ mutationCount: 0,
106
+ });
107
+ expect(getDiagnostics().activation.experiments[0]).toMatchObject({
108
+ experimentId: "exp_123",
109
+ status: "passed",
110
+ assignedVariationId: "var_123",
111
+ });
112
+ });
113
+
114
+
115
+ it("reports an unmatched precompiled campaign in activation diagnostics", async () => {
116
+ const { getDiagnostics, init } = await import("./index");
117
+
118
+ const assignments = init({
119
+ antiFlickerMode: "off",
120
+ delivery: {
121
+ criticalMutationPlan: [{
122
+ e: "exp_precompiled_url",
123
+ r: [{ type: "simple", pattern: "/products/*" }],
124
+ v: [{ id: "var_precompiled_url", w: 1, m: [] }],
125
+ }],
126
+ },
127
+ endpoint: "https://events.test",
128
+ experiments: [{
129
+ id: "exp_precompiled_url",
130
+ name: "Precompiled URL campaign",
131
+ status: "running",
132
+ targeting: { url: [{ type: "simple", pattern: "/products/*" }] },
133
+ trafficAllocation: 1,
134
+ variations: [{ id: "var_precompiled_url", weight: 1, mutations: [] }],
135
+ }],
136
+ shopId: "shop_123",
137
+ trackingStart: "immediate",
138
+ userId: "visitor_precompiled_url",
139
+ });
140
+
141
+ expect(assignments).toHaveLength(0);
142
+ expect(getDiagnostics().activation.experiments).toEqual([
143
+ expect.objectContaining({
144
+ experimentId: "exp_precompiled_url",
145
+ status: "failed",
146
+ reason: "url_mismatch",
147
+ gates: expect.arrayContaining([
148
+ expect.objectContaining({
149
+ id: "page",
150
+ status: "failed",
151
+ reason: "url_mismatch",
152
+ }),
153
+ expect.objectContaining({
154
+ id: "audience",
155
+ status: "skipped",
156
+ reason: "page_mismatch",
157
+ }),
158
+ ]),
159
+ }),
160
+ ]);
161
+ });
162
+
163
+
164
+ it("reports a precompiled campaign outside its traffic allocation", async () => {
165
+ (globalThis as any).location = {
166
+ hash: "",
167
+ hostname: "plain.test",
168
+ href: "https://plain.test/products/a",
169
+ origin: "https://plain.test",
170
+ pathname: "/products/a",
171
+ search: "",
172
+ };
173
+ const { getDiagnostics, init } = await import("./index");
174
+
175
+ const assignments = init({
176
+ antiFlickerMode: "off",
177
+ delivery: {
178
+ criticalMutationPlan: [{
179
+ e: "exp_precompiled_allocation",
180
+ r: [{ type: "simple", pattern: "/products/*" }],
181
+ v: [{ id: "var_precompiled_allocation", w: 1, m: [] }],
182
+ }],
183
+ },
184
+ endpoint: "https://events.test",
185
+ experiments: [{
186
+ id: "exp_precompiled_allocation",
187
+ name: "Precompiled allocation campaign",
188
+ status: "running",
189
+ targeting: { url: [{ type: "simple", pattern: "/products/*" }] },
190
+ trafficAllocation: 0,
191
+ variations: [{ id: "var_precompiled_allocation", weight: 1, mutations: [] }],
192
+ }],
193
+ shopId: "shop_123",
194
+ trackingStart: "immediate",
195
+ userId: "visitor_precompiled_allocation",
196
+ });
197
+
198
+ expect(assignments).toHaveLength(0);
199
+ expect(getDiagnostics().activation.experiments).toEqual([
200
+ expect.objectContaining({
201
+ experimentId: "exp_precompiled_allocation",
202
+ status: "failed",
203
+ reason: "outside_allocation",
204
+ gates: expect.arrayContaining([
205
+ expect.objectContaining({ id: "page", status: "passed" }),
206
+ expect.objectContaining({
207
+ id: "audience",
208
+ status: "skipped",
209
+ reason: "no_audience_rules",
210
+ }),
211
+ expect.objectContaining({
212
+ id: "traffic",
213
+ status: "failed",
214
+ reason: "outside_allocation",
215
+ }),
216
+ ]),
217
+ }),
218
+ ]);
219
+ });
220
+
221
+
222
+ it("preserves successful precompiled assignment diagnostics", async () => {
223
+ (globalThis as any).location = {
224
+ hash: "",
225
+ hostname: "plain.test",
226
+ href: "https://plain.test/products/a",
227
+ origin: "https://plain.test",
228
+ pathname: "/products/a",
229
+ search: "",
230
+ };
231
+ const { getDiagnostics, init } = await import("./index");
232
+
233
+ const assignments = init({
234
+ antiFlickerMode: "off",
235
+ delivery: {
236
+ criticalMutationPlan: [{
237
+ e: "exp_precompiled_success",
238
+ r: [{ type: "simple", pattern: "/products/*" }],
239
+ v: [{ id: "var_precompiled_success", w: 1, m: [] }],
240
+ }],
241
+ },
242
+ endpoint: "https://events.test",
243
+ experiments: [{
244
+ id: "exp_precompiled_success",
245
+ name: "Precompiled success campaign",
246
+ status: "running",
247
+ trafficAllocation: 1,
248
+ variations: [{ id: "var_precompiled_success", weight: 1, mutations: [], artifactSource: "operator_chat" }],
249
+ }],
250
+ shopId: "shop_123",
251
+ trackingStart: "immediate",
252
+ userId: "visitor_precompiled_success",
253
+ });
254
+
255
+ expect(assignments).toEqual([
256
+ expect.objectContaining({
257
+ experimentId: "exp_precompiled_success",
258
+ variationId: "var_precompiled_success",
259
+ artifactSource: "operator_chat",
260
+ }),
261
+ ]);
262
+ expect(getDiagnostics().activation.experiments).toEqual([
263
+ expect.objectContaining({
264
+ experimentId: "exp_precompiled_success",
265
+ status: "passed",
266
+ reason: "precompiled_assignment",
267
+ assignedVariationId: "var_precompiled_success",
268
+ gates: [
269
+ expect.objectContaining({ id: "page", status: "passed" }),
270
+ expect.objectContaining({ id: "audience", status: "skipped", reason: "no_audience_rules" }),
271
+ expect.objectContaining({ id: "traffic", status: "passed" }),
272
+ expect.objectContaining({ id: "assignment", status: "passed" }),
273
+ ],
274
+ }),
275
+ ]);
276
+ });
277
+
278
+
279
+ it("exposes page targeting failures in activation diagnostics", async () => {
280
+ const { getDiagnostics, init } = await import("./index");
281
+
282
+ init({
283
+ antiFlickerMode: "off",
284
+ endpoint: "https://events.test",
285
+ experiments: [
286
+ {
287
+ id: "exp_page",
288
+ status: "running",
289
+ targeting: {
290
+ url: [{ type: "simple", pattern: "/products/*" }],
291
+ },
292
+ trafficAllocation: 1,
293
+ variations: [{ id: "var_page", weight: 1, mutations: [] }],
294
+ },
295
+ ],
296
+ shopId: "shop_123",
297
+ trackingStart: "immediate",
298
+ });
299
+
300
+ const experiment = getDiagnostics().activation.experiments[0];
301
+ expect(experiment).toMatchObject({
302
+ experimentId: "exp_page",
303
+ status: "failed",
304
+ reason: "url_mismatch",
305
+ });
306
+ expect(experiment.gates.find((gate) => gate.id === "page")).toMatchObject({
307
+ status: "failed",
308
+ reason: "url_mismatch",
309
+ });
310
+ });
311
+
312
+
313
+ it("keeps forced QA assignments off-target while preserving page targeting diagnostics", async () => {
314
+ (globalThis as any).location = {
315
+ hash: "",
316
+ hostname: "plain.test",
317
+ href: "https://plain.test/collections/all?drip_qa=1&drip_force=exp_forced:var_forced",
318
+ origin: "https://plain.test",
319
+ pathname: "/collections/all",
320
+ search: "?drip_qa=1&drip_force=exp_forced:var_forced",
321
+ };
322
+
323
+ const { __test, getDiagnostics, init } = await import("./index");
324
+
325
+ const assignments = init({
326
+ antiFlickerMode: "off",
327
+ endpoint: "https://events.test",
328
+ experiments: [
329
+ {
330
+ id: "exp_forced",
331
+ status: "draft",
332
+ targeting: {
333
+ url: [{ type: "simple", pattern: "/products/target-product" }],
334
+ },
335
+ trafficAllocation: 1,
336
+ variations: [
337
+ { id: "var_control", weight: 0.5, mutations: [] },
338
+ { id: "var_forced", weight: 0.5, mutations: [{ selector: "h1", action: "text", text: "Forced" }] },
339
+ ],
340
+ },
341
+ ],
342
+ shopId: "shop_123",
343
+ trackingStart: "immediate",
344
+ });
345
+
346
+ expect(assignments).toEqual([
347
+ expect.objectContaining({
348
+ experimentId: "exp_forced",
349
+ variationId: "var_forced",
350
+ pageActive: false,
351
+ }),
352
+ ]);
353
+ expect(__test!.artifactRenderableAssignments(assignments)).toEqual([]);
354
+ const experiment = getDiagnostics().activation.experiments[0];
355
+ expect(experiment).toMatchObject({
356
+ experimentId: "exp_forced",
357
+ status: "passed",
358
+ reason: "forced_variation",
359
+ forced: true,
360
+ });
361
+ expect(experiment.gates.find((gate) => gate.id === "environment")).toMatchObject({
362
+ status: "skipped",
363
+ reason: "forced_variation",
364
+ });
365
+ expect(experiment.gates.find((gate) => gate.id === "page")).toMatchObject({
366
+ status: "failed",
367
+ reason: "url_mismatch",
368
+ });
369
+ });
370
+
371
+
372
+ it("does not let forced QA assignment bypass audience targeting", async () => {
373
+ (globalThis as any).location = {
374
+ hash: "",
375
+ hostname: "plain.test",
376
+ href: "https://plain.test/products/a?drip_qa=1&drip_force=exp_audience:var_audience",
377
+ origin: "https://plain.test",
378
+ pathname: "/products/a",
379
+ search: "?drip_qa=1&drip_force=exp_audience:var_audience",
380
+ };
381
+ const { getDiagnostics, init } = await import("./index");
382
+ const assignments = init({
383
+ antiFlickerMode: "off",
384
+ endpoint: "https://events.test",
385
+ experiments: [{
386
+ id: "exp_audience",
387
+ status: "draft",
388
+ targeting: {
389
+ segmentRules: [{
390
+ id: "vip",
391
+ rules: {
392
+ version: 1,
393
+ operator: "and",
394
+ conditions: [{ signal: "customer.tier", operator: "equals", value: "vip" }],
395
+ },
396
+ }],
397
+ },
398
+ trafficAllocation: 1,
399
+ variations: [{ id: "var_audience", weight: 1, mutations: [] }],
400
+ }],
401
+ shopId: "shop_123",
402
+ trackingStart: "immediate",
403
+ });
404
+ expect(assignments).toHaveLength(0);
405
+ expect(getDiagnostics().activation.experiments[0]).toMatchObject({
406
+ experimentId: "exp_audience",
407
+ status: "failed",
408
+ reason: "segment_mismatch",
409
+ forced: false,
410
+ });
411
+ });
412
+
413
+
414
+ it.each([
415
+ [
416
+ "desktop",
417
+ "Win32",
418
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
419
+ ],
420
+ [
421
+ "mobile",
422
+ "iPhone",
423
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 Mobile/15E148 Safari/604.1",
424
+ ],
425
+ [
426
+ "tablet",
427
+ "Linux armv8l",
428
+ "Mozilla/5.0 (Linux; Android 13; Pixel C) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
429
+ ],
430
+ ])("applies a forced QA variation for a matching %s device audience", async (token, platform, userAgent) => {
431
+ Object.defineProperty(globalThis, "navigator", {
432
+ configurable: true,
433
+ value: { userAgent, platform, doNotTrack: "0" },
434
+ });
435
+ (globalThis as any).window.innerWidth = 1280;
436
+ (globalThis as any).location = {
437
+ hash: "",
438
+ hostname: "plain.test",
439
+ href: "https://plain.test/products/a?drip_qa=1&drip_force=exp_device:var_device",
440
+ origin: "https://plain.test",
441
+ pathname: "/products/a",
442
+ search: "?drip_qa=1&drip_force=exp_device:var_device",
443
+ };
444
+ const { getDiagnostics, init } = await import("./index");
445
+ const assignments = init({
446
+ antiFlickerMode: "off",
447
+ endpoint: "https://events.test",
448
+ experiments: [{
449
+ id: "exp_device",
450
+ status: "draft",
451
+ targeting: {
452
+ segmentRules: [{
453
+ id: "device-audience",
454
+ rules: {
455
+ version: 1,
456
+ operator: "and",
457
+ conditions: [{ signal: "device.type", operator: "equals", value: token }],
458
+ },
459
+ }],
460
+ },
461
+ trafficAllocation: 1,
462
+ variations: [{ id: "var_device", weight: 1, mutations: [] }],
463
+ }],
464
+ shopId: "shop_123",
465
+ trackingStart: "immediate",
466
+ });
467
+
468
+ expect(assignments).toEqual([
469
+ expect.objectContaining({ experimentId: "exp_device", variationId: "var_device" }),
470
+ ]);
471
+ expect(getDiagnostics().activation.experiments[0]).toMatchObject({
472
+ experimentId: "exp_device",
473
+ status: "passed",
474
+ forced: true,
475
+ });
476
+ });
477
+
478
+
479
+ it("blocks a forced QA variation when the detected device does not match", async () => {
480
+ Object.defineProperty(globalThis, "navigator", {
481
+ configurable: true,
482
+ value: {
483
+ userAgent:
484
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
485
+ platform: "Win32",
486
+ doNotTrack: "0",
487
+ },
488
+ });
489
+ (globalThis as any).window.innerWidth = 390;
490
+ (globalThis as any).location = {
491
+ hash: "",
492
+ hostname: "plain.test",
493
+ href: "https://plain.test/products/a?drip_qa=1&drip_force=exp_device_mismatch:var_device",
494
+ origin: "https://plain.test",
495
+ pathname: "/products/a",
496
+ search: "?drip_qa=1&drip_force=exp_device_mismatch:var_device",
497
+ };
498
+ const { getDiagnostics, init } = await import("./index");
499
+ const assignments = init({
500
+ antiFlickerMode: "off",
501
+ endpoint: "https://events.test",
502
+ experiments: [{
503
+ id: "exp_device_mismatch",
504
+ status: "draft",
505
+ targeting: {
506
+ segmentRules: [{
507
+ id: "mobile-audience",
508
+ rules: {
509
+ version: 1,
510
+ operator: "and",
511
+ conditions: [{ signal: "device.type", operator: "equals", value: "mobile" }],
512
+ },
513
+ }],
514
+ },
515
+ trafficAllocation: 1,
516
+ variations: [{ id: "var_device", weight: 1, mutations: [] }],
517
+ }],
518
+ shopId: "shop_123",
519
+ trackingStart: "immediate",
520
+ });
521
+
522
+ expect(assignments).toHaveLength(0);
523
+ const experiment = getDiagnostics().activation.experiments[0];
524
+ expect(experiment).toMatchObject({
525
+ experimentId: "exp_device_mismatch",
526
+ status: "failed",
527
+ reason: "segment_mismatch",
528
+ forced: false,
529
+ });
530
+ expect(experiment.gates.find((gate) => gate.id === "audience")).toMatchObject({
531
+ status: "failed",
532
+ reason: "segment_mismatch",
533
+ });
534
+ expect(experiment.targetingConditions).toContainEqual(expect.objectContaining({
535
+ signal: "device.type",
536
+ expected: "mobile",
537
+ actual: "desktop",
538
+ }));
539
+ });
540
+
541
+
542
+ it("does not persist forced QA assignments over sticky assignment storage", async () => {
543
+ (globalThis as any).location = {
544
+ hash: "",
545
+ hostname: "plain.test",
546
+ href: "https://plain.test/products/a?drip_qa=1&drip_force=exp_forced:var_forced",
547
+ origin: "https://plain.test",
548
+ pathname: "/products/a",
549
+ search: "?drip_qa=1&drip_force=exp_forced:var_forced",
550
+ };
551
+ localStorage.setItem(
552
+ "drip_assignments_v1",
553
+ JSON.stringify({ exp_forced: ["var_control", 1, 123] }),
554
+ );
555
+
556
+ const { init } = await import("./index");
557
+
558
+ const assignments = init({
559
+ antiFlickerMode: "off",
560
+ endpoint: "https://events.test",
561
+ experiments: [
562
+ {
563
+ id: "exp_forced",
564
+ status: "draft",
565
+ targeting: {
566
+ url: [{ type: "simple", pattern: "/products/a" }],
567
+ },
568
+ trafficAllocation: 1,
569
+ assignmentEpoch: 1,
570
+ variations: [
571
+ { id: "var_control", weight: 1, mutations: [] },
572
+ { id: "var_forced", weight: 0, mutations: [] },
573
+ ],
574
+ },
575
+ ],
576
+ shopId: "shop_123",
577
+ trackingStart: "immediate",
578
+ });
579
+
580
+ expect(assignments[0]?.variationId).toBe("var_forced");
581
+ expect(JSON.parse(localStorage.getItem("drip_assignments_v1") ?? "{}")).toEqual({
582
+ exp_forced: ["var_control", 1, 123],
583
+ });
584
+ });
585
+
586
+
587
+ it("applies forced control mutations, CSS, and JS in QA preview", async () => {
588
+ (globalThis as any).location = {
589
+ hash: "",
590
+ hostname: "plain.test",
591
+ href: "https://plain.test/products/a?drip_qa=1&drip_force=exp_control:var_control",
592
+ origin: "https://plain.test",
593
+ pathname: "/products/a",
594
+ search: "?drip_qa=1&drip_force=exp_control:var_control",
595
+ };
596
+ const title = { textContent: "Original headline" };
597
+ const styles: Array<Record<string, any>> = [];
598
+ (globalThis as any).document.createElement = vi.fn((tag: string) => ({
599
+ id: "",
600
+ className: "",
601
+ remove: vi.fn(),
602
+ tagName: tag.toUpperCase(),
603
+ textContent: "",
604
+ }));
605
+ (globalThis as any).document.querySelectorAll = vi.fn((selector: string) => (
606
+ selector === "#control-title" ? [title] : []
607
+ ));
608
+ (globalThis as any).document.getElementById = vi.fn((id: string) => (
609
+ styles.find((element) => element.id === id) ?? null
610
+ ));
611
+ (globalThis as any).document.head.appendChild = vi.fn((element: Record<string, any>) => {
612
+ styles.push(element);
613
+ return element;
614
+ });
615
+
616
+ const { init } = await import("./index");
617
+
618
+ const assignments = init({
619
+ antiFlickerMode: "off",
620
+ endpoint: "https://events.test",
621
+ experiments: [
622
+ {
623
+ id: "exp_control",
624
+ status: "draft",
625
+ targeting: { url: [{ type: "simple", pattern: "/products/a" }] },
626
+ trafficAllocation: 1,
627
+ variations: [
628
+ {
629
+ id: "var_control",
630
+ weight: 0,
631
+ isControl: true,
632
+ mutations: [{ selector: "#control-title", action: "text", text: "Prepared control headline" }],
633
+ css: "#control-title{display:block}",
634
+ js: "window.__forcedControlJs=true;",
635
+ },
636
+ { id: "var_treatment", weight: 1, mutations: [] },
637
+ ],
638
+ },
639
+ ],
640
+ shopId: "shop_123",
641
+ spa: { activationMode: "manual" },
642
+ trackingStart: "immediate",
643
+ });
644
+ await Promise.resolve();
645
+ await Promise.resolve();
646
+
647
+ expect(assignments[0]).toMatchObject({ variationId: "var_control", isControl: true });
648
+ expect(title.textContent).toBe("Prepared control headline");
649
+ expect((globalThis as any).window.__forcedControlJs).toBe(true);
650
+ expect(styles.find((style) => style.id === "drip-css-exp_control-var_control")?.textContent)
651
+ .toBe("#control-title{display:block}");
652
+ });
653
+
654
+
655
+ it("does not mount legacy devtools alongside QA Assistant in QA mode", async () => {
656
+ (globalThis as any).location.search = "?drip_qa=1&drip_debug=1";
657
+ (globalThis as any).location.href = "https://plain.test/?drip_qa=1&drip_debug=1";
658
+ const mountQaAssistant = vi.fn(() => ({ destroy: vi.fn() }));
659
+ const mountDevtools = vi.fn();
660
+ vi.doMock("./qa-assistant", () => ({ mountQaAssistant }));
661
+ vi.doMock("./devtools", () => ({
662
+ isDevtoolsEnabled: vi.fn(() => true),
663
+ logDevtoolsEvent: vi.fn(),
664
+ mountDevtools,
665
+ unmountDevtools: vi.fn(),
666
+ }));
667
+
668
+ const { init } = await import("./index");
669
+ init({
670
+ antiFlickerMode: "off",
671
+ endpoint: "https://events.test",
672
+ experiments: [],
673
+ shopId: "shop_123",
674
+ trackingStart: "immediate",
675
+ });
676
+ await flushDynamicImports();
677
+
678
+ expect(mountQaAssistant).toHaveBeenCalledTimes(1);
679
+ expect(mountDevtools).not.toHaveBeenCalled();
680
+ });
681
+
682
+
683
+ it("falls back to the legacy QA toolbar and devtools when QA Assistant is unavailable", async () => {
684
+ (globalThis as any).location.search = "?drip_qa=1&drip_debug=1";
685
+ (globalThis as any).location.href = "https://plain.test/?drip_qa=1&drip_debug=1";
686
+ const mountQaAssistant = vi.fn(() => null);
687
+ const mountQaToolbar = vi.fn();
688
+ const mountDevtools = vi.fn();
689
+ vi.doMock("./qa-assistant", () => ({ mountQaAssistant }));
690
+ vi.doMock("./qa-toolbar", () => ({ mountQaToolbar }));
691
+ vi.doMock("./devtools", () => ({
692
+ isDevtoolsEnabled: vi.fn(() => true),
693
+ logDevtoolsEvent: vi.fn(),
694
+ mountDevtools,
695
+ unmountDevtools: vi.fn(),
696
+ }));
697
+
698
+ const { init } = await import("./index");
699
+ init({
700
+ antiFlickerMode: "off",
701
+ endpoint: "https://events.test",
702
+ experiments: [],
703
+ shopId: "shop_123",
704
+ trackingStart: "immediate",
705
+ });
706
+ await flushDynamicImports();
707
+
708
+ expect(mountQaAssistant).toHaveBeenCalledTimes(1);
709
+ expect(mountQaToolbar).toHaveBeenCalledTimes(1);
710
+ expect(mountDevtools).toHaveBeenCalledTimes(1);
711
+ });
712
+
713
+
714
+ it("still mounts legacy devtools outside QA mode when explicitly requested", async () => {
715
+ (globalThis as any).location.search = "?drip_devtools=1";
716
+ (globalThis as any).location.href = "https://plain.test/?drip_devtools=1";
717
+ const mountQaAssistant = vi.fn(() => ({ destroy: vi.fn() }));
718
+ const mountDevtools = vi.fn();
719
+ vi.doMock("./qa-assistant", () => ({ mountQaAssistant }));
720
+ vi.doMock("./devtools", () => ({
721
+ isDevtoolsEnabled: vi.fn(() => true),
722
+ logDevtoolsEvent: vi.fn(),
723
+ mountDevtools,
724
+ unmountDevtools: vi.fn(),
725
+ }));
726
+
727
+ const { init } = await import("./index");
728
+ init({
729
+ antiFlickerMode: "off",
730
+ endpoint: "https://events.test",
731
+ experiments: [],
732
+ shopId: "shop_123",
733
+ trackingStart: "immediate",
734
+ });
735
+ await flushDynamicImports();
736
+
737
+ expect(mountQaAssistant).not.toHaveBeenCalled();
738
+ expect(mountDevtools).toHaveBeenCalledTimes(1);
739
+ });
740
+ });