@delmaredigital/payload-experiments 0.1.0-rc.7 → 0.2.0-rc.1
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 +28 -0
- package/dist/admin/LayerUsage.d.ts +1 -1
- package/dist/admin/LayerUsage.js +23 -11
- package/dist/admin/MetricSourceSelect.js +2 -1
- package/dist/admin/TestsNavLink.d.ts +1 -0
- package/dist/admin/TestsNavLink.js +30 -0
- package/dist/admin/allocation.model.js +1 -1
- package/dist/admin/copy/actions.d.ts +0 -2
- package/dist/admin/copy/actions.js +0 -2
- package/dist/admin/copy/advanced.d.ts +55 -0
- package/dist/admin/copy/advanced.js +55 -0
- package/dist/admin/copy/armSelect.js +1 -1
- package/dist/admin/copy/arms.d.ts +0 -13
- package/dist/admin/copy/arms.js +0 -13
- package/dist/admin/copy/{who.d.ts → audience.d.ts} +1 -9
- package/dist/admin/copy/{who.js → audience.js} +1 -9
- package/dist/admin/copy/history.d.ts +0 -4
- package/dist/admin/copy/history.js +3 -7
- package/dist/admin/copy/metrics.d.ts +2 -2
- package/dist/admin/copy/metrics.js +2 -2
- package/dist/admin/copy/newTest.d.ts +32 -0
- package/dist/admin/copy/newTest.js +32 -0
- package/dist/admin/copy/readiness.d.ts +45 -0
- package/dist/admin/copy/readiness.js +50 -0
- package/dist/admin/copy/recommendation.d.ts +26 -0
- package/dist/admin/copy/recommendation.js +25 -0
- package/dist/admin/copy/results.d.ts +0 -1
- package/dist/admin/copy/results.js +0 -1
- package/dist/admin/copy/review.d.ts +0 -17
- package/dist/admin/copy/review.js +1 -112
- package/dist/admin/copy/setting.d.ts +10 -0
- package/dist/admin/copy/setting.js +10 -0
- package/dist/admin/copy/surface.d.ts +36 -0
- package/dist/admin/copy/surface.js +47 -0
- package/dist/admin/copy/testPage.d.ts +88 -0
- package/dist/admin/copy/testPage.js +88 -0
- package/dist/admin/paths.d.ts +5 -13
- package/dist/admin/paths.js +5 -13
- package/dist/admin/review/review.model.d.ts +3 -26
- package/dist/admin/review/review.model.js +3 -30
- package/dist/admin/surface/ResultsRoute.d.ts +7 -0
- package/dist/admin/surface/ResultsRoute.js +58 -0
- package/dist/admin/surface/SurfaceShell.d.ts +6 -0
- package/dist/admin/surface/SurfaceShell.js +35 -0
- package/dist/admin/surface/TestsHome.d.ts +4 -0
- package/dist/admin/surface/TestsHome.js +212 -0
- package/dist/admin/surface/TestsSurface.d.ts +2 -0
- package/dist/admin/surface/TestsSurface.js +36 -0
- package/dist/admin/surface/contracts.d.ts +48 -0
- package/dist/admin/surface/contracts.js +8 -0
- package/dist/admin/surface/home.model.d.ts +41 -0
- package/dist/admin/surface/home.model.js +59 -0
- package/dist/admin/surface/newTest/NewTest.d.ts +7 -0
- package/dist/admin/surface/newTest/NewTest.js +355 -0
- package/dist/admin/surface/newTest/newTest.css +9 -0
- package/dist/admin/surface/pageBlock/pageBlock.model.d.ts +6 -0
- package/dist/admin/surface/pageBlock/pageBlock.model.js +8 -0
- package/dist/admin/surface/pageBlock/parts.d.ts +2 -0
- package/dist/admin/surface/pageBlock/parts.js +64 -0
- package/dist/admin/surface/readiness/ActionDialogs.d.ts +15 -0
- package/dist/admin/surface/readiness/ActionDialogs.js +274 -0
- package/dist/admin/surface/readiness/Banner.d.ts +2 -0
- package/dist/admin/surface/readiness/Banner.js +138 -0
- package/dist/admin/surface/readiness/SaveStatus.d.ts +4 -0
- package/dist/admin/surface/readiness/SaveStatus.js +50 -0
- package/dist/admin/surface/readiness/parts.d.ts +2 -0
- package/dist/admin/surface/readiness/parts.js +9 -0
- package/dist/admin/surface/readiness/readiness.model.d.ts +22 -0
- package/dist/admin/surface/readiness/readiness.model.js +107 -0
- package/dist/admin/surface/readiness/useTransition.d.ts +10 -0
- package/dist/admin/surface/readiness/useTransition.js +34 -0
- package/dist/admin/surface/results/ResultsBody.d.ts +7 -0
- package/dist/admin/surface/results/ResultsBody.js +262 -0
- package/dist/admin/surface/results/recommendation.model.d.ts +12 -0
- package/dist/admin/surface/results/recommendation.model.js +59 -0
- package/dist/admin/surface/results/useResults.d.ts +56 -0
- package/dist/admin/surface/results/useResults.js +183 -0
- package/dist/admin/surface/setting/parts.d.ts +3 -0
- package/dist/admin/surface/setting/parts.js +263 -0
- package/dist/admin/surface/setting/setting.css +29 -0
- package/dist/admin/surface/setting/setting.model.d.ts +8 -0
- package/dist/admin/surface/setting/setting.model.js +25 -0
- package/dist/admin/surface/surface.css +50 -0
- package/dist/admin/surface/test/AdvancedSection.d.ts +2 -0
- package/dist/admin/surface/test/AdvancedSection.js +153 -0
- package/dist/admin/surface/test/GoalSection.d.ts +2 -0
- package/dist/admin/surface/test/GoalSection.js +198 -0
- package/dist/admin/surface/test/LengthSection.d.ts +2 -0
- package/dist/admin/surface/test/LengthSection.js +87 -0
- package/dist/admin/surface/test/Section.d.ts +7 -0
- package/dist/admin/surface/test/Section.js +27 -0
- package/dist/admin/surface/test/TargetingSection.d.ts +2 -0
- package/dist/admin/surface/test/TargetingSection.js +164 -0
- package/dist/admin/surface/test/TestHeader.d.ts +4 -0
- package/dist/admin/surface/test/TestHeader.js +69 -0
- package/dist/admin/surface/test/TestPage.d.ts +7 -0
- package/dist/admin/surface/test/TestPage.js +84 -0
- package/dist/admin/surface/test/VersionsSection.d.ts +4 -0
- package/dist/admin/surface/test/VersionsSection.js +208 -0
- package/dist/admin/surface/test/fields.d.ts +55 -0
- package/dist/admin/surface/test/fields.js +185 -0
- package/dist/admin/surface/test/pageParts.d.ts +2 -0
- package/dist/admin/surface/test/pageParts.js +39 -0
- package/dist/admin/surface/test/testPage.css +23 -0
- package/dist/admin/surface/test/testPage.model.d.ts +17 -0
- package/dist/admin/surface/test/testPage.model.js +53 -0
- package/dist/admin/surface/testDocument.model.d.ts +39 -0
- package/dist/admin/surface/testDocument.model.js +61 -0
- package/dist/admin/surface/useTestDocument.d.ts +22 -0
- package/dist/admin/surface/useTestDocument.js +345 -0
- package/dist/admin/targeting/targeting.model.js +2 -2
- package/dist/admin/useLayerUsage.d.ts +0 -5
- package/dist/admin/useLayerUsage.js +1 -39
- package/dist/admin/views/CollectionEditRedirect.d.ts +4 -0
- package/dist/admin/views/CollectionEditRedirect.js +14 -0
- package/dist/admin/views/CollectionListRedirect.d.ts +2 -0
- package/dist/admin/views/CollectionListRedirect.js +8 -0
- package/dist/admin/views/ExperimentsRootView.d.ts +2 -0
- package/dist/admin/views/ExperimentsRootView.js +175 -0
- package/dist/admin/views/access.model.d.ts +7 -0
- package/dist/admin/views/access.model.js +5 -0
- package/dist/collections/experiments/config.js +18 -125
- package/dist/collections/experiments/createDefaults.js +6 -0
- package/dist/collections/layers.js +43 -12
- package/dist/collections/metrics.js +70 -14
- package/dist/collections/serialize.js +4 -1
- package/dist/db/snapshots.d.ts +2 -1
- package/dist/db/snapshots.js +2 -2
- package/dist/endpoints/results.js +7 -0
- package/dist/endpoints/summary.d.ts +1 -1
- package/dist/endpoints/summary.js +37 -1
- package/dist/exports/adminClient.d.ts +2 -13
- package/dist/exports/adminClient.js +2 -13
- package/dist/exports/index.d.ts +1 -1
- package/dist/exports/rsc.d.ts +3 -0
- package/dist/exports/rsc.js +3 -0
- package/dist/options.js +29 -0
- package/dist/plugin.js +25 -0
- package/dist/puck/editor/CreateExperimentDrawer.d.ts +2 -1
- package/dist/puck/editor/CreateExperimentDrawer.js +17 -57
- package/dist/puck/editor/ExperimentSlotEditor.js +6 -0
- package/dist/puck/editor/copy.d.ts +10 -9
- package/dist/puck/editor/copy.js +11 -10
- package/dist/puck/editor/createTest.d.ts +7 -0
- package/dist/puck/editor/createTest.js +50 -0
- package/dist/puck/editor/editor.model.d.ts +28 -1
- package/dist/puck/editor/editor.model.js +61 -0
- package/dist/results/analyse.js +4 -1
- package/dist/results/verdict.d.ts +1 -0
- package/dist/results/verdict.js +7 -4
- package/dist/transitions/checkStart.d.ts +1 -0
- package/dist/transitions/placements.d.ts +5 -1
- package/dist/transitions/placements.js +14 -7
- package/dist/transitions/preflightData.d.ts +7 -1
- package/dist/transitions/preflightData.js +199 -148
- package/dist/transitions/start.js +2 -2
- package/dist/types/admin.d.ts +1 -0
- package/dist/types/contracts.d.ts +17 -0
- package/dist/types/endpoints.d.ts +11 -1
- package/dist/types/snapshot.d.ts +1 -0
- package/dist/types/surface.d.ts +29 -0
- package/dist/types/surface.js +1 -0
- package/package.json +1 -1
- package/dist/access.d.ts +0 -1
- package/dist/access.js +0 -2
- package/dist/admin/AllocationField.d.ts +0 -2
- package/dist/admin/AllocationField.js +0 -80
- package/dist/admin/FlagPointSelect.d.ts +0 -2
- package/dist/admin/FlagPointSelect.js +0 -54
- package/dist/admin/LockedFieldNotice.d.ts +0 -2
- package/dist/admin/LockedFieldNotice.js +0 -26
- package/dist/admin/PhaseHistory.d.ts +0 -2
- package/dist/admin/PhaseHistory.js +0 -76
- package/dist/admin/StatusCell.d.ts +0 -3
- package/dist/admin/StatusCell.js +0 -19
- package/dist/admin/SummaryCell.d.ts +0 -2
- package/dist/admin/SummaryCell.js +0 -32
- package/dist/admin/actions/AddMetricDialog.d.ts +0 -6
- package/dist/admin/actions/AddMetricDialog.js +0 -138
- package/dist/admin/actions/AllocateDialog.d.ts +0 -6
- package/dist/admin/actions/AllocateDialog.js +0 -81
- package/dist/admin/actions/ExperimentActions.d.ts +0 -3
- package/dist/admin/actions/ExperimentActions.js +0 -110
- package/dist/admin/arms/ArmsField.d.ts +0 -2
- package/dist/admin/arms/ArmsField.js +0 -310
- package/dist/admin/arms/ParamsEditor.d.ts +0 -16
- package/dist/admin/arms/ParamsEditor.js +0 -79
- package/dist/admin/copy/tabs.d.ts +0 -7
- package/dist/admin/copy/tabs.js +0 -7
- package/dist/admin/permissions.model.d.ts +0 -10
- package/dist/admin/permissions.model.js +0 -4
- package/dist/admin/phaseHistory.model.d.ts +0 -19
- package/dist/admin/phaseHistory.model.js +0 -19
- package/dist/admin/results/ResultsView.d.ts +0 -3
- package/dist/admin/results/ResultsView.js +0 -312
- package/dist/admin/review/ReviewPanel.d.ts +0 -2
- package/dist/admin/review/ReviewPanel.js +0 -268
- package/dist/admin/summaryBatcher.d.ts +0 -8
- package/dist/admin/summaryBatcher.js +0 -33
- package/dist/admin/targeting/TargetingAttributeField.d.ts +0 -2
- package/dist/admin/targeting/TargetingAttributeField.js +0 -31
- package/dist/admin/targeting/TargetingOpField.d.ts +0 -2
- package/dist/admin/targeting/TargetingOpField.js +0 -45
- package/dist/admin/targeting/TargetingValueField.d.ts +0 -2
- package/dist/admin/targeting/TargetingValueField.js +0 -142
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -4
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { editorCopy } from './copy.js';
|
|
2
|
+
import { isSlugCollision, newTestBody, slugify, uniqueSlug } from './editor.model.js';
|
|
3
|
+
/** Only a server refusal may supply editor-facing error text. */ export class CreateRefused extends Error {
|
|
4
|
+
}
|
|
5
|
+
function refusalMessage(body) {
|
|
6
|
+
const error = body;
|
|
7
|
+
const message = error?.errors?.[0]?.data?.errors?.[0]?.message;
|
|
8
|
+
return typeof message === 'string' && message.trim() ? message : editorCopy.createError;
|
|
9
|
+
}
|
|
10
|
+
/** Shared lookup, complete draft creation and one retry for a racing slug collision. */ export async function createTest(apiRoute, input) {
|
|
11
|
+
const api = apiRoute.replace(/\/$/, '');
|
|
12
|
+
const base = slugify(input.name) || 'test';
|
|
13
|
+
for(let attempt = 0;; attempt++){
|
|
14
|
+
// Suffixes shorten a 64-character base; include those neighbouring slugs in the lookup.
|
|
15
|
+
const lookup = await fetch(`${api}/xp-experiments?where[slug][like]=${encodeURIComponent(base.slice(0, 60))}&depth=0&limit=200&select[slug]=true`, {
|
|
16
|
+
credentials: 'include'
|
|
17
|
+
});
|
|
18
|
+
const found = await lookup.json();
|
|
19
|
+
if (!lookup.ok) throw new CreateRefused(refusalMessage(found));
|
|
20
|
+
if (!Array.isArray(found?.docs)) throw new Error(editorCopy.createError);
|
|
21
|
+
const taken = found.docs.flatMap((doc)=>typeof doc.slug === 'string' ? [
|
|
22
|
+
doc.slug
|
|
23
|
+
] : []);
|
|
24
|
+
const response = await fetch(`${api}/xp-experiments`, {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
credentials: 'include',
|
|
27
|
+
headers: {
|
|
28
|
+
'Content-Type': 'application/json'
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify(newTestBody({
|
|
31
|
+
...input,
|
|
32
|
+
slug: uniqueSlug(base, taken)
|
|
33
|
+
}))
|
|
34
|
+
});
|
|
35
|
+
const body = await response.json();
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
if (isSlugCollision(response.status, body)) {
|
|
38
|
+
if (attempt === 0) continue;
|
|
39
|
+
throw new CreateRefused(editorCopy.nameExists);
|
|
40
|
+
}
|
|
41
|
+
throw new CreateRefused(refusalMessage(body));
|
|
42
|
+
}
|
|
43
|
+
const doc = body?.doc;
|
|
44
|
+
if (typeof doc?.id !== 'string' && typeof doc?.id !== 'number' || typeof doc?.slug !== 'string') throw new Error(editorCopy.createError);
|
|
45
|
+
return {
|
|
46
|
+
id: doc.id,
|
|
47
|
+
slug: doc.slug
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Arm, Status } from '../../types/contracts.js';
|
|
1
|
+
import type { Arm, DocId, Status, UnitType } from '../../types/contracts.js';
|
|
2
2
|
import type { ArmSlot } from '../../types/puck.js';
|
|
3
3
|
export type EditorExperiment = {
|
|
4
4
|
name: string;
|
|
@@ -14,3 +14,30 @@ export declare function tabsFor(experiment?: Pick<EditorExperiment, 'arms'> | nu
|
|
|
14
14
|
}>;
|
|
15
15
|
export declare function bannerFor(status?: Status | null, winner?: string): string;
|
|
16
16
|
export declare function slugify(name: string): string;
|
|
17
|
+
/** Complete draft defaults, shared by the page hand-off and setting creation. */
|
|
18
|
+
export declare function newTestBody(input: {
|
|
19
|
+
kind: 'block' | 'flag';
|
|
20
|
+
name: string;
|
|
21
|
+
slug: string;
|
|
22
|
+
unitType: UnitType;
|
|
23
|
+
flagPoint?: string;
|
|
24
|
+
layer?: DocId;
|
|
25
|
+
hypothesis?: string;
|
|
26
|
+
}): {
|
|
27
|
+
hypothesis?: string | undefined;
|
|
28
|
+
layer?: DocId | undefined;
|
|
29
|
+
flagPoint?: string | undefined;
|
|
30
|
+
name: string;
|
|
31
|
+
slug: string;
|
|
32
|
+
kind: "block" | "flag";
|
|
33
|
+
unitType: UnitType;
|
|
34
|
+
exposurePolicy: string;
|
|
35
|
+
arms: Arm[];
|
|
36
|
+
allocation: number;
|
|
37
|
+
analysis: {
|
|
38
|
+
plannedDurationDays: number;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
export declare function uniqueSlug(base: string, taken: readonly string[]): string;
|
|
42
|
+
/** Payload nests field validation errors under data.errors. Only a unique slug refusal is retryable. */
|
|
43
|
+
export declare function isSlugCollision(status: number, body: unknown): boolean;
|
|
@@ -31,3 +31,64 @@ export function bannerFor(status, winner = editorCopy.labels.control) {
|
|
|
31
31
|
export function slugify(name) {
|
|
32
32
|
return name.normalize('NFKD').replace(/[\u0300-\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 64).replace(/-+$/g, '');
|
|
33
33
|
}
|
|
34
|
+
/** Complete draft defaults, shared by the page hand-off and setting creation. */ export function newTestBody(input) {
|
|
35
|
+
const { kind, name, slug, unitType, flagPoint, layer, hypothesis } = input;
|
|
36
|
+
const arms = [
|
|
37
|
+
{
|
|
38
|
+
key: 'control',
|
|
39
|
+
kind: 'control',
|
|
40
|
+
label: 'Original',
|
|
41
|
+
weight: 50,
|
|
42
|
+
...kind === 'block' ? {
|
|
43
|
+
slot: 'control'
|
|
44
|
+
} : {}
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
key: 'variantA',
|
|
48
|
+
kind: 'variant',
|
|
49
|
+
label: 'Variant A',
|
|
50
|
+
weight: 50,
|
|
51
|
+
...kind === 'block' ? {
|
|
52
|
+
slot: 'variantA'
|
|
53
|
+
} : {}
|
|
54
|
+
}
|
|
55
|
+
];
|
|
56
|
+
return {
|
|
57
|
+
name,
|
|
58
|
+
slug,
|
|
59
|
+
kind,
|
|
60
|
+
unitType,
|
|
61
|
+
exposurePolicy: kind === 'block' ? 'render' : 'manual',
|
|
62
|
+
arms,
|
|
63
|
+
allocation: 100,
|
|
64
|
+
analysis: {
|
|
65
|
+
plannedDurationDays: 28
|
|
66
|
+
},
|
|
67
|
+
...flagPoint === undefined ? {} : {
|
|
68
|
+
flagPoint
|
|
69
|
+
},
|
|
70
|
+
...layer === undefined ? {} : {
|
|
71
|
+
layer
|
|
72
|
+
},
|
|
73
|
+
...hypothesis === undefined ? {} : {
|
|
74
|
+
hypothesis
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export function uniqueSlug(base, taken) {
|
|
79
|
+
const root = base.slice(0, 64).replace(/-+$/g, '') || 'test';
|
|
80
|
+
const used = new Set(taken);
|
|
81
|
+
if (!used.has(root)) return root;
|
|
82
|
+
for(let index = 2;; index++){
|
|
83
|
+
const suffix = `-${index}`;
|
|
84
|
+
const candidate = root.slice(0, 64 - suffix.length).replace(/-+$/g, '') + suffix;
|
|
85
|
+
if (!used.has(candidate)) return candidate;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** Payload nests field validation errors under data.errors. Only a unique slug refusal is retryable. */ export function isSlugCollision(status, body) {
|
|
89
|
+
if (status !== 400 || !body || typeof body !== 'object') return false;
|
|
90
|
+
const error = body;
|
|
91
|
+
const message = typeof error.message === 'string' ? error.message : '';
|
|
92
|
+
if ((error.path === 'slug' || error.field === 'slug' || /\bslug\b/i.test(message)) && /unique|already.*(?:use|exist)|duplicate/i.test(message)) return true;
|
|
93
|
+
return Array.isArray(error.errors) && error.errors.some((item)=>isSlugCollision(status, item)) || isSlugCollision(status, error.data);
|
|
94
|
+
}
|
package/dist/results/analyse.js
CHANGED
|
@@ -114,6 +114,7 @@ const severity = {
|
|
|
114
114
|
};
|
|
115
115
|
let worst = 'ok';
|
|
116
116
|
let worstArmLabel = compared[0]?.label ?? '';
|
|
117
|
+
let worstArm = compared[0]?.key ?? '';
|
|
117
118
|
try {
|
|
118
119
|
if (!data || data.unavailable !== undefined) throw new Error(data?.unavailable ?? 'Metric data was not supplied');
|
|
119
120
|
const stats = new Map();
|
|
@@ -248,6 +249,7 @@ const severity = {
|
|
|
248
249
|
if (severity[status] > severity[worst]) {
|
|
249
250
|
worst = status;
|
|
250
251
|
worstArmLabel = arm.label;
|
|
252
|
+
worstArm = arm.key;
|
|
251
253
|
}
|
|
252
254
|
}
|
|
253
255
|
} catch (error) {
|
|
@@ -267,7 +269,8 @@ const severity = {
|
|
|
267
269
|
verdictGuardrails.push(worst === 'failing' ? {
|
|
268
270
|
name: definition.name,
|
|
269
271
|
status: worst,
|
|
270
|
-
worstArmLabel
|
|
272
|
+
worstArmLabel,
|
|
273
|
+
worstArm
|
|
271
274
|
} : {
|
|
272
275
|
name: definition.name,
|
|
273
276
|
status: worst
|
package/dist/results/verdict.js
CHANGED
|
@@ -21,7 +21,7 @@ function chance(value) {
|
|
|
21
21
|
}
|
|
22
22
|
/** Apply spec §8.5 in order to already-computed results; no inference is performed here. */ export function computeVerdict(input) {
|
|
23
23
|
const rollout = input.phaseKind === 'rollout';
|
|
24
|
-
const make = (row, code, finding, action, detail, recommendedArm, line)=>({
|
|
24
|
+
const make = (row, code, finding, action, detail, recommendedArm, line, worstArm)=>({
|
|
25
25
|
row,
|
|
26
26
|
code,
|
|
27
27
|
finding,
|
|
@@ -31,6 +31,9 @@ function chance(value) {
|
|
|
31
31
|
recommendedArm,
|
|
32
32
|
...line === undefined ? {} : {
|
|
33
33
|
guardrailLine: line
|
|
34
|
+
},
|
|
35
|
+
...worstArm === undefined ? {} : {
|
|
36
|
+
worstArm
|
|
34
37
|
}
|
|
35
38
|
});
|
|
36
39
|
if (input.primary.unavailable !== undefined) {
|
|
@@ -56,7 +59,7 @@ function chance(value) {
|
|
|
56
59
|
return make('R3', 'rollout-readout', `Since shipping ${winner}: ${percent(lift)} vs the holdout (CI ${percent(ci[0])} to ${percent(ci[1])}).`, null, 'This describes the shipped version compared with people still seeing the Original.', null, guardrailLine(input));
|
|
57
60
|
}
|
|
58
61
|
const failing = input.guardrails.find((item)=>item.status === 'failing');
|
|
59
|
-
if (failing) return make(3, 'guardrail-failing', `Stop: ${failing.worstArmLabel} harms ${failing.name}.`, null, 'An outcome that must stay safe has become worse.', input.controlArm);
|
|
62
|
+
if (failing) return make(3, 'guardrail-failing', `Stop: ${failing.worstArmLabel} harms ${failing.name}.`, null, 'An outcome that must stay safe has become worse.', input.controlArm, undefined, failing.worstArm);
|
|
60
63
|
if (collecting) return make(4, 'collecting', input.status === 'stopped' ? 'Stopped before enough data was collected.' : `Collecting data: ${minimum} of ${input.exposures.minExposuresPerArm} people, ${minimumConverters} of ${input.primary.minSampleSize} conversions.`, null, 'More people or conversions are needed before drawing a conclusion.', null);
|
|
61
64
|
const variantKeys = new Set(compared.filter((item)=>item.kind === 'variant').map((item)=>item.arm));
|
|
62
65
|
const variants = input.primary.comparisons.filter((item)=>variantKeys.has(item.arm));
|
|
@@ -68,12 +71,12 @@ function chance(value) {
|
|
|
68
71
|
].sort((a, b)=>direction * (b.lift - a.lift))[0];
|
|
69
72
|
const worst = worse[0];
|
|
70
73
|
if (variants.length > 0 && worse.length === variants.length) return make(5, 'original-better', 'The Original is better.', 'Decide for the Original.', 'Every tested alternative performed worse.', input.controlArm);
|
|
71
|
-
if (worst && !best) return make(6, 'worse-none-better', `${labels(worse)} ${worse.length === 1 ? 'is' : 'are'} worse; no variant is better.`, `Decide for the Original or restart without ${worse.length === 1 ? 'it' : 'them'}.`, 'At least one alternative performed worse and none improved the main outcome.', input.controlArm);
|
|
74
|
+
if (worst && !best) return make(6, 'worse-none-better', `${labels(worse)} ${worse.length === 1 ? 'is' : 'are'} worse; no variant is better.`, `Decide for the Original or restart without ${worse.length === 1 ? 'it' : 'them'}.`, 'At least one alternative performed worse and none improved the main outcome.', input.controlArm, undefined, worst.arm);
|
|
72
75
|
if (best && !worst) {
|
|
73
76
|
const finding = better.length >= 2 ? `${better.length === 2 ? 'Both' : `All ${better.length} variants`} beat the Original; not tested against each other — ${best.label} has the larger improvement.` : input.method === 'bayesian' ? `${best.label} is very likely better${chance(best.chanceToWin)}.` : `${best.label} is statistically better: ${percent(best.lift)} (CI ${percent(best.ci[0])} to ${percent(best.ci[1])}).`;
|
|
74
77
|
return make(7, 'winner', finding, null, 'The main outcome improved compared with the Original.', best.arm, guardrailLine(input));
|
|
75
78
|
}
|
|
76
|
-
if (best && worst) return make(8, 'mixed', `Mixed: ${labels(better)} better, ${labels(worse)} worse.`, 'Review before deciding.', 'The tested alternatives moved the main outcome in different directions.', best.arm);
|
|
79
|
+
if (best && worst) return make(8, 'mixed', `Mixed: ${labels(better)} better, ${labels(worse)} worse.`, 'Review before deciding.', 'The tested alternatives moved the main outcome in different directions.', best.arm, undefined, worst.arm);
|
|
77
80
|
if (input.status === 'running' && !input.plannedDurationPassed) {
|
|
78
81
|
if (input.power.feasible === false || input.power.daysRemaining !== null && input.power.daysRemaining > 90) {
|
|
79
82
|
return make(9, 'keep-running', 'A clear answer would take more than 90 days at this traffic.', 'Consider a bigger change or a different metric.', 'Current traffic is too low for a timely answer.', null);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Arm, DocId, ExperimentKind, ExposurePolicy, FlagPoint, MetricSource, ResolvedOptions, Status, TargetingRule, UnitType } from '../types/contracts.js';
|
|
2
2
|
import type { ArmIssueCode, PreflightResult } from '../types/endpoints.js';
|
|
3
3
|
import type { BlockPlacement, FlagPlacement } from './placements.js';
|
|
4
|
+
export type ReadinessCode = ArmIssueCode | 'invalid-status' | 'no-planned-duration' | 'no-primary-metric' | 'no-reach-per-day' | 'page-no-targets' | 'page-user-unit' | 'flag-no-point' | 'flag-params-invalid' | 'flag-point-busy' | 'flag-placement-missing' | 'no-placements' | 'empty-slot' | 'layer-unit-mismatch' | 'layer-full' | 'unknown-metric-source' | 'unit-source-mismatch' | 'undeclared-targeting-key' | 'invalid-targeting-rule';
|
|
4
5
|
export interface StartExperiment {
|
|
5
6
|
id: DocId;
|
|
6
7
|
slug: string;
|
|
@@ -13,10 +13,14 @@ export type FlagPlacement = Awaited<ReturnType<NonNullable<FlagPoint['placements
|
|
|
13
13
|
/** Each matching ExperimentSlot is checked separately, including multiple slots
|
|
14
14
|
* in one document: content elsewhere in the document cannot hide an empty slot.
|
|
15
15
|
*/
|
|
16
|
-
export declare function scanBlockPlacements(req: PayloadRequest,
|
|
16
|
+
export declare function scanBlockPlacements(req: PayloadRequest, args: {
|
|
17
17
|
slug: string;
|
|
18
18
|
puckCollections: NonNullable<ResolvedOptions['puck']>['collections'];
|
|
19
19
|
}): Promise<BlockPlacement[]>;
|
|
20
|
+
export declare function scanBlockPlacements(req: PayloadRequest, args: {
|
|
21
|
+
slugs: ReadonlySet<string>;
|
|
22
|
+
puckCollections: NonNullable<ResolvedOptions['puck']>['collections'];
|
|
23
|
+
}): Promise<Map<string, BlockPlacement[]>>;
|
|
20
24
|
export declare function flagPlacements<TUser extends {
|
|
21
25
|
id: DocId;
|
|
22
26
|
}>(options: ResolvedOptions<TUser>, experiment: {
|
|
@@ -5,10 +5,17 @@ const slots = [
|
|
|
5
5
|
'variantB',
|
|
6
6
|
'variantC'
|
|
7
7
|
];
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
export async function scanBlockPlacements(req, args) {
|
|
9
|
+
const { puckCollections } = args;
|
|
10
|
+
const slugs = args.slugs ?? new Set([
|
|
11
|
+
args.slug
|
|
12
|
+
]);
|
|
13
|
+
const placements = new Map([
|
|
14
|
+
...slugs
|
|
15
|
+
].map((slug)=>[
|
|
16
|
+
slug,
|
|
17
|
+
[]
|
|
18
|
+
]));
|
|
12
19
|
for (const { slug: collection, field } of normalisePuckCollections({
|
|
13
20
|
puck: {
|
|
14
21
|
enabled: true,
|
|
@@ -54,8 +61,8 @@ const slots = [
|
|
|
54
61
|
const props = node.props;
|
|
55
62
|
const ref = props?.experiment;
|
|
56
63
|
const experiment = typeof ref === 'string' ? ref : ref && typeof ref === 'object' && 'slug' in ref ? ref.slug : null;
|
|
57
|
-
if (node.type === 'ExperimentSlot' && props && experiment ===
|
|
58
|
-
placements.push({
|
|
64
|
+
if (node.type === 'ExperimentSlot' && props && typeof experiment === 'string' && slugs.has(experiment)) {
|
|
65
|
+
placements.get(experiment).push({
|
|
59
66
|
collection,
|
|
60
67
|
id: doc.id,
|
|
61
68
|
title: String(doc.title ?? doc.name ?? doc.id),
|
|
@@ -76,7 +83,7 @@ const slots = [
|
|
|
76
83
|
}
|
|
77
84
|
}
|
|
78
85
|
}
|
|
79
|
-
return placements;
|
|
86
|
+
return args.slugs ? placements : placements.get(args.slug);
|
|
80
87
|
}
|
|
81
88
|
export async function flagPlacements(options, experiment, req) {
|
|
82
89
|
const point = options.flagPoints?.find((point)=>point.id === experiment.flagPoint);
|
|
@@ -2,7 +2,7 @@ import type { PayloadRequest } from 'payload';
|
|
|
2
2
|
import type { PowerWeeksInput, PowerWeeksResult } from '@delmaredigital/experiments-stats';
|
|
3
3
|
import type { DocId, ResolvedOptions } from '../types/contracts.js';
|
|
4
4
|
import type { PreflightResult } from '../types/endpoints.js';
|
|
5
|
-
import type { CheckStartInput } from './checkStart.js';
|
|
5
|
+
import type { CheckStartInput, StartExperiment } from './checkStart.js';
|
|
6
6
|
export interface PreflightDependencies {
|
|
7
7
|
now?: Date;
|
|
8
8
|
estimate?: (input: PowerWeeksInput) => PowerWeeksResult | Promise<PowerWeeksResult>;
|
|
@@ -13,6 +13,12 @@ export interface PreflightDependencies {
|
|
|
13
13
|
export declare function loadStartInputs<TUser extends {
|
|
14
14
|
id: DocId;
|
|
15
15
|
}>(req: PayloadRequest, options: ResolvedOptions<TUser>, experimentId: DocId): Promise<CheckStartInput | null>;
|
|
16
|
+
/** Shared, request-scoped reads for summary; a failed row is null in tolerant mode.
|
|
17
|
+
* Preflight/Start use the same builder and retain their throwing read behaviour.
|
|
18
|
+
*/
|
|
19
|
+
export declare function loadStartInputsBatch<TUser extends {
|
|
20
|
+
id: DocId;
|
|
21
|
+
}>(req: PayloadRequest, options: ResolvedOptions<TUser>, experiments: StartExperiment[], tolerateErrors?: boolean): Promise<Map<string, CheckStartInput | null>>;
|
|
16
22
|
export declare function preflightData<TUser extends {
|
|
17
23
|
id: DocId;
|
|
18
24
|
}>(req: PayloadRequest, { experimentId, options }: {
|