@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.
- package/LICENSE +21 -0
- package/README.md +46 -0
- package/dist/_headers +3 -0
- package/dist/bundle-report.json +564 -0
- package/dist/drip.debug.js +2 -0
- package/dist/drip.debug.js.map +1 -0
- package/dist/drip.heatmap.js +2 -0
- package/dist/drip.heatmap.js.map +1 -0
- package/dist/drip.js +2 -0
- package/dist/drip.js.map +1 -0
- package/dist/drip.live.js +2 -0
- package/dist/drip.live.js.map +1 -0
- package/dist/editor.js +480 -0
- package/dist/editor.js.map +7 -0
- package/dist/index.mjs +645 -0
- package/dist/index.mjs.map +7 -0
- package/dist/metafiles/drip.debug.meta.json +973 -0
- package/dist/metafiles/drip.heatmap.meta.json +918 -0
- package/dist/metafiles/drip.live.meta.json +918 -0
- package/dist/metafiles/drip.meta.json +973 -0
- package/dist/metafiles/editor.meta.json +321 -0
- package/dist/metafiles/index.meta.json +1039 -0
- package/dist/metafiles/index.mjs.meta.json +78 -0
- package/dist/react/index.js +16973 -0
- package/dist/react/index.js.map +7 -0
- package/dist/sri.json +27 -0
- package/dist/types/analytics.d.ts +15 -0
- package/dist/types/antiflicker.d.ts +14 -0
- package/dist/types/assignment-store.d.ts +30 -0
- package/dist/types/audit.d.ts +9 -0
- package/dist/types/bucket.d.ts +2 -0
- package/dist/types/bucketing-cleanup.d.ts +5 -0
- package/dist/types/commerce-gift.d.ts +41 -0
- package/dist/types/commerce-price.d.ts +22 -0
- package/dist/types/commerce-shipping.d.ts +28 -0
- package/dist/types/css.d.ts +5 -0
- package/dist/types/cwv.d.ts +10 -0
- package/dist/types/data-layer-normalize.d.ts +1 -0
- package/dist/types/debug.d.ts +23 -0
- package/dist/types/define.d.ts +87 -0
- package/dist/types/devtools.d.ts +32 -0
- package/dist/types/dom-trigger.d.ts +3 -0
- package/dist/types/editor/api.d.ts +37 -0
- package/dist/types/editor/artifacts.d.ts +15 -0
- package/dist/types/editor/command-runtime.d.ts +48 -0
- package/dist/types/editor/controller.d.ts +97 -0
- package/dist/types/editor/device-preview.d.ts +23 -0
- package/dist/types/editor/index.d.ts +9 -0
- package/dist/types/editor/operator.d.ts +39 -0
- package/dist/types/editor/picker.d.ts +23 -0
- package/dist/types/editor/selector.d.ts +2 -0
- package/dist/types/editor/types.d.ts +55 -0
- package/dist/types/editor/view-kit.d.ts +33 -0
- package/dist/types/editor/view.d.ts +5 -0
- package/dist/types/editor-bootstrap.d.ts +37 -0
- package/dist/types/exclusion-groups.d.ts +59 -0
- package/dist/types/exposure-signals.d.ts +7 -0
- package/dist/types/flags.d.ts +65 -0
- package/dist/types/goals.d.ts +16 -0
- package/dist/types/guards.d.ts +39 -0
- package/dist/types/handover.d.ts +10 -0
- package/dist/types/hash.d.ts +19 -0
- package/dist/types/heatmap.d.ts +8 -0
- package/dist/types/hideflicker.d.ts +15 -0
- package/dist/types/hydration.d.ts +11 -0
- package/dist/types/index.d.ts +144 -0
- package/dist/types/js.d.ts +6 -0
- package/dist/types/module.d.ts +4 -0
- package/dist/types/money.d.ts +14 -0
- package/dist/types/mutate.d.ts +34 -0
- package/dist/types/observer-mute.d.ts +3 -0
- package/dist/types/pii-keys.d.ts +1 -0
- package/dist/types/prerequisites.d.ts +12 -0
- package/dist/types/qa-assistant-state.d.ts +68 -0
- package/dist/types/qa-assistant-view.d.ts +6 -0
- package/dist/types/qa-assistant.d.ts +9 -0
- package/dist/types/qa-toolbar.d.ts +19 -0
- package/dist/types/react/index.d.ts +84 -0
- package/dist/types/reassert-sentinel.d.ts +6 -0
- package/dist/types/router.d.ts +9 -0
- package/dist/types/runtime-tools.d.ts +50 -0
- package/dist/types/runtime.d.ts +22 -0
- package/dist/types/safe-regex.d.ts +6 -0
- package/dist/types/sanitize.d.ts +5 -0
- package/dist/types/semantic.d.ts +7 -0
- package/dist/types/shopify-cart-events.d.ts +7 -0
- package/dist/types/shopify-cart.d.ts +62 -0
- package/dist/types/signals.d.ts +31 -0
- package/dist/types/surface-signals.d.ts +6 -0
- package/dist/types/targeting-diagnostics.d.ts +5 -0
- package/dist/types/targeting.d.ts +50 -0
- package/dist/types/test-fixtures/commerce.d.ts +12 -0
- package/dist/types/track.d.ts +79 -0
- package/dist/types/types.d.ts +859 -0
- package/dist/types/wait.d.ts +7 -0
- package/package.json +74 -0
- package/src/analytics.ts +146 -0
- package/src/antiflicker.test.ts +127 -0
- package/src/antiflicker.ts +147 -0
- package/src/assignment-store.test.ts +171 -0
- package/src/assignment-store.ts +208 -0
- package/src/audit.test.ts +17 -0
- package/src/audit.ts +362 -0
- package/src/bucket.test.ts +65 -0
- package/src/bucket.ts +18 -0
- package/src/bucketing-cleanup.test.ts +30 -0
- package/src/bucketing-cleanup.ts +72 -0
- package/src/bucketing-parity.test.ts +41 -0
- package/src/commerce-analytics-lifecycle.test.ts +1416 -0
- package/src/commerce-config-lifecycle.test.ts +274 -0
- package/src/commerce-epoch-lifecycle.test.ts +62 -0
- package/src/commerce-gift.test.ts +1293 -0
- package/src/commerce-gift.ts +979 -0
- package/src/commerce-offers-display.test.ts +154 -0
- package/src/commerce-price.test.ts +1145 -0
- package/src/commerce-price.ts +941 -0
- package/src/commerce-revocation-lifecycle.test.ts +78 -0
- package/src/commerce-shipping.test.ts +630 -0
- package/src/commerce-shipping.ts +447 -0
- package/src/config-fetch.test.ts +325 -0
- package/src/css.ts +54 -0
- package/src/cwv.test.ts +95 -0
- package/src/cwv.ts +129 -0
- package/src/data-layer-normalize.ts +21 -0
- package/src/debug.test.ts +83 -0
- package/src/debug.ts +138 -0
- package/src/define.test.ts +327 -0
- package/src/define.ts +208 -0
- package/src/devtools.ts +582 -0
- package/src/dom-trigger.test.ts +70 -0
- package/src/dom-trigger.ts +55 -0
- package/src/editor/api.test.ts +154 -0
- package/src/editor/api.ts +185 -0
- package/src/editor/artifacts.test.ts +57 -0
- package/src/editor/artifacts.ts +115 -0
- package/src/editor/command-runtime.test.ts +225 -0
- package/src/editor/command-runtime.ts +572 -0
- package/src/editor/controller.test.ts +371 -0
- package/src/editor/controller.ts +596 -0
- package/src/editor/device-preview.test.ts +60 -0
- package/src/editor/device-preview.ts +120 -0
- package/src/editor/index.test.ts +161 -0
- package/src/editor/index.ts +169 -0
- package/src/editor/operator.test.ts +148 -0
- package/src/editor/operator.ts +256 -0
- package/src/editor/picker.test.ts +58 -0
- package/src/editor/picker.ts +152 -0
- package/src/editor/selector.test.ts +44 -0
- package/src/editor/selector.ts +110 -0
- package/src/editor/types.ts +92 -0
- package/src/editor/view-kit.ts +162 -0
- package/src/editor/view.test.ts +535 -0
- package/src/editor/view.ts +1395 -0
- package/src/editor-bootstrap.test.ts +116 -0
- package/src/editor-bootstrap.ts +213 -0
- package/src/event-schema-contract.test.ts +35 -0
- package/src/exclusion-groups.test.ts +383 -0
- package/src/exclusion-groups.ts +270 -0
- package/src/exposure-signals.test.ts +182 -0
- package/src/exposure-signals.ts +110 -0
- package/src/flags.test.ts +322 -0
- package/src/flags.ts +201 -0
- package/src/goals.test.ts +42 -0
- package/src/goals.ts +200 -0
- package/src/guards.test.ts +249 -0
- package/src/guards.ts +250 -0
- package/src/handover-cache-order.test.ts +32 -0
- package/src/handover.test.ts +349 -0
- package/src/handover.ts +309 -0
- package/src/hash.ts +214 -0
- package/src/heatmap.test.ts +305 -0
- package/src/heatmap.ts +373 -0
- package/src/hideflicker.ts +21 -0
- package/src/hydration.test.ts +252 -0
- package/src/hydration.ts +203 -0
- package/src/index.ts +7513 -0
- package/src/init-empty.test.ts +5572 -0
- package/src/js.test.ts +26 -0
- package/src/js.ts +61 -0
- package/src/module.ts +50 -0
- package/src/money.test.ts +35 -0
- package/src/money.ts +53 -0
- package/src/mutate.test.ts +304 -0
- package/src/mutate.ts +469 -0
- package/src/observer-mute.test.ts +88 -0
- package/src/observer-mute.ts +45 -0
- package/src/page-trigger.test.ts +84 -0
- package/src/personalization.test.ts +68 -0
- package/src/pii-keys.ts +17 -0
- package/src/prerequisites.test.ts +39 -0
- package/src/prerequisites.ts +94 -0
- package/src/qa-assistant-state.test.ts +471 -0
- package/src/qa-assistant-state.ts +505 -0
- package/src/qa-assistant-view.test.ts +185 -0
- package/src/qa-assistant-view.ts +768 -0
- package/src/qa-assistant.ts +40 -0
- package/src/qa-toolbar.ts +444 -0
- package/src/react/index.test.tsx +177 -0
- package/src/react/index.tsx +252 -0
- package/src/reassert-sentinel.test.ts +314 -0
- package/src/reassert-sentinel.ts +191 -0
- package/src/router.test.ts +78 -0
- package/src/router.ts +92 -0
- package/src/runtime-tools.test.ts +112 -0
- package/src/runtime-tools.ts +403 -0
- package/src/runtime.test.ts +217 -0
- package/src/runtime.ts +268 -0
- package/src/safe-regex.ts +35 -0
- package/src/sanitize.ts +34 -0
- package/src/semantic.ts +245 -0
- package/src/shopify-cart-events.test.ts +76 -0
- package/src/shopify-cart-events.ts +115 -0
- package/src/shopify-cart.test.ts +2823 -0
- package/src/shopify-cart.ts +1256 -0
- package/src/signals.ts +226 -0
- package/src/surface-signals.test.ts +83 -0
- package/src/surface-signals.ts +165 -0
- package/src/targeting-diagnostics.ts +560 -0
- package/src/targeting.detailed.test.ts +443 -0
- package/src/targeting.test.ts +1054 -0
- package/src/targeting.ts +1024 -0
- package/src/test-fixtures/commerce.ts +65 -0
- package/src/track.test.ts +817 -0
- package/src/track.ts +519 -0
- package/src/types.ts +991 -0
- package/src/wait.test.ts +96 -0
- 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
|
+
}
|