@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,505 @@
1
+ import type {
2
+ Assignment,
3
+ ConditionDiagnostic,
4
+ Experiment,
5
+ ExperimentActivationDiagnostic,
6
+ Variation,
7
+ } from "./types";
8
+ import { exitQaMode } from "./guards";
9
+ import type { DevtoolsEvent } from "./devtools";
10
+ import { subscribeDevtoolsEvents } from "./devtools";
11
+ import {
12
+ __forceQaAssistantVariation,
13
+ __getQaAssistantRuntimeSnapshot,
14
+ __resetQaAssistantForce,
15
+ __subscribeQaAssistantRuntime,
16
+ } from "./index";
17
+
18
+ export type QaExperimentModel = {
19
+ id: string; name: string;
20
+ variationId: string | null; variationName: string | null;
21
+ applied: boolean; forced: boolean;
22
+ subline: string;
23
+ triggerPill: "accepted" | "rejected" | "not evaluated" | "all pages";
24
+ segmentPill: "accepted" | "rejected" | "not evaluated" | "all visitors";
25
+ displayFrequencyPill: "accepted" | "outside traffic %" | "personalization stop" | "excluded by overlap" | "not running" | "not evaluated" | "forced";
26
+ conditions: Array<{ id: string; group: "pages" | "audience"; state: "pass"|"fail"|"skip"|"error"; text: string; detail: string | null; rawCode: string | null }>;
27
+ variations: Array<{ id: string; name: string; isControl: boolean; isCurrent: boolean; isReal: boolean }>;
28
+ };
29
+ export type QaEventModel = { at: number; type: "exposure"|"goal"|"revenue"|"pageview"; label: string; value: string | null; duplicateOf?: number };
30
+ export type QaTransactionsModel =
31
+ | { kind: "unsupported" }
32
+ | { kind: "shopify"; cartStamped: boolean; variationName: string | null; stampedAt: number | null; attribution: Array<{ experimentName: string; variationName: string | null }> }
33
+ | { kind: "order"; orderNumber: string | null; attribution: Array<{ experimentName: string; variationName: string | null }>; serverReceipt: "pending"|"confirmed"|"unknown" };
34
+ export type QaAssistantState = { experiments: QaExperimentModel[]; events: QaEventModel[]; transactions: QaTransactionsModel; forcedSummary: string | null };
35
+ export type QaAssistantStore = {
36
+ getState(): QaAssistantState;
37
+ subscribe(listener: (s: QaAssistantState) => void): () => void;
38
+ forceVariation(experimentId: string, variationId: string): void;
39
+ resetForce(experimentId?: string): void;
40
+ exitQa(): void;
41
+ };
42
+ export function createQaAssistantStore(): QaAssistantStore {
43
+ let state = buildState([], { kind: "unsupported" });
44
+ const listeners = new Set<(s: QaAssistantState) => void>();
45
+ let cartFetchSignature: string | null = null;
46
+ let disposed = false;
47
+
48
+ function emit(): void {
49
+ for (const listener of listeners) {
50
+ listener(state);
51
+ }
52
+ }
53
+
54
+ function setState(next: QaAssistantState): void {
55
+ state = next;
56
+ emit();
57
+ }
58
+
59
+ function refresh(): void {
60
+ const baseTransactions = transactionShell();
61
+ setState(buildState(state.events, baseTransactions));
62
+ void refreshShopifyCart(baseTransactions);
63
+ }
64
+
65
+ async function refreshShopifyCart(baseTransactions: QaTransactionsModel): Promise<void> {
66
+ if (baseTransactions.kind !== "shopify") return;
67
+ const runtime = __getQaAssistantRuntimeSnapshot();
68
+ const signature = runtime.assignments
69
+ .map((assignment) => `${assignment.experimentId}:${assignment.variationId}`)
70
+ .sort()
71
+ .join("|");
72
+ if (cartFetchSignature === signature) return;
73
+ cartFetchSignature = signature;
74
+ const cart = await readShopifyCart();
75
+ if (disposed || cartFetchSignature !== signature) return;
76
+ setState(buildState(state.events, {
77
+ ...baseTransactions,
78
+ cartStamped: cartMatchesAssignments(cart, runtime.assignments),
79
+ variationName: primaryStampedVariationName(cart, runtime.config?.experiments ?? []),
80
+ stampedAt: null,
81
+ }));
82
+ }
83
+
84
+ const unsubscribeRuntime = __subscribeQaAssistantRuntime(refresh);
85
+ const unsubscribeEvents = subscribeDevtoolsEvents((event) => {
86
+ const mapped = mapDevtoolsEvent(event);
87
+ if (!mapped) return;
88
+ const events = appendEvent(state.events, mapped);
89
+ setState(buildState(events, state.transactions));
90
+ });
91
+
92
+ refresh();
93
+
94
+ return {
95
+ getState() {
96
+ return state;
97
+ },
98
+ subscribe(listener) {
99
+ listeners.add(listener);
100
+ return () => {
101
+ listeners.delete(listener);
102
+ };
103
+ },
104
+ forceVariation(experimentId, variationId) {
105
+ __forceQaAssistantVariation(experimentId, variationId);
106
+ },
107
+ resetForce(experimentId) {
108
+ __resetQaAssistantForce(experimentId);
109
+ },
110
+ exitQa() {
111
+ disposed = true;
112
+ unsubscribeRuntime();
113
+ unsubscribeEvents();
114
+ exitQaMode();
115
+ removeAssistantHost();
116
+ },
117
+ };
118
+ }
119
+
120
+ function buildState(events: QaEventModel[], transactions: QaTransactionsModel): QaAssistantState {
121
+ const runtime = __getQaAssistantRuntimeSnapshot();
122
+ return {
123
+ experiments: mapExperiments(
124
+ runtime.config?.experiments ?? [],
125
+ runtime.assignments,
126
+ runtime.activationDiagnostics.experiments,
127
+ ),
128
+ events,
129
+ transactions,
130
+ forcedSummary: forcedSummary(runtime.config?.experiments ?? [], runtime.forceMap),
131
+ };
132
+ }
133
+
134
+ function mapExperiments(
135
+ experiments: Experiment[],
136
+ assignments: Assignment[],
137
+ diagnostics: ExperimentActivationDiagnostic[],
138
+ ): QaExperimentModel[] {
139
+ const assignmentByExperiment = new Map(assignments.map((assignment) => [assignment.experimentId, assignment]));
140
+ const diagnosticByExperiment = new Map(diagnostics.map((diagnostic) => [diagnostic.experimentId, diagnostic]));
141
+ return experiments.map((experiment) => {
142
+ const assignment = assignmentByExperiment.get(experiment.id) ?? null;
143
+ const diagnostic = diagnosticByExperiment.get(experiment.id) ?? null;
144
+ const variation = assignment
145
+ ? experiment.variations.find((item) => item.id === assignment.variationId) ?? null
146
+ : null;
147
+ const variationName = variation ? variationDisplayName(variation, experiment.variations.indexOf(variation)) : null;
148
+ // "Forced" in the model requires the force to be in effect: overlap
149
+ // resolution / stop-after-match remove the assignment while preserving
150
+ // diagnostic.forced, and a non-displayed campaign must not group as forced.
151
+ const forced = Boolean(diagnostic?.forced) && Boolean(assignment);
152
+ const conditions = (diagnostic?.targetingConditions ?? []).map(mapCondition);
153
+ const pageConditions = conditions.filter((condition) => condition.group === "pages");
154
+ const audienceConditions = conditions.filter((condition) => condition.group === "audience");
155
+ return {
156
+ id: experiment.id,
157
+ name: experiment.name || shortId(experiment.id),
158
+ variationId: assignment?.variationId ?? null,
159
+ variationName,
160
+ applied: Boolean(assignment),
161
+ forced,
162
+ triggerPill: targetingPill(pageConditions, "all pages", diagnostic?.gates.find((gate) => gate.id === "page")),
163
+ segmentPill: targetingPill(audienceConditions, "all visitors", diagnostic?.gates.find((gate) => gate.id === "audience")),
164
+ displayFrequencyPill: displayFrequencyPill(diagnostic),
165
+ subline: experimentSubline(Boolean(assignment), forced, variationName, conditions, diagnostic),
166
+ conditions,
167
+ variations: experiment.variations.map((item, index) => ({
168
+ id: item.id,
169
+ name: variationDisplayName(item, index),
170
+ isControl: Boolean(item.isControl),
171
+ isCurrent: item.id === assignment?.variationId,
172
+ isReal: true,
173
+ })),
174
+ };
175
+ });
176
+ }
177
+
178
+ function experimentSubline(
179
+ applied: boolean,
180
+ forced: boolean,
181
+ variationName: string | null,
182
+ conditions: QaExperimentModel["conditions"],
183
+ diagnostic: ExperimentActivationDiagnostic | null,
184
+ ): string {
185
+ if (applied) return `${forced ? "Forced to" : "Showing"} ${variationName ?? "variation"}`;
186
+ const firstFailed = conditions.find((condition) => condition.state === "fail" || condition.state === "error");
187
+ if (firstFailed) return firstFailed.text;
188
+ return diagnostic?.reason || "Not applied";
189
+ }
190
+
191
+ function mapCondition(condition: ConditionDiagnostic): QaExperimentModel["conditions"][number] {
192
+ const reason = condition.reason ?? "";
193
+ const isJsError = condition.sourceType === "js" && reason.startsWith("js_error");
194
+ return {
195
+ id: condition.id,
196
+ group: condition.sourceType === "attribute" || condition.sourceType === "segment" || condition.id.startsWith("segment:") || condition.id.startsWith("attribute:") ? "audience" : "pages",
197
+ state: isJsError ? "error" : condition.status === "passed" ? "pass" : condition.status === "failed" ? "fail" : "skip",
198
+ text: conditionText(condition),
199
+ detail: conditionDetail(condition, isJsError),
200
+ rawCode: condition.sourceType === "js" ? condition.source ?? null : null,
201
+ };
202
+ }
203
+
204
+ function conditionText(condition: ConditionDiagnostic): string {
205
+ if (condition.sourceType === "js") return "Custom JS trigger";
206
+ if (condition.sourceType === "attribute" || condition.sourceType === "segment") {
207
+ return condition.signal
208
+ ? `${condition.signal} ${condition.operator ?? "matches"}`
209
+ : condition.label || condition.source || condition.id;
210
+ }
211
+ return condition.label || condition.source || condition.id;
212
+ }
213
+
214
+ function conditionDetail(condition: ConditionDiagnostic, isJsError: boolean): string | null {
215
+ if (isJsError) return (condition.reason ?? "").replace(/^js_error:\s*/, "") || "JavaScript error";
216
+ if (condition.status === "skipped") {
217
+ return `Skipped — ${condition.reason || "an earlier gate"} failed first`;
218
+ }
219
+ const parts: string[] = [];
220
+ if (condition.signal) parts.push(`Signal: ${condition.signal}`);
221
+ if (condition.operator) parts.push(`Operator: ${condition.operator}`);
222
+ if (condition.sourceType === "page_rule" && condition.source) parts.push(`Rules: ${condition.source}`);
223
+ const jsPatternSummary = condition.sourceType === "js" && isPageRulePatternSummary(condition.expected);
224
+ if (jsPatternSummary) parts.push(`Rules: ${formatValue(condition.expected)}`);
225
+ else if (condition.expected !== undefined) parts.push(`Expected: ${formatValue(condition.expected)}`);
226
+ if (condition.actual !== undefined) parts.push(`Actual: ${formatValue(condition.actual)}`);
227
+ return parts.length ? parts.join(" · ") : condition.reason ?? null;
228
+ }
229
+
230
+ function targetingPill(conditions: QaExperimentModel["conditions"], empty: "all pages", gate?: ExperimentActivationDiagnostic["gates"][number]): QaExperimentModel["triggerPill"];
231
+ function targetingPill(conditions: QaExperimentModel["conditions"], empty: "all visitors", gate?: ExperimentActivationDiagnostic["gates"][number]): QaExperimentModel["segmentPill"];
232
+ function targetingPill(
233
+ conditions: QaExperimentModel["conditions"],
234
+ empty: "all pages" | "all visitors",
235
+ gate?: ExperimentActivationDiagnostic["gates"][number],
236
+ ): "accepted" | "rejected" | "not evaluated" | "all pages" | "all visitors" {
237
+ const noRulesReason = empty === "all pages" ? "no_page_rules" : "no_audience_rules";
238
+ if (gate?.status === "skipped" && gate.reason === noRulesReason) return empty;
239
+ if (gate?.status === "skipped") return "not evaluated";
240
+ if (gate?.status === "passed") return "accepted";
241
+ if (gate?.status === "failed") return "rejected";
242
+ // No page/audience gate at all means evaluation returned before targeting
243
+ // ran (paused campaign, no variations) — configured rules were never
244
+ // checked, so neither the no-rules labels nor acceptance apply.
245
+ if (conditions.length === 0) return "not evaluated";
246
+ if (conditions.every((condition) => condition.state === "skip")) return "not evaluated";
247
+ return conditions.some((condition) => condition.state === "fail" || condition.state === "error")
248
+ ? "rejected"
249
+ : "accepted";
250
+ }
251
+
252
+ function isPageRulePatternSummary(value: unknown): boolean {
253
+ if (typeof value !== "string") return false;
254
+ try {
255
+ const rules = JSON.parse(value) as unknown;
256
+ return Array.isArray(rules) && rules.length > 0 && rules.every((rule) =>
257
+ typeof rule === "object" && rule !== null && "pattern" in rule
258
+ );
259
+ } catch {
260
+ return false;
261
+ }
262
+ }
263
+
264
+ function displayFrequencyPill(
265
+ diagnostic: ExperimentActivationDiagnostic | null,
266
+ ): QaExperimentModel["displayFrequencyPill"] {
267
+ // Suppression reasons outrank the forced flag: a forced lower-priority
268
+ // personalization removed by stop-after-match keeps diagnostic.forced but
269
+ // has no active assignment.
270
+ if (diagnostic?.reason === "personalization_stopped_after_prior_match") return "personalization stop";
271
+ // Exclusion-group suppression ALSO outranks forced: forcing multiple
272
+ // experiments in one exclusion group removes the losers' assignments and
273
+ // sets an overlap reason while preserving diagnostic.forced — labeling
274
+ // the suppressed campaign "forced" would falsely claim it is displayed.
275
+ if (diagnostic?.reason === "exclusion_group_overlap_lost" || diagnostic?.reason === "exclusion_group_sticky_other") return "excluded by overlap";
276
+ // "forced" requires evidence the force actually took effect — a force
277
+ // request that targeting blocked leaves no forced diagnostic and must not
278
+ // render as displayed.
279
+ if (diagnostic?.forced || diagnostic?.reason === "forced_variation") return "forced";
280
+ if (diagnostic?.reason === "outside_allocation") return "outside traffic %";
281
+ if (diagnostic?.reason?.startsWith("status_")) return "not running";
282
+ // Derive from the traffic gate: when targeting rejected the campaign,
283
+ // evaluation returns before the traffic gate runs — display frequency was
284
+ // never checked, so "accepted" would be a false claim.
285
+ const trafficGate = diagnostic?.gates.find((gate) => gate.id === "traffic");
286
+ if (trafficGate?.status === "passed") return "accepted";
287
+ if (trafficGate?.status === "failed") return "outside traffic %";
288
+ if (trafficGate?.status === "skipped" && trafficGate.reason === "forced_variation") return "forced";
289
+ return "not evaluated";
290
+ }
291
+
292
+ function transactionShell(): QaTransactionsModel {
293
+ const runtime = __getQaAssistantRuntimeSnapshot();
294
+ const attribution = attributionFromAssignments(runtime.config?.experiments ?? [], runtime.assignments);
295
+ if (runtime.orderIdentity) {
296
+ return {
297
+ kind: "order",
298
+ orderNumber: runtime.orderIdentity.orderNumber ?? runtime.orderIdentity.platformOrderId,
299
+ attribution,
300
+ serverReceipt: "unknown",
301
+ };
302
+ }
303
+ const shopifyConfigured =
304
+ runtime.platform === "shopify" ||
305
+ runtime.config?.platformHint === "shopify" ||
306
+ runtime.config?.profile?.platform === "shopify";
307
+ const shopifyRuntime = typeof window !== "undefined" && Boolean((window as unknown as { Shopify?: unknown }).Shopify);
308
+ if (shopifyConfigured && shopifyRuntime) {
309
+ return {
310
+ kind: "shopify",
311
+ cartStamped: false,
312
+ variationName: primaryAssignmentVariationName(runtime.config?.experiments ?? [], runtime.assignments),
313
+ stampedAt: null,
314
+ attribution,
315
+ };
316
+ }
317
+ return { kind: "unsupported" };
318
+ }
319
+
320
+ async function readShopifyCart(): Promise<Record<string, unknown> | null> {
321
+ if (typeof fetch !== "function") return null;
322
+ try {
323
+ const response = await fetch("/cart.js", { headers: { Accept: "application/json" } });
324
+ if (!response.ok) return null;
325
+ const cart = await response.json();
326
+ return cart && typeof cart === "object" && !Array.isArray(cart)
327
+ ? cart as Record<string, unknown>
328
+ : null;
329
+ } catch {
330
+ return null;
331
+ }
332
+ }
333
+
334
+ function readCartAttributes(cart: Record<string, unknown> | null): Record<string, unknown> {
335
+ const attributes = cart?.attributes;
336
+ return attributes && typeof attributes === "object" && !Array.isArray(attributes)
337
+ ? attributes as Record<string, unknown>
338
+ : {};
339
+ }
340
+
341
+ function parseCartAssignments(cart: Record<string, unknown> | null): Array<{ experimentId: string; variationId: string }> {
342
+ const attributes = readCartAttributes(cart);
343
+ const raw = attributes.apex_assignments;
344
+ if (typeof raw === "string") {
345
+ try {
346
+ const parsed = JSON.parse(raw);
347
+ if (Array.isArray(parsed)) {
348
+ return parsed
349
+ .map((item) => ({
350
+ experimentId: typeof item?.e === "string" ? item.e : "",
351
+ variationId: typeof item?.v === "string" ? item.v : "",
352
+ }))
353
+ .filter((item) => item.experimentId && item.variationId);
354
+ }
355
+ } catch {}
356
+ }
357
+ const experimentId = typeof attributes.apex_experiment_id === "string" ? attributes.apex_experiment_id : "";
358
+ const variationId = typeof attributes.apex_variation_id === "string" ? attributes.apex_variation_id : "";
359
+ return experimentId && variationId ? [{ experimentId, variationId }] : [];
360
+ }
361
+
362
+ function cartMatchesAssignments(cart: Record<string, unknown> | null, assignments: Assignment[]): boolean {
363
+ const stamped = parseCartAssignments(cart);
364
+ if (!stamped.length || !assignments.length) return false;
365
+ const active = new Set(assignments.map((assignment) => `${assignment.experimentId}:${assignment.variationId}`));
366
+ return stamped.some((assignment) => active.has(`${assignment.experimentId}:${assignment.variationId}`));
367
+ }
368
+
369
+ function primaryStampedVariationName(cart: Record<string, unknown> | null, experiments: Experiment[]): string | null {
370
+ const [primary] = parseCartAssignments(cart);
371
+ if (!primary) return null;
372
+ return variationNameById(experiments, primary.experimentId, primary.variationId);
373
+ }
374
+
375
+ function attributionFromAssignments(
376
+ experiments: Experiment[],
377
+ assignments: Assignment[],
378
+ ): Array<{ experimentName: string; variationName: string | null }> {
379
+ return assignments.map((assignment) => ({
380
+ experimentName: experimentNameById(experiments, assignment.experimentId),
381
+ variationName: variationNameById(experiments, assignment.experimentId, assignment.variationId),
382
+ }));
383
+ }
384
+
385
+ function forcedSummary(experiments: Experiment[], forceMap: Record<string, string> | null): string | null {
386
+ const entries = Object.entries(forceMap ?? {});
387
+ if (!entries.length) return null;
388
+ return entries.map(([experimentId, variationId]) => {
389
+ const experimentName = experimentNameById(experiments, experimentId);
390
+ const variationName = variationNameById(experiments, experimentId, variationId) ?? shortId(variationId);
391
+ return `${experimentName}: ${variationName}`;
392
+ }).join(", ");
393
+ }
394
+
395
+ function primaryAssignmentVariationName(experiments: Experiment[], assignments: Assignment[]): string | null {
396
+ const [assignment] = assignments;
397
+ return assignment ? variationNameById(experiments, assignment.experimentId, assignment.variationId) : null;
398
+ }
399
+
400
+ function experimentNameById(experiments: Experiment[], experimentId: string): string {
401
+ const experiment = experiments.find((item) => item.id === experimentId);
402
+ return experiment?.name || shortId(experimentId);
403
+ }
404
+
405
+ function variationNameById(experiments: Experiment[], experimentId: string, variationId: string): string | null {
406
+ const experiment = experiments.find((item) => item.id === experimentId);
407
+ const index = experiment?.variations.findIndex((item) => item.id === variationId) ?? -1;
408
+ const variation = index >= 0 ? experiment?.variations[index] : null;
409
+ return variation ? variationDisplayName(variation, index) : shortId(variationId);
410
+ }
411
+
412
+ function variationDisplayName(variation: Variation, index: number): string {
413
+ return variation.name || (variation.isControl ? "Control" : shortId(variation.id || `variation-${index + 1}`));
414
+ }
415
+
416
+ function shortId(id: string): string {
417
+ return id.length > 12 ? id.slice(0, 8) : id;
418
+ }
419
+
420
+ function mapDevtoolsEvent(event: DevtoolsEvent): QaEventModel | null {
421
+ if (event.type !== "track") return null;
422
+ const detail = event.detail ?? {};
423
+ const type = eventType(event.label, detail);
424
+ if (!type) return null;
425
+ return {
426
+ at: event.ts,
427
+ type,
428
+ label: eventLabel(type, event.label, detail),
429
+ value: eventValue(type, detail),
430
+ };
431
+ }
432
+
433
+ function eventType(label: string, detail: Record<string, unknown>): QaEventModel["type"] | null {
434
+ if (label === "experiment_viewed" || label === "personalization_rendered") return "exposure";
435
+ if (label === "pageview" || label === "experiment_pageview") return "pageview";
436
+ if (label === "goal") return detail.goalId === "__revenue__" || detail.revenue !== undefined ? "revenue" : "goal";
437
+ return null;
438
+ }
439
+
440
+ function eventLabel(type: QaEventModel["type"], label: string, detail: Record<string, unknown>): string {
441
+ if (type === "goal") return typeof detail.goalId === "string" ? detail.goalId : "Goal";
442
+ if (type === "revenue") return "Revenue";
443
+ if (type === "pageview") return typeof detail.path === "string" ? detail.path : "Pageview";
444
+ const runtime = __getQaAssistantRuntimeSnapshot();
445
+ const experimentId = typeof detail.experimentId === "string" ? detail.experimentId : "";
446
+ const variationId = typeof detail.variationId === "string" ? detail.variationId : "";
447
+ const experimentName = experimentId ? experimentNameById(runtime.config?.experiments ?? [], experimentId) : "Exposure";
448
+ const variationName = experimentId && variationId
449
+ ? variationNameById(runtime.config?.experiments ?? [], experimentId, variationId)
450
+ : null;
451
+ return variationName ? `${experimentName} / ${variationName}` : experimentName || label;
452
+ }
453
+
454
+ function eventValue(type: QaEventModel["type"], detail: Record<string, unknown>): string | null {
455
+ if (type !== "revenue") return null;
456
+ const revenue = detail.revenue;
457
+ const currency = typeof detail.currency === "string" ? detail.currency.trim().toUpperCase() : "";
458
+ if (typeof revenue === "number" && Number.isFinite(revenue)) {
459
+ return currency ? `${formatNumber(revenue)} ${currency}` : formatNumber(revenue);
460
+ }
461
+ return currency && revenue != null ? `${String(revenue)} ${currency}` : revenue == null ? null : String(revenue);
462
+ }
463
+
464
+ function appendEvent(events: QaEventModel[], event: QaEventModel): QaEventModel[] {
465
+ let duplicate: QaEventModel | undefined;
466
+ for (let index = events.length - 1; index >= 0; index -= 1) {
467
+ const candidate = events[index];
468
+ if (
469
+ candidate.type === event.type &&
470
+ candidate.label === event.label &&
471
+ event.at - candidate.at <= 1500
472
+ ) {
473
+ duplicate = candidate;
474
+ break;
475
+ }
476
+ }
477
+ return [...events, duplicate ? { ...event, duplicateOf: duplicate.at } : event].slice(-100);
478
+ }
479
+
480
+ function formatValue(value: unknown): string {
481
+ if (typeof value === "string") return value;
482
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
483
+ if (value == null) return String(value);
484
+ try {
485
+ return JSON.stringify(value);
486
+ } catch {
487
+ return String(value);
488
+ }
489
+ }
490
+
491
+ function formatNumber(value: number): string {
492
+ return Number.isInteger(value) ? String(value) : value.toFixed(2);
493
+ }
494
+
495
+ function removeAssistantHost(): void {
496
+ if (typeof document === "undefined") return;
497
+ const selectors = [
498
+ "#drip-qa-assistant-host",
499
+ "#apex-qa-assistant-host",
500
+ "[data-drip-qa-assistant-host]",
501
+ ];
502
+ for (const selector of selectors) {
503
+ document.querySelectorAll(selector).forEach((element) => element.remove());
504
+ }
505
+ }