@drip-apex/sdk 0.1.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 (227) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -0
  3. package/dist/_headers +3 -0
  4. package/dist/bundle-report.json +564 -0
  5. package/dist/drip.debug.js +2 -0
  6. package/dist/drip.debug.js.map +1 -0
  7. package/dist/drip.heatmap.js +2 -0
  8. package/dist/drip.heatmap.js.map +1 -0
  9. package/dist/drip.js +2 -0
  10. package/dist/drip.js.map +1 -0
  11. package/dist/drip.live.js +2 -0
  12. package/dist/drip.live.js.map +1 -0
  13. package/dist/editor.js +480 -0
  14. package/dist/editor.js.map +7 -0
  15. package/dist/index.mjs +645 -0
  16. package/dist/index.mjs.map +7 -0
  17. package/dist/metafiles/drip.debug.meta.json +973 -0
  18. package/dist/metafiles/drip.heatmap.meta.json +918 -0
  19. package/dist/metafiles/drip.live.meta.json +918 -0
  20. package/dist/metafiles/drip.meta.json +973 -0
  21. package/dist/metafiles/editor.meta.json +321 -0
  22. package/dist/metafiles/index.meta.json +1039 -0
  23. package/dist/metafiles/index.mjs.meta.json +78 -0
  24. package/dist/react/index.js +16973 -0
  25. package/dist/react/index.js.map +7 -0
  26. package/dist/sri.json +27 -0
  27. package/dist/types/analytics.d.ts +15 -0
  28. package/dist/types/antiflicker.d.ts +14 -0
  29. package/dist/types/assignment-store.d.ts +30 -0
  30. package/dist/types/audit.d.ts +9 -0
  31. package/dist/types/bucket.d.ts +2 -0
  32. package/dist/types/bucketing-cleanup.d.ts +5 -0
  33. package/dist/types/commerce-gift.d.ts +41 -0
  34. package/dist/types/commerce-price.d.ts +22 -0
  35. package/dist/types/commerce-shipping.d.ts +28 -0
  36. package/dist/types/css.d.ts +5 -0
  37. package/dist/types/cwv.d.ts +10 -0
  38. package/dist/types/data-layer-normalize.d.ts +1 -0
  39. package/dist/types/debug.d.ts +23 -0
  40. package/dist/types/define.d.ts +87 -0
  41. package/dist/types/devtools.d.ts +32 -0
  42. package/dist/types/dom-trigger.d.ts +3 -0
  43. package/dist/types/editor/api.d.ts +37 -0
  44. package/dist/types/editor/artifacts.d.ts +15 -0
  45. package/dist/types/editor/command-runtime.d.ts +48 -0
  46. package/dist/types/editor/controller.d.ts +97 -0
  47. package/dist/types/editor/device-preview.d.ts +23 -0
  48. package/dist/types/editor/index.d.ts +9 -0
  49. package/dist/types/editor/operator.d.ts +39 -0
  50. package/dist/types/editor/picker.d.ts +23 -0
  51. package/dist/types/editor/selector.d.ts +2 -0
  52. package/dist/types/editor/types.d.ts +55 -0
  53. package/dist/types/editor/view-kit.d.ts +33 -0
  54. package/dist/types/editor/view.d.ts +5 -0
  55. package/dist/types/editor-bootstrap.d.ts +37 -0
  56. package/dist/types/exclusion-groups.d.ts +59 -0
  57. package/dist/types/exposure-signals.d.ts +7 -0
  58. package/dist/types/flags.d.ts +65 -0
  59. package/dist/types/goals.d.ts +16 -0
  60. package/dist/types/guards.d.ts +39 -0
  61. package/dist/types/handover.d.ts +10 -0
  62. package/dist/types/hash.d.ts +19 -0
  63. package/dist/types/heatmap.d.ts +8 -0
  64. package/dist/types/hideflicker.d.ts +15 -0
  65. package/dist/types/hydration.d.ts +11 -0
  66. package/dist/types/index.d.ts +144 -0
  67. package/dist/types/js.d.ts +6 -0
  68. package/dist/types/module.d.ts +4 -0
  69. package/dist/types/money.d.ts +14 -0
  70. package/dist/types/mutate.d.ts +34 -0
  71. package/dist/types/observer-mute.d.ts +3 -0
  72. package/dist/types/pii-keys.d.ts +1 -0
  73. package/dist/types/prerequisites.d.ts +12 -0
  74. package/dist/types/qa-assistant-state.d.ts +68 -0
  75. package/dist/types/qa-assistant-view.d.ts +6 -0
  76. package/dist/types/qa-assistant.d.ts +9 -0
  77. package/dist/types/qa-toolbar.d.ts +19 -0
  78. package/dist/types/react/index.d.ts +84 -0
  79. package/dist/types/reassert-sentinel.d.ts +6 -0
  80. package/dist/types/router.d.ts +9 -0
  81. package/dist/types/runtime-tools.d.ts +50 -0
  82. package/dist/types/runtime.d.ts +22 -0
  83. package/dist/types/safe-regex.d.ts +6 -0
  84. package/dist/types/sanitize.d.ts +5 -0
  85. package/dist/types/semantic.d.ts +7 -0
  86. package/dist/types/shopify-cart-events.d.ts +7 -0
  87. package/dist/types/shopify-cart.d.ts +62 -0
  88. package/dist/types/signals.d.ts +31 -0
  89. package/dist/types/surface-signals.d.ts +6 -0
  90. package/dist/types/targeting-diagnostics.d.ts +5 -0
  91. package/dist/types/targeting.d.ts +50 -0
  92. package/dist/types/test-fixtures/commerce.d.ts +12 -0
  93. package/dist/types/track.d.ts +79 -0
  94. package/dist/types/types.d.ts +859 -0
  95. package/dist/types/wait.d.ts +7 -0
  96. package/package.json +74 -0
  97. package/src/analytics.ts +146 -0
  98. package/src/antiflicker.test.ts +127 -0
  99. package/src/antiflicker.ts +147 -0
  100. package/src/assignment-store.test.ts +171 -0
  101. package/src/assignment-store.ts +208 -0
  102. package/src/audit.test.ts +17 -0
  103. package/src/audit.ts +362 -0
  104. package/src/bucket.test.ts +65 -0
  105. package/src/bucket.ts +18 -0
  106. package/src/bucketing-cleanup.test.ts +30 -0
  107. package/src/bucketing-cleanup.ts +72 -0
  108. package/src/bucketing-parity.test.ts +41 -0
  109. package/src/commerce-analytics-lifecycle.test.ts +1416 -0
  110. package/src/commerce-config-lifecycle.test.ts +274 -0
  111. package/src/commerce-epoch-lifecycle.test.ts +62 -0
  112. package/src/commerce-gift.test.ts +1293 -0
  113. package/src/commerce-gift.ts +979 -0
  114. package/src/commerce-offers-display.test.ts +154 -0
  115. package/src/commerce-price.test.ts +1145 -0
  116. package/src/commerce-price.ts +941 -0
  117. package/src/commerce-revocation-lifecycle.test.ts +78 -0
  118. package/src/commerce-shipping.test.ts +630 -0
  119. package/src/commerce-shipping.ts +447 -0
  120. package/src/config-fetch.test.ts +325 -0
  121. package/src/css.ts +54 -0
  122. package/src/cwv.test.ts +95 -0
  123. package/src/cwv.ts +129 -0
  124. package/src/data-layer-normalize.ts +21 -0
  125. package/src/debug.test.ts +83 -0
  126. package/src/debug.ts +138 -0
  127. package/src/define.test.ts +327 -0
  128. package/src/define.ts +208 -0
  129. package/src/devtools.ts +582 -0
  130. package/src/dom-trigger.test.ts +70 -0
  131. package/src/dom-trigger.ts +55 -0
  132. package/src/editor/api.test.ts +154 -0
  133. package/src/editor/api.ts +185 -0
  134. package/src/editor/artifacts.test.ts +57 -0
  135. package/src/editor/artifacts.ts +115 -0
  136. package/src/editor/command-runtime.test.ts +225 -0
  137. package/src/editor/command-runtime.ts +572 -0
  138. package/src/editor/controller.test.ts +371 -0
  139. package/src/editor/controller.ts +596 -0
  140. package/src/editor/device-preview.test.ts +60 -0
  141. package/src/editor/device-preview.ts +120 -0
  142. package/src/editor/index.test.ts +161 -0
  143. package/src/editor/index.ts +169 -0
  144. package/src/editor/operator.test.ts +148 -0
  145. package/src/editor/operator.ts +256 -0
  146. package/src/editor/picker.test.ts +58 -0
  147. package/src/editor/picker.ts +152 -0
  148. package/src/editor/selector.test.ts +44 -0
  149. package/src/editor/selector.ts +110 -0
  150. package/src/editor/types.ts +92 -0
  151. package/src/editor/view-kit.ts +162 -0
  152. package/src/editor/view.test.ts +535 -0
  153. package/src/editor/view.ts +1395 -0
  154. package/src/editor-bootstrap.test.ts +116 -0
  155. package/src/editor-bootstrap.ts +213 -0
  156. package/src/event-schema-contract.test.ts +35 -0
  157. package/src/exclusion-groups.test.ts +383 -0
  158. package/src/exclusion-groups.ts +270 -0
  159. package/src/exposure-signals.test.ts +182 -0
  160. package/src/exposure-signals.ts +110 -0
  161. package/src/flags.test.ts +322 -0
  162. package/src/flags.ts +201 -0
  163. package/src/goals.test.ts +42 -0
  164. package/src/goals.ts +200 -0
  165. package/src/guards.test.ts +249 -0
  166. package/src/guards.ts +250 -0
  167. package/src/handover-cache-order.test.ts +32 -0
  168. package/src/handover.test.ts +349 -0
  169. package/src/handover.ts +309 -0
  170. package/src/hash.ts +214 -0
  171. package/src/heatmap.test.ts +305 -0
  172. package/src/heatmap.ts +373 -0
  173. package/src/hideflicker.ts +21 -0
  174. package/src/hydration.test.ts +252 -0
  175. package/src/hydration.ts +203 -0
  176. package/src/index.ts +7513 -0
  177. package/src/init-empty.test.ts +5572 -0
  178. package/src/js.test.ts +26 -0
  179. package/src/js.ts +61 -0
  180. package/src/module.ts +50 -0
  181. package/src/money.test.ts +35 -0
  182. package/src/money.ts +53 -0
  183. package/src/mutate.test.ts +304 -0
  184. package/src/mutate.ts +469 -0
  185. package/src/observer-mute.test.ts +88 -0
  186. package/src/observer-mute.ts +45 -0
  187. package/src/page-trigger.test.ts +84 -0
  188. package/src/personalization.test.ts +68 -0
  189. package/src/pii-keys.ts +17 -0
  190. package/src/prerequisites.test.ts +39 -0
  191. package/src/prerequisites.ts +94 -0
  192. package/src/qa-assistant-state.test.ts +471 -0
  193. package/src/qa-assistant-state.ts +505 -0
  194. package/src/qa-assistant-view.test.ts +185 -0
  195. package/src/qa-assistant-view.ts +768 -0
  196. package/src/qa-assistant.ts +40 -0
  197. package/src/qa-toolbar.ts +444 -0
  198. package/src/react/index.test.tsx +177 -0
  199. package/src/react/index.tsx +252 -0
  200. package/src/reassert-sentinel.test.ts +314 -0
  201. package/src/reassert-sentinel.ts +191 -0
  202. package/src/router.test.ts +78 -0
  203. package/src/router.ts +92 -0
  204. package/src/runtime-tools.test.ts +112 -0
  205. package/src/runtime-tools.ts +403 -0
  206. package/src/runtime.test.ts +217 -0
  207. package/src/runtime.ts +268 -0
  208. package/src/safe-regex.ts +35 -0
  209. package/src/sanitize.ts +34 -0
  210. package/src/semantic.ts +245 -0
  211. package/src/shopify-cart-events.test.ts +76 -0
  212. package/src/shopify-cart-events.ts +115 -0
  213. package/src/shopify-cart.test.ts +2823 -0
  214. package/src/shopify-cart.ts +1256 -0
  215. package/src/signals.ts +226 -0
  216. package/src/surface-signals.test.ts +83 -0
  217. package/src/surface-signals.ts +165 -0
  218. package/src/targeting-diagnostics.ts +560 -0
  219. package/src/targeting.detailed.test.ts +443 -0
  220. package/src/targeting.test.ts +1054 -0
  221. package/src/targeting.ts +1024 -0
  222. package/src/test-fixtures/commerce.ts +65 -0
  223. package/src/track.test.ts +817 -0
  224. package/src/track.ts +519 -0
  225. package/src/types.ts +991 -0
  226. package/src/wait.test.ts +96 -0
  227. package/src/wait.ts +238 -0
@@ -0,0 +1,560 @@
1
+ import type {
2
+ ActivationGateDiagnostic,
3
+ AttributeRule,
4
+ ConditionDiagnostic,
5
+ PageTargetingRule,
6
+ SegmentCondition,
7
+ SegmentTargetingRule,
8
+ Targeting,
9
+ TargetingDiagnostics,
10
+ UrlRule,
11
+ } from "./types";
12
+ import {
13
+ evalUrlRule,
14
+ evaluateJsTriggerResult,
15
+ evaluatePageAdvancedConditionResult,
16
+ matchAttributeRule,
17
+ matchSegmentConditionValues,
18
+ matchUrlRules,
19
+ normalizeRuntimeUrlRules,
20
+ pageManualTriggerMatches,
21
+ resolveNamedConditionActual,
22
+ resolveNamedConditionExpected,
23
+ } from "./targeting";
24
+
25
+ function urlRuleOperator(rule: UrlRule): string {
26
+ return rule.matchType ?? rule.match_type ?? rule.type;
27
+ }
28
+
29
+ function diagnoseUrlRules(
30
+ url: string,
31
+ rules: UrlRule[],
32
+ ): { matched: boolean; conditions: ConditionDiagnostic[] } {
33
+ const conditions: ConditionDiagnostic[] = [];
34
+ if (!rules.length) return { matched: true, conditions };
35
+
36
+ let hasInclude = false;
37
+ let included = false;
38
+ let stopped = false;
39
+ for (let index = 0; index < rules.length; index += 1) {
40
+ const rule = rules[index];
41
+ if (stopped) {
42
+ conditions.push({
43
+ id: `url:${index}`,
44
+ label: rule.include === false ? "URL exclude rule" : "URL include rule",
45
+ status: "skipped",
46
+ reason: "url_exclude_matched",
47
+ operator: urlRuleOperator(rule),
48
+ expected: rule.pattern,
49
+ actual: url,
50
+ source: rule.pattern,
51
+ sourceType: "url",
52
+ });
53
+ continue;
54
+ }
55
+
56
+ const rawMatched = evalUrlRule(url, rule);
57
+ const passed = rule.include === false ? !rawMatched : rawMatched;
58
+ conditions.push({
59
+ id: `url:${index}`,
60
+ label: rule.include === false ? "URL exclude rule" : "URL include rule",
61
+ status: passed ? "passed" : "failed",
62
+ reason: passed ? "url_matched" : "url_mismatch",
63
+ operator: urlRuleOperator(rule),
64
+ expected: rule.pattern,
65
+ actual: url,
66
+ source: rule.pattern,
67
+ sourceType: "url",
68
+ });
69
+
70
+ if (rule.include === false) {
71
+ if (rawMatched) stopped = true;
72
+ } else {
73
+ hasInclude = true;
74
+ if (rawMatched) included = true;
75
+ }
76
+ }
77
+
78
+ return { matched: stopped ? false : included || !hasInclude, conditions };
79
+ }
80
+
81
+ type BooleanDiagnosticResult = {
82
+ matched: boolean;
83
+ reason?: string;
84
+ error?: unknown;
85
+ };
86
+
87
+ function pageRulePatternSummary(rule: PageTargetingRule): string {
88
+ const rules = [
89
+ ...(rule.includeRules ?? []).map((item) => ({
90
+ // Preserve explicit negatives: the page-audience migration emits
91
+ // include:false entries INSIDE includeRules (not_contains conditions),
92
+ // and runtime evaluation honors them — the summary must not flip them.
93
+ include: item.include !== false,
94
+ matchType: urlRuleOperator(item),
95
+ pattern: item.pattern,
96
+ })),
97
+ ...(rule.excludeRules ?? []).map((item) => ({
98
+ include: false,
99
+ matchType: urlRuleOperator(item),
100
+ pattern: item.pattern,
101
+ })),
102
+ ];
103
+ return JSON.stringify(rules);
104
+ }
105
+
106
+ function pageRuleSource(rule: PageTargetingRule): Pick<ConditionDiagnostic, "source" | "sourceType"> {
107
+ const advancedCode = rule.advancedCondition?.code?.trim();
108
+ if (advancedCode) return { source: advancedCode, sourceType: "js" };
109
+ const triggerJs = rule.trigger?.triggerJs?.trim();
110
+ if (triggerJs) return { source: triggerJs, sourceType: "js" };
111
+ return { source: pageRulePatternSummary(rule), sourceType: "page_rule" };
112
+ }
113
+
114
+ function evaluatePageRuleForDiagnostics(
115
+ rule: PageTargetingRule,
116
+ ctx: { url: string; attributes: Record<string, unknown> },
117
+ ): BooleanDiagnosticResult {
118
+ if (rule.disabled) return { matched: false, reason: "page_rule_disabled" };
119
+ if (rule.trigger?.disabled) return { matched: false, reason: "page_trigger_disabled" };
120
+
121
+ const includeRules = rule.includeRules ?? [];
122
+ const excludeRules = (rule.excludeRules ?? []).map((item) => ({ ...item, include: false }));
123
+ const urlMatched = matchUrlRules(ctx.url, [...includeRules, ...excludeRules]);
124
+ if (!urlMatched) return { matched: false, reason: "url_mismatch" };
125
+ if (!pageManualTriggerMatches(rule, ctx)) return { matched: false, reason: "manual_trigger_mismatch" };
126
+ const advanced = evaluatePageAdvancedConditionResult(rule, ctx);
127
+ if (!advanced.matched) return advanced;
128
+ return { matched: true };
129
+ }
130
+
131
+ function pageRuleCondition(
132
+ rule: PageTargetingRule,
133
+ index: number,
134
+ status: ConditionDiagnostic["status"],
135
+ reason: string | undefined,
136
+ actual?: unknown,
137
+ ): ConditionDiagnostic {
138
+ const include = rule.include !== false;
139
+ const hasPatterns = Boolean(rule.includeRules?.length || rule.excludeRules?.length);
140
+ const source = pageRuleSource(rule);
141
+ return {
142
+ id: `page:${rule.id || index}`,
143
+ label: rule.name ?? (include ? "Page include rule" : "Page exclude rule"),
144
+ status,
145
+ reason,
146
+ operator: include ? "include" : "exclude",
147
+ // For plain page rules the pattern summary already lives in source; only
148
+ // JS-sourced rules need it carried separately so the panel can show both
149
+ // the patterns and the raw JS. The label already renders the rule name.
150
+ expected: source.sourceType === "js" && hasPatterns ? pageRulePatternSummary(rule) : undefined,
151
+ actual,
152
+ ...source,
153
+ };
154
+ }
155
+
156
+ function diagnosePageRules(
157
+ rules: PageTargetingRule[],
158
+ ctx: { url: string; attributes: Record<string, unknown> },
159
+ ): { matched: boolean; conditions: ConditionDiagnostic[] } {
160
+ const conditions: ConditionDiagnostic[] = [];
161
+ if (!rules.length) return { matched: true, conditions };
162
+
163
+ let hasInclude = false;
164
+ let included = false;
165
+ let stopped = false;
166
+ for (let index = 0; index < rules.length; index += 1) {
167
+ const rule = rules[index];
168
+ if (stopped) {
169
+ conditions.push(pageRuleCondition(rule, index, "skipped", "page_rule_exclude_matched"));
170
+ continue;
171
+ }
172
+
173
+ const result = evaluatePageRuleForDiagnostics(rule, ctx);
174
+ const include = rule.include !== false;
175
+ const passed = include ? result.matched : !result.matched;
176
+ conditions.push(pageRuleCondition(
177
+ rule,
178
+ index,
179
+ passed ? "passed" : "failed",
180
+ passed ? undefined : include ? result.reason ?? "page_rule_mismatch" : "page_rule_exclude_matched",
181
+ pageRuleSource(rule).sourceType === "page_rule"
182
+ ? `${ctx.url} — ${passed ? "passed" : "failed"}`
183
+ : result.matched,
184
+ ));
185
+
186
+ if (rule.include === false) {
187
+ if (result.matched) stopped = true;
188
+ } else {
189
+ hasInclude = true;
190
+ if (result.matched) included = true;
191
+ }
192
+ }
193
+
194
+ return { matched: stopped ? false : included || !hasInclude, conditions };
195
+ }
196
+
197
+ function skippedPageRuleConditions(rules: PageTargetingRule[], reason: string): ConditionDiagnostic[] {
198
+ return rules.map((rule, index) => pageRuleCondition(rule, index, "skipped", reason));
199
+ }
200
+
201
+ function resolveSegmentConditionForDiagnostics(
202
+ condition: SegmentCondition,
203
+ ctx: { url: string; attributes: Record<string, unknown> },
204
+ ): { actual: unknown; expected: SegmentCondition["value"]; jsReason?: string } {
205
+ if (condition.signal === "cookie.value" || condition.signal === "url.query") {
206
+ return {
207
+ actual: resolveNamedConditionActual(condition, ctx),
208
+ expected: resolveNamedConditionExpected(condition),
209
+ };
210
+ }
211
+ if (condition.js) {
212
+ const result = evaluateJsTriggerResult(condition, ctx);
213
+ return {
214
+ actual: result.actual,
215
+ expected: resolveNamedConditionExpected(condition),
216
+ jsReason: result.reason,
217
+ };
218
+ }
219
+ return {
220
+ actual: resolveNamedConditionActual(condition, ctx),
221
+ expected: resolveNamedConditionExpected(condition),
222
+ };
223
+ }
224
+
225
+ function segmentConditionDiagnostic(
226
+ rule: SegmentTargetingRule,
227
+ condition: SegmentCondition,
228
+ index: number,
229
+ status: ConditionDiagnostic["status"],
230
+ reason: string | undefined,
231
+ actual?: unknown,
232
+ expected?: unknown,
233
+ ): ConditionDiagnostic {
234
+ const jsCode = condition.js?.code?.trim();
235
+ return {
236
+ id: `segment:${rule.id || "rule"}:${index}`,
237
+ label: rule.name ? `${rule.name}: ${condition.signal}` : `Segment condition: ${condition.signal}`,
238
+ status,
239
+ reason,
240
+ operator: condition.operator,
241
+ signal: condition.signal,
242
+ expected,
243
+ actual,
244
+ source: jsCode || rule.id,
245
+ sourceType: jsCode ? "js" : "segment",
246
+ };
247
+ }
248
+
249
+ function segmentRuleDiagnosticConditions(rule: SegmentTargetingRule): SegmentCondition[] {
250
+ const groups = Array.isArray(rule.rules?.groups)
251
+ ? rule.rules.groups.filter((group) => Array.isArray(group) && group.length > 0)
252
+ : [];
253
+ return groups.length ? groups.flat() : rule.rules?.conditions ?? [];
254
+ }
255
+
256
+ function skippedSegmentRuleConditions(rule: SegmentTargetingRule, reason: string): ConditionDiagnostic[] {
257
+ const conditions = segmentRuleDiagnosticConditions(rule);
258
+ return conditions.map((condition, index) =>
259
+ segmentConditionDiagnostic(
260
+ rule,
261
+ condition,
262
+ index,
263
+ "skipped",
264
+ reason,
265
+ undefined,
266
+ resolveNamedConditionExpected(condition),
267
+ ),
268
+ );
269
+ }
270
+
271
+ function diagnoseSegmentCondition(
272
+ rule: SegmentTargetingRule,
273
+ condition: SegmentCondition,
274
+ index: number,
275
+ ctx: { url: string; attributes: Record<string, unknown> },
276
+ ): { matched: boolean; diagnostic: ConditionDiagnostic } {
277
+ const resolved = resolveSegmentConditionForDiagnostics(condition, ctx);
278
+ const matched = matchSegmentConditionValues(condition, resolved.actual, resolved.expected);
279
+ return {
280
+ matched,
281
+ diagnostic: segmentConditionDiagnostic(
282
+ rule,
283
+ condition,
284
+ index,
285
+ matched ? "passed" : "failed",
286
+ matched ? undefined : resolved.jsReason ?? "segment_mismatch",
287
+ resolved.actual,
288
+ resolved.expected,
289
+ ),
290
+ };
291
+ }
292
+
293
+ function diagnoseSegmentRule(
294
+ rule: SegmentTargetingRule,
295
+ ctx: { url: string; attributes: Record<string, unknown> },
296
+ ): { matched: boolean; conditions: ConditionDiagnostic[] } {
297
+ const diagnostics: ConditionDiagnostic[] = [];
298
+ const groups = Array.isArray(rule.rules?.groups)
299
+ ? rule.rules.groups.filter((group) => Array.isArray(group) && group.length > 0)
300
+ : [];
301
+
302
+ if (groups.length) {
303
+ let flatIndex = 0;
304
+ for (let groupOffset = 0; groupOffset < groups.length; groupOffset += 1) {
305
+ const group = groups[groupOffset];
306
+ let groupMatched = false;
307
+ for (let groupIndex = 0; groupIndex < group.length; groupIndex += 1) {
308
+ const condition = group[groupIndex];
309
+ if (groupMatched) {
310
+ diagnostics.push(segmentConditionDiagnostic(
311
+ rule,
312
+ condition,
313
+ flatIndex,
314
+ "skipped",
315
+ "segment_group_matched",
316
+ undefined,
317
+ resolveNamedConditionExpected(condition),
318
+ ));
319
+ flatIndex += 1;
320
+ continue;
321
+ }
322
+ const result = diagnoseSegmentCondition(rule, condition, flatIndex, ctx);
323
+ diagnostics.push(result.diagnostic);
324
+ groupMatched = result.matched;
325
+ flatIndex += 1;
326
+ }
327
+ if (!groupMatched) {
328
+ for (const remainingGroup of groups.slice(groupOffset + 1)) {
329
+ for (const condition of remainingGroup) {
330
+ diagnostics.push(segmentConditionDiagnostic(
331
+ rule,
332
+ condition,
333
+ flatIndex,
334
+ "skipped",
335
+ "segment_group_failed",
336
+ undefined,
337
+ resolveNamedConditionExpected(condition),
338
+ ));
339
+ flatIndex += 1;
340
+ }
341
+ }
342
+ return { matched: false, conditions: diagnostics };
343
+ }
344
+ }
345
+ return { matched: true, conditions: diagnostics };
346
+ }
347
+
348
+ const conditions = rule.rules?.conditions ?? [];
349
+ if (!conditions.length) return { matched: true, conditions: diagnostics };
350
+ if (rule.rules.operator === "or") {
351
+ let matched = false;
352
+ for (let index = 0; index < conditions.length; index += 1) {
353
+ const condition = conditions[index];
354
+ if (matched) {
355
+ diagnostics.push(segmentConditionDiagnostic(
356
+ rule,
357
+ condition,
358
+ index,
359
+ "skipped",
360
+ "segment_or_matched",
361
+ undefined,
362
+ resolveNamedConditionExpected(condition),
363
+ ));
364
+ continue;
365
+ }
366
+ const result = diagnoseSegmentCondition(rule, condition, index, ctx);
367
+ diagnostics.push(result.diagnostic);
368
+ matched = result.matched;
369
+ }
370
+ return { matched, conditions: diagnostics };
371
+ }
372
+
373
+ let matched = true;
374
+ for (let index = 0; index < conditions.length; index += 1) {
375
+ const condition = conditions[index];
376
+ if (!matched) {
377
+ diagnostics.push(segmentConditionDiagnostic(
378
+ rule,
379
+ condition,
380
+ index,
381
+ "skipped",
382
+ "segment_and_failed",
383
+ undefined,
384
+ resolveNamedConditionExpected(condition),
385
+ ));
386
+ continue;
387
+ }
388
+ const result = diagnoseSegmentCondition(rule, condition, index, ctx);
389
+ diagnostics.push(result.diagnostic);
390
+ matched = result.matched;
391
+ }
392
+ return { matched, conditions: diagnostics };
393
+ }
394
+
395
+ function attributeCondition(
396
+ rule: AttributeRule,
397
+ index: number,
398
+ status: ConditionDiagnostic["status"],
399
+ reason: string | undefined,
400
+ actual?: unknown,
401
+ ): ConditionDiagnostic {
402
+ return {
403
+ id: `attribute:${rule.attribute}:${index}`,
404
+ label: `Attribute rule: ${rule.attribute}`,
405
+ status,
406
+ reason,
407
+ operator: rule.operator,
408
+ signal: rule.attribute,
409
+ expected: rule.value,
410
+ actual,
411
+ source: rule.attribute,
412
+ sourceType: "attribute",
413
+ };
414
+ }
415
+
416
+ function diagnoseAttributeRules(
417
+ rules: AttributeRule[],
418
+ attrs: Record<string, unknown>,
419
+ ): { matched: boolean; conditions: ConditionDiagnostic[] } {
420
+ const conditions: ConditionDiagnostic[] = [];
421
+ let matched = true;
422
+ for (let index = 0; index < rules.length; index += 1) {
423
+ const rule = rules[index];
424
+ const actual = attrs[rule.attribute];
425
+ if (!matched) {
426
+ conditions.push(attributeCondition(rule, index, "skipped", "attribute_mismatch", actual));
427
+ continue;
428
+ }
429
+ const ruleMatched = matchAttributeRule(attrs, rule);
430
+ conditions.push(attributeCondition(
431
+ rule,
432
+ index,
433
+ ruleMatched ? "passed" : "failed",
434
+ ruleMatched ? undefined : "attribute_mismatch",
435
+ actual,
436
+ ));
437
+ matched = ruleMatched;
438
+ }
439
+ return { matched, conditions };
440
+ }
441
+
442
+ function skippedAttributeConditions(rules: AttributeRule[], reason: string): ConditionDiagnostic[] {
443
+ return rules.map((rule, index) =>
444
+ attributeCondition(rule, index, "skipped", reason, undefined),
445
+ );
446
+ }
447
+
448
+ function diagnoseSegmentRules(
449
+ rules: SegmentTargetingRule[],
450
+ ctx: { url: string; attributes: Record<string, unknown> },
451
+ ): { matched: boolean; conditions: ConditionDiagnostic[] } {
452
+ const conditions: ConditionDiagnostic[] = [];
453
+ if (!rules.length) return { matched: true, conditions };
454
+
455
+ let hasInclude = false;
456
+ let included = false;
457
+ let stopped = false;
458
+ for (const rule of rules) {
459
+ if (stopped) {
460
+ conditions.push(...skippedSegmentRuleConditions(rule, "segment_exclude_matched"));
461
+ continue;
462
+ }
463
+ const result = diagnoseSegmentRule(rule, ctx);
464
+ conditions.push(...result.conditions);
465
+ if (rule.include === false) {
466
+ if (result.matched) stopped = true;
467
+ } else {
468
+ hasInclude = true;
469
+ if (result.matched) included = true;
470
+ }
471
+ }
472
+
473
+ return { matched: stopped ? false : included || !hasInclude, conditions };
474
+ }
475
+
476
+ function skippedSegmentConditions(rules: SegmentTargetingRule[], reason: string): ConditionDiagnostic[] {
477
+ return rules.flatMap((rule) => skippedSegmentRuleConditions(rule, reason));
478
+ }
479
+
480
+ export function evaluateTargetingGatesWithConditionDiagnostics(
481
+ targeting: Targeting | undefined,
482
+ ctx: { url: string; attributes: Record<string, unknown> },
483
+ ): TargetingDiagnostics {
484
+ const conditions: ConditionDiagnostic[] = [];
485
+ if (!targeting) {
486
+ return {
487
+ matched: true,
488
+ gates: [
489
+ { id: "page", label: "Page check", status: "skipped", reason: "no_page_rules" },
490
+ { id: "audience", label: "Audience check", status: "skipped", reason: "no_audience_rules" },
491
+ ],
492
+ conditions,
493
+ };
494
+ }
495
+
496
+ const gates: ActivationGateDiagnostic[] = [];
497
+ const urlRules = normalizeRuntimeUrlRules(targeting);
498
+ const pageRules = targeting.pageRules ?? [];
499
+ const pageHasRules = urlRules.length > 0 || pageRules.length > 0;
500
+ const url = diagnoseUrlRules(ctx.url, urlRules);
501
+ conditions.push(...url.conditions);
502
+
503
+ let pageRulesMatched = true;
504
+ if (url.matched) {
505
+ const page = diagnosePageRules(pageRules, ctx);
506
+ pageRulesMatched = page.matched;
507
+ conditions.push(...page.conditions);
508
+ } else {
509
+ pageRulesMatched = false;
510
+ conditions.push(...skippedPageRuleConditions(pageRules, "url_mismatch"));
511
+ }
512
+
513
+ const pageMatched = pageHasRules ? url.matched && pageRulesMatched : true;
514
+ gates.push({
515
+ id: "page",
516
+ label: "Page check",
517
+ status: pageHasRules ? (pageMatched ? "passed" : "failed") : "skipped",
518
+ reason: pageHasRules ? (pageMatched ? "url_matched" : "url_mismatch") : "no_page_rules",
519
+ detail: pageHasRules ? { url: ctx.url, rules: urlRules.length, pageRules: pageRules.length } : undefined,
520
+ });
521
+
522
+ const attributeRules = targeting.attributes ?? [];
523
+ const segmentRules = targeting.segmentRules ?? [];
524
+ if (!pageMatched) {
525
+ conditions.push(...skippedAttributeConditions(attributeRules, "page_mismatch"));
526
+ conditions.push(...skippedSegmentConditions(segmentRules, "page_mismatch"));
527
+ gates.push({
528
+ id: "audience",
529
+ label: "Audience check",
530
+ status: "skipped",
531
+ reason: "page_mismatch",
532
+ detail: { skipped: true },
533
+ });
534
+ return { matched: false, gates, conditions };
535
+ }
536
+
537
+ const attributes = diagnoseAttributeRules(attributeRules, ctx.attributes);
538
+ conditions.push(...attributes.conditions);
539
+
540
+ const segments = diagnoseSegmentRules(segmentRules, ctx);
541
+ conditions.push(...segments.conditions);
542
+
543
+ const audienceHasRules = attributeRules.length > 0 || segmentRules.length > 0;
544
+ const audienceMatched = attributes.matched && segments.matched;
545
+ gates.push({
546
+ id: "audience",
547
+ label: "Audience check",
548
+ status: audienceHasRules ? (audienceMatched ? "passed" : "failed") : "skipped",
549
+ reason: audienceHasRules
550
+ ? (audienceMatched ? "audience_matched" : !attributes.matched ? "attribute_mismatch" : "segment_mismatch")
551
+ : "no_audience_rules",
552
+ detail: audienceHasRules ? { attributeRules: attributeRules.length, segmentRules: segmentRules.length } : undefined,
553
+ });
554
+
555
+ return {
556
+ matched: pageMatched && audienceMatched,
557
+ gates,
558
+ conditions,
559
+ };
560
+ }