@delmaredigital/payload-experiments 0.3.0-rc.1 → 0.4.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 +172 -21
- package/dist/admin/MetricSourceSelect.js +3 -3
- package/dist/admin/PlacementStatus.d.ts +3 -0
- package/dist/admin/PlacementStatus.js +54 -0
- package/dist/admin/PreviewLinks.d.ts +2 -1
- package/dist/admin/PreviewLinks.js +47 -26
- package/dist/admin/TestsNavLink.d.ts +3 -1
- package/dist/admin/TestsNavLink.js +2 -2
- package/dist/admin/actions/ConfirmDialog.d.ts +2 -1
- package/dist/admin/actions/ConfirmDialog.js +2 -2
- package/dist/admin/actions/DecideDialog.d.ts +2 -1
- package/dist/admin/actions/DecideDialog.js +2 -2
- package/dist/admin/api.d.ts +11 -2
- package/dist/admin/api.js +20 -2
- package/dist/admin/armSelect/ArmSelectField.js +14 -22
- package/dist/admin/arms/arms.model.d.ts +1 -21
- package/dist/admin/arms/arms.model.js +1 -153
- package/dist/admin/blocks/BlockPanel.d.ts +4 -0
- package/dist/admin/blocks/BlockPanel.js +153 -0
- package/dist/admin/blocks/TestBlockField.d.ts +5 -0
- package/dist/admin/blocks/TestBlockField.js +163 -0
- package/dist/admin/blocks/formApi.d.ts +43 -0
- package/dist/admin/blocks/formApi.js +153 -0
- package/dist/admin/blocks/inject.d.ts +7 -0
- package/dist/admin/blocks/inject.js +41 -0
- package/dist/admin/blocks/model.d.ts +17 -0
- package/dist/admin/blocks/model.js +45 -0
- package/dist/admin/blocks/statusCache.d.ts +7 -0
- package/dist/admin/blocks/statusCache.js +83 -0
- package/dist/admin/blocks/useBlockForm.d.ts +17 -0
- package/dist/admin/blocks/useBlockForm.js +46 -0
- package/dist/admin/copy/actions.d.ts +2 -0
- package/dist/admin/copy/actions.js +9 -0
- package/dist/admin/copy/arms.d.ts +1 -6
- package/dist/admin/copy/arms.js +1 -16
- package/dist/admin/copy/blocks.d.ts +27 -0
- package/dist/admin/copy/blocks.js +38 -0
- package/dist/admin/copy/goals.d.ts +2 -0
- package/dist/admin/copy/goals.js +4 -1
- package/dist/admin/copy/newTest.d.ts +10 -0
- package/dist/admin/copy/newTest.js +10 -0
- package/dist/admin/copy/pages.d.ts +26 -0
- package/dist/admin/copy/pages.js +26 -0
- package/dist/admin/copy/readiness.d.ts +25 -3
- package/dist/admin/copy/readiness.js +25 -3
- package/dist/admin/copy/results.d.ts +3 -0
- package/dist/admin/copy/results.js +4 -1
- package/dist/admin/copy/review.d.ts +1 -1
- package/dist/admin/copy/review.js +1 -1
- package/dist/admin/copy/surface.d.ts +0 -1
- package/dist/admin/copy/surface.js +0 -1
- package/dist/admin/copy/testPage.d.ts +4 -0
- package/dist/admin/copy/testPage.js +4 -0
- package/dist/admin/goals/XpGoalField.d.ts +2 -0
- package/dist/admin/goals/XpGoalField.js +152 -0
- package/dist/admin/metricSource.model.d.ts +2 -0
- package/dist/admin/metricSource.model.js +4 -0
- package/dist/admin/pageCollections.d.ts +10 -0
- package/dist/admin/pageCollections.js +20 -0
- package/dist/admin/paths.d.ts +8 -1
- package/dist/admin/paths.js +8 -1
- package/dist/admin/placementStatus.model.d.ts +2 -0
- package/dist/admin/placementStatus.model.js +19 -0
- package/dist/admin/placements/placementLabels.d.ts +11 -0
- package/dist/admin/placements/placementLabels.js +21 -0
- package/dist/admin/placements/placementLabels.model.d.ts +13 -0
- package/dist/admin/placements/placementLabels.model.js +12 -0
- package/dist/admin/placements/usePlacementLabels.d.ts +12 -0
- package/dist/admin/placements/usePlacementLabels.js +102 -0
- package/dist/admin/previewLinks.model.d.ts +2 -0
- package/dist/admin/previewLinks.model.js +3 -0
- package/dist/admin/results/DataQualityPanel.js +4 -1
- package/dist/admin/results/legacy.d.ts +16 -0
- package/dist/admin/results/legacy.js +27 -0
- package/dist/admin/results/results.model.d.ts +9 -6
- package/dist/admin/results/results.model.js +22 -9
- package/dist/admin/results/snapshot.d.ts +14 -0
- package/dist/admin/results/snapshot.js +3 -0
- package/dist/admin/surface/TestsHome.js +43 -15
- package/dist/admin/surface/TestsSurface.js +5 -1
- package/dist/admin/surface/contracts.d.ts +6 -5
- package/dist/admin/surface/goals/GoalForm.js +67 -34
- package/dist/admin/surface/goals/GoalsList.js +3 -3
- package/dist/admin/surface/goals/data.js +19 -16
- package/dist/admin/surface/goals/goals.model.d.ts +1 -2
- package/dist/admin/surface/goals/goals.model.js +1 -3
- package/dist/admin/surface/home.model.d.ts +2 -1
- package/dist/admin/surface/home.model.js +8 -4
- package/dist/admin/surface/newTest/NewTest.js +58 -17
- package/dist/admin/surface/newTest/PageWizard.d.ts +7 -0
- package/dist/admin/surface/newTest/PageWizard.js +209 -0
- package/dist/admin/surface/pagePicker/CopyRoute.d.ts +6 -0
- package/dist/admin/surface/pagePicker/CopyRoute.js +53 -0
- package/dist/admin/surface/pagePicker/PagePicker.d.ts +10 -0
- package/dist/admin/surface/pagePicker/PagePicker.js +172 -0
- package/dist/admin/surface/pagePicker/pagePicker.model.d.ts +25 -0
- package/dist/admin/surface/pagePicker/pagePicker.model.js +25 -0
- package/dist/admin/surface/readiness/ActionDialogs.js +103 -69
- package/dist/admin/surface/readiness/Banner.js +30 -6
- package/dist/admin/surface/readiness/readiness.model.d.ts +8 -1
- package/dist/admin/surface/readiness/readiness.model.js +95 -6
- package/dist/admin/surface/results/ReleasePages.d.ts +3 -1
- package/dist/admin/surface/results/ReleasePages.js +3 -7
- package/dist/admin/surface/results/ResultsBody.js +23 -3
- package/dist/admin/surface/results/ResultsHeader.js +2 -5
- package/dist/admin/surface/results/recommendation.model.d.ts +3 -2
- package/dist/admin/surface/results/recommendation.model.js +10 -3
- package/dist/admin/surface/results/useResults.d.ts +10 -1
- package/dist/admin/surface/surface.css +0 -1
- package/dist/admin/surface/test/AdvancedSection.js +111 -104
- package/dist/admin/surface/test/GoalSection.js +97 -22
- package/dist/admin/surface/test/LengthSection.js +1 -1
- package/dist/admin/surface/test/PageHoldout.d.ts +3 -0
- package/dist/admin/surface/test/PageHoldout.js +67 -0
- package/dist/admin/surface/test/TestPage.js +2 -1
- package/dist/admin/surface/test/VersionsSection.js +12 -5
- package/dist/admin/surface/test/fields.d.ts +13 -4
- package/dist/admin/surface/test/fields.js +46 -14
- package/dist/admin/surface/test/jumpToSection.d.ts +1 -1
- package/dist/admin/surface/test/jumpToSection.js +4 -3
- package/dist/admin/surface/test/pageParts.d.ts +4 -1
- package/dist/admin/surface/test/pageParts.js +79 -2
- package/dist/admin/surface/test/testPage.css +8 -1
- package/dist/admin/surface/test/testPage.model.d.ts +2 -3
- package/dist/admin/surface/test/testPage.model.js +1 -21
- package/dist/admin/surface/testDocument.model.d.ts +1 -0
- package/dist/admin/surface/testDocument.model.js +2 -1
- package/dist/admin/surface/useTestDocument.d.ts +11 -2
- package/dist/admin/surface/useTestDocument.js +55 -16
- package/dist/admin/useApi.d.ts +11 -2
- package/dist/admin/useApi.js +3 -5
- package/dist/admin/useLayerUsage.js +14 -23
- package/dist/admin/views/ExperimentsRootView.js +58 -16
- package/dist/admin/views/TestsNavLinkServer.d.ts +2 -0
- package/dist/admin/views/TestsNavLinkServer.js +43 -0
- package/dist/arms/copy.d.ts +6 -0
- package/dist/arms/copy.js +16 -0
- package/dist/arms/model.d.ts +23 -0
- package/dist/arms/model.js +173 -0
- package/dist/blocks/bindings.d.ts +3 -0
- package/dist/blocks/bindings.js +34 -0
- package/dist/blocks/createExperimentBlock.d.ts +6 -0
- package/dist/blocks/createExperimentBlock.js +77 -0
- package/dist/blocks/formState.d.ts +31 -0
- package/dist/blocks/formState.js +146 -0
- package/dist/blocks/model.d.ts +41 -0
- package/dist/blocks/model.js +34 -0
- package/dist/client/ExperimentGroup.d.ts +6 -0
- package/dist/client/ExperimentGroup.js +9 -0
- package/dist/client/ExperimentsProvider.js +20 -4
- package/dist/client/clickGoals.d.ts +4 -0
- package/dist/client/clickGoals.js +43 -0
- package/dist/collections/experiments/config.js +12 -2
- package/dist/collections/experiments/deleteGuard.d.ts +2 -1
- package/dist/collections/experiments/deleteGuard.js +12 -1
- package/dist/collections/experiments/editability.js +4 -2
- package/dist/collections/experiments/pickSlug.d.ts +2 -0
- package/dist/collections/experiments/pickSlug.js +8 -0
- package/dist/collections/labels.d.ts +3 -0
- package/dist/collections/labels.js +20 -0
- package/dist/collections/metrics.js +12 -2
- package/dist/collections/phases.js +1 -0
- package/dist/collections/serialize.js +7 -0
- package/dist/db/appends.d.ts +6 -2
- package/dist/db/appends.js +10 -4
- package/dist/db/clickGoals.d.ts +9 -0
- package/dist/db/clickGoals.js +22 -0
- package/dist/db/delete.d.ts +1 -0
- package/dist/db/delete.js +1 -0
- package/dist/db/locks.d.ts +14 -0
- package/dist/db/locks.js +36 -1
- package/dist/db/names.d.ts +4 -0
- package/dist/db/names.js +4 -0
- package/dist/db/pageBindings.d.ts +4 -0
- package/dist/db/pageBindings.js +17 -0
- package/dist/db/pageBusy.d.ts +2 -0
- package/dist/db/pageBusy.js +10 -0
- package/dist/db/pageContent.d.ts +10 -0
- package/dist/db/pageContent.js +79 -0
- package/dist/db/pageOccupancy.d.ts +12 -0
- package/dist/db/pageOccupancy.js +45 -0
- package/dist/db/placements.d.ts +33 -0
- package/dist/db/placements.js +54 -0
- package/dist/db/slugs.d.ts +8 -0
- package/dist/db/slugs.js +40 -0
- package/dist/db/snapshots.d.ts +6 -0
- package/dist/db/snapshots.js +9 -5
- package/dist/endpoints/clickKeys.d.ts +5 -0
- package/dist/endpoints/clickKeys.js +17 -0
- package/dist/endpoints/events.js +4 -1
- package/dist/endpoints/index.js +28 -0
- package/dist/endpoints/pages.d.ts +5 -0
- package/dist/endpoints/pages.js +120 -0
- package/dist/endpoints/placementStatus.d.ts +6 -0
- package/dist/endpoints/placementStatus.js +238 -0
- package/dist/endpoints/readBody.d.ts +2 -0
- package/dist/endpoints/readBody.js +24 -0
- package/dist/endpoints/results.js +5 -1
- package/dist/endpoints/summary.js +11 -4
- package/dist/endpoints/wrapBlock.d.ts +6 -0
- package/dist/endpoints/wrapBlock.js +76 -0
- package/dist/errors.d.ts +3 -3
- package/dist/errors.js +4 -4
- package/dist/events/rows.d.ts +1 -1
- package/dist/events/rows.js +5 -3
- package/dist/exports/adminClient.d.ts +5 -0
- package/dist/exports/adminClient.js +5 -0
- package/dist/exports/adminRsc.d.ts +1 -0
- package/dist/exports/adminRsc.js +1 -0
- package/dist/exports/blocks.d.ts +2 -0
- package/dist/exports/blocks.js +1 -0
- package/dist/exports/client.d.ts +1 -0
- package/dist/exports/client.js +1 -0
- package/dist/exports/index.d.ts +6 -1
- package/dist/exports/index.js +3 -0
- package/dist/exports/puck.d.ts +5 -2
- package/dist/exports/puck.js +3 -1
- package/dist/exports/puckEditor.d.ts +6 -1
- package/dist/exports/puckEditor.js +4 -1
- package/dist/exports/rsc.d.ts +4 -0
- package/dist/exports/rsc.js +2 -0
- package/dist/fields/xpGoal.d.ts +3 -0
- package/dist/fields/xpGoal.js +28 -0
- package/dist/globals/state.js +10 -0
- package/dist/goals/cache.d.ts +3 -0
- package/dist/goals/cache.js +35 -0
- package/dist/goals/fairness.d.ts +40 -0
- package/dist/goals/fairness.js +200 -0
- package/dist/goals/key.d.ts +4 -0
- package/dist/goals/key.js +3 -0
- package/dist/goals/picker/clickGoalPicker.model.d.ts +29 -0
- package/dist/goals/picker/clickGoalPicker.model.js +45 -0
- package/dist/goals/picker/copy.d.ts +16 -0
- package/dist/goals/picker/copy.js +15 -0
- package/dist/goals/picker/useClickGoalPicker.d.ts +30 -0
- package/dist/goals/picker/useClickGoalPicker.js +220 -0
- package/dist/hooks/pageGuard.d.ts +5 -1
- package/dist/hooks/pageGuard.js +160 -80
- package/dist/lib/fetchAllDocs.d.ts +16 -0
- package/dist/lib/fetchAllDocs.js +56 -0
- package/dist/onInit.js +29 -0
- package/dist/options.d.ts +2 -0
- package/dist/options.js +43 -10
- package/dist/plugin.js +45 -28
- package/dist/puck/ExperimentPicker.js +47 -16
- package/dist/puck/ExperimentSlot.d.ts +51 -1
- package/dist/puck/ExperimentSlot.js +23 -12
- package/dist/puck/copy.d.ts +6 -4
- package/dist/puck/copy.js +6 -4
- package/dist/puck/editor/CreateExperimentDrawer.js +46 -56
- package/dist/puck/editor/ExperimentSlotEditor.d.ts +52 -1
- package/dist/puck/editor/ExperimentSlotEditor.js +292 -137
- package/dist/puck/editor/ResultsReadout.d.ts +6 -0
- package/dist/puck/editor/ResultsReadout.js +99 -0
- package/dist/puck/editor/VersionBuilder.d.ts +34 -0
- package/dist/puck/editor/VersionBuilder.js +317 -0
- package/dist/puck/editor/armWrites.d.ts +7 -0
- package/dist/puck/editor/armWrites.js +71 -0
- package/dist/puck/editor/armsCache.d.ts +14 -0
- package/dist/puck/editor/armsCache.js +78 -0
- package/dist/puck/editor/copy.d.ts +33 -1
- package/dist/puck/editor/copy.js +45 -8
- package/dist/puck/editor/createTest.d.ts +1 -1
- package/dist/puck/editor/createTest.js +25 -14
- package/dist/puck/editor/editor.chrome.css +48 -161
- package/dist/puck/editor/editor.css +48 -161
- package/dist/puck/editor/editor.model.d.ts +8 -3
- package/dist/puck/editor/editor.model.js +16 -10
- package/dist/puck/editor/experimentOutlinePlugin.d.ts +3 -0
- package/dist/puck/editor/experimentOutlinePlugin.js +89 -0
- package/dist/puck/editor/integration.d.ts +10 -0
- package/dist/puck/editor/integration.js +30 -0
- package/dist/puck/editor/xpGoalPuckEditorField.d.ts +7 -0
- package/dist/puck/editor/xpGoalPuckEditorField.js +134 -0
- package/dist/puck/experimentSlotField.d.ts +2 -0
- package/dist/puck/experimentSlotField.js +11 -3
- package/dist/puck/hostContext.d.ts +10 -0
- package/dist/puck/hostContext.js +12 -0
- package/dist/puck/integration.d.ts +6 -0
- package/dist/puck/integration.js +21 -0
- package/dist/puck/routes.d.ts +12 -0
- package/dist/puck/routes.js +11 -0
- package/dist/puck/slotConfig.d.ts +1 -1
- package/dist/puck/slotConfig.js +2 -2
- package/dist/puck/xpGoalField.d.ts +3 -0
- package/dist/puck/xpGoalField.js +6 -0
- package/dist/results/health.d.ts +4 -0
- package/dist/results/health.js +27 -0
- package/dist/results/snapshot.js +15 -2
- package/dist/sanitizeReadinessDetail.js +51 -1
- package/dist/server/metadata.js +9 -2
- package/dist/server/normaliseContentRelationships.d.ts +7 -0
- package/dist/server/normaliseContentRelationships.js +62 -0
- package/dist/server/pageKindSeoContract.d.ts +27 -0
- package/dist/server/pageKindSeoContract.js +80 -0
- package/dist/server/recordContentChange.d.ts +1 -1
- package/dist/server/recordContentChange.js +72 -13
- package/dist/server/resolveExperimentBlocks.d.ts +5 -0
- package/dist/server/resolveExperimentBlocks.js +54 -0
- package/dist/server/runResults.js +92 -19
- package/dist/server/toClientAssignments.js +2 -1
- package/dist/transitions/addMetric.d.ts +2 -1
- package/dist/transitions/addMetric.js +28 -1
- package/dist/transitions/checkStart.d.ts +17 -1
- package/dist/transitions/checkStart.js +99 -7
- package/dist/transitions/decide.js +40 -1
- package/dist/transitions/deleteExperiment.d.ts +3 -1
- package/dist/transitions/deleteExperiment.js +27 -1
- package/dist/transitions/kernel.js +4 -0
- package/dist/transitions/makeContent.d.ts +5 -0
- package/dist/transitions/makeContent.js +39 -11
- package/dist/transitions/pageCollection.d.ts +10 -0
- package/dist/transitions/pageCollection.js +20 -0
- package/dist/transitions/pageVersions.d.ts +10 -0
- package/dist/transitions/pageVersions.js +44 -0
- package/dist/transitions/phaseRecord.d.ts +1 -0
- package/dist/transitions/placementInteractions.d.ts +40 -0
- package/dist/transitions/placementInteractions.js +94 -0
- package/dist/transitions/placements.d.ts +10 -6
- package/dist/transitions/placements.js +156 -2
- package/dist/transitions/preflightData.d.ts +8 -0
- package/dist/transitions/preflightData.js +248 -36
- package/dist/transitions/resolveCaps.d.ts +1 -0
- package/dist/transitions/resolveCaps.js +6 -2
- package/dist/transitions/start.js +77 -3
- package/dist/types/admin.d.ts +1 -1
- package/dist/types/assignments.d.ts +1 -0
- package/dist/types/contracts.d.ts +31 -10
- package/dist/types/endpoints.d.ts +92 -5
- package/dist/types/index.d.ts +3 -0
- package/dist/types/puck.d.ts +7 -1
- package/dist/types/snapshot.d.ts +17 -4
- package/dist/types/surface.d.ts +13 -0
- package/dist/ui/Tabs.d.ts +4 -2
- package/dist/ui/Tabs.js +33 -26
- package/dist/ui/accent.d.ts +22 -3
- package/dist/ui/accent.js +74 -16
- package/package.json +12 -2
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
import { goalContent, OUTSIDE_GOAL } from '../goals/fairness.js';
|
|
2
|
+
import { placementInteractions, placementPaths, uniquePlacementDocs } from './placementInteractions.js';
|
|
1
3
|
import { Forbidden, NotFound } from 'payload';
|
|
4
|
+
import { normalisePath } from '../runtime/normalisePath.js';
|
|
5
|
+
import { resolvePageCollection } from './pageCollection.js';
|
|
6
|
+
import { identicalPageVersions } from './pageVersions.js';
|
|
2
7
|
import { safeError } from '../log.js';
|
|
3
8
|
import { powerMetricWeeks } from '@delmaredigital/experiments-stats';
|
|
4
9
|
import { checkStart, relationshipId } from './checkStart.js';
|
|
5
10
|
import { flagPlacements, scanBlockPlacements } from './placements.js';
|
|
11
|
+
import { routedPageTests } from '../db/pageOccupancy.js';
|
|
12
|
+
import { pooledExecutor, txExecutor } from '../db/executor.js';
|
|
6
13
|
const failureDetail = (cause)=>{
|
|
7
14
|
const name = cause instanceof Error ? cause.name : undefined;
|
|
8
15
|
return cause instanceof Forbidden || name === 'Forbidden' ? 'forbidden' : cause instanceof NotFound || name === 'NotFound' ? 'not-found' : 'placements-failed';
|
|
@@ -28,6 +35,35 @@ const failureDetail = (cause)=>{
|
|
|
28
35
|
experiment
|
|
29
36
|
])).get(String(experiment.id)) ?? null;
|
|
30
37
|
}
|
|
38
|
+
/** Occupancy from the arms the runtime routes on (frozen rounds), shared by preflight and
|
|
39
|
+
* Start's locked recheck. Raw SQL on the request's transaction when there is one. */ export async function loadPageTestsInUse(req) {
|
|
40
|
+
return routedPageTests(await req.transactionID != null ? txExecutor(req) : pooledExecutor(req.payload));
|
|
41
|
+
}
|
|
42
|
+
/** Page targets no registered collection reports as published. A lookup that throws counts as
|
|
43
|
+
* unpublished (logged) unless propagateErrors is requested by decide. Start calls it again under its page-path locks on a detached request. */ export async function unpublishedPageTargets(req, options, experiment, propagateErrors = false) {
|
|
44
|
+
const resolved = await resolvePageCollection(req, options, experiment, propagateErrors);
|
|
45
|
+
const collections = resolved ? [
|
|
46
|
+
resolved
|
|
47
|
+
] : [];
|
|
48
|
+
const targets = await Promise.all(experiment.arms.filter((arm)=>arm.kind !== 'holdout' && arm.pageTarget?.trim()).map(async (arm)=>{
|
|
49
|
+
for (const collection of collections){
|
|
50
|
+
try {
|
|
51
|
+
const page = await collection.findByPath(arm.pageTarget, req);
|
|
52
|
+
if (page?.published === true) return null;
|
|
53
|
+
} catch (cause) {
|
|
54
|
+
if (propagateErrors) throw cause;
|
|
55
|
+
req.payload.logger.error({
|
|
56
|
+
err: safeError(cause),
|
|
57
|
+
experimentId: experiment.id,
|
|
58
|
+
collection: collection.slug,
|
|
59
|
+
id: arm.pageTarget
|
|
60
|
+
}, 'Experiment page lookup failed');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return arm.pageTarget;
|
|
64
|
+
}));
|
|
65
|
+
return targets.filter((target)=>target !== null);
|
|
66
|
+
}
|
|
31
67
|
const metricIds = (experiment)=>[
|
|
32
68
|
experiment.metrics?.primary,
|
|
33
69
|
...experiment.metrics?.secondary ?? [],
|
|
@@ -58,6 +94,7 @@ const metricIds = (experiment)=>[
|
|
|
58
94
|
})();
|
|
59
95
|
const blockSlugs = new Set(experiments.filter((e)=>e.kind === 'block').map((e)=>e.slug));
|
|
60
96
|
// Lazy promises prevent unhandled rejections and share failed reads as well as successful ones.
|
|
97
|
+
let pageTestsInUse;
|
|
61
98
|
let blocks;
|
|
62
99
|
let state;
|
|
63
100
|
const busyPoints = new Map();
|
|
@@ -84,33 +121,22 @@ const metricIds = (experiment)=>[
|
|
|
84
121
|
return failureDetail(cause);
|
|
85
122
|
};
|
|
86
123
|
if (experiment.kind === 'page') {
|
|
124
|
+
pageTestsInUse ??= loadPageTestsInUse(req);
|
|
125
|
+
input.pageTestsInUse = await pageTestsInUse;
|
|
87
126
|
input.pageTargetsUnchecked = options.pages === undefined;
|
|
127
|
+
input.unknownPageCollection = Boolean(experiment.pageCollection && !options.pages?.collections.some((entry)=>entry.slug === experiment.pageCollection));
|
|
88
128
|
if (options.pages) {
|
|
89
|
-
input.unpublishedPageTargets =
|
|
90
|
-
|
|
91
|
-
let published = false;
|
|
92
|
-
for (const collection of options.pages.collections){
|
|
93
|
-
try {
|
|
94
|
-
const page = await collection.findByPath(arm.pageTarget, req);
|
|
95
|
-
if (page?.published === true) published = true;
|
|
96
|
-
} catch (cause) {
|
|
97
|
-
req.payload.logger.error({
|
|
98
|
-
err: safeError(cause),
|
|
99
|
-
experimentId: experiment.id,
|
|
100
|
-
collection: collection.slug,
|
|
101
|
-
id: arm.pageTarget
|
|
102
|
-
}, 'Experiment page lookup failed');
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
if (!published) input.unpublishedPageTargets.push(arm.pageTarget);
|
|
106
|
-
}
|
|
129
|
+
input.unpublishedPageTargets = await unpublishedPageTargets(req, options, experiment);
|
|
130
|
+
if (options.pages.collections.some((entry)=>entry.contentOf)) input.identicalVersions = await identicalPageVersions(req, await resolvePageCollection(req, options, experiment), experiment.arms);
|
|
107
131
|
}
|
|
108
132
|
}
|
|
109
133
|
if (experiment.kind === 'block') {
|
|
110
134
|
try {
|
|
111
135
|
blocks ??= scanBlockPlacements(req, {
|
|
112
136
|
slugs: blockSlugs,
|
|
113
|
-
|
|
137
|
+
compareVersions: true,
|
|
138
|
+
puckCollections: options.puck?.collections ?? [],
|
|
139
|
+
blocksCollections: options.blocks?.collections ?? []
|
|
114
140
|
});
|
|
115
141
|
input.placements = (await blocks).get(experiment.slug) ?? [];
|
|
116
142
|
} catch (cause) {
|
|
@@ -240,6 +266,13 @@ const metricIds = (experiment)=>[
|
|
|
240
266
|
})());
|
|
241
267
|
input.layer = await layers.get(key);
|
|
242
268
|
}
|
|
269
|
+
if ((experiment.kind === 'page' || experiment.kind === 'block') && options.pages?.collections.length) {
|
|
270
|
+
const paths = experiment.kind === 'page' ? experiment.arms.flatMap((a)=>typeof a.pageTarget === 'string' ? [
|
|
271
|
+
a.pageTarget
|
|
272
|
+
] : []) : await placementPaths(req, options, uniquePlacementDocs(input.placements ?? []));
|
|
273
|
+
input.interactions = await placementInteractions(req, await req.transactionID != null ? txExecutor(req) : pooledExecutor(req.payload), options, experiment, paths.map(normalisePath));
|
|
274
|
+
}
|
|
275
|
+
if (input.metrics.some((metric)=>metric.source.startsWith('click:'))) input.goalContent = await goalContent(req, options, experiment, input.placements ? uniquePlacementDocs(input.placements.filter((placement)=>placement.published)) : undefined);
|
|
243
276
|
inputs.set(String(experiment.id), input);
|
|
244
277
|
} catch (cause) {
|
|
245
278
|
if (!tolerateErrors) throw cause;
|
|
@@ -248,26 +281,72 @@ const metricIds = (experiment)=>[
|
|
|
248
281
|
}
|
|
249
282
|
return inputs;
|
|
250
283
|
}
|
|
251
|
-
|
|
284
|
+
// Bound each optional host preview lookup so it cannot indefinitely hold preflight open.
|
|
285
|
+
const PREVIEW_URL_TIMEOUT_MS = 2000;
|
|
286
|
+
// Resolve host preview lookups in a bounded pool instead of launching one per slot.
|
|
287
|
+
const PREVIEW_URL_CONCURRENCY = 8;
|
|
288
|
+
async function placementUrls(options, placement, pendingPreviews, onFailure, previews) {
|
|
252
289
|
const result = {};
|
|
253
290
|
const { collection, id, published } = placement;
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
291
|
+
// One host lookup per page version: several slots on a page share it.
|
|
292
|
+
const key = JSON.stringify([
|
|
293
|
+
collection,
|
|
294
|
+
String(id),
|
|
295
|
+
published
|
|
296
|
+
]);
|
|
297
|
+
let preview = previews.get(key);
|
|
298
|
+
if (!preview) previews.set(key, preview = previewLookup(options, placement, pendingPreviews, onFailure));
|
|
299
|
+
const url = await preview;
|
|
300
|
+
if (url !== undefined) result.url = url;
|
|
261
301
|
try {
|
|
262
302
|
const url = options.admin?.editorUrl?.({
|
|
263
303
|
collection,
|
|
264
304
|
id,
|
|
265
|
-
draft: !published
|
|
305
|
+
draft: !published,
|
|
306
|
+
editor: placement.editor ?? 'puck',
|
|
307
|
+
doc: placement.doc
|
|
266
308
|
});
|
|
267
309
|
if (typeof url === 'string') result.editorUrl = url;
|
|
268
310
|
} catch {}
|
|
269
311
|
return result;
|
|
270
312
|
}
|
|
313
|
+
async function previewLookup(options, placement, pendingPreviews, onFailure) {
|
|
314
|
+
const { collection, id, title, published, slug } = placement;
|
|
315
|
+
const previewUrl = options.admin?.previewUrl;
|
|
316
|
+
let found;
|
|
317
|
+
if (previewUrl) {
|
|
318
|
+
let timer;
|
|
319
|
+
try {
|
|
320
|
+
if (pendingPreviews.count >= PREVIEW_URL_CONCURRENCY) throw new Error('Preview URL lookups at capacity');
|
|
321
|
+
pendingPreviews.count++;
|
|
322
|
+
// A timeout cannot cancel host work: retain its permit until that work settles.
|
|
323
|
+
const lookup = Promise.resolve().then(()=>previewUrl({
|
|
324
|
+
collection,
|
|
325
|
+
id,
|
|
326
|
+
title,
|
|
327
|
+
published,
|
|
328
|
+
...slug !== undefined ? {
|
|
329
|
+
slug
|
|
330
|
+
} : {}
|
|
331
|
+
})).finally(()=>{
|
|
332
|
+
pendingPreviews.count--;
|
|
333
|
+
});
|
|
334
|
+
const url = await Promise.race([
|
|
335
|
+
lookup,
|
|
336
|
+
new Promise((_, reject)=>{
|
|
337
|
+
timer = setTimeout(()=>reject(new Error('Preview URL lookup timed out')), PREVIEW_URL_TIMEOUT_MS);
|
|
338
|
+
})
|
|
339
|
+
]);
|
|
340
|
+
if (typeof url === 'string') found = url;
|
|
341
|
+
else if (url !== null && url !== undefined) throw new TypeError('Invalid preview URL result');
|
|
342
|
+
} catch (cause) {
|
|
343
|
+
onFailure(cause, placement);
|
|
344
|
+
} finally{
|
|
345
|
+
clearTimeout(timer);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return found;
|
|
349
|
+
}
|
|
271
350
|
async function primaryHistory(source, now) {
|
|
272
351
|
if (!source) return null;
|
|
273
352
|
const from = new Date(now.getTime() - 28 * 86400000);
|
|
@@ -378,15 +457,41 @@ export async function preflightData(req, { experimentId, options }, { now = new
|
|
|
378
457
|
}
|
|
379
458
|
}
|
|
380
459
|
const requiredSlots = e.arms.filter((arm)=>arm.kind !== 'holdout').map((arm)=>arm.slot);
|
|
381
|
-
const
|
|
460
|
+
const blocks = input.placements ?? [];
|
|
461
|
+
const placements = new Array(blocks.length);
|
|
462
|
+
let nextPlacement = 0;
|
|
463
|
+
const pendingPreviews = {
|
|
464
|
+
count: 0
|
|
465
|
+
};
|
|
466
|
+
const previews = new Map();
|
|
467
|
+
let loggedPreviewFailure = false;
|
|
468
|
+
const onPreviewFailure = (cause, placement)=>{
|
|
469
|
+
if (loggedPreviewFailure) return;
|
|
470
|
+
loggedPreviewFailure = true;
|
|
471
|
+
req.payload.logger.warn({
|
|
472
|
+
err: safeError(cause),
|
|
473
|
+
experimentId: e.id,
|
|
382
474
|
collection: placement.collection,
|
|
383
|
-
id: placement.id
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
475
|
+
id: placement.id
|
|
476
|
+
}, 'Experiment preview URL lookup failed');
|
|
477
|
+
};
|
|
478
|
+
await Promise.all(Array.from({
|
|
479
|
+
length: Math.min(PREVIEW_URL_CONCURRENCY, blocks.length)
|
|
480
|
+
}, async ()=>{
|
|
481
|
+
while(nextPlacement < blocks.length){
|
|
482
|
+
const index = nextPlacement++;
|
|
483
|
+
const placement = blocks[index];
|
|
484
|
+
placements[index] = {
|
|
485
|
+
collection: placement.collection,
|
|
486
|
+
id: placement.id,
|
|
487
|
+
title: placement.title,
|
|
488
|
+
slots: placement.slots,
|
|
489
|
+
...await placementUrls(options, placement, pendingPreviews, onPreviewFailure, previews),
|
|
490
|
+
ok: requiredSlots.every((slot)=>slot && placement.slots[slot] > 0),
|
|
491
|
+
detail: placement.published ? 'Published' : 'Draft'
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
}));
|
|
390
495
|
for (const placement of input.flagPlacements ?? [])placements.push({
|
|
391
496
|
collection: e.flagPoint ?? '',
|
|
392
497
|
id: placement.arm,
|
|
@@ -396,9 +501,116 @@ export async function preflightData(req, { experimentId, options }, { now = new
|
|
|
396
501
|
detail: placement.detail
|
|
397
502
|
} : {}
|
|
398
503
|
});
|
|
504
|
+
const pagePlacements = [];
|
|
505
|
+
if (e.kind === 'page') {
|
|
506
|
+
const collection = await resolvePageCollection(req, options, e);
|
|
507
|
+
for (const arm of e.arms.filter((arm)=>arm.kind !== 'holdout')){
|
|
508
|
+
const path = arm.pageTarget ?? '';
|
|
509
|
+
let page = null;
|
|
510
|
+
try {
|
|
511
|
+
page = path && collection ? await collection.findByPath(path, req) : null;
|
|
512
|
+
} catch (cause) {
|
|
513
|
+
onPreviewFailure(cause, {
|
|
514
|
+
collection: collection.slug,
|
|
515
|
+
id: path,
|
|
516
|
+
title: path,
|
|
517
|
+
published: false,
|
|
518
|
+
slots: {
|
|
519
|
+
control: 0,
|
|
520
|
+
variantA: 0,
|
|
521
|
+
variantB: 0,
|
|
522
|
+
variantC: 0
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
if (!page || !collection) {
|
|
527
|
+
pagePlacements.push({
|
|
528
|
+
arm: arm.key,
|
|
529
|
+
path,
|
|
530
|
+
published: false,
|
|
531
|
+
id: null,
|
|
532
|
+
...collection ? {
|
|
533
|
+
collection: collection.slug
|
|
534
|
+
} : {}
|
|
535
|
+
});
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
let doc;
|
|
539
|
+
try {
|
|
540
|
+
doc = await req.payload.findByID({
|
|
541
|
+
collection: collection.slug,
|
|
542
|
+
id: page.id,
|
|
543
|
+
req,
|
|
544
|
+
overrideAccess: false,
|
|
545
|
+
depth: 0,
|
|
546
|
+
draft: !page.published
|
|
547
|
+
});
|
|
548
|
+
} catch (cause) {
|
|
549
|
+
onPreviewFailure(cause, {
|
|
550
|
+
collection: collection.slug,
|
|
551
|
+
id: page.id,
|
|
552
|
+
title: path,
|
|
553
|
+
published: false,
|
|
554
|
+
slots: {
|
|
555
|
+
control: 0,
|
|
556
|
+
variantA: 0,
|
|
557
|
+
variantB: 0,
|
|
558
|
+
variantC: 0
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
pagePlacements.push({
|
|
562
|
+
arm: arm.key,
|
|
563
|
+
path,
|
|
564
|
+
collection: collection.slug,
|
|
565
|
+
published: false,
|
|
566
|
+
id: null
|
|
567
|
+
});
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
const titleField = req.payload.config.collections?.find((entry)=>entry.slug === collection.slug)?.admin.useAsTitle ?? 'title';
|
|
571
|
+
const placement = {
|
|
572
|
+
collection: collection.slug,
|
|
573
|
+
id: page.id,
|
|
574
|
+
title: String(doc[titleField] ?? doc.id),
|
|
575
|
+
published: page.published,
|
|
576
|
+
...page.published && typeof doc.slug === 'string' ? {
|
|
577
|
+
slug: doc.slug
|
|
578
|
+
} : {},
|
|
579
|
+
slots: {
|
|
580
|
+
control: 0,
|
|
581
|
+
variantA: 0,
|
|
582
|
+
variantB: 0,
|
|
583
|
+
variantC: 0
|
|
584
|
+
},
|
|
585
|
+
editor: 'page',
|
|
586
|
+
doc
|
|
587
|
+
};
|
|
588
|
+
pagePlacements.push({
|
|
589
|
+
arm: arm.key,
|
|
590
|
+
path,
|
|
591
|
+
collection: collection.slug,
|
|
592
|
+
id: page.id,
|
|
593
|
+
title: placement.title,
|
|
594
|
+
published: placement.published,
|
|
595
|
+
...placement.slug ? {
|
|
596
|
+
slug: placement.slug
|
|
597
|
+
} : {},
|
|
598
|
+
...await placementUrls(options, placement, pendingPreviews, onPreviewFailure, previews)
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
}
|
|
399
602
|
return {
|
|
400
603
|
checks: checkStart(input),
|
|
604
|
+
...input.goalContent ? {
|
|
605
|
+
goalNotes: input.metrics.filter((m)=>m.source.startsWith('click:') && input.goalContent && !input.goalContent.skipped && !input.goalContent.unchecked.length && !input.goalContent.keys.some((k)=>`click:${k.key}` === m.source)).map((m)=>({
|
|
606
|
+
metricId: m.id,
|
|
607
|
+
message: OUTSIDE_GOAL
|
|
608
|
+
}))
|
|
609
|
+
} : {},
|
|
401
610
|
placements,
|
|
611
|
+
...e.kind === 'page' ? {
|
|
612
|
+
pagePlacements
|
|
613
|
+
} : {},
|
|
402
614
|
baseline,
|
|
403
615
|
trafficPerDay,
|
|
404
616
|
estimate: power
|
|
@@ -2,6 +2,7 @@ import type { DocId, MetricSource } from '../types/contracts.js';
|
|
|
2
2
|
export interface MetricDefinition extends Record<string, unknown> {
|
|
3
3
|
id: DocId;
|
|
4
4
|
source: string;
|
|
5
|
+
direction?: 'increase' | 'decrease';
|
|
5
6
|
attributionWindowHours?: number | null;
|
|
6
7
|
minSampleSize?: number | null;
|
|
7
8
|
guardrailThreshold?: number | null;
|
|
@@ -8,7 +8,7 @@ import { perUnitHistory } from '../results/history.js';
|
|
|
8
8
|
const from = new Date(start.getTime() - 90 * 86400000);
|
|
9
9
|
for (const metric of metrics){
|
|
10
10
|
const source = sources.find((source)=>source.id === metric.source);
|
|
11
|
-
const kind = metric.source.startsWith('events:') ? 'binomial' : source?.kind;
|
|
11
|
+
const kind = metric.source.startsWith('events:') || metric.source.startsWith('click:') ? 'binomial' : source?.kind;
|
|
12
12
|
const definition = {
|
|
13
13
|
...structuredClone(metric),
|
|
14
14
|
...source ? {
|
|
@@ -19,11 +19,15 @@ import { perUnitHistory } from '../results/history.js';
|
|
|
19
19
|
historyUnits: undefined,
|
|
20
20
|
capNotice: undefined
|
|
21
21
|
};
|
|
22
|
+
if (metric.source.startsWith('click:')) {
|
|
23
|
+
definition.direction = 'increase';
|
|
24
|
+
definition.capping = null;
|
|
25
|
+
}
|
|
22
26
|
const cap = metric.capping?.type ? metric.capping : kind === 'revenue' ? {
|
|
23
27
|
type: 'percentile',
|
|
24
28
|
value: 0.99
|
|
25
29
|
} : null;
|
|
26
|
-
if (kind === 'revenue' || !source && cap && !metric.source.startsWith('events:')) {
|
|
30
|
+
if (kind === 'revenue' || !source && cap && !(metric.source.startsWith('events:') || metric.source.startsWith('click:'))) {
|
|
27
31
|
definition.capping = cap;
|
|
28
32
|
if (cap?.type === 'absolute' && typeof cap.value === 'number' && Number.isFinite(cap.value) && cap.value >= 0) {
|
|
29
33
|
definition.resolvedCap = cap.value;
|
|
@@ -1,14 +1,19 @@
|
|
|
1
|
+
import { goalContent } from '../goals/fairness.js';
|
|
2
|
+
import { detached } from './placementInteractions.js';
|
|
3
|
+
import { lockAndRescanPlacements, placementInteractions } from './placementInteractions.js';
|
|
4
|
+
import { lockPagePaths } from '../db/locks.js';
|
|
1
5
|
import { lockMetricsForFreeze } from '../db/metricUsage.js';
|
|
2
6
|
import { DEFAULT_SEQUENTIAL_TUNING_PARAMETER } from '../constants.js';
|
|
3
7
|
import { claimSlices } from '../db/slices.js';
|
|
4
|
-
import { unpublishedPageCopy } from '../admin/copy/readiness.js';
|
|
8
|
+
import { asymmetricGoalsCopy, sameAddressCopy, interactingTestsCopy, pageInUseCopy, unpublishedPageCopy } from '../admin/copy/readiness.js';
|
|
9
|
+
import { GROUP_LABELS } from '../admin/copy/arms.js';
|
|
5
10
|
import { XpRefusal } from '../errors.js';
|
|
6
11
|
import { isOpValid } from '../targeting/ops.js';
|
|
7
12
|
import { newSeed } from '../runtime/seed.js';
|
|
8
|
-
import { checkStart, relationshipId } from './checkStart.js';
|
|
13
|
+
import { checkStart, pageTargetPaths, relationshipId } from './checkStart.js';
|
|
9
14
|
import { withTransition } from './kernel.js';
|
|
10
15
|
import { writePhaseRecord } from './phaseRecord.js';
|
|
11
|
-
import { loadStartInputs } from './preflightData.js';
|
|
16
|
+
import { loadPageTestsInUse, loadStartInputs, unpublishedPageTargets } from './preflightData.js';
|
|
12
17
|
import { resolveCaps } from './resolveCaps.js';
|
|
13
18
|
export async function readExperiment(req, id) {
|
|
14
19
|
// §7.8: internal lookup serves the authorised transition write. Preserve
|
|
@@ -30,6 +35,19 @@ export async function readExperiment(req, id) {
|
|
|
30
35
|
}
|
|
31
36
|
export function refuse(code, detail) {
|
|
32
37
|
const messages = {
|
|
38
|
+
'goal-in-some-versions': asymmetricGoalsCopy(detail),
|
|
39
|
+
'placements-changed': "This test's pages changed while it was starting. Try again.",
|
|
40
|
+
'page-busy': "One of this test's pages is being saved right now. Try again in a moment.",
|
|
41
|
+
'interacting-tests': (()=>{
|
|
42
|
+
const d = detail?.[0];
|
|
43
|
+
return d ? interactingTestsCopy(d.path, d.status, d.name, d.version) : 'Finish one of the tests first.';
|
|
44
|
+
})(),
|
|
45
|
+
'unknown-collection': "That page type isn't set up for tests.",
|
|
46
|
+
'page-in-use': pageInUseCopy(detail?.paths?.[0] ?? 'this page'),
|
|
47
|
+
'version-same-address': (()=>{
|
|
48
|
+
const d = detail;
|
|
49
|
+
return sameAddressCopy(GROUP_LABELS[d?.arm ?? 'variantA'], GROUP_LABELS[d?.otherArm ?? 'control'], d?.path ?? 'this address');
|
|
50
|
+
})(),
|
|
33
51
|
'page-target-unpublished': unpublishedPageCopy(typeof detail === 'string' ? detail : 'This page'),
|
|
34
52
|
'invalid-status': 'This action is not available in the current status.',
|
|
35
53
|
'stale-draft': 'The experiment changed. Review it again before starting.',
|
|
@@ -44,8 +62,12 @@ export function refuse(code, detail) {
|
|
|
44
62
|
'entry-not-dismissed': 'That page is not dismissed.'
|
|
45
63
|
};
|
|
46
64
|
const conflict = [
|
|
65
|
+
'page-busy',
|
|
66
|
+
'placements-changed',
|
|
67
|
+
'interacting-tests',
|
|
47
68
|
'invalid-status',
|
|
48
69
|
'stale-draft',
|
|
70
|
+
'page-in-use',
|
|
49
71
|
'flag-point-busy',
|
|
50
72
|
'layer-full',
|
|
51
73
|
'allocation-lower',
|
|
@@ -107,6 +129,46 @@ export async function start(req, { experimentId, options }) {
|
|
|
107
129
|
const current = await readExperiment(req, experimentId);
|
|
108
130
|
if (current.updatedAt !== updatedAt) refuse('stale-draft');
|
|
109
131
|
if (current.status !== 'draft' && current.status !== 'stopped') refuse('invalid-status');
|
|
132
|
+
let placementDocs;
|
|
133
|
+
if (current.kind === 'block') {
|
|
134
|
+
const rescan = await lockAndRescanPlacements(req, exec, options, current, input.placements ?? []);
|
|
135
|
+
placementDocs = rescan.docs;
|
|
136
|
+
const failure = checkStart({
|
|
137
|
+
...input,
|
|
138
|
+
experiment: current,
|
|
139
|
+
placements: rescan.placements
|
|
140
|
+
}).find((check)=>!check.ok);
|
|
141
|
+
if (failure && failure.code !== 'page-targets-unchecked' && failure.code !== 'make-content-unsupported') refuse(failure.code, failure.detail);
|
|
142
|
+
}
|
|
143
|
+
if (current.kind === 'page') {
|
|
144
|
+
// Independent experiment/layer locks do not serialize overlapping pages.
|
|
145
|
+
// Re-read occupancy after acquiring sorted path locks; hold through commit.
|
|
146
|
+
// A waiting Start then sees the first Start's live test on this transaction.
|
|
147
|
+
await lockPagePaths(exec, pageTargetPaths(current.arms));
|
|
148
|
+
const recheck = checkStart({
|
|
149
|
+
...input,
|
|
150
|
+
experiment: current,
|
|
151
|
+
pageTestsInUse: await loadPageTestsInUse(req)
|
|
152
|
+
}).find((check)=>check.code === 'page-in-use' && !check.ok);
|
|
153
|
+
if (recheck) refuse('page-in-use', recheck.detail);
|
|
154
|
+
const conflicts = await placementInteractions(req, exec, options, current, pageTargetPaths(current.arms));
|
|
155
|
+
if (conflicts.length) refuse('interacting-tests', conflicts);
|
|
156
|
+
if (options.pages) {
|
|
157
|
+
// The page guard takes the same path locks, so a delete, unpublish or rename that committed
|
|
158
|
+
// while Start waited is visible now. Read it on a detached request (committed state, outside
|
|
159
|
+
// this transaction): a refused host read must not kill the transition (CORE-21).
|
|
160
|
+
// (the make-content pattern: drop the transaction id for the reads, restore it after).
|
|
161
|
+
const transactionID = req.transactionID;
|
|
162
|
+
delete req.transactionID;
|
|
163
|
+
let unpublished;
|
|
164
|
+
try {
|
|
165
|
+
unpublished = await unpublishedPageTargets(req, options, current);
|
|
166
|
+
} finally{
|
|
167
|
+
req.transactionID = transactionID;
|
|
168
|
+
}
|
|
169
|
+
if (unpublished.length) refuse('page-target-unpublished', unpublished[0]);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
110
172
|
if (current.kind === 'flag' && current.flagPoint) {
|
|
111
173
|
const busy = await req.payload.find({
|
|
112
174
|
collection: 'xp-experiments',
|
|
@@ -151,6 +213,15 @@ export async function start(req, { experimentId, options }) {
|
|
|
151
213
|
}).find((check)=>!check.ok);
|
|
152
214
|
if (recheck && recheck.code !== 'page-targets-unchecked' && recheck.code !== 'make-content-unsupported') refuse(recheck.code, recheck.detail);
|
|
153
215
|
}
|
|
216
|
+
if (input.metrics.some((metric)=>metric.source.startsWith('click:'))) {
|
|
217
|
+
const content = await detached(req, ()=>goalContent(req, options, current, placementDocs));
|
|
218
|
+
const failure = checkStart({
|
|
219
|
+
...input,
|
|
220
|
+
experiment: current,
|
|
221
|
+
goalContent: content
|
|
222
|
+
}).find((check)=>check.code === 'goal-in-some-versions' && !check.ok);
|
|
223
|
+
if (failure) refuse('goal-in-some-versions', failure.detail);
|
|
224
|
+
}
|
|
154
225
|
const lockedMetrics = await lockMetricsForFreeze(exec, caps.definitions.map((metric)=>metric.id));
|
|
155
226
|
if (!lockedMetrics) {
|
|
156
227
|
throw new XpRefusal('stale-draft', 'A goal in this test was deleted. Review the goals before starting.', undefined, 409);
|
|
@@ -203,6 +274,9 @@ export async function start(req, { experimentId, options }) {
|
|
|
203
274
|
allocationChanges: [],
|
|
204
275
|
targeting,
|
|
205
276
|
metricDefinitions: definitions,
|
|
277
|
+
...placementDocs ? {
|
|
278
|
+
placementDocs
|
|
279
|
+
} : {},
|
|
206
280
|
analysis: {
|
|
207
281
|
...current.analysis,
|
|
208
282
|
method: current.analysis?.method ?? 'sequential',
|
package/dist/types/admin.d.ts
CHANGED
|
@@ -18,4 +18,4 @@ export type SerializedAttributeKeys = Record<string, {
|
|
|
18
18
|
type: 'string' | 'number' | 'boolean';
|
|
19
19
|
values?: string[];
|
|
20
20
|
}>;
|
|
21
|
-
export type EditableField = 'name' | 'hypothesis' | 'arms' | 'targeting' | 'allocation' | 'flagPoint' | 'metrics' | 'analysis' | 'exposurePolicy';
|
|
21
|
+
export type EditableField = 'name' | 'hypothesis' | 'arms' | 'targeting' | 'allocation' | 'flagPoint' | 'metrics' | 'analysis' | 'exposurePolicy' | 'pageCollection';
|
|
@@ -3,6 +3,14 @@ import type { ExperimentsServerRuntime } from './runtime.js';
|
|
|
3
3
|
import type { ArmSlot } from './puck.js';
|
|
4
4
|
import type { TargetingOp } from '../targeting/ops.js';
|
|
5
5
|
export type DocId = string | number;
|
|
6
|
+
export interface PreviewPlacement {
|
|
7
|
+
collection: string;
|
|
8
|
+
id: DocId;
|
|
9
|
+
title: string;
|
|
10
|
+
published: boolean;
|
|
11
|
+
/** The published main row's string slug, including on draft placements; absent for unpublished documents, never a draft/version row's slug. */
|
|
12
|
+
slug?: string;
|
|
13
|
+
}
|
|
6
14
|
export type Attributes = Record<string, string | number | boolean>;
|
|
7
15
|
export type ExposurePolicy = 'render' | 'viewport' | 'pageview' | 'manual';
|
|
8
16
|
export type ExperimentKind = 'block' | 'page' | 'flag' | 'code';
|
|
@@ -93,6 +101,19 @@ export type RateLimiter = (args: {
|
|
|
93
101
|
export type ExperimentsAccess = (args: {
|
|
94
102
|
req: PayloadRequest;
|
|
95
103
|
}) => boolean | Promise<boolean>;
|
|
104
|
+
export interface ExperimentsPageCollection {
|
|
105
|
+
slug: string;
|
|
106
|
+
contentOf?: (doc: Record<string, unknown>) => unknown;
|
|
107
|
+
copyHint?: string;
|
|
108
|
+
copyUrl?: string;
|
|
109
|
+
copyLabel?: string;
|
|
110
|
+
pathOf: (doc: Record<string, unknown>) => string | null;
|
|
111
|
+
/** Lookup with the supplied req and overrideAccess: false. Under an anonymous req (results checks) only published pages are visible, so a hit is { published: true } or null. Under a req that can read drafts (Start), a draft-only page may be reported as { published: false }. */
|
|
112
|
+
findByPath: (path: string, req: PayloadRequest) => Promise<{
|
|
113
|
+
id: string | number;
|
|
114
|
+
published: boolean;
|
|
115
|
+
} | null>;
|
|
116
|
+
}
|
|
96
117
|
export interface ExperimentsPluginOptions<TUser extends {
|
|
97
118
|
id: string | number;
|
|
98
119
|
} = {
|
|
@@ -113,12 +134,12 @@ export interface ExperimentsPluginOptions<TUser extends {
|
|
|
113
134
|
collection: string;
|
|
114
135
|
id: DocId;
|
|
115
136
|
draft?: boolean;
|
|
137
|
+
editor: 'puck' | 'blocks' | 'page';
|
|
138
|
+
doc?: Record<string, unknown>;
|
|
116
139
|
}) => string;
|
|
117
|
-
previewUrl?: (
|
|
118
|
-
collection: string;
|
|
119
|
-
id: DocId;
|
|
120
|
-
}) => string;
|
|
140
|
+
previewUrl?: (placement: PreviewPlacement) => string | null | undefined | Promise<string | null | undefined>;
|
|
121
141
|
descriptions?: {
|
|
142
|
+
page?: string;
|
|
122
143
|
block?: string;
|
|
123
144
|
document?: string;
|
|
124
145
|
setting?: string;
|
|
@@ -135,14 +156,14 @@ export interface ExperimentsPluginOptions<TUser extends {
|
|
|
135
156
|
values?: string[];
|
|
136
157
|
}>;
|
|
137
158
|
pages?: {
|
|
159
|
+
collections: ExperimentsPageCollection[];
|
|
160
|
+
};
|
|
161
|
+
blocks?: {
|
|
138
162
|
collections: Array<{
|
|
139
163
|
slug: string;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
id: string | number;
|
|
144
|
-
published: boolean;
|
|
145
|
-
} | null>;
|
|
164
|
+
field: string;
|
|
165
|
+
blockSlug?: string;
|
|
166
|
+
appliesTo?: (doc: Record<string, unknown>) => boolean;
|
|
146
167
|
}>;
|
|
147
168
|
};
|
|
148
169
|
puck?: {
|