@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,378 @@
|
|
|
1
|
+
import { DEFAULT_GAUSSIAN_PRIOR, bonferroniAlpha, capValues, createThetaAdjustedStatistics, effectBayesianABTest, sequentialOneSidedTreatmentGreaterTTest, sequentialOneSidedTreatmentLesserTTest, sequentialTwoSidedTTest, statisticMean, statisticVariance } from '@delmaredigital/experiments-stats';
|
|
2
|
+
import { timestampMs } from './aggregate.js';
|
|
3
|
+
import { outcomeStatistic } from './cuped.js';
|
|
4
|
+
import { guardrailStatus } from './guardrails.js';
|
|
5
|
+
import { remainingPower } from './power.js';
|
|
6
|
+
import { assembleSnapshot, jsonInterval } from './snapshot.js';
|
|
7
|
+
import { exposureSrm } from './srm.js';
|
|
8
|
+
import { computeVerdict } from './verdict.js';
|
|
9
|
+
const severity = {
|
|
10
|
+
ok: 0,
|
|
11
|
+
insufficient: 1,
|
|
12
|
+
warning: 2,
|
|
13
|
+
failing: 3
|
|
14
|
+
};
|
|
15
|
+
/** Pure exposure-based analysis (§6, §7.6, §8.4). All inference, moments, capping,
|
|
16
|
+
* CUPED, power and SRM are delegated to experiments-stats; now is always injected.
|
|
17
|
+
*/ export function analysePhase(input) {
|
|
18
|
+
const { phase, units } = input;
|
|
19
|
+
const { analysis } = phase;
|
|
20
|
+
const alpha = analysis.alpha ?? 0.05;
|
|
21
|
+
const prior = analysis.prior ?? DEFAULT_GAUSSIAN_PRIOR;
|
|
22
|
+
const bayesian = analysis.bayesian ?? {
|
|
23
|
+
ctw: 0.95,
|
|
24
|
+
risk: 0.0125
|
|
25
|
+
};
|
|
26
|
+
const rollout = phase.phaseKind === 'rollout';
|
|
27
|
+
const baselineArm = phase.arms.find((arm)=>arm.kind === (rollout ? 'holdout' : 'control'));
|
|
28
|
+
const winner = rollout ? phase.arms.find((arm)=>arm.key === input.winnerArm && arm.kind !== 'holdout') : undefined;
|
|
29
|
+
if (!baselineArm || rollout && (!winner || phase.arms.length !== 2)) throw new RangeError('Phase comparison baseline or shipped arm is missing');
|
|
30
|
+
const compared = phase.arms.filter((arm)=>rollout ? arm.key === winner.key : arm.kind === 'variant');
|
|
31
|
+
const k = compared.length;
|
|
32
|
+
const alphaAdjusted = analysis.method === 'sequential' ? bonferroniAlpha(alpha, k) : alpha;
|
|
33
|
+
const definitions = phase.metricDefinitions.map((metric)=>({
|
|
34
|
+
...metric,
|
|
35
|
+
attributionWindowHours: analysis.attributionWindowHours ?? metric.attributionWindowHours,
|
|
36
|
+
minSampleSize: analysis.minSampleSizes?.[metric.metricId] ?? metric.minSampleSize,
|
|
37
|
+
guardrailThreshold: analysis.guardrailThresholds?.[metric.metricId] ?? metric.guardrailThreshold
|
|
38
|
+
}));
|
|
39
|
+
const primaryDefinition = definitions.find((metric)=>metric.role === 'primary');
|
|
40
|
+
if (!primaryDefinition || phase.metricDefinitions.filter((metric)=>metric.role === 'primary').length !== 1) {
|
|
41
|
+
throw new RangeError('Phase must have exactly one primary metric');
|
|
42
|
+
}
|
|
43
|
+
if (new Set(phase.metricDefinitions.map((metric)=>String(metric.metricId))).size !== phase.metricDefinitions.length || new Set(input.metricData.map((metric)=>String(metric.metricId))).size !== input.metricData.length) {
|
|
44
|
+
throw new RangeError('Duplicate metric id');
|
|
45
|
+
}
|
|
46
|
+
const from = timestampMs(phase.startedAt);
|
|
47
|
+
const now = timestampMs(input.now);
|
|
48
|
+
const to = Math.min(now, phase.endedAt == null ? now : timestampMs(phase.endedAt));
|
|
49
|
+
if (to < from) throw new RangeError('Analysis window ends before the phase starts');
|
|
50
|
+
const byArm = new Map(phase.arms.map((arm)=>[
|
|
51
|
+
arm.key,
|
|
52
|
+
[]
|
|
53
|
+
]));
|
|
54
|
+
for (const [id, unit] of units){
|
|
55
|
+
const ids = byArm.get(unit.arm);
|
|
56
|
+
if (!ids) throw new RangeError('Exposed unit has an unknown arm');
|
|
57
|
+
ids.push(id);
|
|
58
|
+
}
|
|
59
|
+
const perArm = Object.fromEntries([
|
|
60
|
+
...byArm
|
|
61
|
+
].map(([arm, ids])=>[
|
|
62
|
+
arm,
|
|
63
|
+
ids.length
|
|
64
|
+
]));
|
|
65
|
+
const excludedMultiArm = input.excludedMultiArm ?? 0;
|
|
66
|
+
const exposures = {
|
|
67
|
+
total: units.size,
|
|
68
|
+
perArm,
|
|
69
|
+
excludedMultiArm,
|
|
70
|
+
multiArmShare: units.size + excludedMultiArm === 0 ? 0 : excludedMultiArm / (units.size + excludedMultiArm),
|
|
71
|
+
armMismatch: input.armMismatch ?? 0,
|
|
72
|
+
identityConflictShare: input.identityConflictShare ?? 0,
|
|
73
|
+
capped: {}
|
|
74
|
+
};
|
|
75
|
+
const params = {
|
|
76
|
+
sequentialTuningParameter: analysis.sequentialTuningParameter,
|
|
77
|
+
prior: {
|
|
78
|
+
...prior
|
|
79
|
+
},
|
|
80
|
+
cupedLookbackDays: analysis.cupedLookbackDays ?? 14,
|
|
81
|
+
bayesian: {
|
|
82
|
+
...bayesian
|
|
83
|
+
},
|
|
84
|
+
caps: {},
|
|
85
|
+
attributionWindows: {},
|
|
86
|
+
guardrailThresholds: {},
|
|
87
|
+
minSampleSizes: {}
|
|
88
|
+
};
|
|
89
|
+
const guardrails = [];
|
|
90
|
+
const verdictGuardrails = [];
|
|
91
|
+
let primaryStatistics;
|
|
92
|
+
const metrics = definitions.map((definition)=>{
|
|
93
|
+
const { metricId, kind } = definition;
|
|
94
|
+
params.attributionWindows[metricId] = definition.attributionWindowHours;
|
|
95
|
+
params.minSampleSizes[metricId] = definition.minSampleSize;
|
|
96
|
+
if (definition.role === 'guardrail') params.guardrailThresholds[metricId] = definition.guardrailThreshold ?? 0.05;
|
|
97
|
+
if (kind === 'revenue' && definition.capping) params.caps[metricId] = {
|
|
98
|
+
...definition.capping,
|
|
99
|
+
resolvedCap: definition.resolvedCap ?? null,
|
|
100
|
+
...definition.historyUnits === undefined ? {} : {
|
|
101
|
+
historyUnits: definition.historyUnits
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
exposures.capped[metricId] = 0;
|
|
105
|
+
const data = input.metricData.find((item)=>item.metricId === metricId);
|
|
106
|
+
const metric = {
|
|
107
|
+
metricId,
|
|
108
|
+
name: definition.name,
|
|
109
|
+
kind,
|
|
110
|
+
role: definition.role,
|
|
111
|
+
cupedApplied: false,
|
|
112
|
+
perArm: [],
|
|
113
|
+
comparisons: []
|
|
114
|
+
};
|
|
115
|
+
let worst = 'ok';
|
|
116
|
+
let worstArmLabel = compared[0]?.label ?? '';
|
|
117
|
+
try {
|
|
118
|
+
if (!data || data.unavailable !== undefined) throw new Error(data?.unavailable ?? 'Metric data was not supplied');
|
|
119
|
+
const stats = new Map();
|
|
120
|
+
const cap = kind === 'revenue' ? definition.resolvedCap ?? null : null;
|
|
121
|
+
const useCovariates = analysis.cuped === 'auto' && data.covariates !== undefined && data.covariates.size > 0;
|
|
122
|
+
for (const arm of phase.arms){
|
|
123
|
+
const ids = byArm.get(arm.key);
|
|
124
|
+
const values = ids.map((id)=>data.values.get(id) ?? 0);
|
|
125
|
+
if (!values.every(Number.isFinite)) throw new RangeError('Metric values must be finite');
|
|
126
|
+
const capped = capValues(values, cap);
|
|
127
|
+
const rawPre = useCovariates ? ids.map((id)=>data.covariates.get(id) ?? 0) : undefined;
|
|
128
|
+
if (rawPre && !rawPre.every(Number.isFinite)) throw new RangeError('Covariates must be finite');
|
|
129
|
+
const pre = rawPre && capValues(rawPre, cap).values;
|
|
130
|
+
const built = outcomeStatistic(kind, capped.values, pre);
|
|
131
|
+
stats.set(arm.key, built.statistic);
|
|
132
|
+
exposures.capped[metricId] += capped.cappedCount;
|
|
133
|
+
// These are the observed post-period moments; pair-specific theta is
|
|
134
|
+
// applied by the stats engine for inference, never to the displayed mean.
|
|
135
|
+
const post = outcomeStatistic(kind, capped.values).statistic;
|
|
136
|
+
metric.perArm.push({
|
|
137
|
+
arm: arm.key,
|
|
138
|
+
...built.moments,
|
|
139
|
+
converters: values.filter((value)=>value > 0).length,
|
|
140
|
+
mean: statisticMean(post),
|
|
141
|
+
variance: statisticVariance(post),
|
|
142
|
+
...built.covariate ? {
|
|
143
|
+
covariate: built.covariate
|
|
144
|
+
} : {},
|
|
145
|
+
...cap === null ? {} : {
|
|
146
|
+
capped: {
|
|
147
|
+
resolvedCap: cap,
|
|
148
|
+
uncappedTotal: capped.uncappedTotal,
|
|
149
|
+
cappedCount: capped.cappedCount
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if (definition.role === 'primary') primaryStatistics = stats;
|
|
155
|
+
for (const arm of compared){
|
|
156
|
+
const [a, b] = createThetaAdjustedStatistics(stats.get(baselineArm.key), stats.get(arm.key));
|
|
157
|
+
metric.cupedApplied ||= a.type === 'regressionAdjusted' && b.type === 'regressionAdjusted';
|
|
158
|
+
const config = {
|
|
159
|
+
alpha,
|
|
160
|
+
sequentialTuningParameter: analysis.sequentialTuningParameter
|
|
161
|
+
};
|
|
162
|
+
const inverse = definition.direction === 'decrease';
|
|
163
|
+
let comparison;
|
|
164
|
+
let status = 'insufficient';
|
|
165
|
+
if (analysis.method === 'sequential') {
|
|
166
|
+
const raw = sequentialTwoSidedTTest(a, b, config);
|
|
167
|
+
const adjusted = definition.role === 'primary' ? sequentialTwoSidedTTest(a, b, {
|
|
168
|
+
...config,
|
|
169
|
+
alpha: alphaAdjusted
|
|
170
|
+
}) : raw;
|
|
171
|
+
const ci = adjusted.errorMessage ? [
|
|
172
|
+
null,
|
|
173
|
+
null
|
|
174
|
+
] : jsonInterval(adjusted.ci);
|
|
175
|
+
const positive = ci[0] !== null && ci[0] > 0;
|
|
176
|
+
const negative = ci[1] !== null && ci[1] < 0;
|
|
177
|
+
comparison = {
|
|
178
|
+
arm: arm.key,
|
|
179
|
+
lift: raw.expected,
|
|
180
|
+
ci: raw.errorMessage ? [
|
|
181
|
+
null,
|
|
182
|
+
null
|
|
183
|
+
] : jsonInterval(raw.ci),
|
|
184
|
+
ciAdjusted: ci,
|
|
185
|
+
...raw.pValue === null ? {} : {
|
|
186
|
+
pValue: raw.pValue
|
|
187
|
+
},
|
|
188
|
+
...adjusted.pValue === null ? {} : {
|
|
189
|
+
pValueAdjusted: Math.min(1, adjusted.pValue * (definition.role === 'primary' ? k : 1))
|
|
190
|
+
},
|
|
191
|
+
significant: positive ? inverse ? 'worse' : 'better' : negative ? inverse ? 'better' : 'worse' : 'none'
|
|
192
|
+
};
|
|
193
|
+
if (definition.role === 'guardrail') {
|
|
194
|
+
const greater = sequentialOneSidedTreatmentGreaterTTest(a, b, config);
|
|
195
|
+
const lesser = sequentialOneSidedTreatmentLesserTTest(a, b, config);
|
|
196
|
+
status = guardrailStatus({
|
|
197
|
+
method: 'sequential',
|
|
198
|
+
direction: definition.direction,
|
|
199
|
+
n: Math.min(...metric.perArm.filter((group)=>group.arm === arm.key || group.arm === baselineArm.key).map((group)=>group.converters)),
|
|
200
|
+
minSampleSize: definition.minSampleSize,
|
|
201
|
+
threshold: definition.guardrailThreshold ?? 0.05,
|
|
202
|
+
pointEstimate: raw.expected,
|
|
203
|
+
lowerBound: greater.errorMessage ? null : jsonInterval(greater.ci)[0],
|
|
204
|
+
upperBound: lesser.errorMessage ? null : jsonInterval(lesser.ci)[1]
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
const result = effectBayesianABTest(a, b, {
|
|
209
|
+
alpha,
|
|
210
|
+
priorEffect: prior,
|
|
211
|
+
inverse
|
|
212
|
+
});
|
|
213
|
+
comparison = {
|
|
214
|
+
arm: arm.key,
|
|
215
|
+
lift: result.expected,
|
|
216
|
+
ci: result.errorMessage ? [
|
|
217
|
+
null,
|
|
218
|
+
null
|
|
219
|
+
] : jsonInterval(result.ci),
|
|
220
|
+
ciAdjusted: result.errorMessage ? [
|
|
221
|
+
null,
|
|
222
|
+
null
|
|
223
|
+
] : jsonInterval(result.ci),
|
|
224
|
+
chanceToWin: result.chanceToWin,
|
|
225
|
+
risk: result.risk[1],
|
|
226
|
+
significant: result.errorMessage ? 'none' : result.chanceToWin >= bayesian.ctw ? 'better' : result.chanceToWin <= 1 - bayesian.ctw ? 'worse' : 'none'
|
|
227
|
+
};
|
|
228
|
+
if (definition.role === 'guardrail') {
|
|
229
|
+
const safety = effectBayesianABTest(a, b, {
|
|
230
|
+
alpha: 0.10,
|
|
231
|
+
priorEffect: prior,
|
|
232
|
+
inverse
|
|
233
|
+
});
|
|
234
|
+
status = guardrailStatus({
|
|
235
|
+
method: 'bayesian',
|
|
236
|
+
direction: definition.direction,
|
|
237
|
+
n: Math.min(...metric.perArm.filter((group)=>group.arm === arm.key || group.arm === baselineArm.key).map((group)=>group.converters)),
|
|
238
|
+
minSampleSize: definition.minSampleSize,
|
|
239
|
+
threshold: definition.guardrailThreshold ?? 0.05,
|
|
240
|
+
pointEstimate: result.expected,
|
|
241
|
+
lowerBound: safety.errorMessage ? null : jsonInterval(safety.ci)[0],
|
|
242
|
+
upperBound: safety.errorMessage ? null : jsonInterval(safety.ci)[1],
|
|
243
|
+
pWorse: safety.errorMessage ? undefined : 1 - safety.chanceToWin
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
metric.comparisons.push(comparison);
|
|
248
|
+
if (severity[status] > severity[worst]) {
|
|
249
|
+
worst = status;
|
|
250
|
+
worstArmLabel = arm.label;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
} catch (error) {
|
|
254
|
+
metric.unavailable = error instanceof Error ? error.message : 'Metric could not be analysed';
|
|
255
|
+
metric.perArm = [];
|
|
256
|
+
metric.comparisons = [];
|
|
257
|
+
metric.cupedApplied = false;
|
|
258
|
+
exposures.capped[metricId] = 0;
|
|
259
|
+
if (definition.role === 'primary') primaryStatistics = undefined;
|
|
260
|
+
worst = 'insufficient';
|
|
261
|
+
}
|
|
262
|
+
if (definition.role === 'guardrail') {
|
|
263
|
+
guardrails.push({
|
|
264
|
+
metricId,
|
|
265
|
+
status: worst
|
|
266
|
+
});
|
|
267
|
+
verdictGuardrails.push(worst === 'failing' ? {
|
|
268
|
+
name: definition.name,
|
|
269
|
+
status: worst,
|
|
270
|
+
worstArmLabel
|
|
271
|
+
} : {
|
|
272
|
+
name: definition.name,
|
|
273
|
+
status: worst
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
return metric;
|
|
277
|
+
});
|
|
278
|
+
const primary = metrics.find((metric)=>metric.role === 'primary');
|
|
279
|
+
const srm = exposureSrm(phase.arms, perArm);
|
|
280
|
+
let power = {
|
|
281
|
+
daysRemaining: null,
|
|
282
|
+
exposuresNeededPerArm: null,
|
|
283
|
+
lowerBound: analysis.method === 'sequential',
|
|
284
|
+
feasible: true
|
|
285
|
+
};
|
|
286
|
+
const common = {
|
|
287
|
+
status: input.status,
|
|
288
|
+
method: analysis.method,
|
|
289
|
+
primary: {
|
|
290
|
+
name: primary.name,
|
|
291
|
+
direction: primaryDefinition.direction,
|
|
292
|
+
...primary.unavailable === undefined ? {} : {
|
|
293
|
+
unavailable: primary.unavailable
|
|
294
|
+
},
|
|
295
|
+
minSampleSize: primaryDefinition.minSampleSize,
|
|
296
|
+
convertersPerArm: primary.perArm.map((group)=>({
|
|
297
|
+
arm: group.arm,
|
|
298
|
+
kind: phase.arms.find((arm)=>arm.key === group.arm).kind,
|
|
299
|
+
converters: group.converters
|
|
300
|
+
})),
|
|
301
|
+
comparisons: primary.comparisons.map((comparison)=>({
|
|
302
|
+
...comparison,
|
|
303
|
+
ci: comparison.ciAdjusted,
|
|
304
|
+
label: phase.arms.find((arm)=>arm.key === comparison.arm).label
|
|
305
|
+
}))
|
|
306
|
+
},
|
|
307
|
+
exposures: {
|
|
308
|
+
perArm: phase.arms.map((arm)=>({
|
|
309
|
+
arm: arm.key,
|
|
310
|
+
label: arm.label,
|
|
311
|
+
kind: arm.kind,
|
|
312
|
+
n: perArm[arm.key]
|
|
313
|
+
})),
|
|
314
|
+
minExposuresPerArm: analysis.minExposuresPerArm,
|
|
315
|
+
multiArmShare: exposures.multiArmShare
|
|
316
|
+
},
|
|
317
|
+
srm,
|
|
318
|
+
guardrails: verdictGuardrails,
|
|
319
|
+
power,
|
|
320
|
+
plannedDurationPassed: to - from >= analysis.plannedDurationDays * 86_400_000,
|
|
321
|
+
originalLabel: phase.arms.find((arm)=>arm.kind === 'control')?.label ?? 'Original',
|
|
322
|
+
controlArm: baselineArm.key
|
|
323
|
+
};
|
|
324
|
+
const verdictInput = rollout ? {
|
|
325
|
+
...common,
|
|
326
|
+
phaseKind: 'rollout',
|
|
327
|
+
winnerArm: winner.key,
|
|
328
|
+
winnerLabel: winner.label
|
|
329
|
+
} : {
|
|
330
|
+
...common,
|
|
331
|
+
phaseKind: 'test'
|
|
332
|
+
};
|
|
333
|
+
let verdict = computeVerdict(verdictInput);
|
|
334
|
+
if (input.status === 'running' && !rollout && (verdict.row === 4 || verdict.row === 9) && primaryStatistics) {
|
|
335
|
+
power = remainingPower({
|
|
336
|
+
baseline: primaryStatistics.get(baselineArm.key),
|
|
337
|
+
variants: compared.map((arm)=>primaryStatistics.get(arm.key)),
|
|
338
|
+
elapsedDays: (to - from) / 86_400_000,
|
|
339
|
+
alpha: alphaAdjusted,
|
|
340
|
+
method: analysis.method,
|
|
341
|
+
prior,
|
|
342
|
+
sequentialTuningParameter: analysis.sequentialTuningParameter
|
|
343
|
+
});
|
|
344
|
+
verdict = computeVerdict({
|
|
345
|
+
...verdictInput,
|
|
346
|
+
power
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
return assembleSnapshot({
|
|
350
|
+
experiment: phase.experiment,
|
|
351
|
+
phase: phase.phase,
|
|
352
|
+
phaseKind: phase.phaseKind,
|
|
353
|
+
computedAt: new Date(now).toISOString(),
|
|
354
|
+
window: {
|
|
355
|
+
from: new Date(from).toISOString(),
|
|
356
|
+
to: new Date(to).toISOString()
|
|
357
|
+
},
|
|
358
|
+
method: analysis.method,
|
|
359
|
+
alpha,
|
|
360
|
+
alphaAdjusted,
|
|
361
|
+
k,
|
|
362
|
+
winnerArm: winner?.key ?? null,
|
|
363
|
+
minExposuresPerArm: analysis.minExposuresPerArm,
|
|
364
|
+
params,
|
|
365
|
+
exposures,
|
|
366
|
+
srm,
|
|
367
|
+
metrics,
|
|
368
|
+
guardrails,
|
|
369
|
+
power,
|
|
370
|
+
verdict,
|
|
371
|
+
contentChanges: (phase.contentChanges ?? []).map((change)=>({
|
|
372
|
+
...change,
|
|
373
|
+
doc: {
|
|
374
|
+
...change.doc
|
|
375
|
+
}
|
|
376
|
+
}))
|
|
377
|
+
});
|
|
378
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { summarizeUnits } from '@delmaredigital/experiments-stats';
|
|
2
|
+
import type { TestStatistic } from '@delmaredigital/experiments-stats';
|
|
3
|
+
import type { MetricSource } from '../types/contracts.js';
|
|
4
|
+
/** Build candidate CUPED statistics; the stats engine pools each pair, infers theta,
|
|
5
|
+
* and falls back to the post statistic when pre-period variance/covariance is zero.
|
|
6
|
+
* Missing units in a supplied pre-period map are zero-filled by the caller.
|
|
7
|
+
*/
|
|
8
|
+
export declare function outcomeStatistic(kind: MetricSource['kind'], values: readonly number[], covariates?: readonly number[]): {
|
|
9
|
+
statistic: TestStatistic;
|
|
10
|
+
moments: ReturnType<typeof summarizeUnits>;
|
|
11
|
+
covariate?: {
|
|
12
|
+
preSum: number;
|
|
13
|
+
preSumSquares: number;
|
|
14
|
+
postPreSumOfProducts: number;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { buildRegressionAdjustedStatistic, buildStatistic, summarizeUnits, summarizeUnitsWithCovariate } from '@delmaredigital/experiments-stats';
|
|
2
|
+
/** Build candidate CUPED statistics; the stats engine pools each pair, infers theta,
|
|
3
|
+
* and falls back to the post statistic when pre-period variance/covariance is zero.
|
|
4
|
+
* Missing units in a supplied pre-period map are zero-filled by the caller.
|
|
5
|
+
*/ export function outcomeStatistic(kind, values, covariates) {
|
|
6
|
+
if (!values.every(Number.isFinite) || covariates && !covariates.every(Number.isFinite)) {
|
|
7
|
+
throw new RangeError('Metric values and covariates must be finite');
|
|
8
|
+
}
|
|
9
|
+
if (kind === 'binomial' && (values.some((value)=>value !== 0 && value !== 1) || covariates?.some((value)=>value !== 0 && value !== 1))) {
|
|
10
|
+
throw new RangeError('Binomial values and covariates must be 0 or 1');
|
|
11
|
+
}
|
|
12
|
+
const moments = summarizeUnits(values);
|
|
13
|
+
if (covariates === undefined) return {
|
|
14
|
+
moments,
|
|
15
|
+
statistic: buildStatistic(kind, moments)
|
|
16
|
+
};
|
|
17
|
+
const { preSum, preSumSquares, postPreSumOfProducts } = summarizeUnitsWithCovariate(values, covariates);
|
|
18
|
+
const covariate = {
|
|
19
|
+
preSum,
|
|
20
|
+
preSumSquares,
|
|
21
|
+
postPreSumOfProducts
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
moments,
|
|
25
|
+
covariate,
|
|
26
|
+
statistic: buildRegressionAdjustedStatistic(kind, {
|
|
27
|
+
...moments,
|
|
28
|
+
...covariate
|
|
29
|
+
})
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** A true result means required: true: typed confirmation and a note are both required (plan §2.7). */ export function decideGuard(snapshot, chosenArm) {
|
|
2
|
+
return snapshot === null || snapshot.verdict.recommendedArm === null || chosenArm !== snapshot.verdict.recommendedArm || [
|
|
3
|
+
1,
|
|
4
|
+
2,
|
|
5
|
+
3,
|
|
6
|
+
4
|
|
7
|
+
].some((row)=>row === snapshot.verdict.row);
|
|
8
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface GuardrailInput {
|
|
2
|
+
method: 'sequential' | 'bayesian';
|
|
3
|
+
n: number;
|
|
4
|
+
minSampleSize: number;
|
|
5
|
+
threshold: number;
|
|
6
|
+
/** Metric collection direction, passed through without translation. */
|
|
7
|
+
direction: 'increase' | 'decrease';
|
|
8
|
+
/** Raw relative-effect bounds: sequential greater-test lower and lesser-test upper.
|
|
9
|
+
* Bayesian bounds use alpha 0.10; pass raw bounds before direction inversion. */
|
|
10
|
+
lowerBound: number | null;
|
|
11
|
+
upperBound: number | null;
|
|
12
|
+
pointEstimate: number;
|
|
13
|
+
/** Probability of harm, already adjusted for direction. */
|
|
14
|
+
pWorse?: number;
|
|
15
|
+
}
|
|
16
|
+
/** Classify precomputed one-sided results in the order specified by plan CORE-15a. */
|
|
17
|
+
export declare function guardrailStatus(input: GuardrailInput): 'failing' | 'ok' | 'warning' | 'insufficient';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Classify precomputed one-sided results in the order specified by plan CORE-15a. */ export function guardrailStatus(input) {
|
|
2
|
+
const inverse = input.direction === 'decrease';
|
|
3
|
+
const lower = inverse ? input.upperBound === null ? null : -input.upperBound : input.lowerBound;
|
|
4
|
+
const upper = inverse ? input.lowerBound === null ? null : -input.lowerBound : input.upperBound;
|
|
5
|
+
const harmful = input.method === 'bayesian' ? (input.pWorse ?? 0) >= 0.95 : upper !== null && upper < 0;
|
|
6
|
+
if (input.n >= input.minSampleSize && harmful) return 'failing';
|
|
7
|
+
if (lower !== null && lower > -input.threshold) return 'ok';
|
|
8
|
+
const warning = input.method === 'bayesian' ? (input.pWorse ?? 0) >= 0.70 : (inverse ? -input.pointEstimate : input.pointEstimate) < 0;
|
|
9
|
+
return warning ? 'warning' : 'insufficient';
|
|
10
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { MetricEvent } from './aggregate.js';
|
|
2
|
+
/**
|
|
3
|
+
* Spec §5.4: "Default for revenue sources is `percentile 0.99`; the threshold is resolved at Start from the source's own `query({ from: start − 90 d, to: start })` (no extra host contract): events are aggregated **per unit** in **repeated greedy windows**: the unit's first event in the lookback anchors a window of the metric's attribution length, every event inside `[anchor, anchor + window)` sums into that window's value, the first event at or after the window's end anchors the next window, and so on; each window sum is one value in the percentile population (each matches what one exposure-anchored analysis window would see, and repeat donors contribute several comparable samples), the percentile is taken over those per-unit values (GrowthBook caps unit-level values, not events), and the result is frozen into the phase (`metricDefinitions[].resolvedCap`, with `historyUnits`); fewer than 20 history units → no cap and a notice in the admin."
|
|
4
|
+
* The caller supplies only lookback events. `units` counts distinct units, not windows.
|
|
5
|
+
* Units without events never contribute: this conservative population is close to
|
|
6
|
+
* GrowthBook's ignore-zeros behavior and avoids a cap of 0 at low conversion.
|
|
7
|
+
*/
|
|
8
|
+
export declare function perUnitHistory(events: readonly MetricEvent[], { windowHours }: {
|
|
9
|
+
windowHours: number;
|
|
10
|
+
}): {
|
|
11
|
+
values: number[];
|
|
12
|
+
units: number;
|
|
13
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { timestampMs, windowMs } from './aggregate.js';
|
|
2
|
+
/**
|
|
3
|
+
* Spec §5.4: "Default for revenue sources is `percentile 0.99`; the threshold is resolved at Start from the source's own `query({ from: start − 90 d, to: start })` (no extra host contract): events are aggregated **per unit** in **repeated greedy windows**: the unit's first event in the lookback anchors a window of the metric's attribution length, every event inside `[anchor, anchor + window)` sums into that window's value, the first event at or after the window's end anchors the next window, and so on; each window sum is one value in the percentile population (each matches what one exposure-anchored analysis window would see, and repeat donors contribute several comparable samples), the percentile is taken over those per-unit values (GrowthBook caps unit-level values, not events), and the result is frozen into the phase (`metricDefinitions[].resolvedCap`, with `historyUnits`); fewer than 20 history units → no cap and a notice in the admin."
|
|
4
|
+
* The caller supplies only lookback events. `units` counts distinct units, not windows.
|
|
5
|
+
* Units without events never contribute: this conservative population is close to
|
|
6
|
+
* GrowthBook's ignore-zeros behavior and avoids a cap of 0 at low conversion.
|
|
7
|
+
*/ export function perUnitHistory(events, { windowHours }) {
|
|
8
|
+
const duration = windowMs(windowHours);
|
|
9
|
+
const byUnit = new Map();
|
|
10
|
+
for (const event of events){
|
|
11
|
+
const row = {
|
|
12
|
+
ts: timestampMs(event.ts),
|
|
13
|
+
value: event.value
|
|
14
|
+
};
|
|
15
|
+
const rows = byUnit.get(event.unitId);
|
|
16
|
+
if (rows) rows.push(row);
|
|
17
|
+
else byUnit.set(event.unitId, [
|
|
18
|
+
row
|
|
19
|
+
]);
|
|
20
|
+
}
|
|
21
|
+
const values = [];
|
|
22
|
+
for (const rows of byUnit.values()){
|
|
23
|
+
rows.sort((a, b)=>a.ts - b.ts);
|
|
24
|
+
let anchor;
|
|
25
|
+
let sum = 0;
|
|
26
|
+
for (const row of rows){
|
|
27
|
+
if (anchor === undefined || row.ts >= anchor + duration) {
|
|
28
|
+
if (anchor !== undefined) values.push(sum);
|
|
29
|
+
anchor = row.ts;
|
|
30
|
+
sum = row.value;
|
|
31
|
+
} else sum += row.value;
|
|
32
|
+
}
|
|
33
|
+
values.push(sum);
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
values,
|
|
37
|
+
units: byUnit.size
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ExposedUnit, MetricEvent, Timestamp } from './aggregate.js';
|
|
2
|
+
/**
|
|
3
|
+
* Spec §8.3: credit once to the earliest exposed linked visitor; conflicts are diagnostic only.
|
|
4
|
+
* Callers must pass user ids through String() for both identities and user-keyed source events.
|
|
5
|
+
*/
|
|
6
|
+
export declare function creditUserEvents(units: ReadonlyMap<string, ExposedUnit>, identities: readonly {
|
|
7
|
+
anonymousId: string;
|
|
8
|
+
userId: string;
|
|
9
|
+
}[], userEvents: readonly {
|
|
10
|
+
userId: string;
|
|
11
|
+
value: number;
|
|
12
|
+
ts: Timestamp;
|
|
13
|
+
}[]): {
|
|
14
|
+
events: MetricEvent[];
|
|
15
|
+
conflicts: number;
|
|
16
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { timestampMs } from './aggregate.js';
|
|
2
|
+
/**
|
|
3
|
+
* Spec §8.3: credit once to the earliest exposed linked visitor; conflicts are diagnostic only.
|
|
4
|
+
* Callers must pass user ids through String() for both identities and user-keyed source events.
|
|
5
|
+
*/ export function creditUserEvents(units, identities, userEvents) {
|
|
6
|
+
const users = new Map();
|
|
7
|
+
const starts = new Map();
|
|
8
|
+
for (const [unitId, unit] of units)starts.set(unitId, timestampMs(unit.firstTs));
|
|
9
|
+
for (const { anonymousId, userId } of identities){
|
|
10
|
+
const unit = units.get(anonymousId);
|
|
11
|
+
if (!unit) continue;
|
|
12
|
+
const firstTs = starts.get(anonymousId);
|
|
13
|
+
const user = users.get(userId);
|
|
14
|
+
if (!user) users.set(userId, {
|
|
15
|
+
unitId: anonymousId,
|
|
16
|
+
firstTs,
|
|
17
|
+
arms: new Set([
|
|
18
|
+
unit.arm
|
|
19
|
+
])
|
|
20
|
+
});
|
|
21
|
+
else {
|
|
22
|
+
user.arms.add(unit.arm);
|
|
23
|
+
// Stable tie-break for simultaneous exposures, independent of identity row order.
|
|
24
|
+
if (firstTs < user.firstTs || firstTs === user.firstTs && anonymousId < user.unitId) {
|
|
25
|
+
user.unitId = anonymousId;
|
|
26
|
+
user.firstTs = firstTs;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
let conflicts = 0;
|
|
31
|
+
for (const user of users.values())if (user.arms.size > 1) conflicts++;
|
|
32
|
+
const events = [];
|
|
33
|
+
for (const event of userEvents){
|
|
34
|
+
timestampMs(event.ts);
|
|
35
|
+
const user = users.get(event.userId);
|
|
36
|
+
if (user) events.push({
|
|
37
|
+
unitId: user.unitId,
|
|
38
|
+
value: event.value,
|
|
39
|
+
ts: event.ts
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
events,
|
|
44
|
+
conflicts
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { GaussianPrior, TestStatistic } from '@delmaredigital/experiments-stats';
|
|
2
|
+
import type { Snapshot } from '../types/snapshot.js';
|
|
3
|
+
/** Present the stats engine's pairwise sample estimate at observed exposed-unit
|
|
4
|
+
* traffic. v1 planning uses 10% relative lift and 80% power (as Review does).
|
|
5
|
+
* Called for running test verdict rows 4 and 9; rollout and concluded tests omit estimates.
|
|
6
|
+
*/
|
|
7
|
+
export declare function remainingPower(input: {
|
|
8
|
+
baseline: TestStatistic;
|
|
9
|
+
variants: readonly TestStatistic[];
|
|
10
|
+
elapsedDays: number;
|
|
11
|
+
alpha: number;
|
|
12
|
+
method: Snapshot['method'];
|
|
13
|
+
sequentialTuningParameter: number;
|
|
14
|
+
prior: GaussianPrior;
|
|
15
|
+
}): Snapshot['power'];
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { abTestMoments, midExperimentPower, sequentialTwoSidedTTest } from '@delmaredigital/experiments-stats';
|
|
2
|
+
/** Present the stats engine's pairwise sample estimate at observed exposed-unit
|
|
3
|
+
* traffic. v1 planning uses 10% relative lift and 80% power (as Review does).
|
|
4
|
+
* Called for running test verdict rows 4 and 9; rollout and concluded tests omit estimates.
|
|
5
|
+
*/ export function remainingPower(input) {
|
|
6
|
+
const lowerBound = input.method === 'sequential';
|
|
7
|
+
const unknown = {
|
|
8
|
+
daysRemaining: null,
|
|
9
|
+
exposuresNeededPerArm: null,
|
|
10
|
+
lowerBound,
|
|
11
|
+
feasible: true
|
|
12
|
+
};
|
|
13
|
+
if (input.elapsedDays <= 0 || input.variants.length === 0) return unknown;
|
|
14
|
+
const estimates = input.variants.map((variant)=>{
|
|
15
|
+
const moments = abTestMoments(input.baseline, variant);
|
|
16
|
+
const result = sequentialTwoSidedTTest(input.baseline, variant, {
|
|
17
|
+
alpha: input.alpha,
|
|
18
|
+
sequentialTuningParameter: input.sequentialTuningParameter
|
|
19
|
+
});
|
|
20
|
+
if (moments.errorMessage) return null;
|
|
21
|
+
try {
|
|
22
|
+
return {
|
|
23
|
+
result: midExperimentPower(moments, result, {
|
|
24
|
+
alpha: input.alpha
|
|
25
|
+
}, {
|
|
26
|
+
targetMde: 0.1,
|
|
27
|
+
targetPower: 0.8,
|
|
28
|
+
sequential: lowerBound,
|
|
29
|
+
sequentialTuningParameter: input.sequentialTuningParameter,
|
|
30
|
+
priorEffect: input.method === 'bayesian' ? input.prior : null
|
|
31
|
+
}),
|
|
32
|
+
n: moments.pairwiseSampleSize
|
|
33
|
+
};
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
if (estimates.some((estimate)=>estimate === null)) return unknown;
|
|
39
|
+
if (estimates.some((estimate)=>estimate.result.updateMessage === 'unsuccessful')) return {
|
|
40
|
+
...unknown,
|
|
41
|
+
feasible: false
|
|
42
|
+
};
|
|
43
|
+
let daysRemaining = 0;
|
|
44
|
+
let exposuresNeededPerArm = 0;
|
|
45
|
+
for (const estimate of estimates){
|
|
46
|
+
const { result, n } = estimate;
|
|
47
|
+
const additional = result.scalingFactor === 0 ? 0 : result.additionalUsers;
|
|
48
|
+
if (additional === null || !Number.isFinite(additional)) return unknown;
|
|
49
|
+
daysRemaining = Math.max(daysRemaining, Math.ceil(additional / (n / input.elapsedDays)));
|
|
50
|
+
// Upstream additionalUsers is pairwise, so the UI's per-arm total is half
|
|
51
|
+
// the projected pair. Day estimates preserve the actual observed arm ratio.
|
|
52
|
+
exposuresNeededPerArm = Math.max(exposuresNeededPerArm, Math.ceil((n + additional) / 2));
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
daysRemaining,
|
|
56
|
+
exposuresNeededPerArm,
|
|
57
|
+
lowerBound,
|
|
58
|
+
feasible: daysRemaining <= 90
|
|
59
|
+
};
|
|
60
|
+
}
|