@drip-apex/sdk 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/bundle-report.json +584 -226
- package/dist/define.cjs +123 -0
- package/dist/define.cjs.map +7 -0
- package/dist/define.mjs +102 -0
- package/dist/define.mjs.map +7 -0
- package/dist/drip.cjs +19283 -0
- package/dist/drip.cjs.map +7 -0
- package/dist/drip.debug.js +1 -1
- package/dist/drip.debug.js.map +1 -1
- package/dist/drip.heatmap.js +1 -1
- package/dist/drip.heatmap.js.map +1 -1
- package/dist/drip.js +1 -1
- package/dist/drip.js.map +1 -1
- package/dist/drip.live.js +1 -1
- package/dist/drip.live.js.map +1 -1
- package/dist/drip.mjs +19267 -0
- package/dist/drip.mjs.map +7 -0
- package/dist/drip.replay-recorder.js +2 -0
- package/dist/drip.replay-recorder.js.map +1 -0
- package/dist/editor.js +147 -142
- package/dist/editor.js.map +4 -4
- package/dist/flags.cjs +1788 -0
- package/dist/flags.cjs.map +7 -0
- package/dist/flags.mjs +1765 -0
- package/dist/flags.mjs.map +7 -0
- package/dist/index.mjs +12 -0
- package/dist/index.mjs.map +2 -2
- package/dist/metafiles/define.cjs.meta.json +28 -0
- package/dist/metafiles/define.mjs.meta.json +32 -0
- package/dist/metafiles/drip.cjs.meta.json +3193 -0
- package/dist/metafiles/drip.debug.meta.json +2859 -574
- package/dist/metafiles/drip.heatmap.meta.json +2732 -460
- package/dist/metafiles/drip.live.meta.json +2732 -460
- package/dist/metafiles/drip.meta.json +2859 -574
- package/dist/metafiles/drip.mjs.meta.json +3235 -0
- package/dist/metafiles/drip.replay-recorder.meta.json +82 -0
- package/dist/metafiles/editor.meta.json +67 -27
- package/dist/metafiles/flags.cjs.meta.json +220 -0
- package/dist/metafiles/flags.mjs.meta.json +225 -0
- package/dist/metafiles/index.meta.json +2717 -491
- package/dist/metafiles/index.mjs.meta.json +14 -4
- package/dist/react/index.js +16636 -13533
- package/dist/react/index.js.map +4 -4
- package/dist/sri.json +20 -15
- package/dist/types/activation-runtime.d.ts +91 -0
- package/dist/types/analytics-runtime.d.ts +166 -0
- package/dist/types/antiflicker.d.ts +9 -3
- package/dist/types/assignment-cookie-sync.d.ts +31 -0
- package/dist/types/assignment-runtime.d.ts +257 -0
- package/dist/types/assignment-store.d.ts +4 -8
- package/dist/types/bucket.d.ts +1 -2
- package/dist/types/compact-store.d.ts +1 -0
- package/dist/types/config-runtime.d.ts +313 -0
- package/dist/types/consent-evaluation.d.ts +28 -0
- package/dist/types/css.d.ts +5 -3
- package/dist/types/debug-loader.d.ts +2 -0
- package/dist/types/decide-assignments.d.ts +31 -0
- package/dist/types/delivery-timing.d.ts +8 -0
- package/dist/types/early-interaction-capture.d.ts +79 -0
- package/dist/types/early-reassert.d.ts +32 -0
- package/dist/types/edge-runtime.d.ts +11 -0
- package/dist/types/editor/api.d.ts +3 -0
- package/dist/types/editor/controller.d.ts +3 -1
- package/dist/types/editor/publication-result.d.ts +11 -0
- package/dist/types/editor/types.d.ts +2 -1
- package/dist/types/editor/view-kit.d.ts +1 -1
- package/dist/types/endpoint.d.ts +8 -0
- package/dist/types/exclusion-groups.d.ts +4 -7
- package/dist/types/flags.d.ts +2 -0
- package/dist/types/goal-eligibility.d.ts +4 -0
- package/dist/types/goals.d.ts +10 -1
- package/dist/types/guards.d.ts +7 -7
- package/dist/types/handover.d.ts +12 -1
- package/dist/types/hash.d.ts +8 -4
- package/dist/types/hydration.d.ts +2 -1
- package/dist/types/index.d.ts +24 -128
- package/dist/types/init-test-helpers.d.ts +48 -0
- package/dist/types/inline-bootstrap.d.ts +26 -0
- package/dist/types/js.d.ts +3 -1
- package/dist/types/money.d.ts +12 -0
- package/dist/types/mutate.d.ts +45 -6
- package/dist/types/performance.d.ts +2 -0
- package/dist/types/persistence-decoders.d.ts +12 -0
- package/dist/types/public-api.d.ts +209 -0
- package/dist/types/qa-assistant-state.d.ts +34 -0
- package/dist/types/react/index.d.ts +28 -3
- package/dist/types/reassert-sentinel.d.ts +6 -0
- package/dist/types/redirect.d.ts +48 -0
- package/dist/types/replay-recorder-entry.d.ts +1 -0
- package/dist/types/replay-signal-state.d.ts +6 -0
- package/dist/types/replay-signals.d.ts +13 -0
- package/dist/types/replay.d.ts +110 -0
- package/dist/types/router.d.ts +12 -2
- package/dist/types/runtime-state.d.ts +166 -0
- package/dist/types/sanitize.d.ts +1 -0
- package/dist/types/shadow.d.ts +3 -0
- package/dist/types/shopify-cart.d.ts +25 -1
- package/dist/types/storefront-vitals.d.ts +24 -0
- package/dist/types/targeting-diagnostics.d.ts +1 -0
- package/dist/types/targeting.d.ts +16 -2
- package/dist/types/track.d.ts +15 -1
- package/dist/types/types.d.ts +115 -9
- package/dist/types/wait.d.ts +14 -1
- package/dist/types/weekday-schedule.d.ts +3 -0
- package/package.json +17 -8
- package/src/activation-runtime.ts +2737 -0
- package/src/analytics-runtime.ts +2175 -0
- package/src/antiflicker.test.ts +216 -44
- package/src/antiflicker.ts +103 -28
- package/src/artifact-concurrency.test.ts +1091 -0
- package/src/assignment-cookie-sync.test.ts +202 -0
- package/src/assignment-cookie-sync.ts +189 -0
- package/src/assignment-runtime.ts +3204 -0
- package/src/assignment-store.test.ts +56 -0
- package/src/assignment-store.ts +7 -38
- package/src/bucket.ts +3 -18
- package/src/commerce-analytics-lifecycle.test.ts +75 -15
- package/src/commerce-config-lifecycle.test.ts +81 -1
- package/src/commerce-price-preserve.test.ts +133 -0
- package/src/commerce-price.test.ts +97 -0
- package/src/commerce-price.ts +74 -20
- package/src/commerce-shipping.test.ts +53 -0
- package/src/commerce-shipping.ts +5 -11
- package/src/compact-store.ts +15 -0
- package/src/config-fetch.test.ts +480 -9
- package/src/config-runtime.ts +2071 -0
- package/src/consent-evaluation-borlabs.test.ts +41 -0
- package/src/consent-evaluation-cookiescript.test.ts +50 -0
- package/src/consent-evaluation.ts +151 -0
- package/src/css.test.ts +30 -0
- package/src/css.ts +120 -6
- package/src/cwv.test.ts +60 -77
- package/src/debug-loader.test.ts +214 -0
- package/src/debug-loader.ts +41 -0
- package/src/decide-assignments.test.ts +196 -0
- package/src/decide-assignments.ts +204 -0
- package/src/delivery-timing.test.ts +104 -0
- package/src/delivery-timing.ts +89 -0
- package/src/early-interaction-capture.test.ts +451 -0
- package/src/early-interaction-capture.ts +230 -0
- package/src/early-reassert.test.ts +207 -0
- package/src/early-reassert.ts +152 -0
- package/src/edge-adoption.test.ts +435 -0
- package/src/edge-runtime.ts +30 -0
- package/src/editor/api.test.ts +109 -0
- package/src/editor/api.ts +34 -3
- package/src/editor/controller.test.ts +297 -14
- package/src/editor/controller.ts +69 -5
- package/src/editor/index.test.ts +2 -0
- package/src/editor/index.ts +1 -0
- package/src/editor/publication-result.ts +58 -0
- package/src/editor/types.ts +10 -1
- package/src/editor/view-kit.ts +2 -1
- package/src/editor/view.test.ts +108 -0
- package/src/editor/view.ts +49 -7
- package/src/endpoint.test.ts +15 -0
- package/src/endpoint.ts +10 -0
- package/src/exclusion-groups.ts +8 -18
- package/src/flags.test.ts +88 -1
- package/src/flags.ts +29 -29
- package/src/global-runtime-reconciler.test.ts +271 -0
- package/src/goal-eligibility.test.ts +29 -0
- package/src/goal-eligibility.ts +17 -0
- package/src/goals.test.ts +144 -1
- package/src/goals.ts +97 -18
- package/src/guards.test.ts +99 -1
- package/src/guards.ts +34 -15
- package/src/handover-cache-order.test.ts +6 -3
- package/src/handover.test.ts +154 -8
- package/src/handover.ts +122 -12
- package/src/hash.ts +29 -54
- package/src/holdout.test.ts +26 -0
- package/src/hydration.test.ts +73 -2
- package/src/hydration.ts +18 -9
- package/src/index.ts +130 -7287
- package/src/init-analytics.test.ts +1694 -0
- package/src/init-anti-flicker.test.ts +1211 -0
- package/src/init-cart-attributes.test.ts +2003 -0
- package/src/init-config-resolution.test.ts +999 -0
- package/src/init-consent.test.ts +971 -0
- package/src/init-empty.test.ts +11 -5406
- package/src/init-integrations.test.ts +846 -0
- package/src/init-page-activation.test.ts +5064 -0
- package/src/init-qa-loading.test.ts +740 -0
- package/src/init-redirects.test.ts +2156 -0
- package/src/init-test-helpers.ts +287 -0
- package/src/inline-bootstrap.test.ts +184 -0
- package/src/inline-bootstrap.ts +73 -0
- package/src/js.test.ts +51 -1
- package/src/js.ts +86 -2
- package/src/money.test.ts +55 -1
- package/src/money.ts +104 -2
- package/src/money.web-parity.contract.test.ts +85 -0
- package/src/mutate.test.ts +474 -10
- package/src/mutate.ts +739 -230
- package/src/page-trigger.test.ts +218 -8
- package/src/performance.ts +10 -0
- package/src/persistence-decoders.ts +18 -0
- package/src/public-api.ts +1828 -0
- package/src/qa-assistant-state.test.ts +249 -3
- package/src/qa-assistant-state.ts +146 -6
- package/src/qa-assistant-view.test.ts +220 -2
- package/src/qa-assistant-view.ts +102 -5
- package/src/react/index.test.tsx +37 -0
- package/src/react/index.tsx +19 -3
- package/src/reassert-sentinel.test.ts +155 -1
- package/src/reassert-sentinel.ts +168 -8
- package/src/redirect.test.ts +541 -0
- package/src/redirect.ts +572 -0
- package/src/replay-recorder-entry.ts +115 -0
- package/src/replay-signal-state.ts +100 -0
- package/src/replay-signals.test.ts +249 -0
- package/src/replay-signals.ts +151 -0
- package/src/replay.test.ts +764 -0
- package/src/replay.ts +514 -0
- package/src/router.test.ts +61 -2
- package/src/router.ts +50 -12
- package/src/runtime-state.ts +507 -0
- package/src/runtime.test.ts +174 -7
- package/src/runtime.ts +88 -14
- package/src/sanitize.test.ts +34 -0
- package/src/sanitize.ts +23 -12
- package/src/shadow.test.ts +187 -0
- package/src/shadow.ts +99 -0
- package/src/shopify-cart.test.ts +733 -23
- package/src/shopify-cart.ts +402 -49
- package/src/storefront-vitals.test.ts +217 -0
- package/src/storefront-vitals.ts +206 -0
- package/src/surface-signals.ts +6 -0
- package/src/targeting-conformance.test.ts +9 -0
- package/src/targeting-diagnostics.ts +38 -8
- package/src/targeting-parity-corpus.json +559 -0
- package/src/targeting-parity-corpus.test.ts +18 -0
- package/src/targeting.detailed.test.ts +105 -0
- package/src/targeting.test.ts +211 -0
- package/src/targeting.ts +88 -207
- package/src/track.test.ts +147 -0
- package/src/track.ts +125 -26
- package/src/types.ts +120 -13
- package/src/wait.test.ts +23 -0
- package/src/wait.ts +129 -13
- package/src/weekday-delivery.test.ts +56 -0
- package/src/weekday-schedule-parity-corpus.json +92 -0
- package/src/weekday-schedule.test.ts +69 -0
- package/src/weekday-schedule.ts +20 -0
package/src/targeting.test.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
55
|
-
return value.replace(/\\([.*+?^${}()|[\]\\\/])/g, "$1");
|
|
56
|
-
}
|
|
22
|
+
export const evalUrlRule = evaluateUrlRule;
|
|
57
23
|
|
|
58
|
-
export function
|
|
59
|
-
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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 =
|
|
139
|
-
if (rule.include === false) {
|
|
140
|
-
|
|
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"]
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
935
|
-
|
|
936
|
-
|
|
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: {
|
|
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: {
|
|
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(
|
|
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
|
|
1008
|
-
const
|
|
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
|
});
|