@delmaredigital/payload-experiments 0.2.0-rc.7 → 0.3.0-rc.4
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 +202 -26
- package/dist/admin/PreviewLinks.d.ts +2 -1
- package/dist/admin/PreviewLinks.js +50 -21
- package/dist/admin/TestsNavLink.d.ts +3 -1
- package/dist/admin/TestsNavLink.js +2 -2
- package/dist/admin/actions/ConfirmDialog.d.ts +7 -4
- package/dist/admin/actions/ConfirmDialog.js +37 -82
- package/dist/admin/actions/DecideDialog.d.ts +3 -1
- package/dist/admin/actions/DecideDialog.js +30 -13
- package/dist/admin/actions/MakeContentResult.d.ts +13 -2
- package/dist/admin/actions/MakeContentResult.js +115 -16
- package/dist/admin/actions/actions.model.d.ts +2 -4
- package/dist/admin/actions/actions.model.js +2 -3
- package/dist/admin/allocation.model.js +3 -5
- package/dist/admin/api.d.ts +10 -4
- package/dist/admin/api.js +11 -7
- 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/copy/actions.d.ts +39 -3
- package/dist/admin/copy/actions.js +40 -4
- package/dist/admin/copy/armSelect.js +5 -5
- package/dist/admin/copy/arms.d.ts +1 -6
- package/dist/admin/copy/arms.js +1 -16
- package/dist/admin/copy/audience.d.ts +2 -1
- package/dist/admin/copy/audience.js +3 -2
- package/dist/admin/copy/common.d.ts +13 -6
- package/dist/admin/copy/common.js +14 -8
- package/dist/admin/copy/goals.d.ts +21 -6
- package/dist/admin/copy/goals.js +27 -6
- package/dist/admin/copy/readiness.d.ts +22 -0
- package/dist/admin/copy/readiness.js +21 -0
- package/dist/admin/copy/recommendation.d.ts +0 -3
- package/dist/admin/copy/recommendation.js +0 -3
- package/dist/admin/copy/results.d.ts +43 -2
- package/dist/admin/copy/results.js +44 -3
- package/dist/admin/copy/review.d.ts +2 -2
- package/dist/admin/copy/review.js +2 -2
- package/dist/admin/copy/setting.d.ts +2 -0
- package/dist/admin/copy/setting.js +2 -0
- package/dist/admin/copy/surface.d.ts +7 -5
- package/dist/admin/copy/surface.js +9 -5
- package/dist/admin/copy/testPage.d.ts +10 -0
- package/dist/admin/copy/testPage.js +10 -0
- package/dist/admin/format/date.d.ts +2 -0
- package/dist/admin/format/date.js +10 -0
- package/dist/admin/paths.d.ts +2 -1
- package/dist/admin/paths.js +2 -1
- package/dist/admin/previewLinks.model.d.ts +4 -2
- package/dist/admin/previewLinks.model.js +8 -7
- package/dist/admin/results/CiChart.js +11 -21
- package/dist/admin/results/DataQualityPanel.d.ts +4 -2
- package/dist/admin/results/DataQualityPanel.js +30 -17
- package/dist/admin/results/DataTable.d.ts +16 -0
- package/dist/admin/results/DataTable.js +152 -0
- package/dist/admin/results/StatTile.d.ts +7 -0
- package/dist/admin/results/StatTile.js +22 -0
- package/dist/admin/results/VerdictHero.d.ts +13 -0
- package/dist/admin/results/{VerdictCard.js → VerdictHero.js} +15 -12
- package/dist/admin/results/legacy.d.ts +16 -0
- package/dist/admin/results/legacy.js +27 -0
- package/dist/admin/results/releasePages.model.d.ts +17 -0
- package/dist/admin/results/releasePages.model.js +10 -0
- package/dist/admin/results/results.css +64 -0
- package/dist/admin/results/results.model.d.ts +11 -7
- package/dist/admin/results/results.model.js +24 -7
- package/dist/admin/results/snapshot.d.ts +14 -0
- package/dist/admin/results/snapshot.js +3 -0
- package/dist/admin/surface/ResultsRoute.d.ts +1 -1
- package/dist/admin/surface/ResultsRoute.js +59 -34
- package/dist/admin/surface/SurfaceShell.d.ts +11 -2
- package/dist/admin/surface/SurfaceShell.js +49 -14
- package/dist/admin/surface/TestsHome.d.ts +6 -1
- package/dist/admin/surface/TestsHome.js +182 -114
- package/dist/admin/surface/TestsSurface.js +56 -16
- package/dist/admin/surface/goals/GoalForm.js +341 -308
- package/dist/admin/surface/goals/GoalsList.js +123 -89
- package/dist/admin/surface/goals/data.d.ts +8 -2
- package/dist/admin/surface/goals/data.js +24 -16
- package/dist/admin/surface/goals/goals.css +10 -13
- package/dist/admin/surface/home.model.d.ts +8 -8
- package/dist/admin/surface/home.model.js +23 -12
- package/dist/admin/surface/newTest/NewTest.js +9 -9
- package/dist/admin/surface/pageBlock/parts.js +31 -26
- package/dist/admin/surface/readiness/ActionDialogs.js +47 -19
- package/dist/admin/surface/readiness/Banner.d.ts +3 -0
- package/dist/admin/surface/readiness/Banner.js +58 -33
- package/dist/admin/surface/readiness/SaveStatus.d.ts +1 -0
- package/dist/admin/surface/readiness/SaveStatus.js +6 -3
- package/dist/admin/surface/readiness/readiness.model.d.ts +2 -0
- package/dist/admin/surface/readiness/readiness.model.js +25 -9
- package/dist/admin/surface/readiness/useTransition.d.ts +1 -0
- package/dist/admin/surface/readiness/useTransition.js +2 -1
- package/dist/admin/surface/results/ReleasePages.d.ts +14 -0
- package/dist/admin/surface/results/ReleasePages.js +116 -0
- package/dist/admin/surface/results/ResultsBody.d.ts +11 -1
- package/dist/admin/surface/results/ResultsBody.js +277 -105
- package/dist/admin/surface/results/ResultsHeader.d.ts +10 -0
- package/dist/admin/surface/results/ResultsHeader.js +177 -0
- package/dist/admin/surface/results/recommendation.model.d.ts +3 -2
- package/dist/admin/surface/results/recommendation.model.js +12 -9
- package/dist/admin/surface/results/useResults.d.ts +21 -3
- package/dist/admin/surface/results/useResults.js +2 -1
- package/dist/admin/surface/setting/parts.js +75 -88
- package/dist/admin/surface/setting/setting.css +3 -15
- package/dist/admin/surface/surface.css +61 -47
- package/dist/admin/surface/test/AdvancedSection.js +55 -48
- package/dist/admin/surface/test/GoalSection.js +46 -47
- package/dist/admin/surface/test/LengthSection.js +46 -34
- package/dist/admin/surface/test/Rail.d.ts +4 -0
- package/dist/admin/surface/test/Rail.js +83 -0
- package/dist/admin/surface/test/ReadinessBar.d.ts +5 -0
- package/dist/admin/surface/test/ReadinessBar.js +33 -0
- package/dist/admin/surface/test/Section.d.ts +1 -1
- package/dist/admin/surface/test/Section.js +36 -16
- package/dist/admin/surface/test/TargetingSection.js +168 -142
- package/dist/admin/surface/test/TestPage.d.ts +2 -1
- package/dist/admin/surface/test/TestPage.js +146 -68
- package/dist/admin/surface/test/VersionsSection.js +151 -120
- package/dist/admin/surface/test/fields.d.ts +11 -4
- package/dist/admin/surface/test/fields.js +84 -85
- package/dist/admin/surface/test/jumpToSection.d.ts +3 -0
- package/dist/admin/surface/test/jumpToSection.js +10 -0
- package/dist/admin/surface/test/pageParts.js +9 -3
- package/dist/admin/surface/test/testPage.css +81 -14
- 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/useTestDocument.js +12 -5
- package/dist/admin/targeting/targeting.model.d.ts +1 -3
- package/dist/admin/targeting/targeting.model.js +3 -8
- package/dist/admin/theme.css +115 -36
- package/dist/admin/theme.d.ts +38 -0
- package/dist/admin/theme.js +38 -1
- package/dist/admin/useApi.d.ts +17 -2
- package/dist/admin/useLayerUsage.js +14 -23
- package/dist/admin/views/ExperimentsRootView.js +12 -2
- 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/collections/experiments/config.js +25 -0
- package/dist/collections/experiments/lockHook.js +3 -1
- package/dist/collections/identities.js +6 -1
- package/dist/collections/metrics.js +7 -0
- package/dist/collections/metricsGuards.d.ts +9 -0
- package/dist/collections/metricsGuards.js +33 -0
- package/dist/contentDone.d.ts +4 -0
- package/dist/contentDone.js +5 -0
- package/dist/db/appends.d.ts +6 -2
- package/dist/db/appends.js +10 -4
- package/dist/db/identities.d.ts +8 -7
- package/dist/db/identities.js +9 -6
- package/dist/db/locks.d.ts +6 -0
- package/dist/db/locks.js +13 -1
- package/dist/db/metricUsage.d.ts +19 -0
- package/dist/db/metricUsage.js +54 -0
- package/dist/db/names.d.ts +5 -0
- package/dist/db/names.js +5 -0
- package/dist/db/pageOccupancy.d.ts +12 -0
- package/dist/db/pageOccupancy.js +45 -0
- package/dist/db/publishedContent.d.ts +10 -0
- package/dist/db/publishedContent.js +43 -0
- package/dist/db/results.d.ts +2 -0
- package/dist/db/results.js +6 -2
- package/dist/db/snapshots.d.ts +18 -6
- package/dist/db/snapshots.js +25 -8
- package/dist/db/sweep.js +11 -6
- package/dist/endpoints/index.js +19 -1
- package/dist/endpoints/results.js +5 -0
- package/dist/endpoints/sanitizeReadinessDetail.d.ts +1 -0
- package/dist/endpoints/sanitizeReadinessDetail.js +2 -0
- package/dist/endpoints/summary.js +20 -2
- package/dist/endpoints/transitions.d.ts +15 -0
- package/dist/endpoints/transitions.js +36 -0
- package/dist/errors.d.ts +8 -0
- package/dist/errors.js +28 -0
- package/dist/exports/adminRsc.d.ts +1 -0
- package/dist/exports/adminRsc.js +1 -0
- package/dist/exports/index.d.ts +2 -1
- package/dist/exports/index.js +1 -0
- package/dist/exports/puck.d.ts +2 -1
- package/dist/exports/puck.js +1 -1
- package/dist/exports/puckEditor.d.ts +4 -1
- package/dist/exports/puckEditor.js +3 -1
- package/dist/exports/rsc.d.ts +6 -1
- package/dist/exports/rsc.js +2 -0
- package/dist/hooks/pageGuard.d.ts +5 -0
- package/dist/hooks/pageGuard.js +139 -0
- package/dist/lib/fetchAllDocs.d.ts +16 -0
- package/dist/lib/fetchAllDocs.js +56 -0
- package/dist/log.js +6 -3
- package/dist/manifest/compile.d.ts +5 -0
- package/dist/manifest/compile.js +5 -1
- package/dist/options.js +16 -0
- package/dist/plugin.js +30 -2
- 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 -5
- package/dist/puck/copy.js +6 -5
- 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 +121 -0
- package/dist/puck/editor/editor.css +288 -204
- package/dist/puck/editor/editor.model.d.ts +4 -1
- package/dist/puck/editor/editor.model.js +10 -7
- package/dist/puck/editor/experimentOutlinePlugin.d.ts +3 -0
- package/dist/puck/editor/experimentOutlinePlugin.js +89 -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/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/results/aggregate.js +4 -3
- package/dist/results/analyse.d.ts +4 -1
- package/dist/results/analyse.js +27 -11
- package/dist/results/cuped.js +3 -2
- package/dist/results/identityCredit.d.ts +15 -7
- package/dist/results/identityCredit.js +32 -33
- package/dist/results/rolloutUnits.js +2 -1
- package/dist/results/snapshot.js +8 -2
- package/dist/results/srm.js +2 -1
- package/dist/sanitizeReadinessDetail.d.ts +7 -0
- package/dist/sanitizeReadinessDetail.js +63 -0
- package/dist/server/linkIdentity.d.ts +10 -1
- package/dist/server/linkIdentity.js +17 -15
- package/dist/server/metadata.d.ts +5 -0
- package/dist/server/metadata.js +18 -3
- package/dist/server/normaliseContentRelationships.d.ts +7 -0
- package/dist/server/normaliseContentRelationships.js +62 -0
- package/dist/server/recordContentChange.d.ts +1 -1
- package/dist/server/recordContentChange.js +17 -8
- package/dist/server/runResults.d.ts +3 -2
- package/dist/server/runResults.js +157 -35
- package/dist/targeting/values.d.ts +4 -0
- package/dist/targeting/values.js +10 -0
- package/dist/transitions/addMetric.js +8 -0
- package/dist/transitions/archive.js +2 -3
- package/dist/transitions/checkStart.d.ts +10 -3
- package/dist/transitions/checkStart.js +25 -0
- package/dist/transitions/contentFields.d.ts +7 -0
- package/dist/transitions/contentFields.js +20 -0
- package/dist/transitions/decide.js +22 -2
- package/dist/transitions/dismissContentEntry.d.ts +8 -0
- package/dist/transitions/dismissContentEntry.js +45 -0
- package/dist/transitions/makeContent.d.ts +5 -2
- package/dist/transitions/makeContent.js +217 -118
- package/dist/transitions/phaseRecord.d.ts +8 -2
- package/dist/transitions/placements.d.ts +3 -6
- package/dist/transitions/placements.js +12 -1
- package/dist/transitions/preflightData.d.ts +9 -1
- package/dist/transitions/preflightData.js +138 -20
- package/dist/transitions/releasePages.d.ts +8 -0
- package/dist/transitions/releasePages.js +26 -0
- package/dist/transitions/restoreContentEntry.d.ts +8 -0
- package/dist/transitions/restoreContentEntry.js +47 -0
- package/dist/transitions/start.js +62 -7
- package/dist/types/contracts.d.ts +24 -4
- package/dist/types/endpoints.d.ts +34 -3
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/reasons.d.ts +3 -0
- package/dist/types/reasons.js +1 -0
- package/dist/types/snapshot.d.ts +6 -1
- package/dist/ui/Banner.d.ts +2 -0
- package/dist/ui/Banner.js +2 -0
- package/dist/ui/Breadcrumb.d.ts +7 -0
- package/dist/ui/Breadcrumb.js +25 -0
- package/dist/ui/Button.d.ts +15 -0
- package/dist/ui/Button.js +32 -0
- package/dist/ui/Card.d.ts +12 -0
- package/dist/ui/Card.js +40 -0
- package/dist/ui/Checkbox.d.ts +2 -0
- package/dist/ui/Checkbox.js +2 -0
- package/dist/{admin/components → ui}/ChipsInput.d.ts +4 -3
- package/dist/{admin/components → ui}/ChipsInput.js +20 -12
- package/dist/ui/Dialog.d.ts +13 -0
- package/dist/ui/Dialog.js +85 -0
- package/dist/ui/EmptyState.d.ts +2 -0
- package/dist/ui/EmptyState.js +2 -0
- package/dist/ui/Feedback.d.ts +24 -0
- package/dist/ui/Feedback.js +40 -0
- package/dist/ui/Inputs.d.ts +34 -0
- package/dist/ui/Inputs.js +225 -0
- package/dist/ui/NumberInput.d.ts +2 -0
- package/dist/ui/NumberInput.js +2 -0
- package/dist/ui/Range.d.ts +2 -0
- package/dist/ui/Range.js +2 -0
- package/dist/ui/SearchInput.d.ts +2 -0
- package/dist/ui/SearchInput.js +2 -0
- package/dist/ui/SectionCard.d.ts +2 -0
- package/dist/ui/SectionCard.js +2 -0
- package/dist/ui/Select.d.ts +2 -0
- package/dist/ui/Select.js +2 -0
- package/dist/ui/StatusChip.d.ts +2 -0
- package/dist/ui/StatusChip.js +2 -0
- package/dist/ui/Tabs.d.ts +16 -0
- package/dist/ui/Tabs.js +61 -0
- package/dist/ui/TextInput.d.ts +2 -0
- package/dist/ui/TextInput.js +2 -0
- package/dist/ui/Textarea.d.ts +2 -0
- package/dist/ui/Textarea.js +2 -0
- package/dist/ui/Toggle.d.ts +2 -0
- package/dist/ui/Toggle.js +2 -0
- package/dist/ui/accent.d.ts +40 -0
- package/dist/ui/accent.js +139 -0
- package/dist/ui/index.d.ts +9 -0
- package/dist/ui/index.js +9 -0
- package/dist/ui/ui.css +92 -0
- package/package.json +5 -2
- package/dist/admin/components/chipsInput.css +0 -11
- package/dist/admin/results/ArmTable.d.ts +0 -8
- package/dist/admin/results/ArmTable.js +0 -113
- package/dist/admin/results/VerdictCard.d.ts +0 -9
- package/dist/admin/surface/goals/GoalsBreadcrumb.d.ts +0 -5
- package/dist/admin/surface/goals/GoalsBreadcrumb.js +0 -47
- package/dist/admin/surface/test/TestHeader.d.ts +0 -4
- package/dist/admin/surface/test/TestHeader.js +0 -70
|
@@ -1,7 +1,14 @@
|
|
|
1
|
+
import { Forbidden, NotFound } from 'payload';
|
|
2
|
+
import { safeError } from '../log.js';
|
|
1
3
|
import { powerMetricWeeks } from '@delmaredigital/experiments-stats';
|
|
2
4
|
import { checkStart, relationshipId } from './checkStart.js';
|
|
3
5
|
import { flagPlacements, scanBlockPlacements } from './placements.js';
|
|
4
|
-
|
|
6
|
+
import { routedPageTests } from '../db/pageOccupancy.js';
|
|
7
|
+
import { pooledExecutor, txExecutor } from '../db/executor.js';
|
|
8
|
+
const failureDetail = (cause)=>{
|
|
9
|
+
const name = cause instanceof Error ? cause.name : undefined;
|
|
10
|
+
return cause instanceof Forbidden || name === 'Forbidden' ? 'forbidden' : cause instanceof NotFound || name === 'NotFound' ? 'not-found' : 'placements-failed';
|
|
11
|
+
};
|
|
5
12
|
/** Read-only inputs shared with Start's pre-transaction checks. Start must re-read
|
|
6
13
|
* the document and lock-sensitive facts inside its transition (CORE-10b).
|
|
7
14
|
*/ export async function loadStartInputs(req, options, experimentId) {
|
|
@@ -23,6 +30,30 @@ const failureDetail = (cause)=>cause instanceof Error ? cause.message : 'Placeme
|
|
|
23
30
|
experiment
|
|
24
31
|
])).get(String(experiment.id)) ?? null;
|
|
25
32
|
}
|
|
33
|
+
/** Occupancy from the arms the runtime routes on (frozen rounds), shared by preflight and
|
|
34
|
+
* Start's locked recheck. Raw SQL on the request's transaction when there is one. */ export async function loadPageTestsInUse(req) {
|
|
35
|
+
return routedPageTests(await req.transactionID != null ? txExecutor(req) : pooledExecutor(req.payload));
|
|
36
|
+
}
|
|
37
|
+
/** Page targets no registered collection reports as published. A lookup that throws counts as
|
|
38
|
+
* unpublished (logged). Start calls it again under its page-path locks on a detached request. */ export async function unpublishedPageTargets(req, options, experiment) {
|
|
39
|
+
const targets = await Promise.all(experiment.arms.filter((arm)=>arm.kind !== 'holdout' && arm.pageTarget?.trim()).map(async (arm)=>{
|
|
40
|
+
for (const collection of options.pages?.collections ?? []){
|
|
41
|
+
try {
|
|
42
|
+
const page = await collection.findByPath(arm.pageTarget, req);
|
|
43
|
+
if (page?.published === true) return null;
|
|
44
|
+
} catch (cause) {
|
|
45
|
+
req.payload.logger.error({
|
|
46
|
+
err: safeError(cause),
|
|
47
|
+
experimentId: experiment.id,
|
|
48
|
+
collection: collection.slug,
|
|
49
|
+
id: arm.pageTarget
|
|
50
|
+
}, 'Experiment page lookup failed');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return arm.pageTarget;
|
|
54
|
+
}));
|
|
55
|
+
return targets.filter((target)=>target !== null);
|
|
56
|
+
}
|
|
26
57
|
const metricIds = (experiment)=>[
|
|
27
58
|
experiment.metrics?.primary,
|
|
28
59
|
...experiment.metrics?.secondary ?? [],
|
|
@@ -53,6 +84,7 @@ const metricIds = (experiment)=>[
|
|
|
53
84
|
})();
|
|
54
85
|
const blockSlugs = new Set(experiments.filter((e)=>e.kind === 'block').map((e)=>e.slug));
|
|
55
86
|
// Lazy promises prevent unhandled rejections and share failed reads as well as successful ones.
|
|
87
|
+
let pageTestsInUse;
|
|
56
88
|
let blocks;
|
|
57
89
|
let state;
|
|
58
90
|
const busyPoints = new Map();
|
|
@@ -68,6 +100,22 @@ const metricIds = (experiment)=>[
|
|
|
68
100
|
flagPoints: options.flagPoints,
|
|
69
101
|
attributeKeys: options.attributeKeys
|
|
70
102
|
};
|
|
103
|
+
const placementFailure = (cause)=>{
|
|
104
|
+
const collection = experiment.flagPoint ?? 'xp-experiments';
|
|
105
|
+
req.payload.logger.error({
|
|
106
|
+
err: safeError(cause),
|
|
107
|
+
experimentId: experiment.id,
|
|
108
|
+
collection,
|
|
109
|
+
id: experiment.id
|
|
110
|
+
}, 'Experiment placement lookup failed');
|
|
111
|
+
return failureDetail(cause);
|
|
112
|
+
};
|
|
113
|
+
if (experiment.kind === 'page') {
|
|
114
|
+
pageTestsInUse ??= loadPageTestsInUse(req);
|
|
115
|
+
input.pageTestsInUse = await pageTestsInUse;
|
|
116
|
+
input.pageTargetsUnchecked = options.pages === undefined;
|
|
117
|
+
if (options.pages) input.unpublishedPageTargets = await unpublishedPageTargets(req, options, experiment);
|
|
118
|
+
}
|
|
71
119
|
if (experiment.kind === 'block') {
|
|
72
120
|
try {
|
|
73
121
|
blocks ??= scanBlockPlacements(req, {
|
|
@@ -76,7 +124,7 @@ const metricIds = (experiment)=>[
|
|
|
76
124
|
});
|
|
77
125
|
input.placements = (await blocks).get(experiment.slug) ?? [];
|
|
78
126
|
} catch (cause) {
|
|
79
|
-
input.placementError =
|
|
127
|
+
input.placementError = placementFailure(cause);
|
|
80
128
|
}
|
|
81
129
|
}
|
|
82
130
|
if (experiment.kind === 'flag') {
|
|
@@ -108,9 +156,9 @@ const metricIds = (experiment)=>[
|
|
|
108
156
|
try {
|
|
109
157
|
const placements = await flagPlacements(options, experiment, req);
|
|
110
158
|
if (Array.isArray(placements) && placements.every((p)=>p && typeof p === 'object' && typeof p.arm === 'string')) input.flagPlacements = placements;
|
|
111
|
-
else input.flagPlacementError = '
|
|
159
|
+
else input.flagPlacementError = placementFailure(new Error('Invalid placement result'));
|
|
112
160
|
} catch (cause) {
|
|
113
|
-
input.flagPlacementError =
|
|
161
|
+
input.flagPlacementError = placementFailure(cause);
|
|
114
162
|
}
|
|
115
163
|
}
|
|
116
164
|
if (experiment.flagPoint) {
|
|
@@ -210,16 +258,23 @@ const metricIds = (experiment)=>[
|
|
|
210
258
|
}
|
|
211
259
|
return inputs;
|
|
212
260
|
}
|
|
213
|
-
|
|
261
|
+
// Bound each optional host preview lookup so it cannot indefinitely hold preflight open.
|
|
262
|
+
const PREVIEW_URL_TIMEOUT_MS = 2000;
|
|
263
|
+
// Resolve host preview lookups in a bounded pool instead of launching one per slot.
|
|
264
|
+
const PREVIEW_URL_CONCURRENCY = 8;
|
|
265
|
+
async function placementUrls(options, placement, pendingPreviews, onFailure, previews) {
|
|
214
266
|
const result = {};
|
|
215
267
|
const { collection, id, published } = placement;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
268
|
+
// One host lookup per page version: several slots on a page share it.
|
|
269
|
+
const key = JSON.stringify([
|
|
270
|
+
collection,
|
|
271
|
+
String(id),
|
|
272
|
+
published
|
|
273
|
+
]);
|
|
274
|
+
let preview = previews.get(key);
|
|
275
|
+
if (!preview) previews.set(key, preview = previewLookup(options, placement, pendingPreviews, onFailure));
|
|
276
|
+
const url = await preview;
|
|
277
|
+
if (url !== undefined) result.url = url;
|
|
223
278
|
try {
|
|
224
279
|
const url = options.admin?.editorUrl?.({
|
|
225
280
|
collection,
|
|
@@ -230,6 +285,43 @@ function placementUrls(options, placement) {
|
|
|
230
285
|
} catch {}
|
|
231
286
|
return result;
|
|
232
287
|
}
|
|
288
|
+
async function previewLookup(options, placement, pendingPreviews, onFailure) {
|
|
289
|
+
const { collection, id, title, published, slug } = placement;
|
|
290
|
+
const previewUrl = options.admin?.previewUrl;
|
|
291
|
+
let found;
|
|
292
|
+
if (previewUrl) {
|
|
293
|
+
let timer;
|
|
294
|
+
try {
|
|
295
|
+
if (pendingPreviews.count >= PREVIEW_URL_CONCURRENCY) throw new Error('Preview URL lookups at capacity');
|
|
296
|
+
pendingPreviews.count++;
|
|
297
|
+
// A timeout cannot cancel host work: retain its permit until that work settles.
|
|
298
|
+
const lookup = Promise.resolve().then(()=>previewUrl({
|
|
299
|
+
collection,
|
|
300
|
+
id,
|
|
301
|
+
title,
|
|
302
|
+
published,
|
|
303
|
+
...slug !== undefined ? {
|
|
304
|
+
slug
|
|
305
|
+
} : {}
|
|
306
|
+
})).finally(()=>{
|
|
307
|
+
pendingPreviews.count--;
|
|
308
|
+
});
|
|
309
|
+
const url = await Promise.race([
|
|
310
|
+
lookup,
|
|
311
|
+
new Promise((_, reject)=>{
|
|
312
|
+
timer = setTimeout(()=>reject(new Error('Preview URL lookup timed out')), PREVIEW_URL_TIMEOUT_MS);
|
|
313
|
+
})
|
|
314
|
+
]);
|
|
315
|
+
if (typeof url === 'string') found = url;
|
|
316
|
+
else if (url !== null && url !== undefined) throw new TypeError('Invalid preview URL result');
|
|
317
|
+
} catch (cause) {
|
|
318
|
+
onFailure(cause, placement);
|
|
319
|
+
} finally{
|
|
320
|
+
clearTimeout(timer);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return found;
|
|
324
|
+
}
|
|
233
325
|
async function primaryHistory(source, now) {
|
|
234
326
|
if (!source) return null;
|
|
235
327
|
const from = new Date(now.getTime() - 28 * 86400000);
|
|
@@ -340,15 +432,41 @@ export async function preflightData(req, { experimentId, options }, { now = new
|
|
|
340
432
|
}
|
|
341
433
|
}
|
|
342
434
|
const requiredSlots = e.arms.filter((arm)=>arm.kind !== 'holdout').map((arm)=>arm.slot);
|
|
343
|
-
const
|
|
435
|
+
const blocks = input.placements ?? [];
|
|
436
|
+
const placements = new Array(blocks.length);
|
|
437
|
+
let nextPlacement = 0;
|
|
438
|
+
const pendingPreviews = {
|
|
439
|
+
count: 0
|
|
440
|
+
};
|
|
441
|
+
const previews = new Map();
|
|
442
|
+
let loggedPreviewFailure = false;
|
|
443
|
+
const onPreviewFailure = (cause, placement)=>{
|
|
444
|
+
if (loggedPreviewFailure) return;
|
|
445
|
+
loggedPreviewFailure = true;
|
|
446
|
+
req.payload.logger.warn({
|
|
447
|
+
err: safeError(cause),
|
|
448
|
+
experimentId: e.id,
|
|
344
449
|
collection: placement.collection,
|
|
345
|
-
id: placement.id
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
450
|
+
id: placement.id
|
|
451
|
+
}, 'Experiment preview URL lookup failed');
|
|
452
|
+
};
|
|
453
|
+
await Promise.all(Array.from({
|
|
454
|
+
length: Math.min(PREVIEW_URL_CONCURRENCY, blocks.length)
|
|
455
|
+
}, async ()=>{
|
|
456
|
+
while(nextPlacement < blocks.length){
|
|
457
|
+
const index = nextPlacement++;
|
|
458
|
+
const placement = blocks[index];
|
|
459
|
+
placements[index] = {
|
|
460
|
+
collection: placement.collection,
|
|
461
|
+
id: placement.id,
|
|
462
|
+
title: placement.title,
|
|
463
|
+
slots: placement.slots,
|
|
464
|
+
...await placementUrls(options, placement, pendingPreviews, onPreviewFailure, previews),
|
|
465
|
+
ok: requiredSlots.every((slot)=>slot && placement.slots[slot] > 0),
|
|
466
|
+
detail: placement.published ? 'Published' : 'Draft'
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
}));
|
|
352
470
|
for (const placement of input.flagPlacements ?? [])placements.push({
|
|
353
471
|
collection: e.flagPoint ?? '',
|
|
354
472
|
id: placement.arm,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PayloadRequest } from 'payload';
|
|
2
|
+
import type { DocId } from '../types/contracts.js';
|
|
3
|
+
export declare function releasePages(req: PayloadRequest, { experimentId }: {
|
|
4
|
+
experimentId: DocId;
|
|
5
|
+
}): Promise<{
|
|
6
|
+
doc: import("payload").JsonObject & import("payload").TypeWithID;
|
|
7
|
+
revision: number;
|
|
8
|
+
}>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { relationshipId } from './checkStart.js';
|
|
2
|
+
import { withTransition } from './kernel.js';
|
|
3
|
+
import { readExperiment, refuse, updateExperiment } from './start.js';
|
|
4
|
+
export async function releasePages(req, { experimentId }) {
|
|
5
|
+
const before = await readExperiment(req, experimentId);
|
|
6
|
+
const { result: doc, revision } = await withTransition(req, {
|
|
7
|
+
experimentId,
|
|
8
|
+
layerId: relationshipId(before.layer)
|
|
9
|
+
}, async ({ req })=>{
|
|
10
|
+
const current = await readExperiment(req, experimentId);
|
|
11
|
+
const decision = current.decision;
|
|
12
|
+
if (current.kind !== 'page' || current.status !== 'archived' || !decision?.winnerArm || decision.discardedAt || decision.pagesReleasedAt) refuse('invalid-status');
|
|
13
|
+
const at = new Date().toISOString();
|
|
14
|
+
return updateExperiment(req, experimentId, {
|
|
15
|
+
decision: {
|
|
16
|
+
...decision,
|
|
17
|
+
pagesReleasedAt: at,
|
|
18
|
+
pagesReleasedBy: req.user ? String(req.user.id) : null
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
return {
|
|
23
|
+
doc,
|
|
24
|
+
revision: revision
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PayloadRequest } from 'payload';
|
|
2
|
+
import type { DocId } from '../types/contracts.js';
|
|
3
|
+
import type { RestoreContentEntryBody } from '../types/endpoints.js';
|
|
4
|
+
export declare function restoreContentEntry(req: PayloadRequest, { experimentId, collection, id }: RestoreContentEntryBody & {
|
|
5
|
+
experimentId: DocId;
|
|
6
|
+
}): Promise<{
|
|
7
|
+
doc: import("payload").JsonObject & import("payload").TypeWithID;
|
|
8
|
+
}>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { relationshipId } from './checkStart.js';
|
|
2
|
+
import { withTransition } from './kernel.js';
|
|
3
|
+
import { readExperiment, refuse, updateExperiment } from './start.js';
|
|
4
|
+
export async function restoreContentEntry(req, { experimentId, collection, id }) {
|
|
5
|
+
const before = await readExperiment(req, experimentId);
|
|
6
|
+
// Only the audit report changes; assignment and the manifest stay the same.
|
|
7
|
+
const { result: doc } = await withTransition(req, {
|
|
8
|
+
experimentId,
|
|
9
|
+
layerId: relationshipId(before.layer),
|
|
10
|
+
flagPointId: before.kind === 'flag' ? before.flagPoint : undefined,
|
|
11
|
+
bump: false
|
|
12
|
+
}, async ({ req })=>{
|
|
13
|
+
const current = await readExperiment(req, experimentId);
|
|
14
|
+
const decision = current.decision;
|
|
15
|
+
const report = decision?.contentReport;
|
|
16
|
+
const index = Array.isArray(report?.docs) ? report.docs.findIndex((entry)=>entry.collection === collection && String(entry.id) === String(id)) : -1;
|
|
17
|
+
const entry = report?.docs?.[index];
|
|
18
|
+
if (current.status !== 'decided' || !decision?.winnerArm || decision.discardedAt) refuse('not-decided');
|
|
19
|
+
if (!entry) refuse('entry-not-found');
|
|
20
|
+
if (!entry.dismissedAt) refuse('entry-not-dismissed');
|
|
21
|
+
const docs = report.docs.map((entry, i)=>{
|
|
22
|
+
if (i !== index) return entry;
|
|
23
|
+
const { dismissedAt: _at, dismissedBy: _by, ...restored } = entry;
|
|
24
|
+
return {
|
|
25
|
+
...restored,
|
|
26
|
+
ok: false
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
return updateExperiment(req, experimentId, {
|
|
30
|
+
decision: {
|
|
31
|
+
...decision,
|
|
32
|
+
contentReport: {
|
|
33
|
+
...report,
|
|
34
|
+
docs
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
req.payload.logger.info({
|
|
40
|
+
experimentId,
|
|
41
|
+
collection,
|
|
42
|
+
id
|
|
43
|
+
}, 'Experiment content entry restored');
|
|
44
|
+
return {
|
|
45
|
+
doc
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
import { lockPagePaths } from '../db/locks.js';
|
|
2
|
+
import { lockMetricsForFreeze } from '../db/metricUsage.js';
|
|
3
|
+
import { DEFAULT_SEQUENTIAL_TUNING_PARAMETER } from '../constants.js';
|
|
1
4
|
import { claimSlices } from '../db/slices.js';
|
|
5
|
+
import { pageInUseCopy, unpublishedPageCopy } from '../admin/copy/readiness.js';
|
|
2
6
|
import { XpRefusal } from '../errors.js';
|
|
3
7
|
import { isOpValid } from '../targeting/ops.js';
|
|
4
8
|
import { newSeed } from '../runtime/seed.js';
|
|
5
|
-
import { checkStart, relationshipId } from './checkStart.js';
|
|
9
|
+
import { checkStart, pageTargetPaths, relationshipId } from './checkStart.js';
|
|
6
10
|
import { withTransition } from './kernel.js';
|
|
7
11
|
import { writePhaseRecord } from './phaseRecord.js';
|
|
8
|
-
import { loadStartInputs } from './preflightData.js';
|
|
12
|
+
import { loadPageTestsInUse, loadStartInputs, unpublishedPageTargets } from './preflightData.js';
|
|
9
13
|
import { resolveCaps } from './resolveCaps.js';
|
|
10
14
|
export async function readExperiment(req, id) {
|
|
11
15
|
// §7.8: internal lookup serves the authorised transition write. Preserve
|
|
@@ -27,21 +31,33 @@ export async function readExperiment(req, id) {
|
|
|
27
31
|
}
|
|
28
32
|
export function refuse(code, detail) {
|
|
29
33
|
const messages = {
|
|
34
|
+
'page-in-use': pageInUseCopy(detail?.paths?.[0] ?? 'this page'),
|
|
35
|
+
'page-target-unpublished': unpublishedPageCopy(typeof detail === 'string' ? detail : 'This page'),
|
|
30
36
|
'invalid-status': 'This action is not available in the current status.',
|
|
31
37
|
'stale-draft': 'The experiment changed. Review it again before starting.',
|
|
32
38
|
'flag-point-busy': 'Another experiment is running or decided on this flag point.',
|
|
33
39
|
'layer-full': 'There is not enough room in this layer.',
|
|
34
40
|
'allocation-lower': 'Allocation can only increase while running. Stop and start a new round to lower it.',
|
|
35
|
-
'invalid-body': 'Check the experiment settings and try again.'
|
|
41
|
+
'invalid-body': 'Check the experiment settings and try again.',
|
|
42
|
+
'not-decided': 'This test has no standing winner.',
|
|
43
|
+
'entry-not-found': 'That page is not in the report.',
|
|
44
|
+
'entry-ok': 'That page was already updated.',
|
|
45
|
+
'entry-dismissed': 'That page was already dismissed.',
|
|
46
|
+
'entry-not-dismissed': 'That page is not dismissed.'
|
|
36
47
|
};
|
|
37
48
|
const conflict = [
|
|
38
49
|
'invalid-status',
|
|
39
50
|
'stale-draft',
|
|
51
|
+
'page-in-use',
|
|
40
52
|
'flag-point-busy',
|
|
41
53
|
'layer-full',
|
|
42
|
-
'allocation-lower'
|
|
54
|
+
'allocation-lower',
|
|
55
|
+
'not-decided',
|
|
56
|
+
'entry-ok',
|
|
57
|
+
'entry-dismissed',
|
|
58
|
+
'entry-not-dismissed'
|
|
43
59
|
].includes(code);
|
|
44
|
-
throw new XpRefusal(code, messages[code] ?? 'The experiment is not ready to start.', detail, conflict ? 409 : 422);
|
|
60
|
+
throw new XpRefusal(code, messages[code] ?? 'The experiment is not ready to start.', detail, code === 'entry-not-found' ? 404 : conflict ? 409 : 422);
|
|
45
61
|
}
|
|
46
62
|
export async function updateExperiment(req, id, data) {
|
|
47
63
|
// §7.8: access-authorised transition writes locked plugin fields.
|
|
@@ -78,8 +94,10 @@ export async function start(req, { experimentId, options }) {
|
|
|
78
94
|
kind: full.kind,
|
|
79
95
|
analysis: full.analysis
|
|
80
96
|
};
|
|
97
|
+
// Warning-only codes always have ok: true. These exclusions narrow
|
|
98
|
+
// ReadinessCode to RefusalCode, which intentionally excludes warnings.
|
|
81
99
|
const failed = checkStart(input).find((check)=>!check.ok);
|
|
82
|
-
if (failed) refuse(failed.code, failed.detail);
|
|
100
|
+
if (failed && failed.code !== 'page-targets-unchecked' && failed.code !== 'make-content-unsupported') refuse(failed.code, failed.detail);
|
|
83
101
|
const before = input.experiment;
|
|
84
102
|
const updatedAt = before.updatedAt;
|
|
85
103
|
const at = new Date();
|
|
@@ -92,6 +110,33 @@ export async function start(req, { experimentId, options }) {
|
|
|
92
110
|
const current = await readExperiment(req, experimentId);
|
|
93
111
|
if (current.updatedAt !== updatedAt) refuse('stale-draft');
|
|
94
112
|
if (current.status !== 'draft' && current.status !== 'stopped') refuse('invalid-status');
|
|
113
|
+
if (current.kind === 'page') {
|
|
114
|
+
// Independent experiment/layer locks do not serialize overlapping pages.
|
|
115
|
+
// Re-read occupancy after acquiring sorted path locks; hold through commit.
|
|
116
|
+
// A waiting Start then sees the first Start's live test on this transaction.
|
|
117
|
+
await lockPagePaths(exec, pageTargetPaths(current.arms));
|
|
118
|
+
const recheck = checkStart({
|
|
119
|
+
...input,
|
|
120
|
+
experiment: current,
|
|
121
|
+
pageTestsInUse: await loadPageTestsInUse(req)
|
|
122
|
+
}).find((check)=>check.code === 'page-in-use' && !check.ok);
|
|
123
|
+
if (recheck) refuse('page-in-use', recheck.detail);
|
|
124
|
+
if (options.pages) {
|
|
125
|
+
// The page guard takes the same path locks, so a delete, unpublish or rename that committed
|
|
126
|
+
// while Start waited is visible now. Read it on a detached request (committed state, outside
|
|
127
|
+
// this transaction): a refused host read must not kill the transition (CORE-21).
|
|
128
|
+
// (the make-content pattern: drop the transaction id for the reads, restore it after).
|
|
129
|
+
const transactionID = req.transactionID;
|
|
130
|
+
delete req.transactionID;
|
|
131
|
+
let unpublished;
|
|
132
|
+
try {
|
|
133
|
+
unpublished = await unpublishedPageTargets(req, options, current);
|
|
134
|
+
} finally{
|
|
135
|
+
req.transactionID = transactionID;
|
|
136
|
+
}
|
|
137
|
+
if (unpublished.length) refuse('page-target-unpublished', unpublished[0]);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
95
140
|
if (current.kind === 'flag' && current.flagPoint) {
|
|
96
141
|
const busy = await req.payload.find({
|
|
97
142
|
collection: 'xp-experiments',
|
|
@@ -134,8 +179,17 @@ export async function start(req, { experimentId, options }) {
|
|
|
134
179
|
...input,
|
|
135
180
|
experiment: current
|
|
136
181
|
}).find((check)=>!check.ok);
|
|
137
|
-
if (recheck) refuse(recheck.code, recheck.detail);
|
|
182
|
+
if (recheck && recheck.code !== 'page-targets-unchecked' && recheck.code !== 'make-content-unsupported') refuse(recheck.code, recheck.detail);
|
|
183
|
+
}
|
|
184
|
+
const lockedMetrics = await lockMetricsForFreeze(exec, caps.definitions.map((metric)=>metric.id));
|
|
185
|
+
if (!lockedMetrics) {
|
|
186
|
+
throw new XpRefusal('stale-draft', 'A goal in this test was deleted. Review the goals before starting.', undefined, 409);
|
|
138
187
|
}
|
|
188
|
+
// Reachable: a slug/source change takes FOR NO KEY UPDATE, which a draft's FK check does not wait on.
|
|
189
|
+
if (caps.definitions.some((metric)=>{
|
|
190
|
+
const locked = lockedMetrics.find((row)=>String(row.id) === String(metric.id));
|
|
191
|
+
return locked.slug !== metric.slug || locked.source !== metric.source;
|
|
192
|
+
})) throw new XpRefusal('stale-draft', 'A goal changed. Review it again before starting.', undefined, 409);
|
|
139
193
|
const targeting = (current.targeting ?? []).map((rule)=>{
|
|
140
194
|
const type = Object.hasOwn(options.attributeKeys ?? {}, rule.attribute) ? options.attributeKeys[rule.attribute].type : undefined;
|
|
141
195
|
const validValue = type === 'string' && [
|
|
@@ -183,6 +237,7 @@ export async function start(req, { experimentId, options }) {
|
|
|
183
237
|
...current.analysis,
|
|
184
238
|
method: current.analysis?.method ?? 'sequential',
|
|
185
239
|
alpha: 0.05,
|
|
240
|
+
sequentialTuningParameter: current.analysis?.sequentialTuningParameter ?? DEFAULT_SEQUENTIAL_TUNING_PARAMETER,
|
|
186
241
|
cupedLookbackDays: 14,
|
|
187
242
|
layerSlices,
|
|
188
243
|
minSampleSizes: Object.fromEntries(definitions.map((metric)=>[
|
|
@@ -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,15 @@ 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
|
+
pathOf: (doc: Record<string, unknown>) => string | null;
|
|
107
|
+
/** 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 }. */
|
|
108
|
+
findByPath: (path: string, req: PayloadRequest) => Promise<{
|
|
109
|
+
id: string | number;
|
|
110
|
+
published: boolean;
|
|
111
|
+
} | null>;
|
|
112
|
+
}
|
|
96
113
|
export interface ExperimentsPluginOptions<TUser extends {
|
|
97
114
|
id: string | number;
|
|
98
115
|
} = {
|
|
@@ -105,16 +122,16 @@ export interface ExperimentsPluginOptions<TUser extends {
|
|
|
105
122
|
readResults?: ExperimentsAccess;
|
|
106
123
|
readIdentities?: ExperimentsAccess;
|
|
107
124
|
};
|
|
125
|
+
theme?: {
|
|
126
|
+
accent?: string;
|
|
127
|
+
};
|
|
108
128
|
admin?: {
|
|
109
129
|
editorUrl?: (args: {
|
|
110
130
|
collection: string;
|
|
111
131
|
id: DocId;
|
|
112
132
|
draft?: boolean;
|
|
113
133
|
}) => string;
|
|
114
|
-
previewUrl?: (
|
|
115
|
-
collection: string;
|
|
116
|
-
id: DocId;
|
|
117
|
-
}) => string;
|
|
134
|
+
previewUrl?: (placement: PreviewPlacement) => string | null | undefined | Promise<string | null | undefined>;
|
|
118
135
|
descriptions?: {
|
|
119
136
|
block?: string;
|
|
120
137
|
document?: string;
|
|
@@ -131,6 +148,9 @@ export interface ExperimentsPluginOptions<TUser extends {
|
|
|
131
148
|
type: 'string' | 'number' | 'boolean';
|
|
132
149
|
values?: string[];
|
|
133
150
|
}>;
|
|
151
|
+
pages?: {
|
|
152
|
+
collections: ExperimentsPageCollection[];
|
|
153
|
+
};
|
|
134
154
|
puck?: {
|
|
135
155
|
enabled: boolean;
|
|
136
156
|
collections: Array<string | {
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import type { ArmKind, DocId, ExposurePolicy, MetricSource, ResolvedOptions } from './contracts.js';
|
|
1
|
+
import type { ArmKind, DocId, ExperimentKind, ExposurePolicy, MetricSource, ResolvedOptions, Status } from './contracts.js';
|
|
2
|
+
import type { ContentReportReason } from './reasons.js';
|
|
2
3
|
import type { ArmSlot } from './puck.js';
|
|
3
4
|
import type { ReadinessCode } from '../transitions/checkStart.js';
|
|
4
5
|
export type { ReadinessCode } from '../transitions/checkStart.js';
|
|
5
6
|
import type { Snapshot, VerdictCode } from './snapshot.js';
|
|
6
7
|
export type ArmIssueCode = 'weights-total' | 'control-count' | 'control-first' | 'holdout-last' | 'holdout-count' | 'variant-count' | 'duplicate-key' | 'duplicate-slot' | 'slot-required' | 'invalid-key';
|
|
7
|
-
|
|
8
|
+
/** Content-report dismiss/restore refusals (CORE-44). */
|
|
9
|
+
export type ContentEntryRefusal = 'not-decided' | 'entry-not-found' | 'entry-ok' | 'entry-dismissed' | 'entry-not-dismissed';
|
|
10
|
+
export type RefusalCode = ArmIssueCode | 'not-found' | ContentEntryRefusal | 'no-planned-duration' | 'no-primary-metric' | 'no-placements' | 'empty-slot' | 'page-in-use' | 'page-no-targets' | 'page-target-unpublished' | 'page-user-unit' | 'version-page-unavailable' | 'version-page-unchecked' | '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' | 'stale-edit' | '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' | 'analysis-failed' | 'undeclared-targeting-key' | 'invalid-targeting-rule' | 'block-path-rule' | 'duplicate-metric';
|
|
8
11
|
export type ErrorEnvelope = {
|
|
9
12
|
errors: Array<{
|
|
10
13
|
code: RefusalCode;
|
|
@@ -32,12 +35,25 @@ export type ArchiveBody = {
|
|
|
32
35
|
mode: 'stopped' | 'after-content' | 'discard' | 'hard-coded';
|
|
33
36
|
confirmation?: string;
|
|
34
37
|
};
|
|
38
|
+
export type DismissContentEntryBody = {
|
|
39
|
+
collection: string;
|
|
40
|
+
id: DocId;
|
|
41
|
+
};
|
|
42
|
+
export type DismissContentEntryResult = Pick<TransitionSuccess, 'doc'>;
|
|
43
|
+
export type RestoreContentEntryBody = DismissContentEntryBody;
|
|
44
|
+
export type RestoreContentEntryResult = Pick<TransitionSuccess, 'doc'>;
|
|
45
|
+
export type ContentReport = {
|
|
46
|
+
docs: MakeContentResult['docs'];
|
|
47
|
+
} | MakeContentResult['docs'];
|
|
35
48
|
export type MakeContentResult = TransitionSuccess & {
|
|
36
49
|
docs: Array<{
|
|
37
50
|
collection: string;
|
|
38
51
|
id: DocId;
|
|
39
52
|
ok: boolean;
|
|
40
|
-
|
|
53
|
+
detail?: ContentReportReason | string;
|
|
54
|
+
error?: ContentReportReason | string;
|
|
55
|
+
dismissedAt?: string;
|
|
56
|
+
dismissedBy?: string | null;
|
|
41
57
|
}>;
|
|
42
58
|
};
|
|
43
59
|
export type PreflightResult = {
|
|
@@ -45,6 +61,7 @@ export type PreflightResult = {
|
|
|
45
61
|
code: ReadinessCode;
|
|
46
62
|
ok: boolean;
|
|
47
63
|
detail?: unknown;
|
|
64
|
+
warning?: boolean;
|
|
48
65
|
}>;
|
|
49
66
|
placements: Array<{
|
|
50
67
|
collection: string;
|
|
@@ -93,6 +110,12 @@ export interface PhaseSummary {
|
|
|
93
110
|
}>;
|
|
94
111
|
}
|
|
95
112
|
export type ResultsResponse = {
|
|
113
|
+
/** Filled by the results endpoint from CORE-48: what a results reader may see about the test. */
|
|
114
|
+
experiment?: {
|
|
115
|
+
name: string;
|
|
116
|
+
kind: ExperimentKind;
|
|
117
|
+
status: Status;
|
|
118
|
+
};
|
|
96
119
|
phases: PhaseSummary[];
|
|
97
120
|
snapshot: Snapshot | null;
|
|
98
121
|
decisionSnapshot?: Snapshot;
|
|
@@ -110,6 +133,14 @@ export type ResultsResponse = {
|
|
|
110
133
|
};
|
|
111
134
|
export type SummaryResponse = Record<DocId, {
|
|
112
135
|
ready?: boolean;
|
|
136
|
+
next?: {
|
|
137
|
+
code: ReadinessCode;
|
|
138
|
+
detail?: unknown;
|
|
139
|
+
};
|
|
140
|
+
health?: Array<{
|
|
141
|
+
code: 'version-page-unavailable' | 'version-page-unchecked';
|
|
142
|
+
path: string;
|
|
143
|
+
}>;
|
|
113
144
|
exposures: number;
|
|
114
145
|
days: number;
|
|
115
146
|
verdict: {
|
package/dist/types/index.d.ts
CHANGED
package/dist/types/index.js
CHANGED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Closed codes written by new results runs; stored snapshots may predate them. */
|
|
2
|
+
export type MetricUnavailableReason = 'source-failed' | 'source-unavailable' | 'unit-type-unavailable' | 'non-finite-values' | 'non-finite-covariates' | 'budget' | 'not-supplied' | 'analysis-failed';
|
|
3
|
+
export type ContentReportReason = 'localized' | 'locked' | 'forbidden' | 'not-found' | 'validation' | 'update-failed';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/** Closed codes written by new results runs; stored snapshots may predate them. */ export { };
|
package/dist/types/snapshot.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { GaussianPrior } from '@delmaredigital/experiments-stats';
|
|
2
2
|
import type { DocId, MetricSource } from './contracts.js';
|
|
3
|
+
import type { MetricUnavailableReason } from './reasons.js';
|
|
3
4
|
export type VerdictCode = 'unavailable' | 'data-problem' | 'guardrail-failing' | 'collecting' | 'original-better' | 'worse-none-better' | 'winner' | 'mixed' | 'keep-running' | 'no-difference' | 'rollout-collecting' | 'rollout-readout';
|
|
4
5
|
export interface Snapshot {
|
|
5
6
|
experiment: DocId;
|
|
@@ -54,7 +55,7 @@ export interface Snapshot {
|
|
|
54
55
|
kind: MetricSource['kind'];
|
|
55
56
|
role: 'primary' | 'secondary' | 'guardrail';
|
|
56
57
|
cupedApplied: boolean;
|
|
57
|
-
unavailable?: string;
|
|
58
|
+
unavailable?: MetricUnavailableReason | string;
|
|
58
59
|
perArm: Array<{
|
|
59
60
|
arm: string;
|
|
60
61
|
n: number;
|
|
@@ -107,6 +108,10 @@ export interface Snapshot {
|
|
|
107
108
|
guardrailLine?: string;
|
|
108
109
|
worstArm?: string;
|
|
109
110
|
};
|
|
111
|
+
health?: Array<{
|
|
112
|
+
code: 'version-page-unavailable' | 'version-page-unchecked';
|
|
113
|
+
path: string;
|
|
114
|
+
}>;
|
|
110
115
|
contentChanges: Array<{
|
|
111
116
|
at: string;
|
|
112
117
|
doc: {
|