@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
|
@@ -19,164 +19,216 @@ const failureDetail = (cause)=>cause instanceof Error ? cause.message : 'Placeme
|
|
|
19
19
|
});
|
|
20
20
|
if (!found.docs[0]) return null;
|
|
21
21
|
const experiment = found.docs[0];
|
|
22
|
-
|
|
22
|
+
return (await loadStartInputsBatch(req, options, [
|
|
23
|
+
experiment
|
|
24
|
+
])).get(String(experiment.id)) ?? null;
|
|
25
|
+
}
|
|
26
|
+
const metricIds = (experiment)=>[
|
|
23
27
|
experiment.metrics?.primary,
|
|
24
28
|
...experiment.metrics?.secondary ?? [],
|
|
25
29
|
...experiment.metrics?.guardrails ?? []
|
|
26
30
|
].map(relationshipId).filter((id)=>id !== undefined);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
page,
|
|
38
|
-
where: {
|
|
39
|
-
id: {
|
|
40
|
-
in: ids
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
metrics.push(...result.docs);
|
|
45
|
-
if (!result.hasNextPage) break;
|
|
46
|
-
page++;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
const input = {
|
|
50
|
-
experiment,
|
|
51
|
-
metrics,
|
|
52
|
-
metricSources: options.metricSources,
|
|
53
|
-
flagPoints: options.flagPoints,
|
|
54
|
-
attributeKeys: options.attributeKeys
|
|
55
|
-
};
|
|
56
|
-
if (experiment.kind === 'block') {
|
|
57
|
-
try {
|
|
58
|
-
input.placements = await scanBlockPlacements(req, {
|
|
59
|
-
slug: experiment.slug,
|
|
60
|
-
puckCollections: options.puck?.collections ?? []
|
|
61
|
-
});
|
|
62
|
-
} catch (cause) {
|
|
63
|
-
input.placementError = failureDetail(cause);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
if (experiment.kind === 'flag') {
|
|
67
|
-
const point = options.flagPoints?.find((point)=>point.id === experiment.flagPoint);
|
|
68
|
-
if (point) {
|
|
69
|
-
// Match the save hook: the control always uses effective point defaults.
|
|
70
|
-
let params = point.defaultParams;
|
|
71
|
-
try {
|
|
72
|
-
const state = await req.payload.findGlobal({
|
|
73
|
-
slug: 'xp-state',
|
|
74
|
-
req,
|
|
75
|
-
overrideAccess: false,
|
|
76
|
-
depth: 0
|
|
77
|
-
});
|
|
78
|
-
const defaults = state.flagDefaults;
|
|
79
|
-
params = defaults?.[point.id]?.params ?? params;
|
|
80
|
-
} catch (cause) {
|
|
81
|
-
const error = cause;
|
|
82
|
-
if (error?.status !== 403 && error?.name !== 'Forbidden') throw cause;
|
|
83
|
-
}
|
|
84
|
-
experiment.arms = experiment.arms.map((arm)=>arm.kind === 'control' ? {
|
|
85
|
-
...arm,
|
|
86
|
-
params
|
|
87
|
-
} : arm);
|
|
88
|
-
}
|
|
89
|
-
if (point?.placements) {
|
|
90
|
-
try {
|
|
91
|
-
const placements = await flagPlacements(options, experiment, req);
|
|
92
|
-
if (Array.isArray(placements) && placements.every((p)=>p && typeof p === 'object' && typeof p.arm === 'string')) input.flagPlacements = placements;
|
|
93
|
-
else input.flagPlacementError = 'Placement lookup returned an invalid result.';
|
|
94
|
-
} catch (cause) {
|
|
95
|
-
input.flagPlacementError = failureDetail(cause);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
if (experiment.flagPoint) {
|
|
99
|
-
// At most one row can be this experiment; two rows suffice to find a peer.
|
|
100
|
-
const busy = await req.payload.find({
|
|
101
|
-
collection: 'xp-experiments',
|
|
102
|
-
req,
|
|
103
|
-
overrideAccess: false,
|
|
104
|
-
depth: 0,
|
|
105
|
-
limit: 2,
|
|
106
|
-
where: {
|
|
107
|
-
and: [
|
|
108
|
-
{
|
|
109
|
-
flagPoint: {
|
|
110
|
-
equals: experiment.flagPoint
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
{
|
|
114
|
-
status: {
|
|
115
|
-
in: [
|
|
116
|
-
'running',
|
|
117
|
-
'decided'
|
|
118
|
-
]
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
]
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
input.flagPointBusy = busy.docs.some((doc)=>String(doc.id) !== String(experiment.id));
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
const layerId = relationshipId(experiment.layer);
|
|
128
|
-
if (layerId !== undefined) {
|
|
129
|
-
const layers = await req.payload.find({
|
|
130
|
-
collection: 'xp-layers',
|
|
31
|
+
/** Shared, request-scoped reads for summary; a failed row is null in tolerant mode.
|
|
32
|
+
* Preflight/Start use the same builder and retain their throwing read behaviour.
|
|
33
|
+
*/ export async function loadStartInputsBatch(req, options, experiments, tolerateErrors = false) {
|
|
34
|
+
const ids = [
|
|
35
|
+
...new Set(experiments.flatMap(metricIds))
|
|
36
|
+
];
|
|
37
|
+
const metrics = (async ()=>{
|
|
38
|
+
if (!ids.length) return [];
|
|
39
|
+
const result = await req.payload.find({
|
|
40
|
+
collection: 'xp-metrics',
|
|
131
41
|
req,
|
|
132
42
|
overrideAccess: false,
|
|
133
43
|
depth: 0,
|
|
134
|
-
|
|
44
|
+
pagination: false,
|
|
45
|
+
limit: ids.length,
|
|
135
46
|
where: {
|
|
136
47
|
id: {
|
|
137
|
-
|
|
48
|
+
in: ids
|
|
138
49
|
}
|
|
139
50
|
}
|
|
140
51
|
});
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
52
|
+
return result.docs;
|
|
53
|
+
})();
|
|
54
|
+
const blockSlugs = new Set(experiments.filter((e)=>e.kind === 'block').map((e)=>e.slug));
|
|
55
|
+
// Lazy promises prevent unhandled rejections and share failed reads as well as successful ones.
|
|
56
|
+
let blocks;
|
|
57
|
+
let state;
|
|
58
|
+
const busyPoints = new Map();
|
|
59
|
+
const layers = new Map();
|
|
60
|
+
const inputs = new Map();
|
|
61
|
+
for (const experiment of experiments){
|
|
62
|
+
try {
|
|
63
|
+
const ownIds = new Set(metricIds(experiment).map(String));
|
|
64
|
+
const input = {
|
|
65
|
+
experiment,
|
|
66
|
+
metrics: (await metrics).filter((metric)=>ownIds.has(String(metric.id))),
|
|
67
|
+
metricSources: options.metricSources,
|
|
68
|
+
flagPoints: options.flagPoints,
|
|
69
|
+
attributeKeys: options.attributeKeys
|
|
70
|
+
};
|
|
71
|
+
if (experiment.kind === 'block') {
|
|
72
|
+
try {
|
|
73
|
+
blocks ??= scanBlockPlacements(req, {
|
|
74
|
+
slugs: blockSlugs,
|
|
75
|
+
puckCollections: options.puck?.collections ?? []
|
|
76
|
+
});
|
|
77
|
+
input.placements = (await blocks).get(experiment.slug) ?? [];
|
|
78
|
+
} catch (cause) {
|
|
79
|
+
input.placementError = failureDetail(cause);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (experiment.kind === 'flag') {
|
|
83
|
+
const point = options.flagPoints?.find((point)=>point.id === experiment.flagPoint);
|
|
84
|
+
if (point) {
|
|
85
|
+
state ??= (async ()=>{
|
|
86
|
+
try {
|
|
87
|
+
const doc = await req.payload.findGlobal({
|
|
88
|
+
slug: 'xp-state',
|
|
89
|
+
req,
|
|
90
|
+
overrideAccess: false,
|
|
91
|
+
depth: 0
|
|
92
|
+
});
|
|
93
|
+
return doc.flagDefaults;
|
|
94
|
+
} catch (cause) {
|
|
95
|
+
const error = cause;
|
|
96
|
+
if (error?.status !== 403 && error?.name !== 'Forbidden') throw cause;
|
|
97
|
+
// Match the existing preflight fallback when the global is access-filtered.
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
})();
|
|
101
|
+
const defaults = await state;
|
|
102
|
+
experiment.arms = experiment.arms.map((arm)=>arm.kind === 'control' ? {
|
|
103
|
+
...arm,
|
|
104
|
+
params: defaults?.[point.id]?.params ?? point.defaultParams
|
|
105
|
+
} : arm);
|
|
106
|
+
}
|
|
107
|
+
if (point?.placements) {
|
|
108
|
+
try {
|
|
109
|
+
const placements = await flagPlacements(options, experiment, req);
|
|
110
|
+
if (Array.isArray(placements) && placements.every((p)=>p && typeof p === 'object' && typeof p.arm === 'string')) input.flagPlacements = placements;
|
|
111
|
+
else input.flagPlacementError = 'Placement lookup returned an invalid result.';
|
|
112
|
+
} catch (cause) {
|
|
113
|
+
input.flagPlacementError = failureDetail(cause);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (experiment.flagPoint) {
|
|
117
|
+
const key = experiment.flagPoint;
|
|
118
|
+
if (!busyPoints.has(key)) busyPoints.set(key, (async ()=>{
|
|
119
|
+
// Two rows suffice to find a peer, including when the batch contains that peer.
|
|
120
|
+
const busy = await req.payload.find({
|
|
121
|
+
collection: 'xp-experiments',
|
|
122
|
+
req,
|
|
123
|
+
overrideAccess: false,
|
|
124
|
+
depth: 0,
|
|
125
|
+
limit: 2,
|
|
126
|
+
where: {
|
|
127
|
+
and: [
|
|
128
|
+
{
|
|
129
|
+
flagPoint: {
|
|
130
|
+
equals: key
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
status: {
|
|
135
|
+
in: [
|
|
136
|
+
'running',
|
|
137
|
+
'decided'
|
|
138
|
+
]
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
]
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
return busy.docs;
|
|
145
|
+
})());
|
|
146
|
+
input.flagPointBusy = (await busyPoints.get(key)).some((doc)=>String(doc.id) !== String(experiment.id));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const layerId = relationshipId(experiment.layer);
|
|
150
|
+
if (layerId !== undefined) {
|
|
151
|
+
const key = String(layerId);
|
|
152
|
+
if (!layers.has(key)) layers.set(key, (async ()=>{
|
|
153
|
+
const result = await req.payload.find({
|
|
154
|
+
collection: 'xp-layers',
|
|
155
|
+
req,
|
|
156
|
+
overrideAccess: false,
|
|
157
|
+
depth: 0,
|
|
158
|
+
limit: 1,
|
|
159
|
+
where: {
|
|
160
|
+
id: {
|
|
156
161
|
equals: layerId
|
|
157
162
|
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
const layer = result.docs[0];
|
|
166
|
+
const usedSlices = [];
|
|
167
|
+
let page = 1;
|
|
168
|
+
while(true){
|
|
169
|
+
const usage = await req.payload.find({
|
|
170
|
+
collection: 'xp-experiments',
|
|
171
|
+
req,
|
|
172
|
+
overrideAccess: false,
|
|
173
|
+
depth: 0,
|
|
174
|
+
limit: 100,
|
|
175
|
+
page,
|
|
176
|
+
where: {
|
|
177
|
+
and: [
|
|
178
|
+
{
|
|
179
|
+
layer: {
|
|
180
|
+
equals: layerId
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
status: {
|
|
185
|
+
in: [
|
|
186
|
+
'running',
|
|
187
|
+
'decided'
|
|
188
|
+
]
|
|
189
|
+
}
|
|
190
|
+
}
|
|
164
191
|
]
|
|
165
192
|
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
193
|
+
});
|
|
194
|
+
for (const doc of usage.docs)if (Array.isArray(doc.layerSlices)) usedSlices.push(...doc.layerSlices);
|
|
195
|
+
if (!usage.hasNextPage) break;
|
|
196
|
+
page++;
|
|
197
|
+
}
|
|
198
|
+
return layer ? {
|
|
199
|
+
unitType: layer.unitType,
|
|
200
|
+
usedSlices
|
|
201
|
+
} : null;
|
|
202
|
+
})());
|
|
203
|
+
input.layer = await layers.get(key);
|
|
204
|
+
}
|
|
205
|
+
inputs.set(String(experiment.id), input);
|
|
206
|
+
} catch (cause) {
|
|
207
|
+
if (!tolerateErrors) throw cause;
|
|
208
|
+
inputs.set(String(experiment.id), null);
|
|
173
209
|
}
|
|
174
|
-
input.layer = layer ? {
|
|
175
|
-
unitType: layer.unitType,
|
|
176
|
-
usedSlices
|
|
177
|
-
} : null;
|
|
178
210
|
}
|
|
179
|
-
return
|
|
211
|
+
return inputs;
|
|
212
|
+
}
|
|
213
|
+
function placementUrls(options, placement) {
|
|
214
|
+
const result = {};
|
|
215
|
+
const { collection, id, published } = placement;
|
|
216
|
+
try {
|
|
217
|
+
const url = options.admin?.previewUrl?.({
|
|
218
|
+
collection,
|
|
219
|
+
id
|
|
220
|
+
});
|
|
221
|
+
if (typeof url === 'string') result.url = url;
|
|
222
|
+
} catch {}
|
|
223
|
+
try {
|
|
224
|
+
const url = options.admin?.editorUrl?.({
|
|
225
|
+
collection,
|
|
226
|
+
id,
|
|
227
|
+
draft: !published
|
|
228
|
+
});
|
|
229
|
+
if (typeof url === 'string') result.editorUrl = url;
|
|
230
|
+
} catch {}
|
|
231
|
+
return result;
|
|
180
232
|
}
|
|
181
233
|
async function primaryHistory(source, now) {
|
|
182
234
|
if (!source) return null;
|
|
@@ -230,8 +282,7 @@ export async function preflightData(req, { experimentId, options }, { now = new
|
|
|
230
282
|
const compared = e.arms.filter((arm)=>arm.kind !== 'holdout');
|
|
231
283
|
const control = compared.find((arm)=>arm.kind === 'control');
|
|
232
284
|
const variant = compared.find((arm)=>arm.kind === 'variant');
|
|
233
|
-
const
|
|
234
|
-
const planned = savedDuration ?? 14;
|
|
285
|
+
const planned = 14;
|
|
235
286
|
const enrolled = trafficPerDay * e.allocation / 100;
|
|
236
287
|
const pairShare = ((control?.weight ?? 0) + (variant?.weight ?? 0)) / 100;
|
|
237
288
|
const tuning = enrolled * planned * pairShare;
|
|
@@ -274,13 +325,11 @@ export async function preflightData(req, { experimentId, options }, { now = new
|
|
|
274
325
|
});
|
|
275
326
|
let result = await estimateAt(tuning);
|
|
276
327
|
let weeks = result.sampleSizeAndRuntime.primary?.weeks ?? result.weekThreshold ?? 13;
|
|
277
|
-
const plannedDurationDays =
|
|
328
|
+
const plannedDurationDays = Math.ceil(weeks * 7);
|
|
278
329
|
const sequentialTuningParameter = enrolled * plannedDurationDays * pairShare;
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
weeks = result.sampleSizeAndRuntime.primary?.weeks ?? result.weekThreshold ?? 13;
|
|
283
|
-
}
|
|
330
|
+
// Re-evaluate using exactly the power-derived duration and tuning Apply will save.
|
|
331
|
+
result = await estimateAt(sequentialTuningParameter);
|
|
332
|
+
weeks = result.sampleSizeAndRuntime.primary?.weeks ?? result.weekThreshold ?? 13;
|
|
284
333
|
power = {
|
|
285
334
|
weeks,
|
|
286
335
|
plannedDurationDays,
|
|
@@ -295,6 +344,8 @@ export async function preflightData(req, { experimentId, options }, { now = new
|
|
|
295
344
|
collection: placement.collection,
|
|
296
345
|
id: placement.id,
|
|
297
346
|
title: placement.title,
|
|
347
|
+
slots: placement.slots,
|
|
348
|
+
...placementUrls(options, placement),
|
|
298
349
|
ok: requiredSlots.every((slot)=>slot && placement.slots[slot] > 0),
|
|
299
350
|
detail: placement.published ? 'Published' : 'Draft'
|
|
300
351
|
}));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { randomBytes } from 'node:crypto';
|
|
2
1
|
import { claimSlices } from '../db/slices.js';
|
|
3
2
|
import { XpRefusal } from '../errors.js';
|
|
4
3
|
import { isOpValid } from '../targeting/ops.js';
|
|
4
|
+
import { newSeed } from '../runtime/seed.js';
|
|
5
5
|
import { checkStart, relationshipId } from './checkStart.js';
|
|
6
6
|
import { withTransition } from './kernel.js';
|
|
7
7
|
import { writePhaseRecord } from './phaseRecord.js';
|
|
@@ -160,7 +160,7 @@ export async function start(req, { experimentId, options }) {
|
|
|
160
160
|
count: current.allocation
|
|
161
161
|
});
|
|
162
162
|
const phase = (current.phase ?? 0) + 1;
|
|
163
|
-
const seed =
|
|
163
|
+
const seed = newSeed();
|
|
164
164
|
const startedAt = at.toISOString();
|
|
165
165
|
const definitions = caps.definitions.map((metric)=>({
|
|
166
166
|
...metric,
|
package/dist/types/admin.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { MetricSource, ParamField } from './contracts.js';
|
|
|
2
2
|
export type SerializedFlagPoint = {
|
|
3
3
|
id: string;
|
|
4
4
|
label: string;
|
|
5
|
+
description?: string;
|
|
5
6
|
params: Record<string, ParamField>;
|
|
6
7
|
defaultParams: Record<string, unknown>;
|
|
7
8
|
effectiveDefaults: Record<string, unknown>;
|
|
@@ -64,6 +64,7 @@ export interface MetricSource {
|
|
|
64
64
|
export interface FlagPoint {
|
|
65
65
|
id: string;
|
|
66
66
|
label: string;
|
|
67
|
+
description?: string;
|
|
67
68
|
params: Record<string, ParamField>;
|
|
68
69
|
defaultParams: Record<string, unknown>;
|
|
69
70
|
placements?(args: {
|
|
@@ -104,6 +105,22 @@ export interface ExperimentsPluginOptions<TUser extends {
|
|
|
104
105
|
readResults?: ExperimentsAccess;
|
|
105
106
|
readIdentities?: ExperimentsAccess;
|
|
106
107
|
};
|
|
108
|
+
admin?: {
|
|
109
|
+
editorUrl?: (args: {
|
|
110
|
+
collection: string;
|
|
111
|
+
id: DocId;
|
|
112
|
+
draft?: boolean;
|
|
113
|
+
}) => string;
|
|
114
|
+
previewUrl?: (args: {
|
|
115
|
+
collection: string;
|
|
116
|
+
id: DocId;
|
|
117
|
+
}) => string;
|
|
118
|
+
descriptions?: {
|
|
119
|
+
block?: string;
|
|
120
|
+
document?: string;
|
|
121
|
+
setting?: string;
|
|
122
|
+
};
|
|
123
|
+
};
|
|
107
124
|
metricSources?: MetricSource[];
|
|
108
125
|
eventSources?: Array<string | {
|
|
109
126
|
name: string;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { ArmKind, DocId, ExposurePolicy, MetricSource, ResolvedOptions } from './contracts.js';
|
|
2
|
+
import type { ArmSlot } from './puck.js';
|
|
3
|
+
import type { ReadinessCode } from '../transitions/checkStart.js';
|
|
4
|
+
export type { ReadinessCode } from '../transitions/checkStart.js';
|
|
2
5
|
import type { Snapshot, VerdictCode } from './snapshot.js';
|
|
3
6
|
export type ArmIssueCode = 'weights-total' | 'control-count' | 'control-first' | 'holdout-last' | 'holdout-count' | 'variant-count' | 'duplicate-key' | 'duplicate-slot' | 'slot-required' | 'invalid-key';
|
|
4
7
|
export type RefusalCode = ArmIssueCode | 'not-found' | 'no-planned-duration' | 'no-primary-metric' | 'no-placements' | 'empty-slot' | 'page-no-targets' | 'page-user-unit' | 'flag-no-point' | 'flag-params-invalid' | 'flag-point-busy' | 'flag-placement-missing' | 'layer-full' | 'layer-unit-mismatch' | 'unknown-metric-source' | 'unit-source-mismatch' | 'no-reach-per-day' | 'invalid-status' | 'stale-draft' | 'commit-unverified' | 'allocation-lower' | 'confirmation-required' | 'invalid-winner' | 'slices-taken' | 'locked-placement' | 'not-implemented' | 'forbidden' | 'unauthenticated' | 'body-too-large' | 'invalid-body' | 'too-many-ids' | 'invalid-phase' | 'invalid-ids' | 'recompute-failed' | 'undeclared-targeting-key' | 'invalid-targeting-rule' | 'duplicate-metric';
|
|
@@ -39,7 +42,7 @@ export type MakeContentResult = TransitionSuccess & {
|
|
|
39
42
|
};
|
|
40
43
|
export type PreflightResult = {
|
|
41
44
|
checks: Array<{
|
|
42
|
-
code:
|
|
45
|
+
code: ReadinessCode;
|
|
43
46
|
ok: boolean;
|
|
44
47
|
detail?: unknown;
|
|
45
48
|
}>;
|
|
@@ -48,6 +51,8 @@ export type PreflightResult = {
|
|
|
48
51
|
id: DocId;
|
|
49
52
|
title: string;
|
|
50
53
|
url?: string;
|
|
54
|
+
editorUrl?: string;
|
|
55
|
+
slots?: Record<ArmSlot, number>;
|
|
51
56
|
ok: boolean;
|
|
52
57
|
detail?: string;
|
|
53
58
|
}>;
|
|
@@ -91,6 +96,10 @@ export type ResultsResponse = {
|
|
|
91
96
|
phases: PhaseSummary[];
|
|
92
97
|
snapshot: Snapshot | null;
|
|
93
98
|
decisionSnapshot?: Snapshot;
|
|
99
|
+
decision?: {
|
|
100
|
+
phase: number;
|
|
101
|
+
winnerArm: string;
|
|
102
|
+
};
|
|
94
103
|
series?: Array<{
|
|
95
104
|
computedAt: string;
|
|
96
105
|
metricId: DocId;
|
|
@@ -100,6 +109,7 @@ export type ResultsResponse = {
|
|
|
100
109
|
}>;
|
|
101
110
|
};
|
|
102
111
|
export type SummaryResponse = Record<DocId, {
|
|
112
|
+
ready?: boolean;
|
|
103
113
|
exposures: number;
|
|
104
114
|
days: number;
|
|
105
115
|
verdict: {
|
package/dist/types/snapshot.d.ts
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { DocId } from './contracts.js';
|
|
2
|
+
import type { SerializedAttributeKeys, SerializedFlagPoint, SerializedMetricSource } from './admin.js';
|
|
3
|
+
export type TestsRoute = 'home' | 'new' | 'test' | 'results';
|
|
4
|
+
export interface TestsSurfaceProps {
|
|
5
|
+
route: TestsRoute;
|
|
6
|
+
id?: string;
|
|
7
|
+
adminRoute: string;
|
|
8
|
+
apiRoute: string;
|
|
9
|
+
isAdmin: boolean;
|
|
10
|
+
pages?: Array<{
|
|
11
|
+
collection: string;
|
|
12
|
+
collectionLabel: string;
|
|
13
|
+
id: DocId;
|
|
14
|
+
title: string;
|
|
15
|
+
editorUrl: string;
|
|
16
|
+
}>;
|
|
17
|
+
canManage: boolean;
|
|
18
|
+
canReadResults: boolean;
|
|
19
|
+
flagPoints: SerializedFlagPoint[];
|
|
20
|
+
attributeKeys: SerializedAttributeKeys;
|
|
21
|
+
metricSources: SerializedMetricSource[];
|
|
22
|
+
admin?: {
|
|
23
|
+
descriptions?: {
|
|
24
|
+
block?: string;
|
|
25
|
+
document?: string;
|
|
26
|
+
setting?: string;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@delmaredigital/payload-experiments",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-rc.1",
|
|
4
4
|
"description": "No-code A/B testing for Payload CMS and Next.js: block, page and flag experiments with holdouts, layers, exposure logging and honest statistics inside the admin — no vendor",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/dist/access.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { deny, isTrue, managedAccess, readOnlyAccess } from './collections/access.js';
|
package/dist/access.js
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { FieldError, useField, useFormFields } from '@payloadcms/ui';
|
|
4
|
-
import { EDITABLE_BY_STATUS, isEditable } from '../collections/experiments/editability.js';
|
|
5
|
-
import { lockedReason } from './lockedNotice.model.js';
|
|
6
|
-
import { allocationGroups, allocationLayer, allocationSentence } from './allocation.model.js';
|
|
7
|
-
import { WHO_COPY } from './copy/who.js';
|
|
8
|
-
import { useLayerUsage } from './useLayerUsage.js';
|
|
9
|
-
export function AllocationField({ path, field, readOnly }) {
|
|
10
|
-
const { value, setValue } = useField({
|
|
11
|
-
path
|
|
12
|
-
});
|
|
13
|
-
const form = useFormFields(([fields])=>({
|
|
14
|
-
status: fields.status?.value ?? 'draft',
|
|
15
|
-
unitType: fields.unitType?.value ?? 'visitor',
|
|
16
|
-
arms: allocationGroups(fields),
|
|
17
|
-
layerId: fields.layer?.value,
|
|
18
|
-
layer: allocationLayer(fields.layer?.value) ?? allocationLayer({
|
|
19
|
-
name: fields['layer.name']?.value,
|
|
20
|
-
used: fields['layer.used']?.value,
|
|
21
|
-
free: fields['layer.free']?.value
|
|
22
|
-
})
|
|
23
|
-
}));
|
|
24
|
-
const locked = Boolean(readOnly || field.admin?.readOnly || !isEditable(form.status, 'allocation'));
|
|
25
|
-
const reason = lockedReason(form.status, 'allocation', EDITABLE_BY_STATUS);
|
|
26
|
-
const layerId = typeof form.layerId === 'object' && form.layerId !== null ? 'id' in form.layerId ? form.layerId.id : null : form.layerId;
|
|
27
|
-
const layer = useLayerUsage(typeof layerId === 'string' || typeof layerId === 'number' ? layerId : null);
|
|
28
|
-
const sentence = allocationSentence({
|
|
29
|
-
...form,
|
|
30
|
-
allocation: value ?? NaN,
|
|
31
|
-
layer: layer ?? form.layer
|
|
32
|
-
});
|
|
33
|
-
// position: relative anchors Payload's absolutely positioned FieldError tooltip to this field.
|
|
34
|
-
return /*#__PURE__*/ _jsxs("div", {
|
|
35
|
-
className: "xp-allocation-field",
|
|
36
|
-
style: {
|
|
37
|
-
position: 'relative'
|
|
38
|
-
},
|
|
39
|
-
children: [
|
|
40
|
-
/*#__PURE__*/ _jsx("label", {
|
|
41
|
-
htmlFor: path,
|
|
42
|
-
children: WHO_COPY.allocation
|
|
43
|
-
}),
|
|
44
|
-
/*#__PURE__*/ _jsx("input", {
|
|
45
|
-
id: path,
|
|
46
|
-
name: path,
|
|
47
|
-
type: "number",
|
|
48
|
-
min: 1,
|
|
49
|
-
max: 100,
|
|
50
|
-
step: 1,
|
|
51
|
-
required: field.required,
|
|
52
|
-
value: value ?? '',
|
|
53
|
-
readOnly: locked,
|
|
54
|
-
"aria-describedby": `${path}-description`,
|
|
55
|
-
style: {
|
|
56
|
-
display: 'block',
|
|
57
|
-
color: 'var(--theme-text)',
|
|
58
|
-
background: 'var(--theme-input-bg)',
|
|
59
|
-
border: '1px solid var(--theme-elevation-200)',
|
|
60
|
-
borderRadius: 'var(--style-radius-s)',
|
|
61
|
-
padding: '0.5rem'
|
|
62
|
-
},
|
|
63
|
-
onChange: (event)=>{
|
|
64
|
-
if (!locked) setValue(event.target.value === '' ? null : event.target.valueAsNumber);
|
|
65
|
-
}
|
|
66
|
-
}),
|
|
67
|
-
/*#__PURE__*/ _jsx(FieldError, {
|
|
68
|
-
path: path
|
|
69
|
-
}),
|
|
70
|
-
/*#__PURE__*/ _jsx("p", {
|
|
71
|
-
id: `${path}-description`,
|
|
72
|
-
children: sentence
|
|
73
|
-
}),
|
|
74
|
-
reason && (form.status !== 'running' || !sentence) ? /*#__PURE__*/ _jsx("p", {
|
|
75
|
-
className: "xp-locked-notice",
|
|
76
|
-
children: reason
|
|
77
|
-
}) : null
|
|
78
|
-
]
|
|
79
|
-
});
|
|
80
|
-
}
|