@delmaredigital/payload-experiments 0.0.0-stage → 0.1.0-rc.3
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/LICENSE +21 -0
- package/README.md +582 -2
- package/dist/access.d.ts +1 -0
- package/dist/access.js +2 -0
- package/dist/admin/AllocationField.d.ts +2 -0
- package/dist/admin/AllocationField.js +80 -0
- package/dist/admin/FlagPointSelect.d.ts +2 -0
- package/dist/admin/FlagPointSelect.js +54 -0
- package/dist/admin/LayerUsage.d.ts +2 -0
- package/dist/admin/LayerUsage.js +24 -0
- package/dist/admin/LockedFieldNotice.d.ts +2 -0
- package/dist/admin/LockedFieldNotice.js +26 -0
- package/dist/admin/MetricSourceSelect.d.ts +2 -0
- package/dist/admin/MetricSourceSelect.js +32 -0
- package/dist/admin/PhaseHistory.d.ts +2 -0
- package/dist/admin/PhaseHistory.js +76 -0
- package/dist/admin/PreviewLinks.d.ts +11 -0
- package/dist/admin/PreviewLinks.js +69 -0
- package/dist/admin/StatusCell.d.ts +3 -0
- package/dist/admin/StatusCell.js +19 -0
- package/dist/admin/SummaryCell.d.ts +2 -0
- package/dist/admin/SummaryCell.js +32 -0
- package/dist/admin/actions/AddMetricDialog.d.ts +6 -0
- package/dist/admin/actions/AddMetricDialog.js +138 -0
- package/dist/admin/actions/AllocateDialog.d.ts +6 -0
- package/dist/admin/actions/AllocateDialog.js +81 -0
- package/dist/admin/actions/ConfirmDialog.d.ts +14 -0
- package/dist/admin/actions/ConfirmDialog.js +192 -0
- package/dist/admin/actions/DecideDialog.d.ts +7 -0
- package/dist/admin/actions/DecideDialog.js +173 -0
- package/dist/admin/actions/ExperimentActions.d.ts +3 -0
- package/dist/admin/actions/ExperimentActions.js +110 -0
- package/dist/admin/actions/MakeContentResult.d.ts +6 -0
- package/dist/admin/actions/MakeContentResult.js +50 -0
- package/dist/admin/actions/actions.model.d.ts +37 -0
- package/dist/admin/actions/actions.model.js +71 -0
- package/dist/admin/actions/decide.model.d.ts +21 -0
- package/dist/admin/actions/decide.model.js +38 -0
- package/dist/admin/allocation.model.d.ts +20 -0
- package/dist/admin/allocation.model.js +38 -0
- package/dist/admin/api.d.ts +32 -0
- package/dist/admin/api.js +95 -0
- package/dist/admin/armSelect/ArmSelectField.d.ts +2 -0
- package/dist/admin/armSelect/ArmSelectField.js +185 -0
- package/dist/admin/armSelect/armSelect.model.d.ts +30 -0
- package/dist/admin/armSelect/armSelect.model.js +20 -0
- package/dist/admin/arms/ArmsField.d.ts +2 -0
- package/dist/admin/arms/ArmsField.js +310 -0
- package/dist/admin/arms/ParamsEditor.d.ts +16 -0
- package/dist/admin/arms/ParamsEditor.js +79 -0
- package/dist/admin/arms/arms.model.d.ts +21 -0
- package/dist/admin/arms/arms.model.js +153 -0
- package/dist/admin/arms/pageTarget.model.d.ts +4 -0
- package/dist/admin/arms/pageTarget.model.js +19 -0
- package/dist/admin/arms/params.model.d.ts +7 -0
- package/dist/admin/arms/params.model.js +35 -0
- package/dist/admin/copy/actions.d.ts +38 -0
- package/dist/admin/copy/actions.js +38 -0
- package/dist/admin/copy/armSelect.d.ts +11 -0
- package/dist/admin/copy/armSelect.js +11 -0
- package/dist/admin/copy/arms.d.ts +37 -0
- package/dist/admin/copy/arms.js +47 -0
- package/dist/admin/copy/common.d.ts +20 -0
- package/dist/admin/copy/common.js +27 -0
- package/dist/admin/copy/history.d.ts +9 -0
- package/dist/admin/copy/history.js +9 -0
- package/dist/admin/copy/metrics.d.ts +9 -0
- package/dist/admin/copy/metrics.js +9 -0
- package/dist/admin/copy/results.d.ts +78 -0
- package/dist/admin/copy/results.js +81 -0
- package/dist/admin/copy/review.d.ts +33 -0
- package/dist/admin/copy/review.js +127 -0
- package/dist/admin/copy/status.d.ts +8 -0
- package/dist/admin/copy/status.js +8 -0
- package/dist/admin/copy/tabs.d.ts +7 -0
- package/dist/admin/copy/tabs.js +7 -0
- package/dist/admin/copy/who.d.ts +36 -0
- package/dist/admin/copy/who.js +36 -0
- package/dist/admin/labels.d.ts +45 -0
- package/dist/admin/labels.js +53 -0
- package/dist/admin/layerUsage.model.d.ts +15 -0
- package/dist/admin/layerUsage.model.js +18 -0
- package/dist/admin/lockedNotice.model.d.ts +3 -0
- package/dist/admin/lockedNotice.model.js +18 -0
- package/dist/admin/metricSource.model.d.ts +7 -0
- package/dist/admin/metricSource.model.js +11 -0
- package/dist/admin/paths.d.ts +16 -0
- package/dist/admin/paths.js +16 -0
- package/dist/admin/permissions.model.d.ts +10 -0
- package/dist/admin/permissions.model.js +4 -0
- package/dist/admin/phaseHistory.model.d.ts +19 -0
- package/dist/admin/phaseHistory.model.js +19 -0
- package/dist/admin/reload.d.ts +1 -0
- package/dist/admin/reload.js +1 -0
- package/dist/admin/results/ArmTable.d.ts +8 -0
- package/dist/admin/results/ArmTable.js +112 -0
- package/dist/admin/results/CappedCell.d.ts +4 -0
- package/dist/admin/results/CappedCell.js +24 -0
- package/dist/admin/results/CiChart.d.ts +10 -0
- package/dist/admin/results/CiChart.js +178 -0
- package/dist/admin/results/DataQualityPanel.d.ts +4 -0
- package/dist/admin/results/DataQualityPanel.js +40 -0
- package/dist/admin/results/ResultsView.d.ts +3 -0
- package/dist/admin/results/ResultsView.js +312 -0
- package/dist/admin/results/VerdictCard.d.ts +9 -0
- package/dist/admin/results/VerdictCard.js +49 -0
- package/dist/admin/results/chart.model.d.ts +32 -0
- package/dist/admin/results/chart.model.js +52 -0
- package/dist/admin/results/format.d.ts +10 -0
- package/dist/admin/results/format.js +55 -0
- package/dist/admin/results/results.model.d.ts +105 -0
- package/dist/admin/results/results.model.js +175 -0
- package/dist/admin/review/ReviewPanel.d.ts +2 -0
- package/dist/admin/review/ReviewPanel.js +268 -0
- package/dist/admin/review/review.model.d.ts +31 -0
- package/dist/admin/review/review.model.js +40 -0
- package/dist/admin/status.model.d.ts +16 -0
- package/dist/admin/status.model.js +17 -0
- package/dist/admin/styles.css +10 -0
- package/dist/admin/styles.d.ts +1 -0
- package/dist/admin/styles.js +2 -0
- package/dist/admin/summaryBatcher.d.ts +8 -0
- package/dist/admin/summaryBatcher.js +33 -0
- package/dist/admin/targeting/TargetingAttributeField.d.ts +2 -0
- package/dist/admin/targeting/TargetingAttributeField.js +31 -0
- package/dist/admin/targeting/TargetingOpField.d.ts +2 -0
- package/dist/admin/targeting/TargetingOpField.js +45 -0
- package/dist/admin/targeting/TargetingValueField.d.ts +2 -0
- package/dist/admin/targeting/TargetingValueField.js +142 -0
- package/dist/admin/targeting/targeting.model.d.ts +18 -0
- package/dist/admin/targeting/targeting.model.js +48 -0
- package/dist/admin/useApi.d.ts +20 -0
- package/dist/admin/useApi.js +10 -0
- package/dist/admin/useLayerUsage.d.ts +15 -0
- package/dist/admin/useLayerUsage.js +92 -0
- package/dist/client/ExperimentPageview.d.ts +1 -0
- package/dist/client/ExperimentPageview.js +27 -0
- package/dist/client/ExperimentsProvider.d.ts +6 -0
- package/dist/client/ExperimentsProvider.js +22 -0
- package/dist/client/ViewportExposure.d.ts +9 -0
- package/dist/client/ViewportExposure.js +69 -0
- package/dist/client/beacon.d.ts +17 -0
- package/dist/client/beacon.js +71 -0
- package/dist/client/context.d.ts +2 -0
- package/dist/client/context.js +2 -0
- package/dist/client/idempotency.d.ts +2 -0
- package/dist/client/idempotency.js +13 -0
- package/dist/client/trackConversionBeacon.d.ts +3 -0
- package/dist/client/trackConversionBeacon.js +16 -0
- package/dist/client/useExperiment.d.ts +5 -0
- package/dist/client/useExperiment.js +27 -0
- package/dist/client/useFlagPoint.d.ts +9 -0
- package/dist/client/useFlagPoint.js +32 -0
- package/dist/collections/access.d.ts +21 -0
- package/dist/collections/access.js +25 -0
- package/dist/collections/events.d.ts +3 -0
- package/dist/collections/events.js +117 -0
- package/dist/collections/experiments/armInvariants.d.ts +12 -0
- package/dist/collections/experiments/armInvariants.js +89 -0
- package/dist/collections/experiments/config.d.ts +3 -0
- package/dist/collections/experiments/config.js +574 -0
- package/dist/collections/experiments/createDefaults.d.ts +2 -0
- package/dist/collections/experiments/createDefaults.js +7 -0
- package/dist/collections/experiments/deleteGuard.d.ts +3 -0
- package/dist/collections/experiments/deleteGuard.js +8 -0
- package/dist/collections/experiments/editability.d.ts +4 -0
- package/dist/collections/experiments/editability.js +35 -0
- package/dist/collections/experiments/flagControlParams.d.ts +2 -0
- package/dist/collections/experiments/flagControlParams.js +32 -0
- package/dist/collections/experiments/lockHook.d.ts +2 -0
- package/dist/collections/experiments/lockHook.js +134 -0
- package/dist/collections/identities.d.ts +3 -0
- package/dist/collections/identities.js +30 -0
- package/dist/collections/index.d.ts +6 -0
- package/dist/collections/index.js +24 -0
- package/dist/collections/layers.d.ts +3 -0
- package/dist/collections/layers.js +77 -0
- package/dist/collections/metrics.d.ts +3 -0
- package/dist/collections/metrics.js +115 -0
- package/dist/collections/phases.d.ts +3 -0
- package/dist/collections/phases.js +59 -0
- package/dist/collections/serialize.d.ts +5 -0
- package/dist/collections/serialize.js +40 -0
- package/dist/collections/snapshots.d.ts +3 -0
- package/dist/collections/snapshots.js +47 -0
- package/dist/constants.d.ts +7 -0
- package/dist/constants.js +7 -0
- package/dist/db/adapter.d.ts +3 -0
- package/dist/db/adapter.js +9 -0
- package/dist/db/appends.d.ts +58 -0
- package/dist/db/appends.js +116 -0
- package/dist/db/delete.d.ts +17 -0
- package/dist/db/delete.js +35 -0
- package/dist/db/erase.d.ts +12 -0
- package/dist/db/erase.js +55 -0
- package/dist/db/events.d.ts +21 -0
- package/dist/db/events.js +100 -0
- package/dist/db/executor.d.ts +10 -0
- package/dist/db/executor.js +37 -0
- package/dist/db/identities.d.ts +11 -0
- package/dist/db/identities.js +29 -0
- package/dist/db/locks.d.ts +15 -0
- package/dist/db/locks.js +23 -0
- package/dist/db/manifest.d.ts +8 -0
- package/dist/db/manifest.js +8 -0
- package/dist/db/names.d.ts +271 -0
- package/dist/db/names.js +270 -0
- package/dist/db/pgExecutor.d.ts +41 -0
- package/dist/db/pgExecutor.js +250 -0
- package/dist/db/results.d.ts +53 -0
- package/dist/db/results.js +103 -0
- package/dist/db/slices.d.ts +18 -0
- package/dist/db/slices.js +51 -0
- package/dist/db/snapshots.d.ts +47 -0
- package/dist/db/snapshots.js +89 -0
- package/dist/db/sql.d.ts +5 -0
- package/dist/db/sql.js +5 -0
- package/dist/db/state.d.ts +12 -0
- package/dist/db/state.js +27 -0
- package/dist/db/sweep.d.ts +13 -0
- package/dist/db/sweep.js +90 -0
- package/dist/db/txid.d.ts +7 -0
- package/dist/db/txid.js +12 -0
- package/dist/define.d.ts +3 -0
- package/dist/define.js +6 -0
- package/dist/endpoints/events.d.ts +7 -0
- package/dist/endpoints/events.js +98 -0
- package/dist/endpoints/index.d.ts +18 -0
- package/dist/endpoints/index.js +160 -0
- package/dist/endpoints/manifest.d.ts +7 -0
- package/dist/endpoints/manifest.js +30 -0
- package/dist/endpoints/preflight.d.ts +8 -0
- package/dist/endpoints/preflight.js +25 -0
- package/dist/endpoints/previewToken.d.ts +7 -0
- package/dist/endpoints/previewToken.js +54 -0
- package/dist/endpoints/recompute.d.ts +7 -0
- package/dist/endpoints/recompute.js +41 -0
- package/dist/endpoints/results.d.ts +24 -0
- package/dist/endpoints/results.js +136 -0
- package/dist/endpoints/summary.d.ts +7 -0
- package/dist/endpoints/summary.js +40 -0
- package/dist/endpoints/transitions.d.ts +42 -0
- package/dist/endpoints/transitions.js +154 -0
- package/dist/errors.d.ts +27 -0
- package/dist/errors.js +33 -0
- package/dist/events/body.d.ts +7 -0
- package/dist/events/body.js +43 -0
- package/dist/events/dimensions.d.ts +4 -0
- package/dist/events/dimensions.js +51 -0
- package/dist/events/preview.d.ts +2 -0
- package/dist/events/preview.js +9 -0
- package/dist/events/request.d.ts +3 -0
- package/dist/events/request.js +14 -0
- package/dist/events/rows.d.ts +7 -0
- package/dist/events/rows.js +65 -0
- package/dist/exports/adminClient.d.ts +16 -0
- package/dist/exports/adminClient.js +17 -0
- package/dist/exports/client.d.ts +7 -0
- package/dist/exports/client.js +7 -0
- package/dist/exports/index.d.ts +4 -0
- package/dist/exports/index.js +3 -0
- package/dist/exports/proxy.d.ts +4 -0
- package/dist/exports/proxy.js +3 -0
- package/dist/exports/puck.d.ts +4 -0
- package/dist/exports/puck.js +3 -0
- package/dist/exports/puckEditor.d.ts +1 -0
- package/dist/exports/puckEditor.js +2 -0
- package/dist/exports/rsc.d.ts +13 -0
- package/dist/exports/rsc.js +13 -0
- package/dist/exports/runtimeProxy.d.ts +4 -0
- package/dist/exports/runtimeProxy.js +2 -0
- package/dist/exports/runtimeServer.d.ts +2 -0
- package/dist/exports/runtimeServer.js +1 -0
- package/dist/fields/armSelect.d.ts +7 -0
- package/dist/fields/armSelect.js +33 -0
- package/dist/globals/state.d.ts +3 -0
- package/dist/globals/state.js +31 -0
- package/dist/hooks/contentChanges.d.ts +3 -0
- package/dist/hooks/contentChanges.js +10 -0
- package/dist/hooks/hostHook.d.ts +5 -0
- package/dist/hooks/hostHook.js +20 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -0
- package/dist/manifest/cache.d.ts +10 -0
- package/dist/manifest/cache.js +51 -0
- package/dist/manifest/compile.d.ts +63 -0
- package/dist/manifest/compile.js +237 -0
- package/dist/onInit.d.ts +5 -0
- package/dist/onInit.js +13 -0
- package/dist/options/fromReq.d.ts +3 -0
- package/dist/options/fromReq.js +5 -0
- package/dist/options.d.ts +9 -0
- package/dist/options.js +99 -0
- package/dist/plugin.d.ts +7 -0
- package/dist/plugin.js +62 -0
- package/dist/proxy/apply.d.ts +3 -0
- package/dist/proxy/apply.js +5 -0
- package/dist/proxy/consentFallback.d.ts +5 -0
- package/dist/proxy/consentFallback.js +27 -0
- package/dist/proxy/control.d.ts +6 -0
- package/dist/proxy/control.js +36 -0
- package/dist/proxy/createProxy.d.ts +3 -0
- package/dist/proxy/createProxy.js +15 -0
- package/dist/proxy/pageRewrite.d.ts +4 -0
- package/dist/proxy/pageRewrite.js +41 -0
- package/dist/proxy/resolve.d.ts +13 -0
- package/dist/proxy/resolve.js +163 -0
- package/dist/proxy/skip.d.ts +1 -0
- package/dist/proxy/skip.js +9 -0
- package/dist/puck/ExperimentPicker.d.ts +5 -0
- package/dist/puck/ExperimentPicker.js +88 -0
- package/dist/puck/ExperimentSlot.d.ts +3 -0
- package/dist/puck/ExperimentSlot.js +16 -0
- package/dist/puck/copy.d.ts +14 -0
- package/dist/puck/copy.js +14 -0
- package/dist/puck/editor/CreateExperimentDrawer.d.ts +5 -0
- package/dist/puck/editor/CreateExperimentDrawer.js +120 -0
- package/dist/puck/editor/ExperimentSlotEditor.d.ts +3 -0
- package/dist/puck/editor/ExperimentSlotEditor.js +225 -0
- package/dist/puck/editor/cloneSlot.d.ts +3 -0
- package/dist/puck/editor/cloneSlot.js +41 -0
- package/dist/puck/editor/copy.d.ts +25 -0
- package/dist/puck/editor/copy.js +25 -0
- package/dist/puck/editor/editor.model.d.ts +16 -0
- package/dist/puck/editor/editor.model.js +33 -0
- package/dist/puck/experimentSlotField.d.ts +5 -0
- package/dist/puck/experimentSlotField.js +13 -0
- package/dist/puck/slot.model.d.ts +2 -0
- package/dist/puck/slot.model.js +11 -0
- package/dist/puck/slotConfig.d.ts +5 -0
- package/dist/puck/slotConfig.js +25 -0
- package/dist/results/aggregate.d.ts +34 -0
- package/dist/results/aggregate.js +66 -0
- package/dist/results/analyse.d.ts +79 -0
- package/dist/results/analyse.js +378 -0
- package/dist/results/cuped.d.ts +16 -0
- package/dist/results/cuped.js +31 -0
- package/dist/results/decideGuard.d.ts +3 -0
- package/dist/results/decideGuard.js +8 -0
- package/dist/results/guardrails.d.ts +17 -0
- package/dist/results/guardrails.js +10 -0
- package/dist/results/history.d.ts +13 -0
- package/dist/results/history.js +39 -0
- package/dist/results/identityCredit.d.ts +16 -0
- package/dist/results/identityCredit.js +46 -0
- package/dist/results/power.d.ts +15 -0
- package/dist/results/power.js +60 -0
- package/dist/results/rolloutUnits.d.ts +17 -0
- package/dist/results/rolloutUnits.js +26 -0
- package/dist/results/snapshot.d.ts +9 -0
- package/dist/results/snapshot.js +154 -0
- package/dist/results/srm.d.ts +9 -0
- package/dist/results/srm.js +22 -0
- package/dist/results/verdict.d.ts +82 -0
- package/dist/results/verdict.js +85 -0
- package/dist/runtime/assign.d.ts +23 -0
- package/dist/runtime/assign.js +104 -0
- package/dist/runtime/bot.d.ts +1 -0
- package/dist/runtime/bot.js +7 -0
- package/dist/runtime/cookie.d.ts +17 -0
- package/dist/runtime/cookie.js +35 -0
- package/dist/runtime/eligibility.d.ts +22 -0
- package/dist/runtime/eligibility.js +20 -0
- package/dist/runtime/header.d.ts +17 -0
- package/dist/runtime/header.js +97 -0
- package/dist/runtime/loaders.d.ts +17 -0
- package/dist/runtime/loaders.js +29 -0
- package/dist/runtime/normalisePath.d.ts +7 -0
- package/dist/runtime/normalisePath.js +15 -0
- package/dist/runtime/previewToken.d.ts +9 -0
- package/dist/runtime/previewToken.js +23 -0
- package/dist/runtime/proxyRuntime.d.ts +2 -0
- package/dist/runtime/proxyRuntime.js +22 -0
- package/dist/runtime/seed.d.ts +2 -0
- package/dist/runtime/seed.js +4 -0
- package/dist/runtime/signing.d.ts +3 -0
- package/dist/runtime/signing.js +16 -0
- package/dist/runtime/targeting.d.ts +6 -0
- package/dist/runtime/targeting.js +9 -0
- package/dist/server/context.d.ts +10 -0
- package/dist/server/context.js +59 -0
- package/dist/server/eraseUser.d.ts +7 -0
- package/dist/server/eraseUser.js +4 -0
- package/dist/server/getAssignments.d.ts +9 -0
- package/dist/server/getAssignments.js +227 -0
- package/dist/server/getFlagPoint.d.ts +11 -0
- package/dist/server/getFlagPoint.js +36 -0
- package/dist/server/headerRevision.d.ts +6 -0
- package/dist/server/headerRevision.js +10 -0
- package/dist/server/linkIdentity.d.ts +9 -0
- package/dist/server/linkIdentity.js +34 -0
- package/dist/server/metadata.d.ts +7 -0
- package/dist/server/metadata.js +24 -0
- package/dist/server/previewDraft.d.ts +7 -0
- package/dist/server/previewDraft.js +51 -0
- package/dist/server/recordContentChange.d.ts +7 -0
- package/dist/server/recordContentChange.js +150 -0
- package/dist/server/recordExposures.d.ts +7 -0
- package/dist/server/recordExposures.js +56 -0
- package/dist/server/request.d.ts +7 -0
- package/dist/server/request.js +39 -0
- package/dist/server/resolveExperimentData.d.ts +6 -0
- package/dist/server/resolveExperimentData.js +64 -0
- package/dist/server/runResults.d.ts +22 -0
- package/dist/server/runResults.js +307 -0
- package/dist/server/serverRuntime.d.ts +7 -0
- package/dist/server/serverRuntime.js +20 -0
- package/dist/server/sitemap.d.ts +3 -0
- package/dist/server/sitemap.js +6 -0
- package/dist/server/toClientAssignments.d.ts +7 -0
- package/dist/server/toClientAssignments.js +56 -0
- package/dist/server/trackConversion.d.ts +12 -0
- package/dist/server/trackConversion.js +69 -0
- package/dist/targeting/ops.d.ts +8 -0
- package/dist/targeting/ops.js +58 -0
- package/dist/transitions/addMetric.d.ts +16 -0
- package/dist/transitions/addMetric.js +126 -0
- package/dist/transitions/allocate.d.ts +9 -0
- package/dist/transitions/allocate.js +84 -0
- package/dist/transitions/archive.d.ts +9 -0
- package/dist/transitions/archive.js +46 -0
- package/dist/transitions/checkStart.d.ts +61 -0
- package/dist/transitions/checkStart.js +104 -0
- package/dist/transitions/confirmPhrases.d.ts +5 -0
- package/dist/transitions/confirmPhrases.js +18 -0
- package/dist/transitions/decide.d.ts +9 -0
- package/dist/transitions/decide.js +138 -0
- package/dist/transitions/deleteExperiment.d.ts +5 -0
- package/dist/transitions/deleteExperiment.js +66 -0
- package/dist/transitions/kernel.d.ts +31 -0
- package/dist/transitions/kernel.js +69 -0
- package/dist/transitions/makeContent.d.ts +20 -0
- package/dist/transitions/makeContent.js +411 -0
- package/dist/transitions/phaseRecord.d.ts +27 -0
- package/dist/transitions/phaseRecord.js +81 -0
- package/dist/transitions/placements.d.ts +26 -0
- package/dist/transitions/placements.js +89 -0
- package/dist/transitions/preflightData.d.ts +21 -0
- package/dist/transitions/preflightData.js +317 -0
- package/dist/transitions/releaseHoldout.d.ts +8 -0
- package/dist/transitions/releaseHoldout.js +30 -0
- package/dist/transitions/resolveCaps.d.ts +28 -0
- package/dist/transitions/resolveCaps.js +68 -0
- package/dist/transitions/start.d.ts +26 -0
- package/dist/transitions/start.js +220 -0
- package/dist/transitions/stop.d.ts +8 -0
- package/dist/transitions/stop.js +25 -0
- package/dist/types/admin.d.ts +20 -0
- package/dist/types/admin.js +1 -0
- package/dist/types/assignments.d.ts +57 -0
- package/dist/types/assignments.js +1 -0
- package/dist/types/contracts.d.ts +133 -0
- package/dist/types/contracts.js +2 -0
- package/dist/types/endpoints.d.ts +135 -0
- package/dist/types/endpoints.js +1 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.js +8 -0
- package/dist/types/manifest.d.ts +42 -0
- package/dist/types/manifest.js +1 -0
- package/dist/types/puck.d.ts +23 -0
- package/dist/types/puck.js +1 -0
- package/dist/types/runtime.d.ts +59 -0
- package/dist/types/runtime.js +1 -0
- package/dist/types/snapshot.d.ts +117 -0
- package/dist/types/snapshot.js +1 -0
- package/package.json +137 -3
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { XpRefusal } from '../errors.js';
|
|
2
|
+
export async function readPhaseRecord(req, experimentId, phase) {
|
|
3
|
+
// §7.8: frozen configuration lookup serves an authorised transition.
|
|
4
|
+
const { docs } = await req.payload.find({
|
|
5
|
+
collection: 'xp-phases',
|
|
6
|
+
req,
|
|
7
|
+
overrideAccess: true,
|
|
8
|
+
depth: 0,
|
|
9
|
+
limit: 1,
|
|
10
|
+
where: {
|
|
11
|
+
and: [
|
|
12
|
+
{
|
|
13
|
+
experiment: {
|
|
14
|
+
equals: experimentId
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
phase: {
|
|
19
|
+
equals: phase
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
if (!docs[0]) throw new XpRefusal('not-found', 'The phase record was not found.', {
|
|
26
|
+
experimentId,
|
|
27
|
+
phase
|
|
28
|
+
}, 404);
|
|
29
|
+
return docs[0];
|
|
30
|
+
}
|
|
31
|
+
/** Caller holds the experiment lock and passes the kernel's transaction req. */ export async function writePhaseRecord(req, record) {
|
|
32
|
+
// §7.8: transition endpoints' writes to plugin collections.
|
|
33
|
+
return req.payload.create({
|
|
34
|
+
collection: 'xp-phases',
|
|
35
|
+
data: record,
|
|
36
|
+
req,
|
|
37
|
+
overrideAccess: true
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/** End only an open phase, preserving the original stop time on a later decide.
|
|
41
|
+
* Caller holds the experiment lock and passes the kernel's transaction req.
|
|
42
|
+
*/ export async function setPhaseEndedAt(req, experimentId, phase, at) {
|
|
43
|
+
// §7.8: phase lookup for transition endpoints' writes to plugin collections.
|
|
44
|
+
const { docs } = await req.payload.find({
|
|
45
|
+
collection: 'xp-phases',
|
|
46
|
+
req,
|
|
47
|
+
limit: 1,
|
|
48
|
+
depth: 0,
|
|
49
|
+
where: {
|
|
50
|
+
and: [
|
|
51
|
+
{
|
|
52
|
+
experiment: {
|
|
53
|
+
equals: experimentId
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
phase: {
|
|
58
|
+
equals: phase
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
overrideAccess: true
|
|
64
|
+
});
|
|
65
|
+
const record = docs[0];
|
|
66
|
+
if (!record) throw new XpRefusal('not-found', 'The phase record was not found.', {
|
|
67
|
+
experimentId,
|
|
68
|
+
phase
|
|
69
|
+
}, 404);
|
|
70
|
+
if (record.endedAt) return record;
|
|
71
|
+
// §7.8: transition endpoints' writes to plugin collections.
|
|
72
|
+
return req.payload.update({
|
|
73
|
+
collection: 'xp-phases',
|
|
74
|
+
req,
|
|
75
|
+
id: record.id,
|
|
76
|
+
data: {
|
|
77
|
+
endedAt: at
|
|
78
|
+
},
|
|
79
|
+
overrideAccess: true
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PayloadRequest } from 'payload';
|
|
2
|
+
import type { Arm, DocId, FlagPoint, ResolvedOptions } from '../types/contracts.js';
|
|
3
|
+
import type { ArmSlot } from '../types/puck.js';
|
|
4
|
+
export interface BlockPlacement {
|
|
5
|
+
collection: string;
|
|
6
|
+
id: DocId;
|
|
7
|
+
title: string;
|
|
8
|
+
published: boolean;
|
|
9
|
+
slots: Record<ArmSlot, number>;
|
|
10
|
+
locked?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export type FlagPlacement = Awaited<ReturnType<NonNullable<FlagPoint['placements']>>>[number];
|
|
13
|
+
/** Each matching ExperimentSlot is checked separately, including multiple slots
|
|
14
|
+
* in one document: content elsewhere in the document cannot hide an empty slot.
|
|
15
|
+
*/
|
|
16
|
+
export declare function scanBlockPlacements(req: PayloadRequest, { slug, puckCollections }: {
|
|
17
|
+
slug: string;
|
|
18
|
+
puckCollections: NonNullable<ResolvedOptions['puck']>['collections'];
|
|
19
|
+
}): Promise<BlockPlacement[]>;
|
|
20
|
+
export declare function flagPlacements<TUser extends {
|
|
21
|
+
id: DocId;
|
|
22
|
+
}>(options: ResolvedOptions<TUser>, experiment: {
|
|
23
|
+
id: DocId;
|
|
24
|
+
flagPoint?: string | null;
|
|
25
|
+
arms: Arm[];
|
|
26
|
+
}, req: PayloadRequest): Promise<FlagPlacement[]>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { puckCollections as normalisePuckCollections } from '../options.js';
|
|
2
|
+
const slots = [
|
|
3
|
+
'control',
|
|
4
|
+
'variantA',
|
|
5
|
+
'variantB',
|
|
6
|
+
'variantC'
|
|
7
|
+
];
|
|
8
|
+
/** Each matching ExperimentSlot is checked separately, including multiple slots
|
|
9
|
+
* in one document: content elsewhere in the document cannot hide an empty slot.
|
|
10
|
+
*/ export async function scanBlockPlacements(req, { slug, puckCollections }) {
|
|
11
|
+
const placements = [];
|
|
12
|
+
for (const { slug: collection, field } of normalisePuckCollections({
|
|
13
|
+
puck: {
|
|
14
|
+
enabled: true,
|
|
15
|
+
collections: puckCollections
|
|
16
|
+
}
|
|
17
|
+
})){
|
|
18
|
+
const publishedIds = new Set();
|
|
19
|
+
const draftsEnabled = req.payload.config.collections?.some((config)=>config.slug === collection && config.versions && config.versions.drafts);
|
|
20
|
+
for (const draft of [
|
|
21
|
+
false,
|
|
22
|
+
true
|
|
23
|
+
]){
|
|
24
|
+
let page = 1;
|
|
25
|
+
while(true){
|
|
26
|
+
const result = await req.payload.find({
|
|
27
|
+
collection,
|
|
28
|
+
req,
|
|
29
|
+
overrideAccess: false,
|
|
30
|
+
limit: 100,
|
|
31
|
+
page,
|
|
32
|
+
depth: 0,
|
|
33
|
+
draft,
|
|
34
|
+
includeLockStatus: true,
|
|
35
|
+
...!draft && draftsEnabled ? {
|
|
36
|
+
where: {
|
|
37
|
+
_status: {
|
|
38
|
+
equals: 'published'
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
} : {}
|
|
42
|
+
});
|
|
43
|
+
for (const doc of result.docs){
|
|
44
|
+
if (!draft && doc._status === 'draft') continue;
|
|
45
|
+
if (draft && doc._status !== 'draft' && publishedIds.has(String(doc.id))) continue;
|
|
46
|
+
if (!draft) publishedIds.add(String(doc.id));
|
|
47
|
+
function walk(value) {
|
|
48
|
+
if (!value || typeof value !== 'object') return;
|
|
49
|
+
if (Array.isArray(value)) {
|
|
50
|
+
value.forEach(walk);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const node = value;
|
|
54
|
+
const props = node.props;
|
|
55
|
+
const ref = props?.experiment;
|
|
56
|
+
const experiment = typeof ref === 'string' ? ref : ref && typeof ref === 'object' && 'slug' in ref ? ref.slug : null;
|
|
57
|
+
if (node.type === 'ExperimentSlot' && props && experiment === slug) {
|
|
58
|
+
placements.push({
|
|
59
|
+
collection,
|
|
60
|
+
id: doc.id,
|
|
61
|
+
title: String(doc.title ?? doc.name ?? doc.id),
|
|
62
|
+
published: !draft || doc._status === 'published',
|
|
63
|
+
slots: Object.fromEntries(slots.map((slot)=>[
|
|
64
|
+
slot,
|
|
65
|
+
Array.isArray(props[slot]) ? props[slot].length : 0
|
|
66
|
+
])),
|
|
67
|
+
locked: doc._isLocked === true || doc.locked === true
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
Object.values(node).forEach(walk);
|
|
71
|
+
}
|
|
72
|
+
walk(doc[field]);
|
|
73
|
+
}
|
|
74
|
+
if (!result.hasNextPage) break;
|
|
75
|
+
page++;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return placements;
|
|
80
|
+
}
|
|
81
|
+
export async function flagPlacements(options, experiment, req) {
|
|
82
|
+
const point = options.flagPoints?.find((point)=>point.id === experiment.flagPoint);
|
|
83
|
+
if (!point?.placements) return [];
|
|
84
|
+
return point.placements({
|
|
85
|
+
experimentId: experiment.id,
|
|
86
|
+
arms: experiment.arms.filter((arm)=>arm.kind !== 'holdout').map((arm)=>arm.key),
|
|
87
|
+
req
|
|
88
|
+
});
|
|
89
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { PayloadRequest } from 'payload';
|
|
2
|
+
import type { PowerWeeksInput, PowerWeeksResult } from '@delmaredigital/experiments-stats';
|
|
3
|
+
import type { DocId, ResolvedOptions } from '../types/contracts.js';
|
|
4
|
+
import type { PreflightResult } from '../types/endpoints.js';
|
|
5
|
+
import type { CheckStartInput } from './checkStart.js';
|
|
6
|
+
export interface PreflightDependencies {
|
|
7
|
+
now?: Date;
|
|
8
|
+
estimate?: (input: PowerWeeksInput) => PowerWeeksResult | Promise<PowerWeeksResult>;
|
|
9
|
+
}
|
|
10
|
+
/** Read-only inputs shared with Start's pre-transaction checks. Start must re-read
|
|
11
|
+
* the document and lock-sensitive facts inside its transition (CORE-10b).
|
|
12
|
+
*/
|
|
13
|
+
export declare function loadStartInputs<TUser extends {
|
|
14
|
+
id: DocId;
|
|
15
|
+
}>(req: PayloadRequest, options: ResolvedOptions<TUser>, experimentId: DocId): Promise<CheckStartInput | null>;
|
|
16
|
+
export declare function preflightData<TUser extends {
|
|
17
|
+
id: DocId;
|
|
18
|
+
}>(req: PayloadRequest, { experimentId, options }: {
|
|
19
|
+
experimentId: DocId;
|
|
20
|
+
options: ResolvedOptions<TUser>;
|
|
21
|
+
}, { now, estimate }?: PreflightDependencies): Promise<PreflightResult | null>;
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { powerMetricWeeks } from '@delmaredigital/experiments-stats';
|
|
2
|
+
import { checkStart, relationshipId } from './checkStart.js';
|
|
3
|
+
import { flagPlacements, scanBlockPlacements } from './placements.js';
|
|
4
|
+
const failureDetail = (cause)=>cause instanceof Error ? cause.message : 'Placement lookup failed.';
|
|
5
|
+
/** Read-only inputs shared with Start's pre-transaction checks. Start must re-read
|
|
6
|
+
* the document and lock-sensitive facts inside its transition (CORE-10b).
|
|
7
|
+
*/ export async function loadStartInputs(req, options, experimentId) {
|
|
8
|
+
const found = await req.payload.find({
|
|
9
|
+
collection: 'xp-experiments',
|
|
10
|
+
req,
|
|
11
|
+
overrideAccess: false,
|
|
12
|
+
depth: 0,
|
|
13
|
+
limit: 1,
|
|
14
|
+
where: {
|
|
15
|
+
id: {
|
|
16
|
+
equals: experimentId
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
if (!found.docs[0]) return null;
|
|
21
|
+
const experiment = found.docs[0];
|
|
22
|
+
const ids = [
|
|
23
|
+
experiment.metrics?.primary,
|
|
24
|
+
...experiment.metrics?.secondary ?? [],
|
|
25
|
+
...experiment.metrics?.guardrails ?? []
|
|
26
|
+
].map(relationshipId).filter((id)=>id !== undefined);
|
|
27
|
+
const metrics = [];
|
|
28
|
+
if (ids.length) {
|
|
29
|
+
let page = 1;
|
|
30
|
+
while(true){
|
|
31
|
+
const result = await req.payload.find({
|
|
32
|
+
collection: 'xp-metrics',
|
|
33
|
+
req,
|
|
34
|
+
overrideAccess: false,
|
|
35
|
+
depth: 0,
|
|
36
|
+
limit: 100,
|
|
37
|
+
page,
|
|
38
|
+
where: {
|
|
39
|
+
id: {
|
|
40
|
+
in: ids
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
metrics.push(...result.docs);
|
|
45
|
+
if (!result.hasNextPage) break;
|
|
46
|
+
page++;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const input = {
|
|
50
|
+
experiment,
|
|
51
|
+
metrics,
|
|
52
|
+
metricSources: options.metricSources,
|
|
53
|
+
flagPoints: options.flagPoints,
|
|
54
|
+
attributeKeys: options.attributeKeys
|
|
55
|
+
};
|
|
56
|
+
if (experiment.kind === 'block') {
|
|
57
|
+
try {
|
|
58
|
+
input.placements = await scanBlockPlacements(req, {
|
|
59
|
+
slug: experiment.slug,
|
|
60
|
+
puckCollections: options.puck?.collections ?? []
|
|
61
|
+
});
|
|
62
|
+
} catch (cause) {
|
|
63
|
+
input.placementError = failureDetail(cause);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (experiment.kind === 'flag') {
|
|
67
|
+
const point = options.flagPoints?.find((point)=>point.id === experiment.flagPoint);
|
|
68
|
+
if (point) {
|
|
69
|
+
// Match the save hook: the control always uses effective point defaults.
|
|
70
|
+
let params = point.defaultParams;
|
|
71
|
+
try {
|
|
72
|
+
const state = await req.payload.findGlobal({
|
|
73
|
+
slug: 'xp-state',
|
|
74
|
+
req,
|
|
75
|
+
overrideAccess: false,
|
|
76
|
+
depth: 0
|
|
77
|
+
});
|
|
78
|
+
const defaults = state.flagDefaults;
|
|
79
|
+
params = defaults?.[point.id]?.params ?? params;
|
|
80
|
+
} catch (cause) {
|
|
81
|
+
const error = cause;
|
|
82
|
+
if (error?.status !== 403 && error?.name !== 'Forbidden') throw cause;
|
|
83
|
+
}
|
|
84
|
+
experiment.arms = experiment.arms.map((arm)=>arm.kind === 'control' ? {
|
|
85
|
+
...arm,
|
|
86
|
+
params
|
|
87
|
+
} : arm);
|
|
88
|
+
}
|
|
89
|
+
if (point?.placements) {
|
|
90
|
+
try {
|
|
91
|
+
const placements = await flagPlacements(options, experiment, req);
|
|
92
|
+
if (Array.isArray(placements) && placements.every((p)=>p && typeof p === 'object' && typeof p.arm === 'string')) input.flagPlacements = placements;
|
|
93
|
+
else input.flagPlacementError = 'Placement lookup returned an invalid result.';
|
|
94
|
+
} catch (cause) {
|
|
95
|
+
input.flagPlacementError = failureDetail(cause);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (experiment.flagPoint) {
|
|
99
|
+
// At most one row can be this experiment; two rows suffice to find a peer.
|
|
100
|
+
const busy = await req.payload.find({
|
|
101
|
+
collection: 'xp-experiments',
|
|
102
|
+
req,
|
|
103
|
+
overrideAccess: false,
|
|
104
|
+
depth: 0,
|
|
105
|
+
limit: 2,
|
|
106
|
+
where: {
|
|
107
|
+
and: [
|
|
108
|
+
{
|
|
109
|
+
flagPoint: {
|
|
110
|
+
equals: experiment.flagPoint
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
status: {
|
|
115
|
+
in: [
|
|
116
|
+
'running',
|
|
117
|
+
'decided'
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
input.flagPointBusy = busy.docs.some((doc)=>String(doc.id) !== String(experiment.id));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const layerId = relationshipId(experiment.layer);
|
|
128
|
+
if (layerId !== undefined) {
|
|
129
|
+
const layers = await req.payload.find({
|
|
130
|
+
collection: 'xp-layers',
|
|
131
|
+
req,
|
|
132
|
+
overrideAccess: false,
|
|
133
|
+
depth: 0,
|
|
134
|
+
limit: 1,
|
|
135
|
+
where: {
|
|
136
|
+
id: {
|
|
137
|
+
equals: layerId
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
const layer = layers.docs[0];
|
|
142
|
+
const usedSlices = [];
|
|
143
|
+
let page = 1;
|
|
144
|
+
while(true){
|
|
145
|
+
const result = await req.payload.find({
|
|
146
|
+
collection: 'xp-experiments',
|
|
147
|
+
req,
|
|
148
|
+
overrideAccess: false,
|
|
149
|
+
depth: 0,
|
|
150
|
+
limit: 100,
|
|
151
|
+
page,
|
|
152
|
+
where: {
|
|
153
|
+
and: [
|
|
154
|
+
{
|
|
155
|
+
layer: {
|
|
156
|
+
equals: layerId
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
status: {
|
|
161
|
+
in: [
|
|
162
|
+
'running',
|
|
163
|
+
'decided'
|
|
164
|
+
]
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
]
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
for (const doc of result.docs)if (Array.isArray(doc.layerSlices)) usedSlices.push(...doc.layerSlices);
|
|
171
|
+
if (!result.hasNextPage) break;
|
|
172
|
+
page++;
|
|
173
|
+
}
|
|
174
|
+
input.layer = layer ? {
|
|
175
|
+
unitType: layer.unitType,
|
|
176
|
+
usedSlices
|
|
177
|
+
} : null;
|
|
178
|
+
}
|
|
179
|
+
return input;
|
|
180
|
+
}
|
|
181
|
+
async function primaryHistory(source, now) {
|
|
182
|
+
if (!source) return null;
|
|
183
|
+
const from = new Date(now.getTime() - 28 * 86400000);
|
|
184
|
+
try {
|
|
185
|
+
const rows = await source.query({
|
|
186
|
+
from,
|
|
187
|
+
to: now
|
|
188
|
+
});
|
|
189
|
+
const units = new Map();
|
|
190
|
+
const dailyUnits = new Set();
|
|
191
|
+
for (const row of rows){
|
|
192
|
+
if (!Number.isFinite(row.value) || !Number.isFinite(new Date(row.ts).getTime())) return null;
|
|
193
|
+
if (new Date(row.ts) < from || new Date(row.ts) >= now) continue;
|
|
194
|
+
units.set(row.unitId, (units.get(row.unitId) ?? 0) + row.value);
|
|
195
|
+
dailyUnits.add(JSON.stringify([
|
|
196
|
+
new Date(row.ts).toISOString().slice(0, 10),
|
|
197
|
+
row.unitId
|
|
198
|
+
]));
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
values: [
|
|
202
|
+
...units.values()
|
|
203
|
+
],
|
|
204
|
+
dailyConverters: dailyUnits.size
|
|
205
|
+
};
|
|
206
|
+
} catch {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
export async function preflightData(req, { experimentId, options }, { now = new Date(), estimate = powerMetricWeeks } = {}) {
|
|
211
|
+
const input = await loadStartInputs(req, options, experimentId);
|
|
212
|
+
if (!input) return null;
|
|
213
|
+
const { experiment: e } = input;
|
|
214
|
+
const primaryId = relationshipId(e.metrics?.primary);
|
|
215
|
+
const metric = input.metrics.find((metric)=>String(metric.id) === String(primaryId));
|
|
216
|
+
const source = options.metricSources?.find((source)=>source.id === metric?.source);
|
|
217
|
+
const historical = await primaryHistory(source, now);
|
|
218
|
+
const history = historical?.values ?? null;
|
|
219
|
+
const reach = e.analysis?.reachPerDay;
|
|
220
|
+
const trafficPerDay = typeof reach === 'number' && Number.isFinite(reach) && reach > 0 ? reach : null;
|
|
221
|
+
const value = history === null ? null : source?.kind === 'binomial' ? trafficPerDay === null ? null : historical.dailyConverters / 28 / trafficPerDay : history.length ? history.reduce((sum, value)=>sum + value, 0) / history.length : 0;
|
|
222
|
+
// A rate of 1 or more means reachPerDay is below the converters; no honest baseline then.
|
|
223
|
+
const baseline = primaryId === undefined || value === null || source?.kind === 'binomial' && value >= 1 ? null : {
|
|
224
|
+
metricId: primaryId,
|
|
225
|
+
value,
|
|
226
|
+
days: 28
|
|
227
|
+
};
|
|
228
|
+
let power = null;
|
|
229
|
+
if (trafficPerDay !== null && baseline && source && history) {
|
|
230
|
+
const compared = e.arms.filter((arm)=>arm.kind !== 'holdout');
|
|
231
|
+
const control = compared.find((arm)=>arm.kind === 'control');
|
|
232
|
+
const variant = compared.find((arm)=>arm.kind === 'variant');
|
|
233
|
+
const savedDuration = e.analysis?.plannedDurationDays && e.analysis.plannedDurationDays > 0 ? e.analysis.plannedDurationDays : undefined;
|
|
234
|
+
const planned = savedDuration ?? 14;
|
|
235
|
+
const enrolled = trafficPerDay * e.allocation / 100;
|
|
236
|
+
const pairShare = ((control?.weight ?? 0) + (variant?.weight ?? 0)) / 100;
|
|
237
|
+
const tuning = enrolled * planned * pairShare;
|
|
238
|
+
// Power's equal-arm model uses the smallest compared arm, so unequal weights
|
|
239
|
+
// and holdout traffic never overstate the information in a comparison.
|
|
240
|
+
const usersPerWeek = enrolled * 7 * Math.min(...compared.map((arm)=>arm.weight / 100)) * compared.length;
|
|
241
|
+
const n = Math.max(trafficPerDay * 28, history.length);
|
|
242
|
+
const mean = history.reduce((sum, value)=>sum + value, 0) / n;
|
|
243
|
+
const variance = history.reduce((sum, value)=>sum + value * value, 0) / n - mean * mean;
|
|
244
|
+
const conversionRate = baseline.value;
|
|
245
|
+
// v1 planning assumption: 10% relative lift at 80% power, alpha fixed at 5%.
|
|
246
|
+
const params = source.kind === 'binomial' ? {
|
|
247
|
+
type: 'binomial',
|
|
248
|
+
conversionRate,
|
|
249
|
+
effectSize: 0.1
|
|
250
|
+
} : {
|
|
251
|
+
type: 'mean',
|
|
252
|
+
mean,
|
|
253
|
+
standardDeviation: Math.sqrt(Math.max(0, variance)),
|
|
254
|
+
effectSize: 0.1
|
|
255
|
+
};
|
|
256
|
+
if (control && variant && usersPerWeek > 0 && tuning > 0 && (source.kind !== 'binomial' || conversionRate > 0 && conversionRate < 1) && (source.kind === 'binomial' || mean > 0)) {
|
|
257
|
+
try {
|
|
258
|
+
const alpha = e.analysis?.method === 'bayesian' ? 0.05 : 0.05 / Math.max(1, compared.filter((arm)=>arm.kind === 'variant').length);
|
|
259
|
+
const estimateAt = (sequentialTuningParameter)=>estimate({
|
|
260
|
+
metrics: {
|
|
261
|
+
primary: params
|
|
262
|
+
},
|
|
263
|
+
nVariations: compared.length,
|
|
264
|
+
nWeeks: 13,
|
|
265
|
+
alpha,
|
|
266
|
+
usersPerWeek,
|
|
267
|
+
targetPower: 0.8,
|
|
268
|
+
method: e.analysis?.method === 'bayesian' ? {
|
|
269
|
+
type: 'bayesian'
|
|
270
|
+
} : {
|
|
271
|
+
type: 'frequentist',
|
|
272
|
+
sequentialTesting: sequentialTuningParameter
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
let result = await estimateAt(tuning);
|
|
276
|
+
let weeks = result.sampleSizeAndRuntime.primary?.weeks ?? result.weekThreshold ?? 13;
|
|
277
|
+
const plannedDurationDays = savedDuration ?? Math.ceil(weeks * 7);
|
|
278
|
+
const sequentialTuningParameter = enrolled * plannedDurationDays * pairShare;
|
|
279
|
+
if (savedDuration === undefined) {
|
|
280
|
+
// Re-evaluate using exactly the duration and tuning Review will prefill.
|
|
281
|
+
result = await estimateAt(sequentialTuningParameter);
|
|
282
|
+
weeks = result.sampleSizeAndRuntime.primary?.weeks ?? result.weekThreshold ?? 13;
|
|
283
|
+
}
|
|
284
|
+
power = {
|
|
285
|
+
weeks,
|
|
286
|
+
plannedDurationDays,
|
|
287
|
+
sequentialTuningParameter,
|
|
288
|
+
feasible: Number.isFinite(weeks) && weeks * 7 <= 90 && result.sampleSizeAndRuntime.primary !== undefined
|
|
289
|
+
};
|
|
290
|
+
} catch {}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const requiredSlots = e.arms.filter((arm)=>arm.kind !== 'holdout').map((arm)=>arm.slot);
|
|
294
|
+
const placements = (input.placements ?? []).map((placement)=>({
|
|
295
|
+
collection: placement.collection,
|
|
296
|
+
id: placement.id,
|
|
297
|
+
title: placement.title,
|
|
298
|
+
ok: requiredSlots.every((slot)=>slot && placement.slots[slot] > 0),
|
|
299
|
+
detail: placement.published ? 'Published' : 'Draft'
|
|
300
|
+
}));
|
|
301
|
+
for (const placement of input.flagPlacements ?? [])placements.push({
|
|
302
|
+
collection: e.flagPoint ?? '',
|
|
303
|
+
id: placement.arm,
|
|
304
|
+
title: placement.arm,
|
|
305
|
+
ok: placement.ok,
|
|
306
|
+
...placement.detail ? {
|
|
307
|
+
detail: placement.detail
|
|
308
|
+
} : {}
|
|
309
|
+
});
|
|
310
|
+
return {
|
|
311
|
+
checks: checkStart(input),
|
|
312
|
+
placements,
|
|
313
|
+
baseline,
|
|
314
|
+
trafficPerDay,
|
|
315
|
+
estimate: power
|
|
316
|
+
};
|
|
317
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PayloadRequest } from 'payload';
|
|
2
|
+
import type { DocId } from '../types/contracts.js';
|
|
3
|
+
export declare function releaseHoldout(req: PayloadRequest, { experimentId }: {
|
|
4
|
+
experimentId: DocId;
|
|
5
|
+
}): Promise<{
|
|
6
|
+
doc: import("payload").JsonObject & import("payload").TypeWithID;
|
|
7
|
+
revision: number;
|
|
8
|
+
}>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { relationshipId } from './checkStart.js';
|
|
2
|
+
import { withTransition } from './kernel.js';
|
|
3
|
+
import { readPhaseRecord, setPhaseEndedAt } from './phaseRecord.js';
|
|
4
|
+
import { readExperiment, refuse, updateExperiment } from './start.js';
|
|
5
|
+
export async function releaseHoldout(req, { experimentId }) {
|
|
6
|
+
const before = await readExperiment(req, experimentId);
|
|
7
|
+
const { result: doc, revision } = await withTransition(req, {
|
|
8
|
+
experimentId,
|
|
9
|
+
layerId: relationshipId(before.layer)
|
|
10
|
+
}, async ({ req })=>{
|
|
11
|
+
const current = await readExperiment(req, experimentId);
|
|
12
|
+
const decision = current.decision;
|
|
13
|
+
if (current.status !== 'decided' || !decision?.winnerArm || decision.holdoutReleasedAt) refuse('invalid-status');
|
|
14
|
+
const phase = await readPhaseRecord(req, experimentId, current.phase);
|
|
15
|
+
if (phase.phaseKind !== 'rollout' || phase.endedAt) refuse('invalid-status');
|
|
16
|
+
const at = new Date().toISOString();
|
|
17
|
+
await setPhaseEndedAt(req, experimentId, phase.phase, at);
|
|
18
|
+
return updateExperiment(req, experimentId, {
|
|
19
|
+
layerSlices: [],
|
|
20
|
+
decision: {
|
|
21
|
+
...decision,
|
|
22
|
+
holdoutReleasedAt: at
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
return {
|
|
27
|
+
doc,
|
|
28
|
+
revision: revision
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { DocId, MetricSource } from '../types/contracts.js';
|
|
2
|
+
export interface MetricDefinition extends Record<string, unknown> {
|
|
3
|
+
id: DocId;
|
|
4
|
+
source: string;
|
|
5
|
+
attributionWindowHours?: number | null;
|
|
6
|
+
minSampleSize?: number | null;
|
|
7
|
+
guardrailThreshold?: number | null;
|
|
8
|
+
capping?: {
|
|
9
|
+
type?: 'percentile' | 'absolute' | null;
|
|
10
|
+
value?: number | null;
|
|
11
|
+
} | null;
|
|
12
|
+
}
|
|
13
|
+
/** Resolve revenue caps before taking transition locks. Host history failures
|
|
14
|
+
* disable capping for that metric and travel back as an explanatory detail.
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolveCaps(metrics: MetricDefinition[], sources: MetricSource[], start: Date): Promise<{
|
|
17
|
+
definitions: (MetricDefinition & {
|
|
18
|
+
kind?: MetricSource["kind"];
|
|
19
|
+
keyedBy?: MetricSource["keyedBy"];
|
|
20
|
+
resolvedCap: number | null;
|
|
21
|
+
historyUnits?: number;
|
|
22
|
+
capNotice?: "no-history";
|
|
23
|
+
})[];
|
|
24
|
+
detail: {
|
|
25
|
+
metricId: DocId;
|
|
26
|
+
message: string;
|
|
27
|
+
}[];
|
|
28
|
+
}>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { resolvePercentileCap } from '@delmaredigital/experiments-stats';
|
|
2
|
+
import { perUnitHistory } from '../results/history.js';
|
|
3
|
+
/** Resolve revenue caps before taking transition locks. Host history failures
|
|
4
|
+
* disable capping for that metric and travel back as an explanatory detail.
|
|
5
|
+
*/ export async function resolveCaps(metrics, sources, start) {
|
|
6
|
+
const detail = [];
|
|
7
|
+
const definitions = [];
|
|
8
|
+
const from = new Date(start.getTime() - 90 * 86400000);
|
|
9
|
+
for (const metric of metrics){
|
|
10
|
+
const source = sources.find((source)=>source.id === metric.source);
|
|
11
|
+
const kind = metric.source.startsWith('events:') ? 'binomial' : source?.kind;
|
|
12
|
+
const definition = {
|
|
13
|
+
...structuredClone(metric),
|
|
14
|
+
...source ? {
|
|
15
|
+
keyedBy: source.keyedBy
|
|
16
|
+
} : {},
|
|
17
|
+
kind,
|
|
18
|
+
resolvedCap: null,
|
|
19
|
+
historyUnits: undefined,
|
|
20
|
+
capNotice: undefined
|
|
21
|
+
};
|
|
22
|
+
const cap = metric.capping?.type ? metric.capping : kind === 'revenue' ? {
|
|
23
|
+
type: 'percentile',
|
|
24
|
+
value: 0.99
|
|
25
|
+
} : null;
|
|
26
|
+
if (kind === 'revenue' || !source && cap && !metric.source.startsWith('events:')) {
|
|
27
|
+
definition.capping = cap;
|
|
28
|
+
if (cap?.type === 'absolute' && typeof cap.value === 'number' && Number.isFinite(cap.value) && cap.value >= 0) {
|
|
29
|
+
definition.resolvedCap = cap.value;
|
|
30
|
+
} else if (cap?.type === 'percentile') {
|
|
31
|
+
definition.historyUnits = 0;
|
|
32
|
+
try {
|
|
33
|
+
if (!source) throw new Error('Metric source unavailable');
|
|
34
|
+
const rows = (await source.query({
|
|
35
|
+
from,
|
|
36
|
+
to: start
|
|
37
|
+
})).filter((row)=>new Date(row.ts) >= from && new Date(row.ts) < start);
|
|
38
|
+
if (rows.some((row)=>!Number.isFinite(row.value))) throw new Error('Invalid metric history');
|
|
39
|
+
const history = perUnitHistory(rows, {
|
|
40
|
+
windowHours: metric.attributionWindowHours ?? 168
|
|
41
|
+
});
|
|
42
|
+
definition.historyUnits = history.units;
|
|
43
|
+
if (history.units >= 20) {
|
|
44
|
+
// The collection permits 1 (the maximum); the stats percentile API requires p < 1.
|
|
45
|
+
definition.resolvedCap = cap.value === 1 ? Math.max(...history.values) : resolvePercentileCap(history.values, cap.value ?? 0.99);
|
|
46
|
+
} else {
|
|
47
|
+
definition.capNotice = 'no-history';
|
|
48
|
+
detail.push({
|
|
49
|
+
metricId: metric.id,
|
|
50
|
+
message: 'Fewer than 20 history units; no cap was applied.'
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
definition.capNotice = 'no-history';
|
|
55
|
+
detail.push({
|
|
56
|
+
metricId: metric.id,
|
|
57
|
+
message: 'Metric history is unavailable; no cap was applied.'
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
definitions.push(definition);
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
definitions,
|
|
66
|
+
detail
|
|
67
|
+
};
|
|
68
|
+
}
|