@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.
Files changed (246) 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 +19283 -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 +19267 -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 +16636 -13533
  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 +28 -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 +2071 -0
  128. package/src/consent-evaluation-borlabs.test.ts +41 -0
  129. package/src/consent-evaluation-cookiescript.test.ts +50 -0
  130. package/src/consent-evaluation.ts +151 -0
  131. package/src/css.test.ts +30 -0
  132. package/src/css.ts +120 -6
  133. package/src/cwv.test.ts +60 -77
  134. package/src/debug-loader.test.ts +214 -0
  135. package/src/debug-loader.ts +41 -0
  136. package/src/decide-assignments.test.ts +196 -0
  137. package/src/decide-assignments.ts +204 -0
  138. package/src/delivery-timing.test.ts +104 -0
  139. package/src/delivery-timing.ts +89 -0
  140. package/src/early-interaction-capture.test.ts +451 -0
  141. package/src/early-interaction-capture.ts +230 -0
  142. package/src/early-reassert.test.ts +207 -0
  143. package/src/early-reassert.ts +152 -0
  144. package/src/edge-adoption.test.ts +435 -0
  145. package/src/edge-runtime.ts +30 -0
  146. package/src/editor/api.test.ts +109 -0
  147. package/src/editor/api.ts +34 -3
  148. package/src/editor/controller.test.ts +297 -14
  149. package/src/editor/controller.ts +69 -5
  150. package/src/editor/index.test.ts +2 -0
  151. package/src/editor/index.ts +1 -0
  152. package/src/editor/publication-result.ts +58 -0
  153. package/src/editor/types.ts +10 -1
  154. package/src/editor/view-kit.ts +2 -1
  155. package/src/editor/view.test.ts +108 -0
  156. package/src/editor/view.ts +49 -7
  157. package/src/endpoint.test.ts +15 -0
  158. package/src/endpoint.ts +10 -0
  159. package/src/exclusion-groups.ts +8 -18
  160. package/src/flags.test.ts +88 -1
  161. package/src/flags.ts +29 -29
  162. package/src/global-runtime-reconciler.test.ts +271 -0
  163. package/src/goal-eligibility.test.ts +29 -0
  164. package/src/goal-eligibility.ts +17 -0
  165. package/src/goals.test.ts +144 -1
  166. package/src/goals.ts +97 -18
  167. package/src/guards.test.ts +99 -1
  168. package/src/guards.ts +34 -15
  169. package/src/handover-cache-order.test.ts +6 -3
  170. package/src/handover.test.ts +154 -8
  171. package/src/handover.ts +122 -12
  172. package/src/hash.ts +29 -54
  173. package/src/holdout.test.ts +26 -0
  174. package/src/hydration.test.ts +73 -2
  175. package/src/hydration.ts +18 -9
  176. package/src/index.ts +130 -7287
  177. package/src/init-analytics.test.ts +1694 -0
  178. package/src/init-anti-flicker.test.ts +1211 -0
  179. package/src/init-cart-attributes.test.ts +2003 -0
  180. package/src/init-config-resolution.test.ts +999 -0
  181. package/src/init-consent.test.ts +971 -0
  182. package/src/init-empty.test.ts +11 -5406
  183. package/src/init-integrations.test.ts +846 -0
  184. package/src/init-page-activation.test.ts +5064 -0
  185. package/src/init-qa-loading.test.ts +740 -0
  186. package/src/init-redirects.test.ts +2156 -0
  187. package/src/init-test-helpers.ts +287 -0
  188. package/src/inline-bootstrap.test.ts +184 -0
  189. package/src/inline-bootstrap.ts +73 -0
  190. package/src/js.test.ts +51 -1
  191. package/src/js.ts +86 -2
  192. package/src/money.test.ts +55 -1
  193. package/src/money.ts +104 -2
  194. package/src/money.web-parity.contract.test.ts +85 -0
  195. package/src/mutate.test.ts +474 -10
  196. package/src/mutate.ts +739 -230
  197. package/src/page-trigger.test.ts +218 -8
  198. package/src/performance.ts +10 -0
  199. package/src/persistence-decoders.ts +18 -0
  200. package/src/public-api.ts +1828 -0
  201. package/src/qa-assistant-state.test.ts +249 -3
  202. package/src/qa-assistant-state.ts +146 -6
  203. package/src/qa-assistant-view.test.ts +220 -2
  204. package/src/qa-assistant-view.ts +102 -5
  205. package/src/react/index.test.tsx +37 -0
  206. package/src/react/index.tsx +19 -3
  207. package/src/reassert-sentinel.test.ts +155 -1
  208. package/src/reassert-sentinel.ts +168 -8
  209. package/src/redirect.test.ts +541 -0
  210. package/src/redirect.ts +572 -0
  211. package/src/replay-recorder-entry.ts +115 -0
  212. package/src/replay-signal-state.ts +100 -0
  213. package/src/replay-signals.test.ts +249 -0
  214. package/src/replay-signals.ts +151 -0
  215. package/src/replay.test.ts +764 -0
  216. package/src/replay.ts +514 -0
  217. package/src/router.test.ts +61 -2
  218. package/src/router.ts +50 -12
  219. package/src/runtime-state.ts +507 -0
  220. package/src/runtime.test.ts +174 -7
  221. package/src/runtime.ts +88 -14
  222. package/src/sanitize.test.ts +34 -0
  223. package/src/sanitize.ts +23 -12
  224. package/src/shadow.test.ts +187 -0
  225. package/src/shadow.ts +99 -0
  226. package/src/shopify-cart.test.ts +733 -23
  227. package/src/shopify-cart.ts +402 -49
  228. package/src/storefront-vitals.test.ts +217 -0
  229. package/src/storefront-vitals.ts +206 -0
  230. package/src/surface-signals.ts +6 -0
  231. package/src/targeting-conformance.test.ts +9 -0
  232. package/src/targeting-diagnostics.ts +38 -8
  233. package/src/targeting-parity-corpus.json +559 -0
  234. package/src/targeting-parity-corpus.test.ts +18 -0
  235. package/src/targeting.detailed.test.ts +105 -0
  236. package/src/targeting.test.ts +211 -0
  237. package/src/targeting.ts +88 -207
  238. package/src/track.test.ts +147 -0
  239. package/src/track.ts +125 -26
  240. package/src/types.ts +120 -13
  241. package/src/wait.test.ts +23 -0
  242. package/src/wait.ts +129 -13
  243. package/src/weekday-delivery.test.ts +56 -0
  244. package/src/weekday-schedule-parity-corpus.json +92 -0
  245. package/src/weekday-schedule.test.ts +69 -0
  246. package/src/weekday-schedule.ts +20 -0
@@ -82,6 +82,33 @@ describe("targeting", () => {
82
82
  expect(matchesTargeting(undefined, { url: "https://shop.test/p", attributes: {} })).toBe(true);
83
83
  });
84
84
 
85
+ it("never matches the invalid stored-targeting sentinel", () => {
86
+ const targeting: Targeting = {
87
+ attributes: [
88
+ {
89
+ attribute: "apex.internal.invalid_stored_targeting",
90
+ operator: "equals",
91
+ value: "experiment:exp_unsafe",
92
+ },
93
+ ],
94
+ };
95
+
96
+ expect(
97
+ matchesTargeting(targeting, {
98
+ url: "https://shop.test/products/shirt",
99
+ attributes: {},
100
+ }),
101
+ ).toBe(false);
102
+ expect(
103
+ matchesTargeting(targeting, {
104
+ url: "https://shop.test/products/shirt",
105
+ attributes: {
106
+ "apex.internal.invalid_stored_targeting": "experiment:exp_unsafe",
107
+ },
108
+ }),
109
+ ).toBe(false);
110
+ });
111
+
85
112
  it("evaluates Page rules before Audience rules and skips Audience on page mismatch", () => {
86
113
  const targeting: Targeting = {
87
114
  pageRules: [
@@ -208,6 +235,102 @@ describe("targeting", () => {
208
235
  ).toBe(true);
209
236
  });
210
237
 
238
+ it("scopes host-agnostic include rules to every configured public storefront origin", () => {
239
+ const targeting: Targeting = {
240
+ url: [{ type: "simple", pattern: "/products/*", include: true }],
241
+ };
242
+
243
+ expect(
244
+ matchesTargeting(targeting, {
245
+ url: "https://shop.test/products/source",
246
+ attributes: {},
247
+ hostAgnosticIncludeOrigin: [
248
+ "https://shop.test",
249
+ "https://de.shop.test",
250
+ ],
251
+ }),
252
+ ).toBe(true);
253
+ expect(
254
+ matchesTargeting(targeting, {
255
+ url: "https://de.shop.test/products/source",
256
+ attributes: {},
257
+ hostAgnosticIncludeOrigin: [
258
+ "https://shop.test",
259
+ "https://de.shop.test",
260
+ ],
261
+ }),
262
+ ).toBe(true);
263
+ expect(
264
+ matchesTargeting(targeting, {
265
+ url: "https://landing.test/products/destination",
266
+ attributes: {},
267
+ hostAgnosticIncludeOrigin: [
268
+ "https://shop.test",
269
+ "https://de.shop.test",
270
+ ],
271
+ }),
272
+ ).toBe(false);
273
+ });
274
+
275
+ it("keeps explicit-host include rules independent from the storefront origin", () => {
276
+ const targeting: Targeting = {
277
+ url: [
278
+ {
279
+ type: "simple",
280
+ pattern: "landing.test/products/*",
281
+ include: true,
282
+ },
283
+ ],
284
+ };
285
+
286
+ expect(
287
+ matchesTargeting(targeting, {
288
+ url: "https://landing.test/products/destination",
289
+ attributes: {},
290
+ hostAgnosticIncludeOrigin: "https://shop.test",
291
+ }),
292
+ ).toBe(true);
293
+ });
294
+
295
+ it("keeps regex-prefixed absolute include rules independent from the storefront origin", () => {
296
+ for (const pattern of [
297
+ "^https?:\\/\\/other\\.test/products/.*$",
298
+ "^(https?://other\\.test/products/.*)$",
299
+ ]) {
300
+ const targeting: Targeting = {
301
+ url: [{ type: "regex", pattern, include: true }],
302
+ };
303
+
304
+ expect(
305
+ matchesTargeting(targeting, {
306
+ url: "https://other.test/products/destination",
307
+ attributes: {},
308
+ hostAgnosticIncludeOrigin: "https://shop.test",
309
+ }),
310
+ ).toBe(true);
311
+ }
312
+ });
313
+
314
+ it("does not treat an embedded query-parameter URL as an explicit host", () => {
315
+ const targeting: Targeting = {
316
+ url: [
317
+ {
318
+ type: "simple",
319
+ pattern: "/products?return=https://example.com",
320
+ include: true,
321
+ },
322
+ ],
323
+ };
324
+
325
+ expect(
326
+ matchesTargeting(targeting, {
327
+ url: "https://other.test/products?return=https://example.com",
328
+ attributes: {},
329
+ hostAgnosticIncludeOrigin: "https://shop.test",
330
+ }),
331
+ ).toBe(false);
332
+ });
333
+
211
334
  it("normalizes legacy url_contains rules at runtime", () => {
212
335
  const targeting = {
213
336
  url: "https://www.abtestinglibrary.com/products/book",
@@ -424,6 +547,30 @@ describe("targeting", () => {
424
547
  ).toBe(false);
425
548
  });
426
549
 
550
+ it("matches not_in operator when value is outside the array", () => {
551
+ const targeting: Targeting = {
552
+ attributes: [
553
+ {
554
+ attribute: "device.type",
555
+ operator: "not_in",
556
+ value: ["desktop"],
557
+ },
558
+ ],
559
+ };
560
+ expect(
561
+ matchesTargeting(targeting, {
562
+ url: "https://shop.test/",
563
+ attributes: { "device.type": "mobile" },
564
+ }),
565
+ ).toBe(true);
566
+ expect(
567
+ matchesTargeting(targeting, {
568
+ url: "https://shop.test/",
569
+ attributes: { "device.type": "desktop" },
570
+ }),
571
+ ).toBe(false);
572
+ });
573
+
427
574
  it("rejects contains when value is not string", () => {
428
575
  const targeting: Targeting = {
429
576
  attributes: [{ attribute: "count", operator: "contains", value: "3" }],
@@ -586,6 +733,70 @@ describe("targeting", () => {
586
733
  );
587
734
  });
588
735
 
736
+ it("normalizes valid device overrides and falls back from empty or invalid overrides", () => {
737
+ setRuntimeEnv({
738
+ platform: "iPhone",
739
+ userAgent:
740
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 Mobile/15E148 Safari/604.1",
741
+ width: 1440,
742
+ });
743
+ expect(
744
+ buildTargetingAttributes({ device: "" }, undefined, { persistSignals: false })["device.type"],
745
+ ).toBe("mobile");
746
+ expect(
747
+ buildTargetingAttributes({ device: "Desktop" }, undefined, { persistSignals: false })["device.type"],
748
+ ).toBe("desktop");
749
+ expect(
750
+ buildTargetingAttributes({ "device.type": " Tablet " }, undefined, { persistSignals: false })["device.type"],
751
+ ).toBe("tablet");
752
+
753
+ setRuntimeEnv({
754
+ platform: "Win32",
755
+ userAgent:
756
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
757
+ width: 390,
758
+ });
759
+ expect(
760
+ buildTargetingAttributes({ device: "television" }, undefined, { persistSignals: false })["device.type"],
761
+ ).toBe("desktop");
762
+ });
763
+
764
+ it.each([
765
+ [
766
+ "desktop",
767
+ "Win32",
768
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
769
+ ],
770
+ [
771
+ "mobile",
772
+ "iPhone",
773
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 Mobile/15E148 Safari/604.1",
774
+ ],
775
+ [
776
+ "tablet",
777
+ "Linux armv8l",
778
+ "Mozilla/5.0 (Linux; Android 13; Pixel C) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
779
+ ],
780
+ ])("matches only the %s device audience for its detected user agent", (token, platform, userAgent) => {
781
+ setRuntimeEnv({ platform, userAgent, width: 1280 });
782
+ const attributes = buildTargetingAttributes({}, undefined, { persistSignals: false });
783
+ const targetingFor = (value: string): Targeting => ({
784
+ segmentRules: [{
785
+ id: `device-${value}`,
786
+ rules: {
787
+ version: 1,
788
+ operator: "and",
789
+ conditions: [{ signal: "device.type", operator: "equals", value }],
790
+ },
791
+ }],
792
+ });
793
+
794
+ expect(attributes["device.type"]).toBe(token);
795
+ expect(matchesTargeting(targetingFor(token), { url: "https://shop.test/", attributes })).toBe(true);
796
+ const mismatchedToken = token === "desktop" ? "mobile" : "desktop";
797
+ expect(matchesTargeting(targetingFor(mismatchedToken), { url: "https://shop.test/", attributes })).toBe(false);
798
+ });
799
+
589
800
  it("classifies Android client-hints with mobile=false as tablet, not desktop", () => {
590
801
  setRuntimeEnv({
591
802
  platform: "",
package/src/targeting.ts CHANGED
@@ -10,142 +10,47 @@ import {
10
10
  UrlRule,
11
11
  } from "./types";
12
12
  import { createApexRuntimeTools } from "./runtime-tools";
13
- import { safeRegexTest } from "./safe-regex";
13
+ import { evaluateUrlRule, matchSegmentConditionValues as matchCoreSegmentConditionValues, normalizeUrlRules, urlRuleExplicitlyNamesHost } from "@drip/event-schema/targeting-core";
14
14
  import { getSignals, rememberProduct } from "./signals";
15
15
  import { evaluateTargetingGatesWithConditionDiagnostics } from "./targeting-diagnostics";
16
16
 
17
17
  declare const __DRIP_QA_BUILD__: boolean;
18
18
 
19
- function evalSimpleUrlPart(actual: string, pattern: string, isPath: boolean): boolean {
20
- try {
21
- let escaped = pattern
22
- .replace(/[*.+?^${}()|[\]\\]/g, "\\$&")
23
- .replace(/_____/g, ".*");
24
- if (isPath) {
25
- escaped = "\\/?" + escaped.replace(/(^\/|\/$)/g, "") + "\\/?";
26
- }
27
- const regex = new RegExp("^" + escaped + "$", "i");
28
- return regex.test(actual);
29
- } catch {
30
- return false;
31
- }
32
- }
33
-
34
- function evalSimpleUrlTarget(actual: URL, pattern: string): boolean {
35
- try {
36
- const expected = new URL(
37
- pattern.replace(/^([^:/?]*)\./i, "https://$1.").replace(/\*/g, "_____"),
38
- "https://_____",
39
- );
40
- const comps: Array<[string, string, boolean]> = [
41
- [actual.host, expected.host, false],
42
- [actual.pathname, expected.pathname, true],
43
- ];
44
- if (expected.hash) comps.push([actual.hash, expected.hash, false]);
45
- expected.searchParams.forEach((v, k) => {
46
- comps.push([actual.searchParams.get(k) || "", v, false]);
47
- });
48
- return !comps.some((data) => !evalSimpleUrlPart(data[0], data[1], data[2]));
49
- } catch {
50
- return false;
51
- }
52
- }
19
+ const FAIL_CLOSED_TARGETING_ATTRIBUTE =
20
+ "apex.internal.invalid_stored_targeting";
53
21
 
54
- function unescapeRegexLiteral(value: string): string {
55
- return value.replace(/\\([.*+?^${}()|[\]\\\/])/g, "$1");
56
- }
22
+ export const evalUrlRule = evaluateUrlRule;
57
23
 
58
- export function evalUrlRule(url: string, rule: UrlRule): boolean {
59
- try {
60
- const parsed = new URL(url, "https://_");
61
- const matchType = rule.matchType ?? rule.match_type;
62
- if (matchType === "contains") {
63
- const actual = parsed.href.toLowerCase();
64
- const pattern = rule.pattern.toLowerCase();
65
- return actual.includes(pattern) || actual.includes(unescapeRegexLiteral(pattern));
66
- }
67
- if (rule.type === "regex") {
68
- const escaped = rule.pattern.replace(/([^\\])\//g, "$1\\/");
69
- return safeRegexTest(escaped, parsed.href) || safeRegexTest(escaped, parsed.href.substring(parsed.origin.length));
70
- }
71
- return evalSimpleUrlTarget(parsed, rule.pattern);
72
- } catch {
73
- return false;
74
- }
24
+ export function normalizeRuntimeUrlRules(targeting: Targeting | undefined): UrlRule[] {
25
+ return normalizeUrlRules(targeting) as UrlRule[];
75
26
  }
76
27
 
77
- function normalizeRuntimeUrlRule(value: unknown): UrlRule | null {
78
- if (typeof value === "string") {
79
- const pattern = value.trim();
80
- return pattern ? { type: "simple", pattern, include: true } : null;
28
+ function normalizedHttpOrigins(value: string | string[] | undefined): Set<string> {
29
+ const origins = new Set<string>();
30
+ for (const candidate of Array.isArray(value) ? value : value ? [value] : []) {
31
+ try { const parsed = new URL(candidate); if (/^https?:$/.test(parsed.protocol)) origins.add(parsed.origin); } catch {}
81
32
  }
82
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
83
-
84
- const raw = value as Record<string, unknown>;
85
- const rawPattern = raw.pattern ?? raw.value ?? raw.url ?? raw.path;
86
- const pattern = typeof rawPattern === "string" ? rawPattern.trim() : "";
87
- if (!pattern) return null;
88
-
89
- const include = typeof raw.include === "boolean" ? raw.include : true;
90
- const matchType =
91
- raw.matchType === "exact" ||
92
- raw.matchType === "contains" ||
93
- raw.matchType === "wildcard" ||
94
- raw.matchType === "regex"
95
- ? raw.matchType
96
- : raw.match_type === "exact" ||
97
- raw.match_type === "contains" ||
98
- raw.match_type === "wildcard" ||
99
- raw.match_type === "regex"
100
- ? raw.match_type
101
- : raw.type === "url_contains"
102
- ? "contains"
103
- : undefined;
104
-
105
- if (raw.type === "url_contains") {
106
- return { type: "simple", pattern, include, matchType: "contains" };
107
- }
108
- return {
109
- type: raw.type === "regex" || raw.type === "url_regex" ? "regex" : "simple",
110
- pattern,
111
- include,
112
- ...(matchType ? { matchType } : {}),
113
- };
114
- }
115
-
116
- export function normalizeRuntimeUrlRules(targeting: Targeting | undefined): UrlRule[] {
117
- const raw = targeting as unknown as Record<string, unknown> | undefined;
118
- if (!raw || typeof raw !== "object") return [];
119
-
120
- const urlSource = Array.isArray(raw.url)
121
- ? raw.url
122
- : raw.url === undefined || raw.url === null
123
- ? []
124
- : [raw.url];
125
- const legacyRules = Array.isArray(raw.rules) ? raw.rules : [];
126
-
127
- return urlSource
128
- .concat(legacyRules)
129
- .map(normalizeRuntimeUrlRule)
130
- .filter((rule): rule is UrlRule => Boolean(rule));
33
+ return origins;
131
34
  }
132
35
 
133
- export function matchUrlRules(url: string, rules: UrlRule[]): boolean {
36
+ export function matchUrlRules(url: string, rules: UrlRule[], hostAgnosticIncludeOrigin?: string | string[]): boolean {
134
37
  if (!rules.length) return true;
135
- let hasInclude = false;
136
- let included = false;
38
+ let hasInclude = false; let included = false;
137
39
  for (const rule of rules) {
138
- const match = evalUrlRule(url, rule);
139
- if (rule.include === false) {
140
- if (match) return false;
141
- } else {
142
- hasInclude = true;
143
- if (match) included = true;
144
- }
40
+ const match = evalUrlRuleForIncludeOrigin(url, rule, hostAgnosticIncludeOrigin);
41
+ if (rule.include === false) { if (match) return false; }
42
+ else { hasInclude = true; if (match) included = true; }
145
43
  }
146
44
  return included || !hasInclude;
147
45
  }
148
46
 
47
+ export function evalUrlRuleForIncludeOrigin(url: string, rule: UrlRule, origins?: string | string[]): boolean {
48
+ const allowed = normalizedHttpOrigins(origins);
49
+ let origin = "";
50
+ try { origin = new URL(url, "https://_").origin; } catch {}
51
+ return (rule.include === false || !allowed.size || urlRuleExplicitlyNamesHost(rule) || allowed.has(origin)) && evaluateUrlRule(url, rule);
52
+ }
53
+
149
54
  function errorMessage(error: unknown): string {
150
55
  return error instanceof Error ? error.message : String(error);
151
56
  }
@@ -216,6 +121,10 @@ export function pageManualTriggerMatches(
216
121
  rule: PageTargetingRule,
217
122
  ctx: { attributes: Record<string, unknown> },
218
123
  ): boolean {
124
+ // Shop defaults map to page overrides as follows: direct -> immediate,
125
+ // url_change -> history/polling, dom_change -> dom, manual -> manual-*.
126
+ // Non-manual modes choose when this rule is checked; they are not an
127
+ // additional targeting condition.
219
128
  const mode = rule.trigger?.mode;
220
129
  if (mode !== "manual-callback" && mode !== "manual-api") return true;
221
130
  const signal = rule.trigger?.signal;
@@ -223,7 +132,7 @@ export function pageManualTriggerMatches(
223
132
  return ctx.attributes[signal] === true;
224
133
  }
225
134
 
226
- function matchPageTargetingRule(
135
+ export function matchPageTargetingRule(
227
136
  rule: PageTargetingRule,
228
137
  ctx: { url: string; attributes: Record<string, unknown> },
229
138
  ): boolean {
@@ -234,6 +143,17 @@ function matchPageTargetingRule(
234
143
  return urlMatched && pageManualTriggerMatches(rule, ctx) && evaluatePageAdvancedCondition(rule, ctx);
235
144
  }
236
145
 
146
+ export function matchedPageRuleIds(
147
+ rules: PageTargetingRule[],
148
+ ctx: { url: string; attributes: Record<string, unknown> },
149
+ ): Set<string> {
150
+ const ids = new Set<string>();
151
+ for (const rule of rules) {
152
+ if (rule.id && matchPageTargetingRule(rule, ctx)) ids.add(rule.id);
153
+ }
154
+ return ids;
155
+ }
156
+
237
157
  function matchPageRules(
238
158
  rules: PageTargetingRule[],
239
159
  ctx: { url: string; attributes: Record<string, unknown> },
@@ -259,6 +179,7 @@ export function matchAttributeRule(
259
179
  attrs: Record<string, unknown>,
260
180
  rule: AttributeRule,
261
181
  ): boolean {
182
+ if (rule.attribute === FAIL_CLOSED_TARGETING_ATTRIBUTE) return false;
262
183
  const value = attrs[rule.attribute];
263
184
  switch (rule.operator) {
264
185
  case "exists":
@@ -271,6 +192,8 @@ export function matchAttributeRule(
271
192
  : false;
272
193
  case "in":
273
194
  return Array.isArray(rule.value) ? rule.value.includes(String(value)) : false;
195
+ case "not_in":
196
+ return Array.isArray(rule.value) ? !rule.value.includes(String(value)) : false;
274
197
  default:
275
198
  return false;
276
199
  }
@@ -377,6 +300,14 @@ function getDeviceType(): string {
377
300
  return "desktop";
378
301
  }
379
302
 
303
+ const DEVICE_TYPE_TOKENS = new Set(["desktop", "mobile", "tablet"]);
304
+
305
+ function normalizeDeviceType(value: unknown): string | undefined {
306
+ if (typeof value !== "string") return undefined;
307
+ const normalized = value.trim().toLowerCase();
308
+ return DEVICE_TYPE_TOKENS.has(normalized) ? normalized : undefined;
309
+ }
310
+
380
311
  function browserInfoFromBrands(brands: unknown): BrowserInfo {
381
312
  if (!Array.isArray(brands)) return EMPTY_BROWSER_INFO;
382
313
 
@@ -559,7 +490,7 @@ function normalizeTrafficSource(utmSource: unknown, referrer: unknown): string {
559
490
  return "direct";
560
491
  }
561
492
 
562
- function readShopifySignal(signal: string): unknown {
493
+ export function readShopifySignal(signal: string): unknown {
563
494
  if (typeof window === "undefined") return undefined;
564
495
  const root = (window as any).ShopifyAnalytics?.meta ?? (window as any).Shopify?.analytics?.meta;
565
496
  if (!root || typeof root !== "object") return undefined;
@@ -599,7 +530,8 @@ export function buildTargetingAttributes(
599
530
  attrs["utm.medium"] ??= baseAttributes.utm_medium ?? getPersistedUtmValue("utm_medium");
600
531
  attrs["utm.campaign"] ??= baseAttributes.utm_campaign ?? getPersistedUtmValue("utm_campaign");
601
532
  attrs.referrer ??= normalizeReferrer();
602
- attrs["device.type"] ??= baseAttributes.device ?? getDeviceType();
533
+ const deviceTypeOverride = attrs["device.type"] ?? baseAttributes.device;
534
+ attrs["device.type"] = normalizeDeviceType(deviceTypeOverride) ?? normalizeDeviceType(getDeviceType()) ?? "";
603
535
  const browser = getBrowserInfo();
604
536
  attrs["device.browser"] ??= browser.name;
605
537
  attrs["device.browser.version"] ??= browser.version;
@@ -791,31 +723,6 @@ export function evaluateJsTriggerResult(
791
723
  }
792
724
  }
793
725
 
794
- function stringValues(value: unknown): string[] {
795
- if (Array.isArray(value)) {
796
- return value
797
- .map((item) => String(item).trim())
798
- .filter(Boolean);
799
- }
800
- if (value === undefined || value === null) return [];
801
- return [String(value)];
802
- }
803
-
804
- function hasMeaningfulValue(value: unknown): boolean {
805
- if (value === undefined || value === null) return false;
806
- if (Array.isArray(value)) return value.length > 0;
807
- if (typeof value === "string") return value.trim().length > 0;
808
- return true;
809
- }
810
-
811
- function compareNumber(actual: unknown, expected: unknown, predicate: (a: number, b: number) => boolean): boolean {
812
- const actualNumber = Number(Array.isArray(actual) ? actual[0] : actual);
813
- const expectedNumber = Number(Array.isArray(expected) ? expected[0] : expected);
814
- return Number.isFinite(actualNumber) &&
815
- Number.isFinite(expectedNumber) &&
816
- predicate(actualNumber, expectedNumber);
817
- }
818
-
819
726
  function matchSegmentCondition(
820
727
  condition: SegmentCondition,
821
728
  ctx: { url: string; attributes: Record<string, unknown> },
@@ -826,58 +733,9 @@ function matchSegmentCondition(
826
733
  }
827
734
 
828
735
  export function matchSegmentConditionValues(
829
- condition: SegmentCondition,
830
- actual: unknown,
831
- expected: SegmentCondition["value"],
736
+ condition: SegmentCondition, actual: unknown, expected: SegmentCondition["value"],
832
737
  ): boolean {
833
- const actualValues = stringValues(actual);
834
- const expectedValues = stringValues(expected);
835
- const firstExpected = expectedValues[0] ?? "";
836
-
837
- switch (condition.operator) {
838
- case "equals":
839
- return actualValues.some((value) => value === String(expected ?? ""));
840
- case "not_equals":
841
- return !actualValues.some((value) => value === String(expected ?? ""));
842
- case "contains":
843
- return actualValues.some((value) =>
844
- expectedValues.length > 1
845
- ? expectedValues.includes(value)
846
- : value.includes(firstExpected),
847
- );
848
- case "not_contains":
849
- return !actualValues.some((value) =>
850
- expectedValues.length > 1
851
- ? expectedValues.includes(value)
852
- : value.includes(firstExpected),
853
- );
854
- case "starts_with":
855
- return actualValues.some((value) => value.startsWith(firstExpected));
856
- case "ends_with":
857
- return actualValues.some((value) => value.endsWith(firstExpected));
858
- case "is_set":
859
- return hasMeaningfulValue(actual);
860
- case "is_empty":
861
- return !hasMeaningfulValue(actual);
862
- case "in":
863
- return actualValues.some((value) => expectedValues.includes(value));
864
- case "not_in":
865
- return !actualValues.some((value) => expectedValues.includes(value));
866
- case "matches_wildcard":
867
- return actualValues.some((value) => safeRegexTest(wildcardToRegex(firstExpected), value));
868
- case "matches_regex":
869
- return actualValues.some((value) => safeRegexTest(firstExpected, value));
870
- case "greater_than":
871
- return compareNumber(actual, expected, (a, b) => a > b);
872
- case "greater_than_or_equal":
873
- return compareNumber(actual, expected, (a, b) => a >= b);
874
- case "less_than":
875
- return compareNumber(actual, expected, (a, b) => a < b);
876
- case "less_than_or_equal":
877
- return compareNumber(actual, expected, (a, b) => a <= b);
878
- default:
879
- return false;
880
- }
738
+ return matchCoreSegmentConditionValues(condition, actual, expected);
881
739
  }
882
740
 
883
741
  function matchSegmentRule(
@@ -924,16 +782,23 @@ function matchSegmentRules(
924
782
 
925
783
  export function matchesTargeting(
926
784
  targeting: Targeting | undefined,
927
- ctx: { url: string; attributes: Record<string, unknown> },
785
+ ctx: {
786
+ url: string;
787
+ attributes: Record<string, unknown>;
788
+ hostAgnosticIncludeOrigin?: string | string[];
789
+ },
928
790
  ): boolean {
929
791
  return evaluateTargetingGates(targeting, ctx).matched;
930
792
  }
931
793
 
932
794
  export function targetingHasJsTriggers(experiments: Experiment[]): boolean {
933
795
  return experiments.some((experiment) =>
934
- (experiment.targeting?.pageRules ?? []).some((rule) =>
935
- Boolean(rule.advancedCondition?.code?.trim()) ||
936
- Boolean(rule.trigger?.triggerJs?.trim()),
796
+ [
797
+ ...(experiment.targeting?.pageRules ?? []),
798
+ ...(experiment.goalPageRules ?? []),
799
+ ].some((rule) =>
800
+ Boolean(rule.trigger?.triggerJs?.trim()) ||
801
+ Boolean(rule.advancedCondition?.code?.trim()),
937
802
  ) ||
938
803
  (experiment.targeting?.segmentRules ?? []).some((rule) =>
939
804
  (rule.rules?.conditions ?? []).some((condition) =>
@@ -945,7 +810,11 @@ export function targetingHasJsTriggers(experiments: Experiment[]): boolean {
945
810
 
946
811
  export function evaluateTargetingGatesDetailed(
947
812
  targeting: Targeting | undefined,
948
- ctx: { url: string; attributes: Record<string, unknown> },
813
+ ctx: {
814
+ url: string;
815
+ attributes: Record<string, unknown>;
816
+ hostAgnosticIncludeOrigin?: string | string[];
817
+ },
949
818
  ): TargetingDiagnostics {
950
819
  if (!__DRIP_QA_BUILD__) {
951
820
  const aggregate = evaluateTargetingGates(targeting, ctx);
@@ -956,7 +825,11 @@ export function evaluateTargetingGatesDetailed(
956
825
 
957
826
  export function evaluateTargetingGates(
958
827
  targeting: Targeting | undefined,
959
- ctx: { url: string; attributes: Record<string, unknown> },
828
+ ctx: {
829
+ url: string;
830
+ attributes: Record<string, unknown>;
831
+ hostAgnosticIncludeOrigin?: string | string[];
832
+ },
960
833
  ): { matched: boolean; gates: ActivationGateDiagnostic[] } {
961
834
  const gates: ActivationGateDiagnostic[] = [];
962
835
  if (!targeting) {
@@ -973,7 +846,11 @@ export function evaluateTargetingGates(
973
846
  const pageRules = targeting.pageRules ?? [];
974
847
  const pageHasRules = urlRules.length > 0 || pageRules.length > 0;
975
848
  const pageMatched = pageHasRules
976
- ? matchUrlRules(ctx.url, urlRules) && matchPageRules(pageRules, ctx)
849
+ ? matchUrlRules(
850
+ ctx.url,
851
+ urlRules,
852
+ ctx.hostAgnosticIncludeOrigin,
853
+ ) && matchPageRules(pageRules, ctx)
977
854
  : true;
978
855
  gates.push({
979
856
  id: "page",
@@ -1004,15 +881,19 @@ export function evaluateTargetingGates(
1004
881
  }
1005
882
 
1006
883
  const segmentRules = targeting.segmentRules ?? [];
1007
- const segmentRulesMatched = matchSegmentRules(segmentRules, ctx);
1008
- const audienceHasRules = attributeRules.length > 0 || segmentRules.length > 0;
884
+ const segmentRefs = targeting.segments ?? [];
885
+ const segmentsExpanded = segmentRefs.every((ref) =>
886
+ segmentRules.some((rule) => rule.id === ref.id)
887
+ );
888
+ const segmentRulesMatched = segmentsExpanded && matchSegmentRules(segmentRules, ctx);
889
+ const audienceHasRules = attributeRules.length > 0 || segmentRules.length > 0 || segmentRefs.length > 0;
1009
890
  const audienceMatched = attributesMatched && segmentRulesMatched;
1010
891
  gates.push({
1011
892
  id: "audience",
1012
893
  label: "Audience check",
1013
894
  status: audienceHasRules ? (audienceMatched ? "passed" : "failed") : "skipped",
1014
895
  reason: audienceHasRules
1015
- ? (audienceMatched ? "audience_matched" : !attributesMatched ? "attribute_mismatch" : "segment_mismatch")
896
+ ? (audienceMatched ? "audience_matched" : !segmentsExpanded ? "segments_unexpanded" : !attributesMatched ? "attribute_mismatch" : "segment_mismatch")
1016
897
  : "no_audience_rules",
1017
898
  detail: audienceHasRules ? { attributeRules: attributeRules.length, segmentRules: segmentRules.length } : undefined,
1018
899
  });