@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
package/dist/db/state.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { COLUMNS, TABLES } from './names.js';
|
|
2
|
+
const columns = COLUMNS.state;
|
|
3
|
+
const quote = (name)=>`"${name.replaceAll('"', '""')}"`;
|
|
4
|
+
function table(exec, options = {}) {
|
|
5
|
+
const schemaName = options.schemaName ?? exec.schemaName;
|
|
6
|
+
return `${schemaName ? `${quote(schemaName)}.` : ''}${quote(TABLES.state)}`;
|
|
7
|
+
}
|
|
8
|
+
/** Always take this lock last: layer → flag point → experiment → state. */ export async function bumpRevision(exec) {
|
|
9
|
+
await exec.query("SELECT pg_advisory_xact_lock(hashtext('xp:state'), 0)");
|
|
10
|
+
const updated = await exec.query(`UPDATE ${table(exec)} SET ${quote(columns.revision)} = COALESCE(${quote(columns.revision)}, 0) + 1, ${quote(columns.updatedAt)} = now() RETURNING ${quote(columns.revision)}`);
|
|
11
|
+
if (updated.rows[0]) return updated.rows[0].revision;
|
|
12
|
+
const inserted = await exec.query(`INSERT INTO ${table(exec)} (${quote(columns.revision)}, ${quote(columns.createdAt)}, ${quote(columns.updatedAt)}) VALUES (1, now(), now()) RETURNING ${quote(columns.revision)}`);
|
|
13
|
+
return inserted.rows[0].revision;
|
|
14
|
+
}
|
|
15
|
+
export async function writeManifest(exec, manifest) {
|
|
16
|
+
await exec.query(`UPDATE ${table(exec)} SET ${quote(columns.manifest)} = $1::jsonb, ${quote(columns.updatedAt)} = now()`, [
|
|
17
|
+
JSON.stringify(manifest)
|
|
18
|
+
]);
|
|
19
|
+
}
|
|
20
|
+
export async function readState(exec, options = {}) {
|
|
21
|
+
const result = await exec.query(`SELECT ${quote(columns.revision)}, ${quote(columns.manifest)}, ${quote(columns.flagDefaults)} AS "flagDefaults" FROM ${table(exec, options)} LIMIT 1`);
|
|
22
|
+
const row = result.rows[0];
|
|
23
|
+
return row ? {
|
|
24
|
+
...row,
|
|
25
|
+
flagDefaults: row.flagDefaults ?? {}
|
|
26
|
+
} : null;
|
|
27
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Executor } from './executor.js';
|
|
2
|
+
/** §5.5 / §7.8: privileged retention sweep. Deleted experiments' FKs become
|
|
3
|
+
* NULL; their exposure and arm-mismatch rows are handled by the two NULL branches. Global
|
|
4
|
+
* conversions stay while an open/recent experiment could still need them. */
|
|
5
|
+
export declare function sweepEvents(exec: Executor, { now, retentionDays, batchSize, shouldContinue, logger }: {
|
|
6
|
+
now: Date;
|
|
7
|
+
retentionDays?: number;
|
|
8
|
+
batchSize?: number;
|
|
9
|
+
shouldContinue?: () => boolean;
|
|
10
|
+
logger?: {
|
|
11
|
+
warn(message: string): void;
|
|
12
|
+
};
|
|
13
|
+
}): Promise<number>;
|
package/dist/db/sweep.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { COLUMNS, TABLES } from './names.js';
|
|
2
|
+
import { qualifiedTable, quoteIdentifier as q } from './sql.js';
|
|
3
|
+
import { ResultsBudgetError } from './pgExecutor.js';
|
|
4
|
+
/** §5.5 / §7.8: privileged retention sweep. Deleted experiments' FKs become
|
|
5
|
+
* NULL; their exposure and arm-mismatch rows are handled by the two NULL branches. Global
|
|
6
|
+
* conversions stay while an open/recent experiment could still need them. */ export async function sweepEvents(exec, { now, retentionDays = 180, batchSize = 1000, shouldContinue = ()=>true, logger }) {
|
|
7
|
+
if (!Number.isSafeInteger(retentionDays) || retentionDays < 1 || !Number.isSafeInteger(batchSize) || batchSize < 1) throw new RangeError('Invalid retention settings');
|
|
8
|
+
const cutoffMs = now.getTime() - retentionDays * 86_400_000;
|
|
9
|
+
const cutoff = new Date(cutoffMs).toISOString();
|
|
10
|
+
const c = COLUMNS.events, p = COLUMNS.phases, s = COLUMNS.state;
|
|
11
|
+
const events = qualifiedTable(exec, TABLES.events), phases = qualifiedTable(exec, TABLES.phases);
|
|
12
|
+
const revision = `(SELECT ${q(s.revision)} FROM ${qualifiedTable(exec, TABLES.state)} LIMIT 1)`;
|
|
13
|
+
let swept = 0;
|
|
14
|
+
// Each statement still uses the caller's budget/race-aware executor.
|
|
15
|
+
async function batches(predicate, order, params = []) {
|
|
16
|
+
while(shouldContinue()){
|
|
17
|
+
const result = await exec.query(`DELETE FROM ${events} WHERE ${q(c.id)} IN (
|
|
18
|
+
SELECT v.${q(c.id)} FROM ${events} v WHERE ${predicate}
|
|
19
|
+
ORDER BY ${order} LIMIT $${params.length + 1})`, [
|
|
20
|
+
...params,
|
|
21
|
+
batchSize
|
|
22
|
+
]);
|
|
23
|
+
swept += result.rowCount;
|
|
24
|
+
if (result.rowCount < batchSize) break;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
// Keep the leading NULL key in ORDER BY too: PostgreSQL does not infer
|
|
29
|
+
// its ordering equivalence from IS NULL when choosing a bounded index scan.
|
|
30
|
+
const byEventType = `v.${q(c.experimentId)}, v.${q(c.type)}, v.${q(c.ts)}`;
|
|
31
|
+
// Drain the two allow-listed diagnostic types with equality probes. An
|
|
32
|
+
// ordered IN scan can otherwise read conversion entries and filter them.
|
|
33
|
+
for (const type of [
|
|
34
|
+
'exposure',
|
|
35
|
+
'arm-mismatch'
|
|
36
|
+
]){
|
|
37
|
+
await batches(`v.${q(c.experimentId)} IS NULL AND v.${q(c.type)} IN ($1)`, byEventType, [
|
|
38
|
+
type
|
|
39
|
+
]);
|
|
40
|
+
}
|
|
41
|
+
if (!shouldContinue()) return swept;
|
|
42
|
+
// Final-endedAt retention protects every phase while any phase is open.
|
|
43
|
+
const ended = await exec.query(`SELECT ${q(p.experimentId)} AS "experimentId" FROM ${phases}
|
|
44
|
+
WHERE ${q(p.experimentId)} IS NOT NULL GROUP BY ${q(p.experimentId)}
|
|
45
|
+
HAVING NOT bool_or(${q(p.endedAt)} IS NULL) AND max(${q(p.endedAt)}) < $1`, [
|
|
46
|
+
cutoff
|
|
47
|
+
]);
|
|
48
|
+
if (ended.rows.length) {
|
|
49
|
+
// Recheck in every batch in case a new phase started after the id read.
|
|
50
|
+
await batches(`v.${q(c.experimentId)} = ANY($1) AND NOT EXISTS (
|
|
51
|
+
SELECT 1 FROM ${phases} p WHERE p.${q(p.experimentId)} = v.${q(c.experimentId)}
|
|
52
|
+
AND (p.${q(p.endedAt)} IS NULL OR p.${q(p.endedAt)} >= $2))`, byEventType, [
|
|
53
|
+
ended.rows.map((row)=>row.experimentId),
|
|
54
|
+
cutoff
|
|
55
|
+
]);
|
|
56
|
+
}
|
|
57
|
+
if (!shouldContinue()) return swept;
|
|
58
|
+
// Every protected window ends at/after cutoff (or is open). For old
|
|
59
|
+
// conversions their union therefore starts at the earliest lookback start;
|
|
60
|
+
// everything before that single bound is safe. Compute it once, in the same
|
|
61
|
+
// snapshot as the state revision. Epoch milliseconds avoid timestamp parsing.
|
|
62
|
+
const protectedRows = await exec.query(`SELECT
|
|
63
|
+
${revision} AS revision, min(w."from") AS "before" FROM (
|
|
64
|
+
SELECT extract(epoch FROM min(${q(p.startedAt)}) - interval '14 days') * 1000 AS "from"
|
|
65
|
+
FROM ${phases} WHERE ${q(p.experimentId)} IS NOT NULL GROUP BY ${q(p.experimentId)}
|
|
66
|
+
HAVING bool_or(${q(p.endedAt)} IS NULL) OR max(${q(p.endedAt)}) >= $1
|
|
67
|
+
) w`, [
|
|
68
|
+
cutoff
|
|
69
|
+
]);
|
|
70
|
+
const frozen = protectedRows.rows[0] ?? {
|
|
71
|
+
revision: null,
|
|
72
|
+
before: null
|
|
73
|
+
};
|
|
74
|
+
const before = new Date(Math.min(cutoffMs, frozen.before ?? cutoffMs)).toISOString();
|
|
75
|
+
// Transitions bump revision in the same transaction as phase writes. This
|
|
76
|
+
// uncorrelated initplan checks it once per statement, not per candidate. If a
|
|
77
|
+
// phase starts mid-sweep, stale windows can delete no further conversions.
|
|
78
|
+
await batches(`v.${q(c.experimentId)} IS NULL AND v.${q(c.type)} = 'conversion' AND v.${q(c.ts)} < $1
|
|
79
|
+
AND ${revision} IS NOT DISTINCT FROM $2`, byEventType, [
|
|
80
|
+
before,
|
|
81
|
+
frozen.revision
|
|
82
|
+
]);
|
|
83
|
+
return swept;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (swept === 0 && error instanceof ResultsBudgetError) {
|
|
86
|
+
logger?.warn('Experiment retention sweep made no progress before its budget expired; eligible rows may remain.');
|
|
87
|
+
}
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Executor } from './executor.js';
|
|
2
|
+
/** Capture on the transaction executor before committing; keep the bigint as text. */
|
|
3
|
+
export declare function currentTxid(exec: Executor): Promise<string>;
|
|
4
|
+
/** Verify on the pool after commit, so another transaction's revision cannot
|
|
5
|
+
* masquerade as this transaction's success. Null means its status is unavailable.
|
|
6
|
+
*/
|
|
7
|
+
export declare function txidStatus(exec: Executor, xid: string): Promise<'committed' | 'aborted' | 'in progress' | null>;
|
package/dist/db/txid.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Capture on the transaction executor before committing; keep the bigint as text. */ export async function currentTxid(exec) {
|
|
2
|
+
const { rows } = await exec.query('SELECT txid_current()::text AS xid');
|
|
3
|
+
return rows[0].xid;
|
|
4
|
+
}
|
|
5
|
+
/** Verify on the pool after commit, so another transaction's revision cannot
|
|
6
|
+
* masquerade as this transaction's success. Null means its status is unavailable.
|
|
7
|
+
*/ export async function txidStatus(exec, xid) {
|
|
8
|
+
const { rows } = await exec.query('SELECT txid_status($1::bigint) AS status', [
|
|
9
|
+
xid
|
|
10
|
+
]);
|
|
11
|
+
return rows[0]?.status ?? null;
|
|
12
|
+
}
|
package/dist/define.d.ts
ADDED
package/dist/define.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PayloadRequest } from 'payload';
|
|
2
|
+
import type { EndpointContext } from '../types/endpoints.js';
|
|
3
|
+
export declare function handleEvents<TUser extends {
|
|
4
|
+
id: string | number;
|
|
5
|
+
} = {
|
|
6
|
+
id: string | number;
|
|
7
|
+
}>(req: PayloadRequest, { options }: EndpointContext<TUser>): Promise<Response>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { NextRequest } from 'next/server.js';
|
|
2
|
+
import { DEFAULT_MAX_BODY_BYTES } from '../constants.js';
|
|
3
|
+
import { pooledExecutor } from '../db/executor.js';
|
|
4
|
+
import { insertEvents } from '../db/events.js';
|
|
5
|
+
import { EventsBodyError, readEvents } from '../events/body.js';
|
|
6
|
+
import { isPreview } from '../events/preview.js';
|
|
7
|
+
import { sourceRequest } from '../events/request.js';
|
|
8
|
+
import { eventRows } from '../events/rows.js';
|
|
9
|
+
import { assign } from '../runtime/assign.js';
|
|
10
|
+
import { isBot } from '../runtime/bot.js';
|
|
11
|
+
import { readUnitCookie } from '../runtime/cookie.js';
|
|
12
|
+
import { requestAttributes } from '../runtime/normalisePath.js';
|
|
13
|
+
import { serverManifest } from '../server/context.js';
|
|
14
|
+
const accepted = ()=>Response.json({}, {
|
|
15
|
+
status: 202
|
|
16
|
+
});
|
|
17
|
+
export async function handleEvents(req, { options }) {
|
|
18
|
+
const runtime = options.runtime, proxy = runtime.proxyRuntime;
|
|
19
|
+
const visitorId = readUnitCookie(req.headers, proxy);
|
|
20
|
+
if (!visitorId) return Response.json({
|
|
21
|
+
errors: [
|
|
22
|
+
{
|
|
23
|
+
code: 'unauthenticated',
|
|
24
|
+
message: 'Invalid unit cookie'
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}, {
|
|
28
|
+
status: 401
|
|
29
|
+
});
|
|
30
|
+
let events;
|
|
31
|
+
try {
|
|
32
|
+
events = await readEvents(req.body, options.events?.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (error instanceof EventsBodyError) return Response.json({
|
|
35
|
+
errors: [
|
|
36
|
+
{
|
|
37
|
+
code: error.code,
|
|
38
|
+
message: error.message
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}, {
|
|
42
|
+
status: error.status
|
|
43
|
+
});
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const request = new NextRequest(req.url ?? 'https://xp.invalid/', {
|
|
48
|
+
headers: req.headers,
|
|
49
|
+
method: req.method
|
|
50
|
+
});
|
|
51
|
+
// API paths bypass the proxy: re-check consent even when a stale signed cookie exists.
|
|
52
|
+
try {
|
|
53
|
+
if (proxy.cookie.consent?.(request) === false) return accepted();
|
|
54
|
+
} catch {
|
|
55
|
+
return accepted();
|
|
56
|
+
}
|
|
57
|
+
if (options.events?.rateLimit && await options.events.rateLimit({
|
|
58
|
+
req,
|
|
59
|
+
unitId: visitorId
|
|
60
|
+
}) !== true) return accepted();
|
|
61
|
+
const manifest = await serverManifest(proxy);
|
|
62
|
+
if (!manifest) return accepted();
|
|
63
|
+
const source = sourceRequest(request);
|
|
64
|
+
const unit = await proxy.getUnit?.(source ?? request);
|
|
65
|
+
const user = req.user ?? null;
|
|
66
|
+
const userId = user ? runtime.userIdOf(user) : undefined;
|
|
67
|
+
// The beacon URL is an API route, not the page whose content was rendered.
|
|
68
|
+
// A referrer supplies page context only, never an identity or an assigned arm.
|
|
69
|
+
const attributes = requestAttributes({
|
|
70
|
+
...unit?.attributes,
|
|
71
|
+
...runtime.attributesOf(user)
|
|
72
|
+
}, source?.nextUrl.pathname);
|
|
73
|
+
const unresolvedStaff = Boolean(options.attributeKeys?.staff && unit?.sessionUnknown && !user);
|
|
74
|
+
const assignments = assign(manifest, {
|
|
75
|
+
visitorId,
|
|
76
|
+
userId
|
|
77
|
+
}, attributes, {
|
|
78
|
+
now: Math.floor(Date.now() / 1000),
|
|
79
|
+
method: req.method,
|
|
80
|
+
staff: unit?.attributes.staff === true || attributes.staff === true,
|
|
81
|
+
bot: isBot(req.headers.get('user-agent')),
|
|
82
|
+
preview: isPreview(request)
|
|
83
|
+
});
|
|
84
|
+
if (unresolvedStaff) for (const assignment of Object.values(assignments.bySlug))assignment.exposable = false;
|
|
85
|
+
const rows = eventRows(events, manifest, assignments, options, visitorId, userId, new Date().toISOString(), attributes);
|
|
86
|
+
// §7.8 events insert / assignment lookup exception: pooled SQL and cached manifest,
|
|
87
|
+
// no Local API calls (and hence no implicit overrideAccess defaults).
|
|
88
|
+
if (rows.length) await insertEvents(pooledExecutor(req.payload), rows, (message)=>req.payload.logger.error(message));
|
|
89
|
+
} catch (error) {
|
|
90
|
+
try {
|
|
91
|
+
req.payload.logger.error({
|
|
92
|
+
err: error,
|
|
93
|
+
msg: 'Experiments: events endpoint failed'
|
|
94
|
+
});
|
|
95
|
+
} catch {}
|
|
96
|
+
}
|
|
97
|
+
return accepted();
|
|
98
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Endpoint, PayloadRequest } from 'payload';
|
|
2
|
+
import type { ResolvedOptions } from '../types/contracts.js';
|
|
3
|
+
import type { AccessKind, EndpointContext } from '../types/endpoints.js';
|
|
4
|
+
export declare const ROUTES: Array<{
|
|
5
|
+
method: Endpoint['method'];
|
|
6
|
+
path: string;
|
|
7
|
+
access: AccessKind;
|
|
8
|
+
handler: <TUser extends {
|
|
9
|
+
id: string | number;
|
|
10
|
+
}>(req: PayloadRequest, ctx: EndpointContext<TUser>) => Promise<Response>;
|
|
11
|
+
}>;
|
|
12
|
+
export declare function buildEndpoints<TUser extends {
|
|
13
|
+
id: string | number;
|
|
14
|
+
} = {
|
|
15
|
+
id: string | number;
|
|
16
|
+
}>(ctx: {
|
|
17
|
+
options: ResolvedOptions<TUser>;
|
|
18
|
+
}): Endpoint[];
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { XP_MANIFEST_KEY_HEADER } from '../constants.js';
|
|
3
|
+
import { XpNotImplemented, XpRefusal } from '../errors.js';
|
|
4
|
+
import { readUnitCookie } from '../runtime/cookie.js';
|
|
5
|
+
import { handleManifest } from './manifest.js';
|
|
6
|
+
import { handleEvents } from './events.js';
|
|
7
|
+
import { handleStart, handleAllocate, handleAddMetric, handleStop, handleDecide, handleReleaseHoldout, handleMakeContent, handleArchive } from './transitions.js';
|
|
8
|
+
import { handlePreflight } from './preflight.js';
|
|
9
|
+
import { handlePreviewToken } from './previewToken.js';
|
|
10
|
+
import { handleResults } from './results.js';
|
|
11
|
+
import { handleRecompute } from './recompute.js';
|
|
12
|
+
import { handleSummary } from './summary.js';
|
|
13
|
+
export const ROUTES = [
|
|
14
|
+
{
|
|
15
|
+
method: 'get',
|
|
16
|
+
path: '/xp/manifest',
|
|
17
|
+
access: 'manifestKey',
|
|
18
|
+
handler: handleManifest
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
method: 'post',
|
|
22
|
+
path: '/xp/events',
|
|
23
|
+
access: 'signedCookie',
|
|
24
|
+
handler: handleEvents
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
method: 'post',
|
|
28
|
+
path: '/xp/experiments/:id/start',
|
|
29
|
+
access: 'manage',
|
|
30
|
+
handler: handleStart
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
method: 'post',
|
|
34
|
+
path: '/xp/experiments/:id/allocate',
|
|
35
|
+
access: 'manage',
|
|
36
|
+
handler: handleAllocate
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
method: 'post',
|
|
40
|
+
path: '/xp/experiments/:id/add-metric',
|
|
41
|
+
access: 'manage',
|
|
42
|
+
handler: handleAddMetric
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
method: 'post',
|
|
46
|
+
path: '/xp/experiments/:id/stop',
|
|
47
|
+
access: 'manage',
|
|
48
|
+
handler: handleStop
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
method: 'post',
|
|
52
|
+
path: '/xp/experiments/:id/decide',
|
|
53
|
+
access: 'manage',
|
|
54
|
+
handler: handleDecide
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
method: 'post',
|
|
58
|
+
path: '/xp/experiments/:id/release-holdout',
|
|
59
|
+
access: 'manage',
|
|
60
|
+
handler: handleReleaseHoldout
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
method: 'post',
|
|
64
|
+
path: '/xp/experiments/:id/make-content',
|
|
65
|
+
access: 'manage',
|
|
66
|
+
handler: handleMakeContent
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
method: 'post',
|
|
70
|
+
path: '/xp/experiments/:id/archive',
|
|
71
|
+
access: 'manage',
|
|
72
|
+
handler: handleArchive
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
method: 'get',
|
|
76
|
+
path: '/xp/experiments/:id/preflight',
|
|
77
|
+
access: 'manage',
|
|
78
|
+
handler: handlePreflight
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
method: 'get',
|
|
82
|
+
path: '/xp/experiments/:id/preview-token',
|
|
83
|
+
access: 'manage',
|
|
84
|
+
handler: handlePreviewToken
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
method: 'get',
|
|
88
|
+
path: '/xp/experiments/:id/results',
|
|
89
|
+
access: 'readResults',
|
|
90
|
+
handler: handleResults
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
method: 'post',
|
|
94
|
+
path: '/xp/experiments/:id/recompute',
|
|
95
|
+
access: 'manage',
|
|
96
|
+
handler: handleRecompute
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
method: 'get',
|
|
100
|
+
path: '/xp/experiments/summary',
|
|
101
|
+
access: 'readResults',
|
|
102
|
+
handler: handleSummary
|
|
103
|
+
}
|
|
104
|
+
];
|
|
105
|
+
function error(status, code, message, detail) {
|
|
106
|
+
return Response.json({
|
|
107
|
+
errors: [
|
|
108
|
+
{
|
|
109
|
+
code,
|
|
110
|
+
message,
|
|
111
|
+
...detail === undefined ? {} : {
|
|
112
|
+
detail
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
}, {
|
|
117
|
+
status
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
export function buildEndpoints(ctx) {
|
|
121
|
+
return ROUTES.map((route)=>({
|
|
122
|
+
method: route.method,
|
|
123
|
+
path: route.path,
|
|
124
|
+
handler: async (req)=>{
|
|
125
|
+
const { options } = ctx;
|
|
126
|
+
const handlerContext = {
|
|
127
|
+
options
|
|
128
|
+
};
|
|
129
|
+
try {
|
|
130
|
+
if (route.access === 'manage' || route.access === 'readResults') {
|
|
131
|
+
if (!req.user) return error(401, 'unauthenticated', 'Authentication required');
|
|
132
|
+
const access = route.access === 'readResults' ? options.access.readResults ?? options.access.manage : options.access.manage;
|
|
133
|
+
if (await access({
|
|
134
|
+
req
|
|
135
|
+
}) !== true) return error(403, 'forbidden', 'Access denied');
|
|
136
|
+
} else if (route.access === 'manifestKey') {
|
|
137
|
+
const secret = options.runtime.proxyRuntime.secrets[0];
|
|
138
|
+
if (!secret) return error(401, 'unauthenticated', 'Invalid manifest key');
|
|
139
|
+
const expected = createHmac('sha256', secret).update('xp:manifest-key').digest('hex');
|
|
140
|
+
const supplied = req.headers.get(XP_MANIFEST_KEY_HEADER);
|
|
141
|
+
// Compare the wire hex bytes, including length and case, rather than permissive hex decoding.
|
|
142
|
+
if (!supplied || Buffer.byteLength(supplied) !== Buffer.byteLength(expected) || !timingSafeEqual(Buffer.from(supplied), Buffer.from(expected))) {
|
|
143
|
+
return error(401, 'unauthenticated', 'Invalid manifest key');
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
const visitorId = readUnitCookie(req.headers, options.runtime.proxyRuntime);
|
|
147
|
+
if (!visitorId) return error(401, 'unauthenticated', 'Invalid unit cookie');
|
|
148
|
+
handlerContext.unit = {
|
|
149
|
+
visitorId
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return await route.handler(req, handlerContext);
|
|
153
|
+
} catch (cause) {
|
|
154
|
+
if (cause instanceof XpRefusal) return error(cause.status, cause.code, cause.message, cause.detail);
|
|
155
|
+
if (cause instanceof XpNotImplemented) return error(501, 'not-implemented', cause.lane);
|
|
156
|
+
throw cause;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PayloadRequest } from 'payload';
|
|
2
|
+
import type { EndpointContext } from '../types/endpoints.js';
|
|
3
|
+
export declare function handleManifest<TUser extends {
|
|
4
|
+
id: string | number;
|
|
5
|
+
} = {
|
|
6
|
+
id: string | number;
|
|
7
|
+
}>(req: PayloadRequest, _ctx: EndpointContext<TUser>): Promise<Response>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { pooledExecutor } from '../db/executor.js';
|
|
2
|
+
import { readManifestRow } from '../db/manifest.js';
|
|
3
|
+
export async function handleManifest(req, _ctx) {
|
|
4
|
+
const row = await readManifestRow(pooledExecutor(req.payload));
|
|
5
|
+
if (!row) return Response.json({
|
|
6
|
+
errors: [
|
|
7
|
+
{
|
|
8
|
+
code: 'not-found',
|
|
9
|
+
message: 'Manifest not found'
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}, {
|
|
13
|
+
status: 404,
|
|
14
|
+
headers: {
|
|
15
|
+
'Cache-Control': 'no-store'
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
const headers = {
|
|
19
|
+
ETag: `"${row.revision}"`,
|
|
20
|
+
'Cache-Control': 'no-store'
|
|
21
|
+
};
|
|
22
|
+
const ifRevision = req.headers.get('if-none-match');
|
|
23
|
+
if (ifRevision === String(row.revision) || ifRevision === headers.ETag) return new Response(null, {
|
|
24
|
+
status: 304,
|
|
25
|
+
headers
|
|
26
|
+
});
|
|
27
|
+
return Response.json(row.manifest, {
|
|
28
|
+
headers
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PayloadRequest } from 'payload';
|
|
2
|
+
import type { EndpointContext } from '../types/endpoints.js';
|
|
3
|
+
import type { PreflightDependencies } from '../transitions/preflightData.js';
|
|
4
|
+
export declare function handlePreflight<TUser extends {
|
|
5
|
+
id: string | number;
|
|
6
|
+
} = {
|
|
7
|
+
id: string | number;
|
|
8
|
+
}>(req: PayloadRequest, ctx: EndpointContext<TUser>, dependencies?: PreflightDependencies): Promise<Response>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { preflightData } from '../transitions/preflightData.js';
|
|
2
|
+
export async function handlePreflight(req, ctx, dependencies) {
|
|
3
|
+
const id = req.routeParams?.id;
|
|
4
|
+
const result = typeof id === 'string' || typeof id === 'number' ? await preflightData(req, {
|
|
5
|
+
experimentId: id,
|
|
6
|
+
options: ctx.options
|
|
7
|
+
}, dependencies) : null;
|
|
8
|
+
const headers = {
|
|
9
|
+
'Cache-Control': 'no-store'
|
|
10
|
+
};
|
|
11
|
+
if (!result) return Response.json({
|
|
12
|
+
errors: [
|
|
13
|
+
{
|
|
14
|
+
code: 'not-found',
|
|
15
|
+
message: 'Experiment not found'
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}, {
|
|
19
|
+
status: 404,
|
|
20
|
+
headers
|
|
21
|
+
});
|
|
22
|
+
return Response.json(result, {
|
|
23
|
+
headers
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PayloadRequest } from 'payload';
|
|
2
|
+
import type { EndpointContext } from '../types/endpoints.js';
|
|
3
|
+
export declare function handlePreviewToken<TUser extends {
|
|
4
|
+
id: string | number;
|
|
5
|
+
} = {
|
|
6
|
+
id: string | number;
|
|
7
|
+
}>(req: PayloadRequest, { options }: EndpointContext<TUser>): Promise<Response>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { signPreviewToken } from '../runtime/previewToken.js';
|
|
2
|
+
export async function handlePreviewToken(req, { options }) {
|
|
3
|
+
const headers = {
|
|
4
|
+
'Cache-Control': 'no-store'
|
|
5
|
+
};
|
|
6
|
+
const refuse = (status, code, message)=>Response.json({
|
|
7
|
+
errors: [
|
|
8
|
+
{
|
|
9
|
+
code,
|
|
10
|
+
message
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}, {
|
|
14
|
+
status,
|
|
15
|
+
headers
|
|
16
|
+
});
|
|
17
|
+
const id = req.routeParams?.id;
|
|
18
|
+
if (typeof id !== 'string' && typeof id !== 'number') return refuse(404, 'not-found', 'Experiment not found');
|
|
19
|
+
const result = await req.payload.find({
|
|
20
|
+
collection: 'xp-experiments',
|
|
21
|
+
req,
|
|
22
|
+
overrideAccess: false,
|
|
23
|
+
depth: 0,
|
|
24
|
+
limit: 1,
|
|
25
|
+
where: {
|
|
26
|
+
id: {
|
|
27
|
+
equals: id
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
const experiment = result.docs[0];
|
|
32
|
+
if (!experiment) return refuse(404, 'not-found', 'Experiment not found');
|
|
33
|
+
if (![
|
|
34
|
+
'draft',
|
|
35
|
+
'running',
|
|
36
|
+
'stopped',
|
|
37
|
+
'decided'
|
|
38
|
+
].includes(experiment.status)) return refuse(409, 'invalid-status', 'This experiment cannot be previewed');
|
|
39
|
+
const arm = req.searchParams?.get('arm') ?? (req.url ? new URL(req.url).searchParams.get('arm') : null);
|
|
40
|
+
const arms = experiment.arms;
|
|
41
|
+
if (!arm || !arms.some((candidate)=>candidate.key === arm)) return refuse(400, 'invalid-body', 'Unknown experiment arm');
|
|
42
|
+
const now = Math.floor(Date.now() / 1000);
|
|
43
|
+
const token = signPreviewToken({
|
|
44
|
+
e: experiment.id,
|
|
45
|
+
a: arm,
|
|
46
|
+
p: experiment.phase,
|
|
47
|
+
exp: now + 3600
|
|
48
|
+
}, options.runtime.proxyRuntime.secrets, now);
|
|
49
|
+
return Response.json({
|
|
50
|
+
token
|
|
51
|
+
}, {
|
|
52
|
+
headers
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PayloadRequest } from 'payload';
|
|
2
|
+
import type { EndpointContext } from '../types/endpoints.js';
|
|
3
|
+
export declare function handleRecompute<TUser extends {
|
|
4
|
+
id: string | number;
|
|
5
|
+
} = {
|
|
6
|
+
id: string | number;
|
|
7
|
+
}>(req: PayloadRequest, _ctx: EndpointContext<TUser>): Promise<Response>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { runResults } from '../server/runResults.js';
|
|
2
|
+
import { readSnapshots, resultsError } from './results.js';
|
|
3
|
+
export async function handleRecompute(req, _ctx) {
|
|
4
|
+
const id = req.routeParams?.id;
|
|
5
|
+
if (typeof id !== 'string' && typeof id !== 'number') return resultsError(404, 'not-found', 'Experiment not found');
|
|
6
|
+
const { docs } = await req.payload.find({
|
|
7
|
+
collection: 'xp-experiments',
|
|
8
|
+
req,
|
|
9
|
+
overrideAccess: false,
|
|
10
|
+
depth: 0,
|
|
11
|
+
limit: 1,
|
|
12
|
+
where: {
|
|
13
|
+
id: {
|
|
14
|
+
equals: id
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
if (!docs[0]) return resultsError(404, 'not-found', 'Experiment not found');
|
|
19
|
+
try {
|
|
20
|
+
const result = await runResults(req.payload, {
|
|
21
|
+
experimentId: docs[0].id,
|
|
22
|
+
sweep: false
|
|
23
|
+
});
|
|
24
|
+
if (result.skipped || result.budgetExhausted) return resultsError(503, 'recompute-failed', 'Results could not be recomputed. Try again.');
|
|
25
|
+
const latest = (await readSnapshots(req, [
|
|
26
|
+
docs[0].id
|
|
27
|
+
], {
|
|
28
|
+
per: 'experiment'
|
|
29
|
+
}))[0];
|
|
30
|
+
if (!latest) return resultsError(422, 'recompute-failed', 'There is no round to analyse yet.');
|
|
31
|
+
return Response.json({
|
|
32
|
+
snapshotId: latest.id
|
|
33
|
+
}, {
|
|
34
|
+
headers: {
|
|
35
|
+
'Cache-Control': 'no-store'
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
} catch {
|
|
39
|
+
return resultsError(503, 'recompute-failed', 'Results could not be recomputed. Try again.');
|
|
40
|
+
}
|
|
41
|
+
}
|