@drip-apex/sdk 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/bundle-report.json +584 -226
- package/dist/define.cjs +123 -0
- package/dist/define.cjs.map +7 -0
- package/dist/define.mjs +102 -0
- package/dist/define.mjs.map +7 -0
- package/dist/drip.cjs +19262 -0
- package/dist/drip.cjs.map +7 -0
- package/dist/drip.debug.js +1 -1
- package/dist/drip.debug.js.map +1 -1
- package/dist/drip.heatmap.js +1 -1
- package/dist/drip.heatmap.js.map +1 -1
- package/dist/drip.js +1 -1
- package/dist/drip.js.map +1 -1
- package/dist/drip.live.js +1 -1
- package/dist/drip.live.js.map +1 -1
- package/dist/drip.mjs +19246 -0
- package/dist/drip.mjs.map +7 -0
- package/dist/drip.replay-recorder.js +2 -0
- package/dist/drip.replay-recorder.js.map +1 -0
- package/dist/editor.js +147 -142
- package/dist/editor.js.map +4 -4
- package/dist/flags.cjs +1788 -0
- package/dist/flags.cjs.map +7 -0
- package/dist/flags.mjs +1765 -0
- package/dist/flags.mjs.map +7 -0
- package/dist/index.mjs +12 -0
- package/dist/index.mjs.map +2 -2
- package/dist/metafiles/define.cjs.meta.json +28 -0
- package/dist/metafiles/define.mjs.meta.json +32 -0
- package/dist/metafiles/drip.cjs.meta.json +3193 -0
- package/dist/metafiles/drip.debug.meta.json +2859 -574
- package/dist/metafiles/drip.heatmap.meta.json +2732 -460
- package/dist/metafiles/drip.live.meta.json +2732 -460
- package/dist/metafiles/drip.meta.json +2859 -574
- package/dist/metafiles/drip.mjs.meta.json +3235 -0
- package/dist/metafiles/drip.replay-recorder.meta.json +82 -0
- package/dist/metafiles/editor.meta.json +67 -27
- package/dist/metafiles/flags.cjs.meta.json +220 -0
- package/dist/metafiles/flags.mjs.meta.json +225 -0
- package/dist/metafiles/index.meta.json +2717 -491
- package/dist/metafiles/index.mjs.meta.json +14 -4
- package/dist/react/index.js +16621 -13539
- package/dist/react/index.js.map +4 -4
- package/dist/sri.json +20 -15
- package/dist/types/activation-runtime.d.ts +91 -0
- package/dist/types/analytics-runtime.d.ts +166 -0
- package/dist/types/antiflicker.d.ts +9 -3
- package/dist/types/assignment-cookie-sync.d.ts +31 -0
- package/dist/types/assignment-runtime.d.ts +257 -0
- package/dist/types/assignment-store.d.ts +4 -8
- package/dist/types/bucket.d.ts +1 -2
- package/dist/types/compact-store.d.ts +1 -0
- package/dist/types/config-runtime.d.ts +313 -0
- package/dist/types/consent-evaluation.d.ts +27 -0
- package/dist/types/css.d.ts +5 -3
- package/dist/types/debug-loader.d.ts +2 -0
- package/dist/types/decide-assignments.d.ts +31 -0
- package/dist/types/delivery-timing.d.ts +8 -0
- package/dist/types/early-interaction-capture.d.ts +79 -0
- package/dist/types/early-reassert.d.ts +32 -0
- package/dist/types/edge-runtime.d.ts +11 -0
- package/dist/types/editor/api.d.ts +3 -0
- package/dist/types/editor/controller.d.ts +3 -1
- package/dist/types/editor/publication-result.d.ts +11 -0
- package/dist/types/editor/types.d.ts +2 -1
- package/dist/types/editor/view-kit.d.ts +1 -1
- package/dist/types/endpoint.d.ts +8 -0
- package/dist/types/exclusion-groups.d.ts +4 -7
- package/dist/types/flags.d.ts +2 -0
- package/dist/types/goal-eligibility.d.ts +4 -0
- package/dist/types/goals.d.ts +10 -1
- package/dist/types/guards.d.ts +7 -7
- package/dist/types/handover.d.ts +12 -1
- package/dist/types/hash.d.ts +8 -4
- package/dist/types/hydration.d.ts +2 -1
- package/dist/types/index.d.ts +24 -128
- package/dist/types/init-test-helpers.d.ts +48 -0
- package/dist/types/inline-bootstrap.d.ts +26 -0
- package/dist/types/js.d.ts +3 -1
- package/dist/types/money.d.ts +12 -0
- package/dist/types/mutate.d.ts +45 -6
- package/dist/types/performance.d.ts +2 -0
- package/dist/types/persistence-decoders.d.ts +12 -0
- package/dist/types/public-api.d.ts +209 -0
- package/dist/types/qa-assistant-state.d.ts +34 -0
- package/dist/types/react/index.d.ts +28 -3
- package/dist/types/reassert-sentinel.d.ts +6 -0
- package/dist/types/redirect.d.ts +48 -0
- package/dist/types/replay-recorder-entry.d.ts +1 -0
- package/dist/types/replay-signal-state.d.ts +6 -0
- package/dist/types/replay-signals.d.ts +13 -0
- package/dist/types/replay.d.ts +110 -0
- package/dist/types/router.d.ts +12 -2
- package/dist/types/runtime-state.d.ts +166 -0
- package/dist/types/sanitize.d.ts +1 -0
- package/dist/types/shadow.d.ts +3 -0
- package/dist/types/shopify-cart.d.ts +25 -1
- package/dist/types/storefront-vitals.d.ts +24 -0
- package/dist/types/targeting-diagnostics.d.ts +1 -0
- package/dist/types/targeting.d.ts +16 -2
- package/dist/types/track.d.ts +15 -1
- package/dist/types/types.d.ts +115 -9
- package/dist/types/wait.d.ts +14 -1
- package/dist/types/weekday-schedule.d.ts +3 -0
- package/package.json +17 -8
- package/src/activation-runtime.ts +2737 -0
- package/src/analytics-runtime.ts +2175 -0
- package/src/antiflicker.test.ts +216 -44
- package/src/antiflicker.ts +103 -28
- package/src/artifact-concurrency.test.ts +1091 -0
- package/src/assignment-cookie-sync.test.ts +202 -0
- package/src/assignment-cookie-sync.ts +189 -0
- package/src/assignment-runtime.ts +3204 -0
- package/src/assignment-store.test.ts +56 -0
- package/src/assignment-store.ts +7 -38
- package/src/bucket.ts +3 -18
- package/src/commerce-analytics-lifecycle.test.ts +75 -15
- package/src/commerce-config-lifecycle.test.ts +81 -1
- package/src/commerce-price-preserve.test.ts +133 -0
- package/src/commerce-price.test.ts +97 -0
- package/src/commerce-price.ts +74 -20
- package/src/commerce-shipping.test.ts +53 -0
- package/src/commerce-shipping.ts +5 -11
- package/src/compact-store.ts +15 -0
- package/src/config-fetch.test.ts +480 -9
- package/src/config-runtime.ts +2070 -0
- package/src/consent-evaluation-cookiescript.test.ts +50 -0
- package/src/consent-evaluation.ts +138 -0
- package/src/css.test.ts +30 -0
- package/src/css.ts +120 -6
- package/src/cwv.test.ts +60 -77
- package/src/debug-loader.test.ts +214 -0
- package/src/debug-loader.ts +41 -0
- package/src/decide-assignments.test.ts +196 -0
- package/src/decide-assignments.ts +204 -0
- package/src/delivery-timing.test.ts +104 -0
- package/src/delivery-timing.ts +89 -0
- package/src/early-interaction-capture.test.ts +451 -0
- package/src/early-interaction-capture.ts +230 -0
- package/src/early-reassert.test.ts +207 -0
- package/src/early-reassert.ts +152 -0
- package/src/edge-adoption.test.ts +435 -0
- package/src/edge-runtime.ts +30 -0
- package/src/editor/api.test.ts +109 -0
- package/src/editor/api.ts +34 -3
- package/src/editor/controller.test.ts +297 -14
- package/src/editor/controller.ts +69 -5
- package/src/editor/index.test.ts +2 -0
- package/src/editor/index.ts +1 -0
- package/src/editor/publication-result.ts +58 -0
- package/src/editor/types.ts +10 -1
- package/src/editor/view-kit.ts +2 -1
- package/src/editor/view.test.ts +108 -0
- package/src/editor/view.ts +49 -7
- package/src/endpoint.test.ts +15 -0
- package/src/endpoint.ts +10 -0
- package/src/exclusion-groups.ts +8 -18
- package/src/flags.test.ts +88 -1
- package/src/flags.ts +29 -29
- package/src/global-runtime-reconciler.test.ts +271 -0
- package/src/goal-eligibility.test.ts +29 -0
- package/src/goal-eligibility.ts +17 -0
- package/src/goals.test.ts +144 -1
- package/src/goals.ts +97 -18
- package/src/guards.test.ts +99 -1
- package/src/guards.ts +34 -15
- package/src/handover-cache-order.test.ts +6 -3
- package/src/handover.test.ts +154 -8
- package/src/handover.ts +122 -12
- package/src/hash.ts +29 -54
- package/src/holdout.test.ts +26 -0
- package/src/hydration.test.ts +73 -2
- package/src/hydration.ts +18 -9
- package/src/index.ts +130 -7287
- package/src/init-analytics.test.ts +1694 -0
- package/src/init-anti-flicker.test.ts +1211 -0
- package/src/init-cart-attributes.test.ts +2003 -0
- package/src/init-config-resolution.test.ts +999 -0
- package/src/init-consent.test.ts +971 -0
- package/src/init-empty.test.ts +11 -5406
- package/src/init-integrations.test.ts +846 -0
- package/src/init-page-activation.test.ts +5064 -0
- package/src/init-qa-loading.test.ts +740 -0
- package/src/init-redirects.test.ts +2156 -0
- package/src/init-test-helpers.ts +287 -0
- package/src/inline-bootstrap.test.ts +184 -0
- package/src/inline-bootstrap.ts +73 -0
- package/src/js.test.ts +51 -1
- package/src/js.ts +86 -2
- package/src/money.test.ts +55 -1
- package/src/money.ts +104 -2
- package/src/money.web-parity.contract.test.ts +85 -0
- package/src/mutate.test.ts +474 -10
- package/src/mutate.ts +739 -230
- package/src/page-trigger.test.ts +218 -8
- package/src/performance.ts +10 -0
- package/src/persistence-decoders.ts +18 -0
- package/src/public-api.ts +1828 -0
- package/src/qa-assistant-state.test.ts +249 -3
- package/src/qa-assistant-state.ts +146 -6
- package/src/qa-assistant-view.test.ts +220 -2
- package/src/qa-assistant-view.ts +102 -5
- package/src/react/index.test.tsx +37 -0
- package/src/react/index.tsx +19 -3
- package/src/reassert-sentinel.test.ts +155 -1
- package/src/reassert-sentinel.ts +168 -8
- package/src/redirect.test.ts +541 -0
- package/src/redirect.ts +572 -0
- package/src/replay-recorder-entry.ts +115 -0
- package/src/replay-signal-state.ts +100 -0
- package/src/replay-signals.test.ts +249 -0
- package/src/replay-signals.ts +151 -0
- package/src/replay.test.ts +764 -0
- package/src/replay.ts +514 -0
- package/src/router.test.ts +61 -2
- package/src/router.ts +50 -12
- package/src/runtime-state.ts +507 -0
- package/src/runtime.test.ts +174 -7
- package/src/runtime.ts +88 -14
- package/src/sanitize.test.ts +34 -0
- package/src/sanitize.ts +23 -12
- package/src/shadow.test.ts +187 -0
- package/src/shadow.ts +99 -0
- package/src/shopify-cart.test.ts +733 -23
- package/src/shopify-cart.ts +402 -49
- package/src/storefront-vitals.test.ts +217 -0
- package/src/storefront-vitals.ts +206 -0
- package/src/surface-signals.ts +6 -0
- package/src/targeting-conformance.test.ts +9 -0
- package/src/targeting-diagnostics.ts +38 -8
- package/src/targeting-parity-corpus.json +559 -0
- package/src/targeting-parity-corpus.test.ts +18 -0
- package/src/targeting.detailed.test.ts +105 -0
- package/src/targeting.test.ts +211 -0
- package/src/targeting.ts +88 -207
- package/src/track.test.ts +147 -0
- package/src/track.ts +125 -26
- package/src/types.ts +120 -13
- package/src/wait.test.ts +23 -0
- package/src/wait.ts +129 -13
- package/src/weekday-delivery.test.ts +56 -0
- package/src/weekday-schedule-parity-corpus.json +92 -0
- package/src/weekday-schedule.test.ts +69 -0
- package/src/weekday-schedule.ts +20 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
import type { QaAssistantStore } from "./qa-assistant-state";
|
|
3
|
+
import type { PageActivationDiagnostic } from "./types";
|
|
3
4
|
|
|
4
5
|
type RuntimeSnapshot = ReturnType<typeof baseSnapshot>;
|
|
5
6
|
|
|
@@ -22,6 +23,12 @@ function baseSnapshot() {
|
|
|
22
23
|
{ id: "var_control", name: "Control", weight: 0.5, mutations: [], isControl: true },
|
|
23
24
|
{ id: "var_b", name: "Variant B", weight: 0.5, mutations: [] },
|
|
24
25
|
],
|
|
26
|
+
goals: [
|
|
27
|
+
{ id: "goal_named", name: "Completed checkout", type: "custom" as const, eventName: "checkout_completed" },
|
|
28
|
+
{ id: "goal_event", type: "custom" as const, eventName: "signup" },
|
|
29
|
+
{ id: "goal_a", name: "Conversion", type: "custom" as const, eventName: "conversion_a" },
|
|
30
|
+
{ id: "goal_b", name: "Conversion", type: "custom" as const, eventName: "conversion_b" },
|
|
31
|
+
],
|
|
25
32
|
},
|
|
26
33
|
],
|
|
27
34
|
},
|
|
@@ -51,7 +58,17 @@ function baseSnapshot() {
|
|
|
51
58
|
},
|
|
52
59
|
],
|
|
53
60
|
},
|
|
61
|
+
pageActivation: [] as PageActivationDiagnostic[],
|
|
62
|
+
goalScopeUrl: null as string | null,
|
|
54
63
|
forceMap: null as Record<string, string> | null,
|
|
64
|
+
forcedUnavailable: [] as Array<{
|
|
65
|
+
experimentId: string;
|
|
66
|
+
reason: "archived" | "runtime_disabled" | "not_found" | "not_in_config";
|
|
67
|
+
}>,
|
|
68
|
+
edgeDeviceAudienceUnsupported: [] as Array<{
|
|
69
|
+
experimentId: string;
|
|
70
|
+
audienceName: string;
|
|
71
|
+
}>,
|
|
55
72
|
platform: "spa" as const,
|
|
56
73
|
qaMode: true,
|
|
57
74
|
orderIdentity: null as {
|
|
@@ -129,6 +146,131 @@ afterEach(() => {
|
|
|
129
146
|
});
|
|
130
147
|
|
|
131
148
|
describe("createQaAssistantStore", () => {
|
|
149
|
+
it("increments the store version on every state write", async () => {
|
|
150
|
+
const store = await loadStore();
|
|
151
|
+
const initialVersion = store.getState().storeVersion;
|
|
152
|
+
|
|
153
|
+
runtimeListener?.();
|
|
154
|
+
expect(store.getState().storeVersion).toBe(initialVersion + 1);
|
|
155
|
+
|
|
156
|
+
devtoolsListener?.({ ts: 1, type: "track", label: "pageview", detail: { path: "/next" } });
|
|
157
|
+
expect(store.getState().storeVersion).toBe(initialVersion + 2);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("reports a configured Page-scoped goal as active or inactive", async () => {
|
|
161
|
+
vi.stubGlobal("location", { href: "https://shop.test/products/shirt" });
|
|
162
|
+
const store = await loadStore();
|
|
163
|
+
const experiment = snapshot.config.experiments[0] as typeof snapshot.config.experiments[0] & {
|
|
164
|
+
targeting?: unknown;
|
|
165
|
+
};
|
|
166
|
+
experiment.targeting = {
|
|
167
|
+
pageRules: [{
|
|
168
|
+
id: "page-products",
|
|
169
|
+
name: "product",
|
|
170
|
+
includeRules: [{ type: "simple", pattern: "shop.test/products/*" }],
|
|
171
|
+
}],
|
|
172
|
+
};
|
|
173
|
+
experiment.goals.push({
|
|
174
|
+
id: "goal-scoped",
|
|
175
|
+
name: "Add to cart",
|
|
176
|
+
type: "click",
|
|
177
|
+
pageId: "page-products",
|
|
178
|
+
} as never);
|
|
179
|
+
snapshot.pageActivation = [{
|
|
180
|
+
pageId: "page-products",
|
|
181
|
+
pageName: "product",
|
|
182
|
+
active: true,
|
|
183
|
+
appliedArtifactCount: 1,
|
|
184
|
+
}];
|
|
185
|
+
runtimeListener?.();
|
|
186
|
+
|
|
187
|
+
expect(store.getState().goalScopes).toContainEqual({
|
|
188
|
+
id: "goal-scoped",
|
|
189
|
+
name: "Add to cart",
|
|
190
|
+
pageId: "page-products",
|
|
191
|
+
pageLabel: "product",
|
|
192
|
+
urlPattern: null,
|
|
193
|
+
status: "active",
|
|
194
|
+
assignmentPageActive: null,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
vi.stubGlobal("location", { href: "https://shop.test/collections/sale" });
|
|
198
|
+
snapshot.pageActivation[0]!.active = false;
|
|
199
|
+
runtimeListener?.();
|
|
200
|
+
expect(
|
|
201
|
+
store.getState().goalScopes.find((goal) => goal.id === "goal-scoped")?.status,
|
|
202
|
+
).toBe("inactive");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("uses the configured URL and normalized attributes for goal scope status", async () => {
|
|
206
|
+
vi.stubGlobal("location", { href: "https://shop.test/collections/sale" });
|
|
207
|
+
const store = await loadStore();
|
|
208
|
+
Object.assign(snapshot.config, {
|
|
209
|
+
url: "https://shop.test/products/shirt",
|
|
210
|
+
attributes: { device: "Mobile" },
|
|
211
|
+
});
|
|
212
|
+
const experiment = snapshot.config.experiments[0] as typeof snapshot.config.experiments[0] & {
|
|
213
|
+
targeting?: unknown;
|
|
214
|
+
};
|
|
215
|
+
experiment.targeting = {
|
|
216
|
+
pageRules: [{
|
|
217
|
+
id: "page-mobile-products",
|
|
218
|
+
name: "mobile product",
|
|
219
|
+
includeRules: [{ type: "simple", pattern: "shop.test/products/*" }],
|
|
220
|
+
advancedCondition: {
|
|
221
|
+
code: 'return attributes["device.type"] === "mobile";',
|
|
222
|
+
},
|
|
223
|
+
}],
|
|
224
|
+
};
|
|
225
|
+
experiment.goals.push({
|
|
226
|
+
id: "goal-mobile-product",
|
|
227
|
+
name: "Mobile add to cart",
|
|
228
|
+
type: "click",
|
|
229
|
+
pageId: "page-mobile-products",
|
|
230
|
+
} as never);
|
|
231
|
+
snapshot.pageActivation = [{
|
|
232
|
+
pageId: "page-mobile-products",
|
|
233
|
+
pageName: "mobile product",
|
|
234
|
+
active: true,
|
|
235
|
+
appliedArtifactCount: 1,
|
|
236
|
+
}];
|
|
237
|
+
runtimeListener?.();
|
|
238
|
+
|
|
239
|
+
expect(
|
|
240
|
+
store.getState().goalScopes.find((goal) => goal.id === "goal-mobile-product")?.status,
|
|
241
|
+
).toBe("active");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("uses the URL that current goal wiring used instead of the configured URL", async () => {
|
|
245
|
+
vi.stubGlobal("location", { href: "https://shop.test/collections/sale" });
|
|
246
|
+
const store = await loadStore();
|
|
247
|
+
Object.assign(snapshot.config, {
|
|
248
|
+
url: "https://shop.test/products/shirt",
|
|
249
|
+
});
|
|
250
|
+
snapshot.goalScopeUrl = "https://shop.test/collections/sale";
|
|
251
|
+
const experiment = snapshot.config.experiments[0] as typeof snapshot.config.experiments[0] & {
|
|
252
|
+
targeting?: unknown;
|
|
253
|
+
};
|
|
254
|
+
experiment.targeting = {
|
|
255
|
+
pageRules: [{
|
|
256
|
+
id: "page-products",
|
|
257
|
+
name: "product",
|
|
258
|
+
includeRules: [{ type: "simple", pattern: "shop.test/products/*" }],
|
|
259
|
+
}],
|
|
260
|
+
};
|
|
261
|
+
experiment.goals.push({
|
|
262
|
+
id: "goal-scoped",
|
|
263
|
+
name: "Add to cart",
|
|
264
|
+
type: "click",
|
|
265
|
+
pageId: "page-products",
|
|
266
|
+
} as never);
|
|
267
|
+
runtimeListener?.();
|
|
268
|
+
|
|
269
|
+
expect(
|
|
270
|
+
store.getState().goalScopes.find((goal) => goal.id === "goal-scoped")?.status,
|
|
271
|
+
).toBe("inactive");
|
|
272
|
+
});
|
|
273
|
+
|
|
132
274
|
it("maps condition diagnostics to pass, fail, skip, and error rows with raw JS", async () => {
|
|
133
275
|
const store = await loadStore();
|
|
134
276
|
snapshot.assignments = [];
|
|
@@ -370,7 +512,79 @@ describe("createQaAssistantStore", () => {
|
|
|
370
512
|
});
|
|
371
513
|
});
|
|
372
514
|
|
|
373
|
-
it("
|
|
515
|
+
it("shows required and detected device values for audience failures", async () => {
|
|
516
|
+
const store = await loadStore();
|
|
517
|
+
snapshot.activationDiagnostics.experiments[0].targetingConditions = [{
|
|
518
|
+
id: "segment:device:0",
|
|
519
|
+
label: "Device audience",
|
|
520
|
+
status: "failed",
|
|
521
|
+
reason: "segment_mismatch",
|
|
522
|
+
sourceType: "segment",
|
|
523
|
+
signal: "device.type",
|
|
524
|
+
operator: "equals",
|
|
525
|
+
expected: "Mobile",
|
|
526
|
+
actual: "Desktop",
|
|
527
|
+
}];
|
|
528
|
+
runtimeListener?.();
|
|
529
|
+
expect(store.getState().experiments[0].conditions[0]?.detail).toBe(
|
|
530
|
+
"Required: Mobile · Detected: Desktop",
|
|
531
|
+
);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it("shows an unknown detected device when the signal is unresolved", async () => {
|
|
535
|
+
const store = await loadStore();
|
|
536
|
+
snapshot.activationDiagnostics.experiments[0].targetingConditions = [{
|
|
537
|
+
id: "segment:device:0",
|
|
538
|
+
label: "Device audience",
|
|
539
|
+
status: "failed",
|
|
540
|
+
reason: "signal_unresolved",
|
|
541
|
+
sourceType: "segment",
|
|
542
|
+
signal: "device.type",
|
|
543
|
+
operator: "equals",
|
|
544
|
+
expected: "Mobile",
|
|
545
|
+
}];
|
|
546
|
+
runtimeListener?.();
|
|
547
|
+
expect(store.getState().experiments[0].conditions[0]?.detail).toBe(
|
|
548
|
+
"Required: Mobile · Detected: unknown",
|
|
549
|
+
);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it("resolves goal labels by configured name, event name, and short id", async () => {
|
|
553
|
+
const store = await loadStore();
|
|
554
|
+
|
|
555
|
+
devtoolsListener?.({
|
|
556
|
+
ts: 1000,
|
|
557
|
+
type: "track",
|
|
558
|
+
label: "goal",
|
|
559
|
+
detail: { goalId: "goal_named" },
|
|
560
|
+
});
|
|
561
|
+
devtoolsListener?.({
|
|
562
|
+
ts: 3000,
|
|
563
|
+
type: "track",
|
|
564
|
+
label: "goal",
|
|
565
|
+
detail: { goalId: "signup" },
|
|
566
|
+
});
|
|
567
|
+
devtoolsListener?.({
|
|
568
|
+
ts: 5000,
|
|
569
|
+
type: "track",
|
|
570
|
+
label: "goal",
|
|
571
|
+
detail: { goalId: "b6f8724b-1111-4222-8333-123456789abc" },
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
expect(store.getState().events).toEqual([
|
|
575
|
+
{ at: 1000, type: "goal", label: "Completed checkout", value: null, goalId: "goal_named" },
|
|
576
|
+
{ at: 3000, type: "goal", label: "signup", value: null, goalId: "signup" },
|
|
577
|
+
{
|
|
578
|
+
at: 5000,
|
|
579
|
+
type: "goal",
|
|
580
|
+
label: "b6f8724b",
|
|
581
|
+
value: null,
|
|
582
|
+
goalId: "b6f8724b-1111-4222-8333-123456789abc",
|
|
583
|
+
},
|
|
584
|
+
]);
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it("marks the newer duplicate goal event within 1500ms", async () => {
|
|
374
588
|
const store = await loadStore();
|
|
375
589
|
|
|
376
590
|
devtoolsListener?.({
|
|
@@ -387,8 +601,30 @@ describe("createQaAssistantStore", () => {
|
|
|
387
601
|
});
|
|
388
602
|
|
|
389
603
|
expect(store.getState().events).toEqual([
|
|
390
|
-
{ at: 1000, type: "goal", label: "signup", value: null },
|
|
391
|
-
{ at: 2200, type: "goal", label: "signup", value: null, duplicateOf: 1000 },
|
|
604
|
+
{ at: 1000, type: "goal", label: "signup", value: null, goalId: "signup" },
|
|
605
|
+
{ at: 2200, type: "goal", label: "signup", value: null, goalId: "signup", duplicateOf: 1000 },
|
|
606
|
+
]);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it("does not merge distinct goal events with the same configured name", async () => {
|
|
610
|
+
const store = await loadStore();
|
|
611
|
+
|
|
612
|
+
devtoolsListener?.({
|
|
613
|
+
ts: 1000,
|
|
614
|
+
type: "track",
|
|
615
|
+
label: "goal",
|
|
616
|
+
detail: { goalId: "goal_a" },
|
|
617
|
+
});
|
|
618
|
+
devtoolsListener?.({
|
|
619
|
+
ts: 1200,
|
|
620
|
+
type: "track",
|
|
621
|
+
label: "goal",
|
|
622
|
+
detail: { goalId: "goal_b" },
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
expect(store.getState().events).toEqual([
|
|
626
|
+
{ at: 1000, type: "goal", label: "Conversion", value: null, goalId: "goal_a" },
|
|
627
|
+
{ at: 1200, type: "goal", label: "Conversion", value: null, goalId: "goal_b" },
|
|
392
628
|
]);
|
|
393
629
|
});
|
|
394
630
|
|
|
@@ -443,6 +679,16 @@ describe("createQaAssistantStore", () => {
|
|
|
443
679
|
});
|
|
444
680
|
});
|
|
445
681
|
|
|
682
|
+
it("surfaces an unavailable forced experiment from the runtime", async () => {
|
|
683
|
+
const store = await loadStore();
|
|
684
|
+
snapshot.forcedUnavailable = [{ experimentId: "exp_archived", reason: "archived" }];
|
|
685
|
+
runtimeListener?.();
|
|
686
|
+
|
|
687
|
+
expect(store.getState().forcedUnavailable).toEqual([
|
|
688
|
+
{ experimentId: "exp_archived", reason: "archived" },
|
|
689
|
+
]);
|
|
690
|
+
});
|
|
691
|
+
|
|
446
692
|
it("keeps a targeting-rejected force request non-forced in the experiment model", async () => {
|
|
447
693
|
const store = await loadStore();
|
|
448
694
|
snapshot.forceMap = { exp_123456789: "var_b" };
|
|
@@ -8,6 +8,8 @@ import type {
|
|
|
8
8
|
import { exitQaMode } from "./guards";
|
|
9
9
|
import type { DevtoolsEvent } from "./devtools";
|
|
10
10
|
import { subscribeDevtoolsEvents } from "./devtools";
|
|
11
|
+
import { safeRegexTest } from "./safe-regex";
|
|
12
|
+
import { normalizeEventName } from "@drip/event-schema/event-name";
|
|
11
13
|
import {
|
|
12
14
|
__forceQaAssistantVariation,
|
|
13
15
|
__getQaAssistantRuntimeSnapshot,
|
|
@@ -26,12 +28,40 @@ export type QaExperimentModel = {
|
|
|
26
28
|
conditions: Array<{ id: string; group: "pages" | "audience"; state: "pass"|"fail"|"skip"|"error"; text: string; detail: string | null; rawCode: string | null }>;
|
|
27
29
|
variations: Array<{ id: string; name: string; isControl: boolean; isCurrent: boolean; isReal: boolean }>;
|
|
28
30
|
};
|
|
29
|
-
export type QaEventModel = { at: number; type: "exposure"|"goal"|"revenue"|"pageview"; label: string; value: string | null; duplicateOf?: number };
|
|
31
|
+
export type QaEventModel = { at: number; type: "exposure"|"goal"|"revenue"|"pageview"; label: string; value: string | null; goalId?: string; duplicateOf?: number };
|
|
32
|
+
export type QaGoalScopeModel = {
|
|
33
|
+
id: string;
|
|
34
|
+
name: string;
|
|
35
|
+
pageId: string | null;
|
|
36
|
+
pageLabel: string | null;
|
|
37
|
+
urlPattern?: string | null;
|
|
38
|
+
status: "active" | "inactive" | "not_wired";
|
|
39
|
+
assignmentPageActive?: boolean | null;
|
|
40
|
+
};
|
|
30
41
|
export type QaTransactionsModel =
|
|
31
42
|
| { kind: "unsupported" }
|
|
32
43
|
| { kind: "shopify"; cartStamped: boolean; variationName: string | null; stampedAt: number | null; attribution: Array<{ experimentName: string; variationName: string | null }> }
|
|
33
44
|
| { kind: "order"; orderNumber: string | null; attribution: Array<{ experimentName: string; variationName: string | null }>; serverReceipt: "pending"|"confirmed"|"unknown" };
|
|
34
|
-
export type
|
|
45
|
+
export type ForcedUnavailable = {
|
|
46
|
+
experimentId: string;
|
|
47
|
+
reason: "archived" | "runtime_disabled" | "not_found" | "environment_mismatch" | "not_in_config";
|
|
48
|
+
};
|
|
49
|
+
export type EdgeDeviceAudienceUnsupported = {
|
|
50
|
+
experimentId: string;
|
|
51
|
+
audienceName: string;
|
|
52
|
+
};
|
|
53
|
+
export type QaPageActivationModel = {
|
|
54
|
+
pageId: string;
|
|
55
|
+
pageName: string;
|
|
56
|
+
active: boolean;
|
|
57
|
+
lastTransitionMode: "reset" | "deactivate" | "force_reset" | "persistent" | null;
|
|
58
|
+
isolationFallback: "whole_experiment" | null;
|
|
59
|
+
triggerDetector?: "history" | "polling" | "dom" | null;
|
|
60
|
+
triggerPollAttempts?: number;
|
|
61
|
+
triggerOutcome?: "activated" | "condition_failed" | "window_expired" | null;
|
|
62
|
+
triggerElapsedMs?: number;
|
|
63
|
+
};
|
|
64
|
+
export type QaAssistantState = { storeVersion: number; experiments: QaExperimentModel[]; events: QaEventModel[]; goalScopes: QaGoalScopeModel[]; pageActivation: QaPageActivationModel[]; transactions: QaTransactionsModel; forcedSummary: string | null; forcedUnavailable: ForcedUnavailable[]; edgeDeviceAudienceUnsupported: EdgeDeviceAudienceUnsupported[] };
|
|
35
65
|
export type QaAssistantStore = {
|
|
36
66
|
getState(): QaAssistantState;
|
|
37
67
|
subscribe(listener: (s: QaAssistantState) => void): () => void;
|
|
@@ -41,6 +71,7 @@ export type QaAssistantStore = {
|
|
|
41
71
|
};
|
|
42
72
|
export function createQaAssistantStore(): QaAssistantStore {
|
|
43
73
|
let state = buildState([], { kind: "unsupported" });
|
|
74
|
+
let storeVersion = state.storeVersion;
|
|
44
75
|
const listeners = new Set<(s: QaAssistantState) => void>();
|
|
45
76
|
let cartFetchSignature: string | null = null;
|
|
46
77
|
let disposed = false;
|
|
@@ -52,7 +83,8 @@ export function createQaAssistantStore(): QaAssistantStore {
|
|
|
52
83
|
}
|
|
53
84
|
|
|
54
85
|
function setState(next: QaAssistantState): void {
|
|
55
|
-
|
|
86
|
+
storeVersion += 1;
|
|
87
|
+
state = { ...next, storeVersion };
|
|
56
88
|
emit();
|
|
57
89
|
}
|
|
58
90
|
|
|
@@ -119,18 +151,95 @@ export function createQaAssistantStore(): QaAssistantStore {
|
|
|
119
151
|
|
|
120
152
|
function buildState(events: QaEventModel[], transactions: QaTransactionsModel): QaAssistantState {
|
|
121
153
|
const runtime = __getQaAssistantRuntimeSnapshot();
|
|
154
|
+
const experiments = runtime.config?.experiments ?? [];
|
|
122
155
|
return {
|
|
156
|
+
storeVersion: 0,
|
|
123
157
|
experiments: mapExperiments(
|
|
124
|
-
|
|
158
|
+
experiments,
|
|
125
159
|
runtime.assignments,
|
|
126
160
|
runtime.activationDiagnostics.experiments,
|
|
127
161
|
),
|
|
128
162
|
events,
|
|
163
|
+
goalScopes: mapGoalScopes(
|
|
164
|
+
experiments,
|
|
165
|
+
runtime.assignments,
|
|
166
|
+
runtime.goalScopeUrl ?? runtime.config?.url,
|
|
167
|
+
new Set(
|
|
168
|
+
runtime.pageActivation
|
|
169
|
+
.filter((page) => page.active)
|
|
170
|
+
.map((page) => page.pageId),
|
|
171
|
+
),
|
|
172
|
+
),
|
|
173
|
+
pageActivation: runtime.pageActivation.map((page) => ({
|
|
174
|
+
pageId: page.pageId,
|
|
175
|
+
pageName: page.pageName || shortId(page.pageId),
|
|
176
|
+
active: page.active,
|
|
177
|
+
lastTransitionMode: page.lastTransitionMode ?? null,
|
|
178
|
+
isolationFallback: page.isolationFallback ?? null,
|
|
179
|
+
triggerDetector: page.triggerDetector ?? null,
|
|
180
|
+
triggerPollAttempts: page.triggerPollAttempts ?? 0,
|
|
181
|
+
triggerOutcome: page.triggerOutcome ?? null,
|
|
182
|
+
triggerElapsedMs: page.triggerElapsedMs ?? 0,
|
|
183
|
+
})),
|
|
129
184
|
transactions,
|
|
130
185
|
forcedSummary: forcedSummary(runtime.config?.experiments ?? [], runtime.forceMap),
|
|
186
|
+
forcedUnavailable: runtime.forcedUnavailable,
|
|
187
|
+
edgeDeviceAudienceUnsupported: runtime.edgeDeviceAudienceUnsupported,
|
|
131
188
|
};
|
|
132
189
|
}
|
|
133
190
|
|
|
191
|
+
function mapGoalScopes(
|
|
192
|
+
experiments: Experiment[],
|
|
193
|
+
assignments: Assignment[],
|
|
194
|
+
configuredUrl?: string,
|
|
195
|
+
activePageIds: ReadonlySet<string> = new Set(),
|
|
196
|
+
): QaGoalScopeModel[] {
|
|
197
|
+
const url = configuredUrl ??
|
|
198
|
+
(typeof location !== "undefined" ? location.href : "https://_");
|
|
199
|
+
const pageRules = experiments.flatMap((experiment) => [...(experiment.targeting?.pageRules ?? []), ...(experiment.goalPageRules ?? [])]);
|
|
200
|
+
const pageLabels = new Map(
|
|
201
|
+
pageRules.map((rule) => [rule.id, rule.name || shortId(rule.id)]),
|
|
202
|
+
);
|
|
203
|
+
const assignmentByExperiment = new Map(assignments.map((assignment) => [assignment.experimentId, assignment]));
|
|
204
|
+
const byGoal = new Map<string, QaGoalScopeModel>();
|
|
205
|
+
|
|
206
|
+
for (const experiment of experiments) {
|
|
207
|
+
for (const goal of experiment.goals ?? []) {
|
|
208
|
+
const assignment = assignmentByExperiment.get(experiment.id);
|
|
209
|
+
const wired = Boolean(assignment);
|
|
210
|
+
const active = wired && (goal.pageId
|
|
211
|
+
? activePageIds.has(goal.pageId)
|
|
212
|
+
: !goal.urlPattern || safeRegexTest(goal.urlPattern, url));
|
|
213
|
+
const status: QaGoalScopeModel["status"] = active
|
|
214
|
+
? "active"
|
|
215
|
+
: wired
|
|
216
|
+
? "inactive"
|
|
217
|
+
: "not_wired";
|
|
218
|
+
const existing = byGoal.get(goal.id);
|
|
219
|
+
if (
|
|
220
|
+
existing &&
|
|
221
|
+
(existing.status === "active" ||
|
|
222
|
+
(existing.status === "inactive" && status === "not_wired"))
|
|
223
|
+
) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
byGoal.set(goal.id, {
|
|
227
|
+
id: goal.id,
|
|
228
|
+
name: goal.name || goal.eventName || shortId(goal.id),
|
|
229
|
+
pageId: goal.pageId ?? null,
|
|
230
|
+
pageLabel: goal.pageId
|
|
231
|
+
? pageLabels.get(goal.pageId) ?? shortId(goal.pageId)
|
|
232
|
+
: null,
|
|
233
|
+
urlPattern: goal.urlPattern ?? null,
|
|
234
|
+
status,
|
|
235
|
+
assignmentPageActive: assignment?.pageActive ?? null,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return Array.from(byGoal.values());
|
|
241
|
+
}
|
|
242
|
+
|
|
134
243
|
function mapExperiments(
|
|
135
244
|
experiments: Experiment[],
|
|
136
245
|
assignments: Assignment[],
|
|
@@ -216,6 +325,15 @@ function conditionDetail(condition: ConditionDiagnostic, isJsError: boolean): st
|
|
|
216
325
|
if (condition.status === "skipped") {
|
|
217
326
|
return `Skipped — ${condition.reason || "an earlier gate"} failed first`;
|
|
218
327
|
}
|
|
328
|
+
if (condition.signal === "device.type" && condition.status === "failed") {
|
|
329
|
+
const detected =
|
|
330
|
+
condition.actual === undefined ||
|
|
331
|
+
condition.actual === null ||
|
|
332
|
+
(typeof condition.actual === "string" && condition.actual.trim() === "")
|
|
333
|
+
? "unknown"
|
|
334
|
+
: formatValue(condition.actual);
|
|
335
|
+
return `Required: ${formatValue(condition.expected)} · Detected: ${detected}`;
|
|
336
|
+
}
|
|
219
337
|
const parts: string[] = [];
|
|
220
338
|
if (condition.signal) parts.push(`Signal: ${condition.signal}`);
|
|
221
339
|
if (condition.operator) parts.push(`Operator: ${condition.operator}`);
|
|
@@ -409,6 +527,16 @@ function variationNameById(experiments: Experiment[], experimentId: string, vari
|
|
|
409
527
|
return variation ? variationDisplayName(variation, index) : shortId(variationId);
|
|
410
528
|
}
|
|
411
529
|
|
|
530
|
+
function goalNameById(experiments: Experiment[], goalId: string): string {
|
|
531
|
+
const goals = experiments.flatMap((experiment) => experiment.goals ?? []);
|
|
532
|
+
const normalizedGoalId = normalizeEventName(goalId);
|
|
533
|
+
const goal = goals.find((item) => item.id === goalId)
|
|
534
|
+
?? goals.find(
|
|
535
|
+
(item) => item.eventName && normalizeEventName(item.eventName) === normalizedGoalId,
|
|
536
|
+
);
|
|
537
|
+
return goal?.name || goal?.eventName || shortId(goalId);
|
|
538
|
+
}
|
|
539
|
+
|
|
412
540
|
function variationDisplayName(variation: Variation, index: number): string {
|
|
413
541
|
return variation.name || (variation.isControl ? "Control" : shortId(variation.id || `variation-${index + 1}`));
|
|
414
542
|
}
|
|
@@ -427,6 +555,9 @@ function mapDevtoolsEvent(event: DevtoolsEvent): QaEventModel | null {
|
|
|
427
555
|
type,
|
|
428
556
|
label: eventLabel(type, event.label, detail),
|
|
429
557
|
value: eventValue(type, detail),
|
|
558
|
+
...(type === "goal" && typeof detail.goalId === "string"
|
|
559
|
+
? { goalId: detail.goalId }
|
|
560
|
+
: {}),
|
|
430
561
|
};
|
|
431
562
|
}
|
|
432
563
|
|
|
@@ -438,7 +569,11 @@ function eventType(label: string, detail: Record<string, unknown>): QaEventModel
|
|
|
438
569
|
}
|
|
439
570
|
|
|
440
571
|
function eventLabel(type: QaEventModel["type"], label: string, detail: Record<string, unknown>): string {
|
|
441
|
-
if (type === "goal")
|
|
572
|
+
if (type === "goal") {
|
|
573
|
+
if (typeof detail.goalId !== "string") return "Goal";
|
|
574
|
+
const runtime = __getQaAssistantRuntimeSnapshot();
|
|
575
|
+
return goalNameById(runtime.config?.experiments ?? [], detail.goalId);
|
|
576
|
+
}
|
|
442
577
|
if (type === "revenue") return "Revenue";
|
|
443
578
|
if (type === "pageview") return typeof detail.path === "string" ? detail.path : "Pageview";
|
|
444
579
|
const runtime = __getQaAssistantRuntimeSnapshot();
|
|
@@ -465,9 +600,14 @@ function appendEvent(events: QaEventModel[], event: QaEventModel): QaEventModel[
|
|
|
465
600
|
let duplicate: QaEventModel | undefined;
|
|
466
601
|
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
467
602
|
const candidate = events[index];
|
|
603
|
+
const sameIdentity = event.type === "goal" && candidate.type === "goal"
|
|
604
|
+
? event.goalId != null && candidate.goalId != null
|
|
605
|
+
? candidate.goalId === event.goalId
|
|
606
|
+
: candidate.label === event.label
|
|
607
|
+
: candidate.label === event.label;
|
|
468
608
|
if (
|
|
469
609
|
candidate.type === event.type &&
|
|
470
|
-
|
|
610
|
+
sameIdentity &&
|
|
471
611
|
event.at - candidate.at <= 1500
|
|
472
612
|
) {
|
|
473
613
|
duplicate = candidate;
|