@delmaredigital/payload-experiments 0.0.0-stage → 0.1.0-rc.6
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 +590 -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 +107 -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 +141 -0
- package/dist/types/contracts.js +1 -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 +138 -3
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Assignments } from '../types/assignments.js';
|
|
2
|
+
import type { Manifest } from '../types/manifest.js';
|
|
3
|
+
import { type AssignmentResult } from './assign.js';
|
|
4
|
+
/** Throws if compression still exceeds the 1 KB budget; callers fall back to recompute. */
|
|
5
|
+
export declare function encodeAssignmentsHeader(assignments: Assignments & {
|
|
6
|
+
deferred?: readonly string[];
|
|
7
|
+
}, opts: {
|
|
8
|
+
secrets: readonly string[];
|
|
9
|
+
manifest?: Manifest;
|
|
10
|
+
}): string;
|
|
11
|
+
/** A header newer than the supplied manifest returns null; callers reload the manifest and retry. */
|
|
12
|
+
export declare function decodeAssignmentsHeader(value: string, opts: {
|
|
13
|
+
secrets: readonly string[];
|
|
14
|
+
visitorId?: string;
|
|
15
|
+
userId?: string;
|
|
16
|
+
manifest: Manifest;
|
|
17
|
+
}): AssignmentResult | null;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { fillFlagPoints } from './assign.js';
|
|
3
|
+
import { sign, verify } from './signing.js';
|
|
4
|
+
const MAX_BYTES = 1024;
|
|
5
|
+
const SLUG = /^[A-Za-z0-9_-]+$/;
|
|
6
|
+
const ARM = /^(control|variantA|variantB|variantC|holdout)$/;
|
|
7
|
+
const slugHash = (slug)=>`#${createHash('sha256').update(slug).digest('hex').slice(0, 8)}`;
|
|
8
|
+
const message = (visitorId, userId, flags, entries, revision)=>`xp:assign:${visitorId ?? ''}|${userId ?? ''}|${flags}|${entries}|${revision}`;
|
|
9
|
+
/** Throws if compression still exceeds the 1 KB budget; callers fall back to recompute. */ export function encodeAssignmentsHeader(assignments, opts) {
|
|
10
|
+
if (!Number.isSafeInteger(assignments.revision) || assignments.revision < 0) throw new Error('Invalid revision');
|
|
11
|
+
const flags = `${assignments.staff ? 's' : ''}${assignments.bot ? 'b' : ''}${assignments.preview ? 'p' : ''}${assignments.sessionUnknown ? 'u' : ''}`;
|
|
12
|
+
const slugs = [
|
|
13
|
+
...Object.keys(assignments.bySlug),
|
|
14
|
+
...assignments.deferred ?? []
|
|
15
|
+
];
|
|
16
|
+
if (new Set(slugs).size !== slugs.length || slugs.some((slug)=>!SLUG.test(slug))) throw new Error('Invalid or duplicate slug');
|
|
17
|
+
for (const entry of Object.values(assignments.bySlug))if (!ARM.test(entry.arm)) throw new Error('Invalid arm');
|
|
18
|
+
const encode = (hashed)=>{
|
|
19
|
+
const entries = [
|
|
20
|
+
...Object.entries(assignments.bySlug).map(([slug, entry])=>`${hashed ? slugHash(slug) : slug}=${entry.arm}${entry.exposable ? '' : '!'}`),
|
|
21
|
+
...(assignments.deferred ?? []).map((slug)=>`?${hashed ? slugHash(slug) : slug}`)
|
|
22
|
+
].join(';');
|
|
23
|
+
const revision = String(assignments.revision);
|
|
24
|
+
return `v1.${revision}.${flags}.${entries}.${sign(message(assignments.visitorId, assignments.userId, flags, entries, revision), opts.secrets)}`;
|
|
25
|
+
};
|
|
26
|
+
const plain = encode(false);
|
|
27
|
+
if (Buffer.byteLength(plain) <= MAX_BYTES) return plain;
|
|
28
|
+
const hashes = (opts.manifest?.experiments.map((experiment)=>experiment.slug) ?? slugs).map(slugHash);
|
|
29
|
+
if (new Set(hashes).size !== hashes.length) throw new Error('Ambiguous slug hash');
|
|
30
|
+
const hashed = encode(true);
|
|
31
|
+
if (Buffer.byteLength(hashed) > MAX_BYTES) throw new RangeError('Assignments header exceeds 1024 bytes');
|
|
32
|
+
return hashed;
|
|
33
|
+
}
|
|
34
|
+
/** A header newer than the supplied manifest returns null; callers reload the manifest and retry. */ export function decodeAssignmentsHeader(value, opts) {
|
|
35
|
+
if (typeof value !== 'string' || Buffer.byteLength(value) > MAX_BYTES) return null;
|
|
36
|
+
const parts = value.split('.');
|
|
37
|
+
if (parts.length !== 5) return null;
|
|
38
|
+
const [version, revision, flags, entries, mac] = parts;
|
|
39
|
+
if (version !== 'v1' || !/^(0|[1-9]\d*)$/.test(revision) || !Number.isSafeInteger(Number(revision))) return null;
|
|
40
|
+
// Phase and policy are reconstructed from this exact manifest, never a different revision.
|
|
41
|
+
if (Number(revision) !== opts.manifest.revision) return null;
|
|
42
|
+
if (!/^[sbpu]*$/.test(flags) || new Set(flags).size !== flags.length) return null;
|
|
43
|
+
if (!verify(message(opts.visitorId, opts.userId, flags, entries, revision), mac, opts.secrets)) return null;
|
|
44
|
+
const manifestBySlug = new Map(opts.manifest.experiments.map((experiment)=>[
|
|
45
|
+
experiment.slug,
|
|
46
|
+
experiment
|
|
47
|
+
]));
|
|
48
|
+
const hashes = new Map();
|
|
49
|
+
for (const experiment of opts.manifest.experiments){
|
|
50
|
+
const key = slugHash(experiment.slug);
|
|
51
|
+
hashes.set(key, hashes.has(key) ? null : experiment.slug);
|
|
52
|
+
}
|
|
53
|
+
const result = {
|
|
54
|
+
revision: Number(revision),
|
|
55
|
+
visitorId: opts.visitorId,
|
|
56
|
+
userId: opts.userId,
|
|
57
|
+
staff: flags.includes('s'),
|
|
58
|
+
bot: flags.includes('b'),
|
|
59
|
+
preview: flags.includes('p'),
|
|
60
|
+
sessionUnknown: flags.includes('u'),
|
|
61
|
+
bySlug: Object.create(null),
|
|
62
|
+
flagPoints: Object.create(null),
|
|
63
|
+
archivedWinners: {
|
|
64
|
+
...opts.manifest.archivedWinners
|
|
65
|
+
},
|
|
66
|
+
deferred: [],
|
|
67
|
+
method: 'GET'
|
|
68
|
+
};
|
|
69
|
+
const seen = new Set();
|
|
70
|
+
for (const entry of entries ? entries.split(';') : []){
|
|
71
|
+
const match = /^(\?)?([A-Za-z0-9_-]+|#[a-f0-9]{8})(?:=(control|variantA|variantB|variantC|holdout)(!)?)?$/.exec(entry);
|
|
72
|
+
if (!match) return null;
|
|
73
|
+
const [, deferred, key, arm, nonExposable] = match;
|
|
74
|
+
if (Boolean(deferred) === Boolean(arm)) return null;
|
|
75
|
+
const slug = key.startsWith('#') ? hashes.get(key) : key;
|
|
76
|
+
if (!slug || seen.has(slug)) return null;
|
|
77
|
+
const experiment = manifestBySlug.get(slug);
|
|
78
|
+
if (!experiment) return null;
|
|
79
|
+
seen.add(slug);
|
|
80
|
+
if (deferred) {
|
|
81
|
+
result.deferred.push(slug);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (!experiment.arms.some((candidate)=>candidate.key === arm)) return null;
|
|
85
|
+
result.bySlug[slug] = {
|
|
86
|
+
slug,
|
|
87
|
+
experimentId: experiment.id,
|
|
88
|
+
arm: arm,
|
|
89
|
+
phase: experiment.phase,
|
|
90
|
+
policy: experiment.exposurePolicy,
|
|
91
|
+
exposable: !nonExposable && !result.staff && !result.bot && !result.preview,
|
|
92
|
+
source: 'header'
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
fillFlagPoints(result, opts.manifest);
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Pool } from 'pg';
|
|
2
|
+
import type { ExperimentsProxyRuntimeOptions } from '../types/runtime.js';
|
|
3
|
+
export type ManifestLoader = NonNullable<ExperimentsProxyRuntimeOptions['loadManifest']>;
|
|
4
|
+
export interface PostgresManifestLoaderOptions {
|
|
5
|
+
pool: Pool;
|
|
6
|
+
schemaName?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface HttpManifestLoaderOptions {
|
|
9
|
+
url: string;
|
|
10
|
+
key: string;
|
|
11
|
+
refreshTimeoutMs?: number;
|
|
12
|
+
}
|
|
13
|
+
/** Hosts must set connectionTimeoutMillis on their pg pool to bound connection
|
|
14
|
+
* acquisition; the manifest cache's refresh ceiling does not cancel pg work.
|
|
15
|
+
*/
|
|
16
|
+
export declare function createPostgresManifestLoader({ pool, schemaName }: PostgresManifestLoaderOptions): ManifestLoader;
|
|
17
|
+
export declare function createHttpManifestLoader({ url, key, refreshTimeoutMs }: HttpManifestLoaderOptions): ManifestLoader;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { pgPoolExecutor } from '../db/pgExecutor.js';
|
|
2
|
+
import { readManifestRow } from '../db/manifest.js';
|
|
3
|
+
/** Hosts must set connectionTimeoutMillis on their pg pool to bound connection
|
|
4
|
+
* acquisition; the manifest cache's refresh ceiling does not cancel pg work.
|
|
5
|
+
*/ export function createPostgresManifestLoader({ pool, schemaName }) {
|
|
6
|
+
return async ({ ifRevision } = {})=>{
|
|
7
|
+
const row = await readManifestRow(pgPoolExecutor(pool), {
|
|
8
|
+
schemaName
|
|
9
|
+
});
|
|
10
|
+
return row ? row.revision === ifRevision ? 'not-modified' : row.manifest : null;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function createHttpManifestLoader({ url, key, refreshTimeoutMs = 5_000 }) {
|
|
14
|
+
return async ({ ifRevision } = {})=>{
|
|
15
|
+
const response = await fetch(url, {
|
|
16
|
+
headers: {
|
|
17
|
+
'x-xp-manifest-key': key,
|
|
18
|
+
...ifRevision != null ? {
|
|
19
|
+
'if-none-match': String(ifRevision)
|
|
20
|
+
} : {}
|
|
21
|
+
},
|
|
22
|
+
signal: AbortSignal.timeout(refreshTimeoutMs),
|
|
23
|
+
cache: 'no-store'
|
|
24
|
+
});
|
|
25
|
+
if (response.status === 304) return 'not-modified';
|
|
26
|
+
if (!response.ok) throw new Error(`Manifest request failed (${response.status})`);
|
|
27
|
+
return await response.json();
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Attributes } from '../types/contracts.js';
|
|
2
|
+
/** Normalise a pathname only: preserve root and percent encoding. */
|
|
3
|
+
export declare function normalisePath(pathname: string): string;
|
|
4
|
+
/** Match own path keys with the same rules as live page targets. */
|
|
5
|
+
export declare function findNormalisedPathKey<T>(paths: Readonly<Record<string, T>>, pathname: string): string | undefined;
|
|
6
|
+
/** The attribute set targeting sees, shared by the proxy and the server: the unit's attributes plus the normalised path (removed when the path is unknown). */
|
|
7
|
+
export declare function requestAttributes(unit: Attributes, pathname: string | undefined): Attributes;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Normalise a pathname only: preserve root and percent encoding. */ export function normalisePath(pathname) {
|
|
2
|
+
return pathname.replace(/\/+$/, '') || '/';
|
|
3
|
+
}
|
|
4
|
+
/** Match own path keys with the same rules as live page targets. */ export function findNormalisedPathKey(paths, pathname) {
|
|
5
|
+
const path = normalisePath(pathname);
|
|
6
|
+
return Object.keys(paths).find((key)=>normalisePath(key) === path);
|
|
7
|
+
}
|
|
8
|
+
/** The attribute set targeting sees, shared by the proxy and the server: the unit's attributes plus the normalised path (removed when the path is unknown). */ export function requestAttributes(unit, pathname) {
|
|
9
|
+
const attributes = {
|
|
10
|
+
...unit
|
|
11
|
+
};
|
|
12
|
+
if (pathname === undefined) delete attributes.path;
|
|
13
|
+
else attributes.path = normalisePath(pathname);
|
|
14
|
+
return attributes;
|
|
15
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DocId } from '../types/contracts.js';
|
|
2
|
+
export interface PreviewToken {
|
|
3
|
+
e: DocId;
|
|
4
|
+
a: string;
|
|
5
|
+
p: number;
|
|
6
|
+
exp: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function signPreviewToken(token: PreviewToken, secrets: readonly string[], now?: number): string;
|
|
9
|
+
export declare function verifyPreviewToken(value: string, secrets: readonly string[], now?: number): PreviewToken | null;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { sign, verify } from './signing.js';
|
|
2
|
+
function validToken(value, now) {
|
|
3
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
4
|
+
const token = value;
|
|
5
|
+
return Object.keys(token).sort().join(',') === 'a,e,exp,p' && (typeof token.e === 'string' && token.e.length > 0 || typeof token.e === 'number' && Number.isSafeInteger(token.e)) && typeof token.a === 'string' && /^(control|variantA|variantB|variantC|holdout)$/.test(token.a) && typeof token.p === 'number' && Number.isSafeInteger(token.p) && token.p >= 0 && typeof token.exp === 'number' && Number.isSafeInteger(token.exp) && token.exp > now && token.exp <= now + 3600;
|
|
6
|
+
}
|
|
7
|
+
export function signPreviewToken(token, secrets, now = Math.floor(Date.now() / 1000)) {
|
|
8
|
+
if (!validToken(token, now)) throw new Error('Invalid preview token or expiry');
|
|
9
|
+
const value = Buffer.from(JSON.stringify(token)).toString('base64url');
|
|
10
|
+
return `${value}.${sign(`xp:preview:${value}`, secrets)}`;
|
|
11
|
+
}
|
|
12
|
+
export function verifyPreviewToken(value, secrets, now = Math.floor(Date.now() / 1000)) {
|
|
13
|
+
try {
|
|
14
|
+
const match = /^([A-Za-z0-9_-]+)\.([A-Za-z0-9_-]{43})$/.exec(value);
|
|
15
|
+
if (!match || !verify(`xp:preview:${match[1]}`, match[2], secrets)) return null;
|
|
16
|
+
const bytes = Buffer.from(match[1], 'base64url');
|
|
17
|
+
if (bytes.toString('base64url') !== match[1]) return null;
|
|
18
|
+
const token = JSON.parse(bytes.toString('utf8'));
|
|
19
|
+
return validToken(token, now) ? token : null;
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { XP_COOKIE } from '../constants.js';
|
|
2
|
+
export function defineExperimentsProxyRuntime(opts) {
|
|
3
|
+
const secrets = typeof opts.secret === 'string' ? [
|
|
4
|
+
opts.secret
|
|
5
|
+
] : [
|
|
6
|
+
...opts.secret
|
|
7
|
+
];
|
|
8
|
+
if (!secrets.length || secrets.some((secret)=>!secret.trim())) throw new Error('At least one non-empty secret is required');
|
|
9
|
+
const cookie = Object.freeze({
|
|
10
|
+
...opts.cookie,
|
|
11
|
+
name: opts.cookie?.name ?? XP_COOKIE,
|
|
12
|
+
maxAgeDays: opts.cookie?.maxAgeDays ?? 365
|
|
13
|
+
});
|
|
14
|
+
Object.freeze(secrets);
|
|
15
|
+
return Object.freeze({
|
|
16
|
+
secrets,
|
|
17
|
+
apiRoute: opts.apiRoute,
|
|
18
|
+
cookie,
|
|
19
|
+
loadManifest: opts.loadManifest,
|
|
20
|
+
getUnit: opts.getUnit
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
/** New signatures use the first key; verification accepts the entire rotation. */ export function sign(value, secrets) {
|
|
3
|
+
if (!secrets.length || !secrets[0]) throw new Error('A signing secret is required');
|
|
4
|
+
return createHmac('sha256', secrets[0]).update(value).digest('base64url');
|
|
5
|
+
}
|
|
6
|
+
export function verify(value, mac, secrets) {
|
|
7
|
+
if (!/^[A-Za-z0-9_-]{43}$/.test(mac)) return false;
|
|
8
|
+
const supplied = Buffer.from(mac, 'base64url');
|
|
9
|
+
if (supplied.toString('base64url') !== mac) return false;
|
|
10
|
+
let valid = false;
|
|
11
|
+
for (const secret of secrets){
|
|
12
|
+
const expected = createHmac('sha256', secret).update(value).digest();
|
|
13
|
+
if (supplied.length === expected.length && timingSafeEqual(supplied, expected)) valid = true;
|
|
14
|
+
}
|
|
15
|
+
return valid;
|
|
16
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Attributes, ExperimentsPluginOptions, TargetingRule } from '../types/contracts.js';
|
|
2
|
+
export type AttributeKeys = NonNullable<ExperimentsPluginOptions['attributeKeys']>;
|
|
3
|
+
/** Frozen manifest types take precedence; declarations still support draft rules. */
|
|
4
|
+
export declare function evaluateTargeting(rules: readonly (TargetingRule & {
|
|
5
|
+
type?: 'string' | 'number' | 'boolean';
|
|
6
|
+
})[], attributes: Attributes, attributeKeys?: AttributeKeys): boolean;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { evaluateOp } from '../targeting/ops.js';
|
|
2
|
+
/** Frozen manifest types take precedence; declarations still support draft rules. */ export function evaluateTargeting(rules, attributes, attributeKeys) {
|
|
3
|
+
return rules.every((rule)=>{
|
|
4
|
+
if (!Object.hasOwn(attributes, rule.attribute)) return false;
|
|
5
|
+
if (rule.type === undefined && attributeKeys && !Object.hasOwn(attributeKeys, rule.attribute)) return false;
|
|
6
|
+
const type = rule.type ?? attributeKeys?.[rule.attribute]?.type ?? (Array.isArray(rule.value) ? 'string' : typeof rule.value);
|
|
7
|
+
return evaluateOp(type, rule.op, attributes[rule.attribute], rule.value);
|
|
8
|
+
});
|
|
9
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Attributes } from '../types/contracts.js';
|
|
2
|
+
import type { Assignments } from '../types/assignments.js';
|
|
3
|
+
import type { Manifest } from '../types/manifest.js';
|
|
4
|
+
import type { ExperimentsProxyRuntime } from '../types/runtime.js';
|
|
5
|
+
export declare const emptyManifest: () => Manifest;
|
|
6
|
+
export declare function serverManifest(runtime: ExperimentsProxyRuntime, reload?: boolean): Promise<Manifest | null>;
|
|
7
|
+
export declare function rememberManifest(assignments: Assignments, manifest: Manifest, attributes?: Attributes): Assignments;
|
|
8
|
+
export declare const requestAttributesOf: (assignments: Assignments) => Readonly<Attributes>;
|
|
9
|
+
export declare const manifestOf: (assignments: Assignments) => Manifest | undefined;
|
|
10
|
+
export declare const suppressExposure: (a: Assignments) => boolean;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createManifestCache } from '../manifest/cache.js';
|
|
2
|
+
const caches = new WeakMap();
|
|
3
|
+
// Rendering details stay server-side, outside the frozen public assignment wire shape.
|
|
4
|
+
const manifests = new WeakMap();
|
|
5
|
+
const requestAttributeSets = new WeakMap();
|
|
6
|
+
export const emptyManifest = ()=>({
|
|
7
|
+
v: 1,
|
|
8
|
+
revision: 0,
|
|
9
|
+
experiments: [],
|
|
10
|
+
flagDefaults: {},
|
|
11
|
+
archivedWinners: {},
|
|
12
|
+
archivedPages: {}
|
|
13
|
+
});
|
|
14
|
+
export async function serverManifest(runtime, reload = false) {
|
|
15
|
+
if (!runtime.loadManifest) return null;
|
|
16
|
+
let entry = caches.get(runtime);
|
|
17
|
+
if (!entry) {
|
|
18
|
+
entry = {
|
|
19
|
+
cache: createManifestCache(runtime.loadManifest)
|
|
20
|
+
};
|
|
21
|
+
caches.set(runtime, entry);
|
|
22
|
+
}
|
|
23
|
+
if (entry.forced) return entry.forced;
|
|
24
|
+
if (!reload) return entry.cache.get();
|
|
25
|
+
const state = entry, load = runtime.loadManifest;
|
|
26
|
+
state.forced = (async ()=>{
|
|
27
|
+
const previous = await state.cache.get();
|
|
28
|
+
let initial = true;
|
|
29
|
+
const refreshed = createManifestCache(async (opts)=>{
|
|
30
|
+
const first = initial;
|
|
31
|
+
initial = false;
|
|
32
|
+
const value = await load(first && previous ? {
|
|
33
|
+
ifRevision: previous.revision
|
|
34
|
+
} : opts);
|
|
35
|
+
// Only the cold forced read needs seeding. Later refreshes use this cache's
|
|
36
|
+
// own revision and last-good retention, rather than the original snapshot.
|
|
37
|
+
return first && (value === 'not-modified' || value === null) ? previous : value;
|
|
38
|
+
});
|
|
39
|
+
const value = await refreshed.get();
|
|
40
|
+
if (value) state.cache = refreshed;
|
|
41
|
+
return value ?? previous;
|
|
42
|
+
})().finally(()=>{
|
|
43
|
+
state.forced = undefined;
|
|
44
|
+
});
|
|
45
|
+
return state.forced;
|
|
46
|
+
}
|
|
47
|
+
export function rememberManifest(assignments, manifest, attributes) {
|
|
48
|
+
manifests.set(assignments, manifest);
|
|
49
|
+
if (attributes) requestAttributeSets.set(assignments, Object.freeze({
|
|
50
|
+
...attributes
|
|
51
|
+
}));
|
|
52
|
+
return assignments;
|
|
53
|
+
}
|
|
54
|
+
export const requestAttributesOf = (assignments)=>requestAttributeSets.get(assignments) ?? {};
|
|
55
|
+
export const manifestOf = (assignments)=>manifests.get(assignments);
|
|
56
|
+
export const suppressExposure = (a)=>a.staff || a.bot || a.preview || [
|
|
57
|
+
'HEAD',
|
|
58
|
+
'UNKNOWN'
|
|
59
|
+
].includes(a.method.toUpperCase());
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ExperimentsServerRuntime } from '../types/runtime.js';
|
|
2
|
+
import type { Assignments } from '../types/assignments.js';
|
|
3
|
+
export declare function getAssignments<TUser extends {
|
|
4
|
+
id: string | number;
|
|
5
|
+
} = {
|
|
6
|
+
id: string | number;
|
|
7
|
+
}>(serverRuntime: ExperimentsServerRuntime<TUser>, opts?: {
|
|
8
|
+
user?: TUser | null;
|
|
9
|
+
}): Promise<Assignments>;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { cache } from 'react';
|
|
2
|
+
import { headers } from 'next/headers.js';
|
|
3
|
+
import { XP_HEADER } from '../constants.js';
|
|
4
|
+
import { assign, fillFlagPoints } from '../runtime/assign.js';
|
|
5
|
+
import { readUnitCookie } from '../runtime/cookie.js';
|
|
6
|
+
import { decodeAssignmentsHeader } from '../runtime/header.js';
|
|
7
|
+
import { verifyPreviewToken } from '../runtime/previewToken.js';
|
|
8
|
+
import { isBot } from '../runtime/bot.js';
|
|
9
|
+
import { consentFallback } from '../proxy/consentFallback.js';
|
|
10
|
+
import { requestAttributes } from '../runtime/normalisePath.js';
|
|
11
|
+
import { controlAssignments, emptyAssignments } from '../proxy/control.js';
|
|
12
|
+
import { emptyManifest, rememberManifest, serverManifest, suppressExposure } from './context.js';
|
|
13
|
+
import { serverRequest } from './request.js';
|
|
14
|
+
import { previewDraft } from './previewDraft.js';
|
|
15
|
+
import { verifiedHeaderRevision } from './headerRevision.js';
|
|
16
|
+
// The mutable cell lives in React's request cache, never a process-wide user cache.
|
|
17
|
+
const requestCell = cache((_runtime, _userId)=>({
|
|
18
|
+
value: undefined
|
|
19
|
+
}));
|
|
20
|
+
export async function getAssignments(serverRuntime, opts) {
|
|
21
|
+
const key = opts?.user === undefined ? 'auto' : opts.user === null ? 'anonymous' : `user:${serverRuntime.userIdOf(opts.user)}`;
|
|
22
|
+
const cell = requestCell(serverRuntime, key);
|
|
23
|
+
return cell.value ??= readAssignments(serverRuntime, opts?.user);
|
|
24
|
+
}
|
|
25
|
+
async function readAssignments(runtime, suppliedUser) {
|
|
26
|
+
const requestHeaders = new Headers(await headers());
|
|
27
|
+
const proxy = runtime.proxyRuntime;
|
|
28
|
+
const { request, hasContext, consent } = serverRequest(requestHeaders, proxy.secrets);
|
|
29
|
+
const visitorId = consent === false ? undefined : readUnitCookie(requestHeaders, proxy) ?? undefined;
|
|
30
|
+
let payload;
|
|
31
|
+
let payloadFailed = false;
|
|
32
|
+
try {
|
|
33
|
+
payload = await runtime.getPayload();
|
|
34
|
+
} catch {
|
|
35
|
+
payloadFailed = true;
|
|
36
|
+
console.error('[payload-experiments] getPayload failed; serving non-exposable control');
|
|
37
|
+
}
|
|
38
|
+
const options = payload?.config?.custom?.xp;
|
|
39
|
+
let unit = {
|
|
40
|
+
attributes: {}
|
|
41
|
+
};
|
|
42
|
+
try {
|
|
43
|
+
unit = await proxy.getUnit?.(request) ?? unit;
|
|
44
|
+
} catch {
|
|
45
|
+
unit = {
|
|
46
|
+
attributes: {},
|
|
47
|
+
sessionUnknown: true
|
|
48
|
+
};
|
|
49
|
+
console.error('[payload-experiments] getUnit failed; treating session as unknown');
|
|
50
|
+
}
|
|
51
|
+
if (consent === false) unit = {
|
|
52
|
+
attributes: unit.attributes,
|
|
53
|
+
sessionUnknown: unit.sessionUnknown
|
|
54
|
+
};
|
|
55
|
+
let user = consent === false ? null : suppliedUser;
|
|
56
|
+
const canResolveUser = consent !== false && (suppliedUser !== undefined || Boolean(runtime.getUser));
|
|
57
|
+
let userFailed = false;
|
|
58
|
+
const resolveUser = async ()=>{
|
|
59
|
+
if (user !== undefined) return user;
|
|
60
|
+
try {
|
|
61
|
+
user = await runtime.getUser?.() ?? null;
|
|
62
|
+
} catch {
|
|
63
|
+
userFailed = true;
|
|
64
|
+
user = null;
|
|
65
|
+
console.error('[payload-experiments] getUser failed; suppressing exposures');
|
|
66
|
+
}
|
|
67
|
+
return user;
|
|
68
|
+
};
|
|
69
|
+
const value = requestHeaders.get(XP_HEADER);
|
|
70
|
+
// Consent-false proxy headers are signed without either identity.
|
|
71
|
+
let headerUserId = visitorId ? unit.userId : undefined;
|
|
72
|
+
const authenticate = (userId)=>verifiedHeaderRevision(value, {
|
|
73
|
+
secrets: proxy.secrets,
|
|
74
|
+
visitorId,
|
|
75
|
+
userId
|
|
76
|
+
});
|
|
77
|
+
let revision = authenticate(headerUserId);
|
|
78
|
+
if (revision === null && visitorId && value && (runtime.getUser || suppliedUser)) {
|
|
79
|
+
const resolved = await resolveUser();
|
|
80
|
+
if (resolved) {
|
|
81
|
+
const resolvedId = runtime.userIdOf(resolved);
|
|
82
|
+
revision = authenticate(resolvedId);
|
|
83
|
+
if (revision !== null) headerUserId = resolvedId;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const cached = await serverManifest(proxy);
|
|
87
|
+
let manifest = cached ?? emptyManifest();
|
|
88
|
+
// A cold miss has already spent its sole coldStartMs window. Do not wait again.
|
|
89
|
+
if (cached && revision !== null && revision > manifest.revision) manifest = await serverManifest(proxy, true) ?? manifest;
|
|
90
|
+
const decoded = value && revision !== null ? decodeAssignmentsHeader(value, {
|
|
91
|
+
secrets: proxy.secrets,
|
|
92
|
+
visitorId,
|
|
93
|
+
userId: headerUserId,
|
|
94
|
+
manifest
|
|
95
|
+
}) : null;
|
|
96
|
+
const mustResolveStaff = Boolean(options?.attributeKeys?.staff && (decoded?.sessionUnknown || unit.sessionUnknown));
|
|
97
|
+
const needsUser = !decoded || decoded.deferred.length > 0 || mustResolveStaff || suppliedUser !== undefined;
|
|
98
|
+
if (needsUser && canResolveUser) await resolveUser();
|
|
99
|
+
const pathname = hasContext ? request.nextUrl.pathname : undefined;
|
|
100
|
+
let attributes = requestAttributes(unit.attributes, pathname);
|
|
101
|
+
if (needsUser && canResolveUser) {
|
|
102
|
+
try {
|
|
103
|
+
attributes = requestAttributes({
|
|
104
|
+
...attributes,
|
|
105
|
+
...runtime.attributesOf(user ?? null)
|
|
106
|
+
}, pathname);
|
|
107
|
+
} catch {
|
|
108
|
+
userFailed = true;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const userId = visitorId ? user === undefined ? unit.userId : user ? runtime.userIdOf(user) : undefined : undefined;
|
|
112
|
+
const context = {
|
|
113
|
+
visitorId,
|
|
114
|
+
userId,
|
|
115
|
+
staff: decoded?.staff === true || unit.attributes.staff === true || attributes.staff === true,
|
|
116
|
+
bot: decoded?.bot === true || isBot(requestHeaders.get('user-agent')),
|
|
117
|
+
preview: decoded?.preview ?? false,
|
|
118
|
+
sessionUnknown: needsUser && canResolveUser && !userFailed ? false : decoded?.sessionUnknown ?? unit.sessionUnknown ?? false,
|
|
119
|
+
method: request.method
|
|
120
|
+
};
|
|
121
|
+
const fillHostDefaults = (result)=>{
|
|
122
|
+
for (const point of options?.flagPoints ?? [])if (!Object.hasOwn(result.flagPoints, point.id)) {
|
|
123
|
+
result.flagPoints[point.id] = {
|
|
124
|
+
arm: 'control',
|
|
125
|
+
params: {
|
|
126
|
+
...point.defaultParams
|
|
127
|
+
},
|
|
128
|
+
source: 'default',
|
|
129
|
+
exposable: false
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
};
|
|
134
|
+
const lastResort = ()=>rememberManifest(fillHostDefaults(emptyAssignments(context)), emptyManifest());
|
|
135
|
+
let result;
|
|
136
|
+
if (payloadFailed) {
|
|
137
|
+
try {
|
|
138
|
+
result = controlAssignments(manifest, context);
|
|
139
|
+
} catch {
|
|
140
|
+
return lastResort();
|
|
141
|
+
}
|
|
142
|
+
} else if (consent === false && !decoded) {
|
|
143
|
+
try {
|
|
144
|
+
result = consentFallback(manifest, attributes, context);
|
|
145
|
+
} catch {
|
|
146
|
+
console.error('[payload-experiments] consent fallback failed; serving non-exposable control');
|
|
147
|
+
try {
|
|
148
|
+
result = controlAssignments(manifest, context);
|
|
149
|
+
} catch {
|
|
150
|
+
return lastResort();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
} else if (decoded) {
|
|
154
|
+
result = {
|
|
155
|
+
...decoded,
|
|
156
|
+
...context,
|
|
157
|
+
archivedWinners: {
|
|
158
|
+
...manifest.archivedWinners
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
const slugs = new Set(hasContext ? decoded.deferred : []);
|
|
162
|
+
if (visitorId && user !== undefined && userId !== headerUserId) {
|
|
163
|
+
for (const e of manifest.experiments)if (e.unitType === 'user') slugs.add(e.slug);
|
|
164
|
+
}
|
|
165
|
+
if (visitorId && slugs.size && canResolveUser && !userFailed) {
|
|
166
|
+
try {
|
|
167
|
+
const resolved = assign(manifest, {
|
|
168
|
+
visitorId,
|
|
169
|
+
userId,
|
|
170
|
+
sessionUnknown: context.sessionUnknown
|
|
171
|
+
}, attributes, {
|
|
172
|
+
...context,
|
|
173
|
+
now: Math.floor(Date.now() / 1000)
|
|
174
|
+
});
|
|
175
|
+
for (const slug of slugs){
|
|
176
|
+
result.bySlug[slug] = resolved.bySlug[slug] ?? controlAssignments(manifest, context).bySlug[slug];
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
userFailed = true;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} else if (!visitorId || !hasContext) result = controlAssignments(manifest, context);
|
|
183
|
+
else {
|
|
184
|
+
try {
|
|
185
|
+
result = assign(manifest, {
|
|
186
|
+
visitorId,
|
|
187
|
+
userId,
|
|
188
|
+
sessionUnknown: context.sessionUnknown
|
|
189
|
+
}, attributes, {
|
|
190
|
+
...context,
|
|
191
|
+
now: Math.floor(Date.now() / 1000)
|
|
192
|
+
});
|
|
193
|
+
} catch {
|
|
194
|
+
console.error('[payload-experiments] assign failed; serving non-exposable control');
|
|
195
|
+
result = controlAssignments(manifest, context);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Missing users left deferred by assign must still render explicit, safe control.
|
|
199
|
+
const controls = controlAssignments(manifest, context);
|
|
200
|
+
for (const experiment of manifest.experiments){
|
|
201
|
+
if (!result.bySlug[experiment.slug]) result.bySlug[experiment.slug] = controls.bySlug[experiment.slug];
|
|
202
|
+
}
|
|
203
|
+
fillFlagPoints(result, manifest);
|
|
204
|
+
fillHostDefaults(result);
|
|
205
|
+
const token = hasContext ? verifyPreviewToken(request.nextUrl.searchParams.get('xp_preview') ?? '', proxy.secrets) : null;
|
|
206
|
+
if (token && visitorId && payload) {
|
|
207
|
+
const live = manifest.experiments.find((e)=>e.id === token.e && e.phase === token.p && e.arms.some((a)=>a.key === token.a));
|
|
208
|
+
if (live) {
|
|
209
|
+
result.preview = true;
|
|
210
|
+
result.bySlug[live.slug] = {
|
|
211
|
+
slug: live.slug,
|
|
212
|
+
experimentId: live.id,
|
|
213
|
+
arm: token.a,
|
|
214
|
+
phase: token.p,
|
|
215
|
+
policy: live.exposurePolicy,
|
|
216
|
+
exposable: false,
|
|
217
|
+
source: 'preview'
|
|
218
|
+
};
|
|
219
|
+
fillFlagPoints(result, manifest);
|
|
220
|
+
} else await previewDraft(payload, await resolveUser(), token, result, requestHeaders);
|
|
221
|
+
}
|
|
222
|
+
if (suppressExposure(result) || !hasContext || !visitorId || payloadFailed || userFailed || mustResolveStaff && !runtime.getUser && suppliedUser === undefined) {
|
|
223
|
+
for (const assignment of Object.values(result.bySlug))assignment.exposable = false;
|
|
224
|
+
for (const point of Object.values(result.flagPoints))point.exposable = false;
|
|
225
|
+
}
|
|
226
|
+
return rememberManifest(result, manifest, attributes);
|
|
227
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ExperimentsServerRuntime } from '../types/runtime.js';
|
|
2
|
+
import type { FlagPointResult } from '../types/assignments.js';
|
|
3
|
+
export declare function getFlagPoint<TUser extends {
|
|
4
|
+
id: string | number;
|
|
5
|
+
} = {
|
|
6
|
+
id: string | number;
|
|
7
|
+
}>(serverRuntime: ExperimentsServerRuntime<TUser>, id: string, opts?: {
|
|
8
|
+
user?: TUser | null;
|
|
9
|
+
}): Promise<FlagPointResult & {
|
|
10
|
+
expose(renderedArm: string): Promise<void>;
|
|
11
|
+
}>;
|