@drip-apex/sdk 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/bundle-report.json +584 -226
- package/dist/define.cjs +123 -0
- package/dist/define.cjs.map +7 -0
- package/dist/define.mjs +102 -0
- package/dist/define.mjs.map +7 -0
- package/dist/drip.cjs +19283 -0
- package/dist/drip.cjs.map +7 -0
- package/dist/drip.debug.js +1 -1
- package/dist/drip.debug.js.map +1 -1
- package/dist/drip.heatmap.js +1 -1
- package/dist/drip.heatmap.js.map +1 -1
- package/dist/drip.js +1 -1
- package/dist/drip.js.map +1 -1
- package/dist/drip.live.js +1 -1
- package/dist/drip.live.js.map +1 -1
- package/dist/drip.mjs +19267 -0
- package/dist/drip.mjs.map +7 -0
- package/dist/drip.replay-recorder.js +2 -0
- package/dist/drip.replay-recorder.js.map +1 -0
- package/dist/editor.js +147 -142
- package/dist/editor.js.map +4 -4
- package/dist/flags.cjs +1788 -0
- package/dist/flags.cjs.map +7 -0
- package/dist/flags.mjs +1765 -0
- package/dist/flags.mjs.map +7 -0
- package/dist/index.mjs +12 -0
- package/dist/index.mjs.map +2 -2
- package/dist/metafiles/define.cjs.meta.json +28 -0
- package/dist/metafiles/define.mjs.meta.json +32 -0
- package/dist/metafiles/drip.cjs.meta.json +3193 -0
- package/dist/metafiles/drip.debug.meta.json +2859 -574
- package/dist/metafiles/drip.heatmap.meta.json +2732 -460
- package/dist/metafiles/drip.live.meta.json +2732 -460
- package/dist/metafiles/drip.meta.json +2859 -574
- package/dist/metafiles/drip.mjs.meta.json +3235 -0
- package/dist/metafiles/drip.replay-recorder.meta.json +82 -0
- package/dist/metafiles/editor.meta.json +67 -27
- package/dist/metafiles/flags.cjs.meta.json +220 -0
- package/dist/metafiles/flags.mjs.meta.json +225 -0
- package/dist/metafiles/index.meta.json +2717 -491
- package/dist/metafiles/index.mjs.meta.json +14 -4
- package/dist/react/index.js +16636 -13533
- package/dist/react/index.js.map +4 -4
- package/dist/sri.json +20 -15
- package/dist/types/activation-runtime.d.ts +91 -0
- package/dist/types/analytics-runtime.d.ts +166 -0
- package/dist/types/antiflicker.d.ts +9 -3
- package/dist/types/assignment-cookie-sync.d.ts +31 -0
- package/dist/types/assignment-runtime.d.ts +257 -0
- package/dist/types/assignment-store.d.ts +4 -8
- package/dist/types/bucket.d.ts +1 -2
- package/dist/types/compact-store.d.ts +1 -0
- package/dist/types/config-runtime.d.ts +313 -0
- package/dist/types/consent-evaluation.d.ts +28 -0
- package/dist/types/css.d.ts +5 -3
- package/dist/types/debug-loader.d.ts +2 -0
- package/dist/types/decide-assignments.d.ts +31 -0
- package/dist/types/delivery-timing.d.ts +8 -0
- package/dist/types/early-interaction-capture.d.ts +79 -0
- package/dist/types/early-reassert.d.ts +32 -0
- package/dist/types/edge-runtime.d.ts +11 -0
- package/dist/types/editor/api.d.ts +3 -0
- package/dist/types/editor/controller.d.ts +3 -1
- package/dist/types/editor/publication-result.d.ts +11 -0
- package/dist/types/editor/types.d.ts +2 -1
- package/dist/types/editor/view-kit.d.ts +1 -1
- package/dist/types/endpoint.d.ts +8 -0
- package/dist/types/exclusion-groups.d.ts +4 -7
- package/dist/types/flags.d.ts +2 -0
- package/dist/types/goal-eligibility.d.ts +4 -0
- package/dist/types/goals.d.ts +10 -1
- package/dist/types/guards.d.ts +7 -7
- package/dist/types/handover.d.ts +12 -1
- package/dist/types/hash.d.ts +8 -4
- package/dist/types/hydration.d.ts +2 -1
- package/dist/types/index.d.ts +24 -128
- package/dist/types/init-test-helpers.d.ts +48 -0
- package/dist/types/inline-bootstrap.d.ts +26 -0
- package/dist/types/js.d.ts +3 -1
- package/dist/types/money.d.ts +12 -0
- package/dist/types/mutate.d.ts +45 -6
- package/dist/types/performance.d.ts +2 -0
- package/dist/types/persistence-decoders.d.ts +12 -0
- package/dist/types/public-api.d.ts +209 -0
- package/dist/types/qa-assistant-state.d.ts +34 -0
- package/dist/types/react/index.d.ts +28 -3
- package/dist/types/reassert-sentinel.d.ts +6 -0
- package/dist/types/redirect.d.ts +48 -0
- package/dist/types/replay-recorder-entry.d.ts +1 -0
- package/dist/types/replay-signal-state.d.ts +6 -0
- package/dist/types/replay-signals.d.ts +13 -0
- package/dist/types/replay.d.ts +110 -0
- package/dist/types/router.d.ts +12 -2
- package/dist/types/runtime-state.d.ts +166 -0
- package/dist/types/sanitize.d.ts +1 -0
- package/dist/types/shadow.d.ts +3 -0
- package/dist/types/shopify-cart.d.ts +25 -1
- package/dist/types/storefront-vitals.d.ts +24 -0
- package/dist/types/targeting-diagnostics.d.ts +1 -0
- package/dist/types/targeting.d.ts +16 -2
- package/dist/types/track.d.ts +15 -1
- package/dist/types/types.d.ts +115 -9
- package/dist/types/wait.d.ts +14 -1
- package/dist/types/weekday-schedule.d.ts +3 -0
- package/package.json +17 -8
- package/src/activation-runtime.ts +2737 -0
- package/src/analytics-runtime.ts +2175 -0
- package/src/antiflicker.test.ts +216 -44
- package/src/antiflicker.ts +103 -28
- package/src/artifact-concurrency.test.ts +1091 -0
- package/src/assignment-cookie-sync.test.ts +202 -0
- package/src/assignment-cookie-sync.ts +189 -0
- package/src/assignment-runtime.ts +3204 -0
- package/src/assignment-store.test.ts +56 -0
- package/src/assignment-store.ts +7 -38
- package/src/bucket.ts +3 -18
- package/src/commerce-analytics-lifecycle.test.ts +75 -15
- package/src/commerce-config-lifecycle.test.ts +81 -1
- package/src/commerce-price-preserve.test.ts +133 -0
- package/src/commerce-price.test.ts +97 -0
- package/src/commerce-price.ts +74 -20
- package/src/commerce-shipping.test.ts +53 -0
- package/src/commerce-shipping.ts +5 -11
- package/src/compact-store.ts +15 -0
- package/src/config-fetch.test.ts +480 -9
- package/src/config-runtime.ts +2071 -0
- package/src/consent-evaluation-borlabs.test.ts +41 -0
- package/src/consent-evaluation-cookiescript.test.ts +50 -0
- package/src/consent-evaluation.ts +151 -0
- package/src/css.test.ts +30 -0
- package/src/css.ts +120 -6
- package/src/cwv.test.ts +60 -77
- package/src/debug-loader.test.ts +214 -0
- package/src/debug-loader.ts +41 -0
- package/src/decide-assignments.test.ts +196 -0
- package/src/decide-assignments.ts +204 -0
- package/src/delivery-timing.test.ts +104 -0
- package/src/delivery-timing.ts +89 -0
- package/src/early-interaction-capture.test.ts +451 -0
- package/src/early-interaction-capture.ts +230 -0
- package/src/early-reassert.test.ts +207 -0
- package/src/early-reassert.ts +152 -0
- package/src/edge-adoption.test.ts +435 -0
- package/src/edge-runtime.ts +30 -0
- package/src/editor/api.test.ts +109 -0
- package/src/editor/api.ts +34 -3
- package/src/editor/controller.test.ts +297 -14
- package/src/editor/controller.ts +69 -5
- package/src/editor/index.test.ts +2 -0
- package/src/editor/index.ts +1 -0
- package/src/editor/publication-result.ts +58 -0
- package/src/editor/types.ts +10 -1
- package/src/editor/view-kit.ts +2 -1
- package/src/editor/view.test.ts +108 -0
- package/src/editor/view.ts +49 -7
- package/src/endpoint.test.ts +15 -0
- package/src/endpoint.ts +10 -0
- package/src/exclusion-groups.ts +8 -18
- package/src/flags.test.ts +88 -1
- package/src/flags.ts +29 -29
- package/src/global-runtime-reconciler.test.ts +271 -0
- package/src/goal-eligibility.test.ts +29 -0
- package/src/goal-eligibility.ts +17 -0
- package/src/goals.test.ts +144 -1
- package/src/goals.ts +97 -18
- package/src/guards.test.ts +99 -1
- package/src/guards.ts +34 -15
- package/src/handover-cache-order.test.ts +6 -3
- package/src/handover.test.ts +154 -8
- package/src/handover.ts +122 -12
- package/src/hash.ts +29 -54
- package/src/holdout.test.ts +26 -0
- package/src/hydration.test.ts +73 -2
- package/src/hydration.ts +18 -9
- package/src/index.ts +130 -7287
- package/src/init-analytics.test.ts +1694 -0
- package/src/init-anti-flicker.test.ts +1211 -0
- package/src/init-cart-attributes.test.ts +2003 -0
- package/src/init-config-resolution.test.ts +999 -0
- package/src/init-consent.test.ts +971 -0
- package/src/init-empty.test.ts +11 -5406
- package/src/init-integrations.test.ts +846 -0
- package/src/init-page-activation.test.ts +5064 -0
- package/src/init-qa-loading.test.ts +740 -0
- package/src/init-redirects.test.ts +2156 -0
- package/src/init-test-helpers.ts +287 -0
- package/src/inline-bootstrap.test.ts +184 -0
- package/src/inline-bootstrap.ts +73 -0
- package/src/js.test.ts +51 -1
- package/src/js.ts +86 -2
- package/src/money.test.ts +55 -1
- package/src/money.ts +104 -2
- package/src/money.web-parity.contract.test.ts +85 -0
- package/src/mutate.test.ts +474 -10
- package/src/mutate.ts +739 -230
- package/src/page-trigger.test.ts +218 -8
- package/src/performance.ts +10 -0
- package/src/persistence-decoders.ts +18 -0
- package/src/public-api.ts +1828 -0
- package/src/qa-assistant-state.test.ts +249 -3
- package/src/qa-assistant-state.ts +146 -6
- package/src/qa-assistant-view.test.ts +220 -2
- package/src/qa-assistant-view.ts +102 -5
- package/src/react/index.test.tsx +37 -0
- package/src/react/index.tsx +19 -3
- package/src/reassert-sentinel.test.ts +155 -1
- package/src/reassert-sentinel.ts +168 -8
- package/src/redirect.test.ts +541 -0
- package/src/redirect.ts +572 -0
- package/src/replay-recorder-entry.ts +115 -0
- package/src/replay-signal-state.ts +100 -0
- package/src/replay-signals.test.ts +249 -0
- package/src/replay-signals.ts +151 -0
- package/src/replay.test.ts +764 -0
- package/src/replay.ts +514 -0
- package/src/router.test.ts +61 -2
- package/src/router.ts +50 -12
- package/src/runtime-state.ts +507 -0
- package/src/runtime.test.ts +174 -7
- package/src/runtime.ts +88 -14
- package/src/sanitize.test.ts +34 -0
- package/src/sanitize.ts +23 -12
- package/src/shadow.test.ts +187 -0
- package/src/shadow.ts +99 -0
- package/src/shopify-cart.test.ts +733 -23
- package/src/shopify-cart.ts +402 -49
- package/src/storefront-vitals.test.ts +217 -0
- package/src/storefront-vitals.ts +206 -0
- package/src/surface-signals.ts +6 -0
- package/src/targeting-conformance.test.ts +9 -0
- package/src/targeting-diagnostics.ts +38 -8
- package/src/targeting-parity-corpus.json +559 -0
- package/src/targeting-parity-corpus.test.ts +18 -0
- package/src/targeting.detailed.test.ts +105 -0
- package/src/targeting.test.ts +211 -0
- package/src/targeting.ts +88 -207
- package/src/track.test.ts +147 -0
- package/src/track.ts +125 -26
- package/src/types.ts +120 -13
- package/src/wait.test.ts +23 -0
- package/src/wait.ts +129 -13
- package/src/weekday-delivery.test.ts +56 -0
- package/src/weekday-schedule-parity-corpus.json +92 -0
- package/src/weekday-schedule.test.ts +69 -0
- package/src/weekday-schedule.ts +20 -0
package/src/editor/view.test.ts
CHANGED
|
@@ -160,6 +160,56 @@ describe("editor view v2", () => {
|
|
|
160
160
|
cleanup();
|
|
161
161
|
});
|
|
162
162
|
|
|
163
|
+
it("renders saved-but-pending as a warning instead of saved or failed", () => {
|
|
164
|
+
const controller = makeController(
|
|
165
|
+
state({
|
|
166
|
+
saveState: "pending",
|
|
167
|
+
saveMessage: "Saved, but publication is pending.",
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
const { shadow, cleanup } = mount(controller);
|
|
171
|
+
|
|
172
|
+
const chip = shadow.querySelector(".save-chip");
|
|
173
|
+
expect(chip?.classList.contains("pending")).toBe(true);
|
|
174
|
+
expect(chip?.textContent).toContain("Saved — publication pending");
|
|
175
|
+
expect(chip?.classList.contains("saved")).toBe(false);
|
|
176
|
+
expect(chip?.classList.contains("failed")).toBe(false);
|
|
177
|
+
cleanup();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("renders a soft publication warning separately from full success", () => {
|
|
181
|
+
const controller = makeController(
|
|
182
|
+
state({
|
|
183
|
+
saveState: "warning",
|
|
184
|
+
saveMessage: "Saved and published, but one publication step is incomplete.",
|
|
185
|
+
}),
|
|
186
|
+
);
|
|
187
|
+
const { shadow, cleanup } = mount(controller);
|
|
188
|
+
|
|
189
|
+
const chip = shadow.querySelector(".save-chip");
|
|
190
|
+
expect(chip?.classList.contains("warning")).toBe(true);
|
|
191
|
+
expect(chip?.textContent).toContain("Saved — publication warning");
|
|
192
|
+
expect(chip?.classList.contains("saved")).toBe(false);
|
|
193
|
+
cleanup();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("renders committed publication failure separately from a save failure", () => {
|
|
197
|
+
const controller = makeController(
|
|
198
|
+
state({
|
|
199
|
+
saveState: "publication_failed",
|
|
200
|
+
saveMessage: "Saved, but storefront publication failed.",
|
|
201
|
+
}),
|
|
202
|
+
);
|
|
203
|
+
const { shadow, cleanup } = mount(controller);
|
|
204
|
+
|
|
205
|
+
const chip = shadow.querySelector(".save-chip");
|
|
206
|
+
expect(chip?.classList.contains("publication-failed")).toBe(true);
|
|
207
|
+
expect(chip?.textContent).toContain("Saved — publication failed");
|
|
208
|
+
expect(chip?.textContent).not.toContain("Save failed");
|
|
209
|
+
expect(chip?.querySelector('[data-action="retry-save"]')).not.toBeNull();
|
|
210
|
+
cleanup();
|
|
211
|
+
});
|
|
212
|
+
|
|
163
213
|
it("runs an Operator turn from the conversation and reports the result card", async () => {
|
|
164
214
|
const generated = {
|
|
165
215
|
mutations: [{ selector: "h1", action: "setText" as const, text: "Generated" }],
|
|
@@ -326,6 +376,45 @@ describe("editor view v2", () => {
|
|
|
326
376
|
cleanup();
|
|
327
377
|
});
|
|
328
378
|
|
|
379
|
+
it("reports an Operator result as saved with failed publication", async () => {
|
|
380
|
+
const generated = {
|
|
381
|
+
mutations: [{ selector: "h1", action: "setText" as const, text: "Committed result" }],
|
|
382
|
+
customCss: "",
|
|
383
|
+
customJs: "",
|
|
384
|
+
};
|
|
385
|
+
vi.spyOn(operator, "runEditorOperatorTurn").mockResolvedValueOnce({
|
|
386
|
+
artifacts: generated,
|
|
387
|
+
explanation: "Updated the heading.",
|
|
388
|
+
artifactCount: 1,
|
|
389
|
+
mergeStrategy: "append",
|
|
390
|
+
});
|
|
391
|
+
const controller = makeController();
|
|
392
|
+
controller.applyCommandArtifacts.mockImplementationOnce((artifacts) => {
|
|
393
|
+
controller.emit(state({ artifacts, saveState: "idle", saveMessage: "Unsaved changes" }));
|
|
394
|
+
return { ok: true };
|
|
395
|
+
});
|
|
396
|
+
controller.saveNow.mockImplementationOnce(async () => {
|
|
397
|
+
controller.emit(state({
|
|
398
|
+
artifacts: generated,
|
|
399
|
+
saveState: "publication_failed",
|
|
400
|
+
saveMessage: "Saved, but storefront publication failed.",
|
|
401
|
+
}));
|
|
402
|
+
});
|
|
403
|
+
const { shadow, cleanup } = mount(controller);
|
|
404
|
+
|
|
405
|
+
const prompt = shadow.querySelector<HTMLTextAreaElement>('[data-field="operator-prompt"]')!;
|
|
406
|
+
prompt.value = "Update the heading";
|
|
407
|
+
prompt.dispatchEvent(new Event("input", { bubbles: true }));
|
|
408
|
+
prompt.closest("form")?.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true }));
|
|
409
|
+
|
|
410
|
+
await vi.waitFor(() =>
|
|
411
|
+
expect(shadow.textContent).toContain("Change saved, but publication failed"),
|
|
412
|
+
);
|
|
413
|
+
expect(shadow.textContent).toContain("The change is saved in Apex");
|
|
414
|
+
expect(shadow.textContent).not.toContain("Change applied, but not saved");
|
|
415
|
+
cleanup();
|
|
416
|
+
});
|
|
417
|
+
|
|
329
418
|
it("disables run-level undo after a later manual edit changes the artifact", async () => {
|
|
330
419
|
const generated = {
|
|
331
420
|
mutations: [{ selector: "h1", action: "setText" as const, text: "Generated" }],
|
|
@@ -505,6 +594,25 @@ describe("editor view v2", () => {
|
|
|
505
594
|
cleanup();
|
|
506
595
|
});
|
|
507
596
|
|
|
597
|
+
it("keeps the editor open when Save and close cannot publish", async () => {
|
|
598
|
+
const controller = makeController();
|
|
599
|
+
controller.saveNow.mockImplementationOnce(async () => {
|
|
600
|
+
controller.emit(state({
|
|
601
|
+
saveState: "publication_failed",
|
|
602
|
+
saveMessage: "Saved, but storefront publication failed.",
|
|
603
|
+
}));
|
|
604
|
+
});
|
|
605
|
+
const { shadow, cleanup } = mount(controller);
|
|
606
|
+
|
|
607
|
+
shadow.querySelector<HTMLButtonElement>('[data-action="done"]')?.click();
|
|
608
|
+
shadow.querySelector<HTMLButtonElement>('[data-action="modal-save-close"]')?.click();
|
|
609
|
+
|
|
610
|
+
await vi.waitFor(() => expect(controller.saveNow).toHaveBeenCalled());
|
|
611
|
+
expect(controller.exit).not.toHaveBeenCalled();
|
|
612
|
+
expect(shadow.textContent).toContain("Done editing?");
|
|
613
|
+
cleanup();
|
|
614
|
+
});
|
|
615
|
+
|
|
508
616
|
it("extends the session and refreshes the displayed time", async () => {
|
|
509
617
|
const controller = makeController();
|
|
510
618
|
const extended = new Date(Date.now() + 4 * 60 * 1000).toISOString();
|
package/src/editor/view.ts
CHANGED
|
@@ -198,7 +198,7 @@ type ThreadUserMessage = {
|
|
|
198
198
|
type ThreadOperatorMessage = {
|
|
199
199
|
id: number;
|
|
200
200
|
role: "op";
|
|
201
|
-
phase: "working" | "done" | "failed" | "save-failed";
|
|
201
|
+
phase: "working" | "done" | "failed" | "save-failed" | "publication-failed";
|
|
202
202
|
step: number;
|
|
203
203
|
steps: string[];
|
|
204
204
|
prompt: string;
|
|
@@ -224,7 +224,16 @@ type ChangeEntry = {
|
|
|
224
224
|
risky: boolean;
|
|
225
225
|
};
|
|
226
226
|
|
|
227
|
-
type SaveChipStatus =
|
|
227
|
+
type SaveChipStatus =
|
|
228
|
+
| "unsaved"
|
|
229
|
+
| "saving"
|
|
230
|
+
| "saved"
|
|
231
|
+
| "pending"
|
|
232
|
+
| "warning"
|
|
233
|
+
| "publication-failed"
|
|
234
|
+
| "failed"
|
|
235
|
+
| "conflict"
|
|
236
|
+
| "loading";
|
|
228
237
|
|
|
229
238
|
const OPERATOR_SUGGESTIONS = [
|
|
230
239
|
"Make the announcement bar mention free returns",
|
|
@@ -269,6 +278,9 @@ function elementNameFromSelector(selector: string): string {
|
|
|
269
278
|
function saveChipStatus(state: EditorState): SaveChipStatus {
|
|
270
279
|
if (state.saveState === "saving") return "saving";
|
|
271
280
|
if (state.saveState === "saved") return "saved";
|
|
281
|
+
if (state.saveState === "pending") return "pending";
|
|
282
|
+
if (state.saveState === "warning") return "warning";
|
|
283
|
+
if (state.saveState === "publication_failed") return "publication-failed";
|
|
272
284
|
if (state.saveState === "conflict") return "conflict";
|
|
273
285
|
if (state.saveState === "error") return "failed";
|
|
274
286
|
if (!state.variation) return "loading";
|
|
@@ -281,15 +293,19 @@ function saveChipHtml(state: EditorState): string {
|
|
|
281
293
|
status === "unsaved" ? "Unsaved changes"
|
|
282
294
|
: status === "saving" ? "Saving…"
|
|
283
295
|
: status === "saved" ? "Saved"
|
|
296
|
+
: status === "pending" ? "Saved — publication pending"
|
|
297
|
+
: status === "warning" ? "Saved — publication warning"
|
|
298
|
+
: status === "publication-failed" ? "Saved — publication failed"
|
|
284
299
|
: status === "failed" ? "Save failed"
|
|
285
300
|
: status === "conflict" ? "Conflict"
|
|
286
301
|
: "Loading draft…";
|
|
287
302
|
const iconHtml =
|
|
288
303
|
status === "saving" || status === "loading" ? icon("refresh", 12, 'class="spin"')
|
|
289
304
|
: status === "saved" ? icon("check", 12)
|
|
305
|
+
: status === "pending" || status === "warning" ? icon("alert", 12)
|
|
290
306
|
: status === "unsaved" ? icon("dot", 12)
|
|
291
307
|
: icon("alert", 12);
|
|
292
|
-
const retry = status === "failed"
|
|
308
|
+
const retry = status === "failed" || status === "publication-failed"
|
|
293
309
|
? `<button type="button" class="retry" data-action="retry-save">Retry</button>`
|
|
294
310
|
: "";
|
|
295
311
|
const chipClass = status === "loading" ? "saving" : status;
|
|
@@ -602,6 +618,13 @@ export function mountEditorView(
|
|
|
602
618
|
<div class="row"><button type="button" class="btn sm soft" data-action="retry-save" data-msg="${message.id}">${icon("refresh", 12)}Retry save</button><button type="button" class="btn sm bare" data-action="op-all-changes">${icon("history", 12)}Review changes</button></div>
|
|
603
619
|
</div>`;
|
|
604
620
|
}
|
|
621
|
+
if (message.phase === "publication-failed") {
|
|
622
|
+
return `<div class="op-card failed ee-in" role="alert">
|
|
623
|
+
<div class="head">${icon("alert", 13)} Change saved, but publication failed</div>
|
|
624
|
+
<p>${esc(message.error ?? "Storefront publication failed.")} The change is saved in Apex. Keep the editor open and retry publication.</p>
|
|
625
|
+
<div class="row"><button type="button" class="btn sm soft" data-action="retry-save" data-msg="${message.id}">${icon("refresh", 12)}Retry publication</button><button type="button" class="btn sm bare" data-action="op-all-changes">${icon("history", 12)}Review changes</button></div>
|
|
626
|
+
</div>`;
|
|
627
|
+
}
|
|
605
628
|
const count = message.results.length;
|
|
606
629
|
const canUndo = canUndoRun(message);
|
|
607
630
|
return `<div class="op-card done ee-in">
|
|
@@ -1017,8 +1040,15 @@ export function mountEditorView(
|
|
|
1017
1040
|
opMessage.abort = null;
|
|
1018
1041
|
await controller.saveNow();
|
|
1019
1042
|
const saveState = controller.getState().saveState;
|
|
1020
|
-
if (
|
|
1021
|
-
|
|
1043
|
+
if (
|
|
1044
|
+
saveState === "error" ||
|
|
1045
|
+
saveState === "conflict" ||
|
|
1046
|
+
saveState === "publication_failed"
|
|
1047
|
+
) {
|
|
1048
|
+
opMessage.phase =
|
|
1049
|
+
saveState === "publication_failed"
|
|
1050
|
+
? "publication-failed"
|
|
1051
|
+
: "save-failed";
|
|
1022
1052
|
opMessage.abort = null;
|
|
1023
1053
|
opMessage.results = describeRunChanges(preArtifacts, controller.getState().artifacts);
|
|
1024
1054
|
opMessage.error = controller.getState().saveMessage;
|
|
@@ -1091,7 +1121,13 @@ export function mountEditorView(
|
|
|
1091
1121
|
);
|
|
1092
1122
|
await controller.saveNow();
|
|
1093
1123
|
const saveState = controller.getState().saveState;
|
|
1094
|
-
if (
|
|
1124
|
+
if (
|
|
1125
|
+
(message?.phase === "save-failed" ||
|
|
1126
|
+
message?.phase === "publication-failed") &&
|
|
1127
|
+
saveState !== "error" &&
|
|
1128
|
+
saveState !== "conflict" &&
|
|
1129
|
+
saveState !== "publication_failed"
|
|
1130
|
+
) {
|
|
1095
1131
|
message.phase = "done";
|
|
1096
1132
|
message.error = null;
|
|
1097
1133
|
message.postArtifacts = stillMatchesRun
|
|
@@ -1218,7 +1254,13 @@ export function mountEditorView(
|
|
|
1218
1254
|
case "modal-save-close":
|
|
1219
1255
|
void controller.saveNow().then(() => {
|
|
1220
1256
|
const saveState = controller.getState().saveState;
|
|
1221
|
-
if (
|
|
1257
|
+
if (
|
|
1258
|
+
saveState !== "error" &&
|
|
1259
|
+
saveState !== "conflict" &&
|
|
1260
|
+
saveState !== "publication_failed"
|
|
1261
|
+
) {
|
|
1262
|
+
return controller.exit();
|
|
1263
|
+
}
|
|
1222
1264
|
render();
|
|
1223
1265
|
}).catch(() => render());
|
|
1224
1266
|
break;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { normalizeEndpoint } from "./endpoint";
|
|
3
|
+
|
|
4
|
+
describe("normalizeEndpoint", () => {
|
|
5
|
+
it("resolves every configured endpoint form to the same base", () => {
|
|
6
|
+
expect(normalizeEndpoint("https://events.example.com")).toBe("https://events.example.com");
|
|
7
|
+
expect(normalizeEndpoint("https://events.example.com/")).toBe("https://events.example.com");
|
|
8
|
+
expect(normalizeEndpoint("https://events.example.com/events")).toBe("https://events.example.com");
|
|
9
|
+
expect(normalizeEndpoint("https://events.example.com/v1/events")).toBe("https://events.example.com");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("keeps the edge-routed prefix so /_drip siblings still resolve", () => {
|
|
13
|
+
expect(normalizeEndpoint("https://shop.example.com/_drip/events")).toBe("https://shop.example.com/_drip");
|
|
14
|
+
});
|
|
15
|
+
});
|
package/src/endpoint.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize an SDK endpoint: strip trailing slashes and a trailing events path
|
|
3
|
+
* so configured bare, /events, and /v1/events endpoints resolve to the base.
|
|
4
|
+
* Single chokepoint — every SDK helper that derives a sibling route from
|
|
5
|
+
* config.endpoint must go through this (events, handover, order-identity,
|
|
6
|
+
* replay, shopify cart authorization).
|
|
7
|
+
*/
|
|
8
|
+
export function normalizeEndpoint(endpoint: string): string {
|
|
9
|
+
return endpoint.replace(/\/+$/, "").replace(/\/(?:v1\/)?events$/, "");
|
|
10
|
+
}
|
package/src/exclusion-groups.ts
CHANGED
|
@@ -2,17 +2,15 @@ import type { Assignment, ExclusionGroupConfig } from "./types";
|
|
|
2
2
|
import { hashToFloat } from "./hash";
|
|
3
3
|
import { chooseVariation } from "./bucket";
|
|
4
4
|
import { deleteCookie, readCookie, writeCookie } from "./guards";
|
|
5
|
+
import { decodeCompactStore } from "./compact-store";
|
|
6
|
+
import { decodePersistedGroupSelections, type PersistedGroupSelection } from "./persistence-decoders";
|
|
7
|
+
export { decodePersistedGroupSelections, type PersistedGroupSelection } from "./persistence-decoders";
|
|
5
8
|
|
|
6
9
|
const STORAGE_KEY = "drip_xgroups_v1"; // gitleaks:allow — public storage key name, not a credential
|
|
7
10
|
const COOKIE_NAME = "drip_xgroups_v1";
|
|
8
11
|
const COOKIE_MAX_AGE_SECONDS = 365 * 24 * 60 * 60;
|
|
9
12
|
|
|
10
13
|
/** A remembered group decision: which member won and at what group epoch. */
|
|
11
|
-
export type PersistedGroupSelection = {
|
|
12
|
-
experimentId: string;
|
|
13
|
-
epoch: number;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
14
|
/** Pluggable persistence so the runtime uses browser storage and tests use memory. */
|
|
17
15
|
export interface GroupStickyStore {
|
|
18
16
|
read(groupId: string): PersistedGroupSelection | null;
|
|
@@ -26,16 +24,8 @@ function normalizeEpoch(epoch: number | null | undefined): number {
|
|
|
26
24
|
return Number.isFinite(epoch) ? Math.max(1, Math.floor(epoch as number)) : 1;
|
|
27
25
|
}
|
|
28
26
|
|
|
29
|
-
function
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
const parsed = JSON.parse(raw) as unknown;
|
|
33
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
34
|
-
? (parsed as CompactStore)
|
|
35
|
-
: {};
|
|
36
|
-
} catch {
|
|
37
|
-
return {};
|
|
38
|
-
}
|
|
27
|
+
export function decodeCompactGroupStore(raw: string | null): CompactStore {
|
|
28
|
+
return decodeCompactStore(raw, 2) as CompactStore;
|
|
39
29
|
}
|
|
40
30
|
|
|
41
31
|
function readLocalStorage(): string | null {
|
|
@@ -48,8 +38,8 @@ function readLocalStorage(): string | null {
|
|
|
48
38
|
|
|
49
39
|
function readCompactStore(): CompactStore {
|
|
50
40
|
return {
|
|
51
|
-
...
|
|
52
|
-
...
|
|
41
|
+
...decodeCompactGroupStore(readLocalStorage()),
|
|
42
|
+
...decodeCompactGroupStore(readCookie(COOKIE_NAME)),
|
|
53
43
|
};
|
|
54
44
|
}
|
|
55
45
|
|
|
@@ -265,6 +255,6 @@ export function resolveExclusionGroups(
|
|
|
265
255
|
export const __exclusionGroupInternals = {
|
|
266
256
|
STORAGE_KEY,
|
|
267
257
|
COOKIE_NAME,
|
|
268
|
-
parseStore,
|
|
258
|
+
parseStore: decodeCompactGroupStore,
|
|
269
259
|
normalizeEpoch,
|
|
270
260
|
};
|
package/src/flags.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
import {
|
|
3
3
|
evaluateFlag,
|
|
4
4
|
evaluateFlagAssignment,
|
|
@@ -7,6 +7,15 @@ import {
|
|
|
7
7
|
} from "./flags";
|
|
8
8
|
import type { Experiment } from "./types";
|
|
9
9
|
|
|
10
|
+
// Hermeticity: weekday tests pin the most recent UTC Monday at 12:00 instead
|
|
11
|
+
// of a literal calendar date.
|
|
12
|
+
const RECENT_MONDAY_NOON = (() => {
|
|
13
|
+
const d = new Date();
|
|
14
|
+
d.setUTCDate(d.getUTCDate() - ((d.getUTCDay() + 6) % 7));
|
|
15
|
+
d.setUTCHours(12, 0, 0, 0);
|
|
16
|
+
return d;
|
|
17
|
+
})();
|
|
18
|
+
|
|
10
19
|
describe("defineFlagExperiment", () => {
|
|
11
20
|
it("creates exactly 2 variations", () => {
|
|
12
21
|
const flag = defineFlagExperiment({
|
|
@@ -320,3 +329,81 @@ describe("evaluateFlagValue", () => {
|
|
|
320
329
|
).toBe("fallback");
|
|
321
330
|
});
|
|
322
331
|
});
|
|
332
|
+
|
|
333
|
+
describe("weekday flag delivery", () => {
|
|
334
|
+
afterEach(() => {
|
|
335
|
+
vi.useRealTimers();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
function scheduledFlag() {
|
|
339
|
+
return {
|
|
340
|
+
...defineFlagExperiment({
|
|
341
|
+
slug: "weekday-flag",
|
|
342
|
+
name: "Weekday Flag",
|
|
343
|
+
rolloutPercent: 0,
|
|
344
|
+
onValue: "enabled-value",
|
|
345
|
+
}),
|
|
346
|
+
weekdaySchedule: {
|
|
347
|
+
excludedDays: ["monday"],
|
|
348
|
+
timezone: "UTC",
|
|
349
|
+
},
|
|
350
|
+
} as Experiment;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
it("disables all public flag evaluators on an excluded weekday", () => {
|
|
354
|
+
vi.useFakeTimers();
|
|
355
|
+
vi.setSystemTime(new Date(RECENT_MONDAY_NOON));
|
|
356
|
+
const flag = {
|
|
357
|
+
...scheduledFlag(),
|
|
358
|
+
variations: scheduledFlag().variations.map((variation) => ({
|
|
359
|
+
...variation,
|
|
360
|
+
weight: variation.id === "enabled" ? 1 : 0,
|
|
361
|
+
})),
|
|
362
|
+
};
|
|
363
|
+
const context = { userId: "user-abc" };
|
|
364
|
+
|
|
365
|
+
expect(evaluateFlag("weekday-flag", [flag], context)).toBe(false);
|
|
366
|
+
expect(evaluateFlagAssignment("weekday-flag", [flag], context)).toBeNull();
|
|
367
|
+
expect(
|
|
368
|
+
evaluateFlagValue("weekday-flag", [flag], context, "fallback"),
|
|
369
|
+
).toBe("fallback");
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("keeps a forced variation enabled on an excluded weekday", () => {
|
|
373
|
+
vi.useFakeTimers();
|
|
374
|
+
vi.setSystemTime(new Date(RECENT_MONDAY_NOON));
|
|
375
|
+
const flag = scheduledFlag();
|
|
376
|
+
const context = {
|
|
377
|
+
userId: "user-abc",
|
|
378
|
+
forceMap: { "weekday-flag": "enabled" },
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
expect(evaluateFlag("weekday-flag", [flag], context)).toBe(true);
|
|
382
|
+
expect(
|
|
383
|
+
evaluateFlagAssignment("weekday-flag", [flag], context)?.variationId,
|
|
384
|
+
).toBe("enabled");
|
|
385
|
+
expect(
|
|
386
|
+
evaluateFlagValue("weekday-flag", [flag], context, "fallback"),
|
|
387
|
+
).toBe("enabled-value");
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it("keeps flag evaluation unchanged when no weekday schedule exists", () => {
|
|
391
|
+
vi.useFakeTimers();
|
|
392
|
+
vi.setSystemTime(new Date(RECENT_MONDAY_NOON));
|
|
393
|
+
const flag = defineFlagExperiment({
|
|
394
|
+
slug: "weekday-flag",
|
|
395
|
+
name: "Weekday Flag",
|
|
396
|
+
rolloutPercent: 100,
|
|
397
|
+
onValue: "enabled-value",
|
|
398
|
+
});
|
|
399
|
+
const context = { userId: "user-abc" };
|
|
400
|
+
|
|
401
|
+
expect(evaluateFlag("weekday-flag", [flag], context)).toBe(true);
|
|
402
|
+
expect(
|
|
403
|
+
evaluateFlagAssignment("weekday-flag", [flag], context)?.variationId,
|
|
404
|
+
).toBe("enabled");
|
|
405
|
+
expect(
|
|
406
|
+
evaluateFlagValue("weekday-flag", [flag], context, "fallback"),
|
|
407
|
+
).toBe("enabled-value");
|
|
408
|
+
});
|
|
409
|
+
});
|
package/src/flags.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { defineExperiment } from "./define";
|
|
|
22
22
|
import type { Experiment, Assignment } from "./types";
|
|
23
23
|
import { getUserId, hashToFloat, variationHashToFloat } from "./hash";
|
|
24
24
|
import { buildTargetingAttributes, matchesTargeting } from "./targeting";
|
|
25
|
+
import { weekdayScheduleBlocksDelivery } from "./weekday-schedule";
|
|
25
26
|
|
|
26
27
|
// ---------------------------------------------------------------------------
|
|
27
28
|
// evaluateFlag — check if a flag is ON for a given user
|
|
@@ -31,6 +32,8 @@ export interface FlagContext {
|
|
|
31
32
|
url?: string;
|
|
32
33
|
attributes?: Record<string, unknown>;
|
|
33
34
|
userId?: string;
|
|
35
|
+
/** QA variation overrides use the same weekday bypass as assignment delivery. */
|
|
36
|
+
forceMap?: Record<string, string>;
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
/**
|
|
@@ -46,32 +49,8 @@ export function evaluateFlag(
|
|
|
46
49
|
experiments: Experiment[],
|
|
47
50
|
context?: FlagContext,
|
|
48
51
|
): boolean {
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const url = context?.url ?? (typeof location !== "undefined" ? location.href : "");
|
|
53
|
-
const attributes = buildTargetingAttributes(context?.attributes ?? {}, url);
|
|
54
|
-
|
|
55
|
-
if (!matchesTargeting(experiment.targeting, { url, attributes })) {
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const userId = getUserId(context?.userId);
|
|
60
|
-
const hashValue = hashToFloat(experiment.id, userId);
|
|
61
|
-
|
|
62
|
-
if (!inTrafficAllocation(hashValue, experiment.trafficAllocation ?? 1)) {
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const allocation = experiment.trafficAllocation ?? 1;
|
|
67
|
-
const variationHashValue =
|
|
68
|
-
allocation >= 1 ? hashValue : variationHashToFloat(experiment.id, userId);
|
|
69
|
-
const weights = experiment.variations.map((v) => v.weight);
|
|
70
|
-
const index = chooseVariation(variationHashValue, weights);
|
|
71
|
-
if (index < 0 || index >= experiment.variations.length) return false;
|
|
72
|
-
|
|
73
|
-
const variation = experiment.variations[index];
|
|
74
|
-
return !variation.isControl;
|
|
52
|
+
const assignment = evaluateFlagAssignment(flagId, experiments, context);
|
|
53
|
+
return assignment !== null && !assignment.isControl;
|
|
75
54
|
}
|
|
76
55
|
|
|
77
56
|
/**
|
|
@@ -85,6 +64,21 @@ export function evaluateFlagAssignment(
|
|
|
85
64
|
const experiment = experiments.find((e) => e.id === flagId);
|
|
86
65
|
if (!experiment) return null;
|
|
87
66
|
|
|
67
|
+
const forcedVariationId = context?.forceMap?.[experiment.id];
|
|
68
|
+
const forcedIndex = forcedVariationId
|
|
69
|
+
? experiment.variations.findIndex(
|
|
70
|
+
(variation) => variation.id === forcedVariationId,
|
|
71
|
+
)
|
|
72
|
+
: -1;
|
|
73
|
+
const forcedVariation =
|
|
74
|
+
forcedIndex >= 0 ? experiment.variations[forcedIndex] : null;
|
|
75
|
+
if (
|
|
76
|
+
!forcedVariation &&
|
|
77
|
+
weekdayScheduleBlocksDelivery(experiment.weekdaySchedule, new Date())
|
|
78
|
+
) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
88
82
|
const url = context?.url ?? (typeof location !== "undefined" ? location.href : "");
|
|
89
83
|
const attributes = buildTargetingAttributes(context?.attributes ?? {}, url);
|
|
90
84
|
|
|
@@ -95,7 +89,10 @@ export function evaluateFlagAssignment(
|
|
|
95
89
|
const userId = getUserId(context?.userId);
|
|
96
90
|
const hashValue = hashToFloat(experiment.id, userId);
|
|
97
91
|
|
|
98
|
-
if (
|
|
92
|
+
if (
|
|
93
|
+
!forcedVariation &&
|
|
94
|
+
!inTrafficAllocation(hashValue, experiment.trafficAllocation ?? 1)
|
|
95
|
+
) {
|
|
99
96
|
return null;
|
|
100
97
|
}
|
|
101
98
|
|
|
@@ -103,15 +100,18 @@ export function evaluateFlagAssignment(
|
|
|
103
100
|
const variationHashValue =
|
|
104
101
|
allocation >= 1 ? hashValue : variationHashToFloat(experiment.id, userId);
|
|
105
102
|
const weights = experiment.variations.map((v) => v.weight);
|
|
106
|
-
const index =
|
|
103
|
+
const index = forcedVariation
|
|
104
|
+
? forcedIndex
|
|
105
|
+
: chooseVariation(variationHashValue, weights);
|
|
107
106
|
if (index < 0 || index >= experiment.variations.length) return null;
|
|
108
107
|
|
|
109
|
-
const variation = experiment.variations[index];
|
|
108
|
+
const variation = forcedVariation ?? experiment.variations[index];
|
|
110
109
|
|
|
111
110
|
return {
|
|
112
111
|
experimentId: experiment.id,
|
|
113
112
|
experimentName: undefined,
|
|
114
113
|
variationId: variation.id,
|
|
114
|
+
pageActive: true,
|
|
115
115
|
index,
|
|
116
116
|
mutations: variation.mutations ?? [],
|
|
117
117
|
value: variation.value,
|