@delmaredigital/payload-experiments 0.3.0-rc.1 → 0.4.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +172 -21
- package/dist/admin/MetricSourceSelect.js +3 -3
- package/dist/admin/PlacementStatus.d.ts +3 -0
- package/dist/admin/PlacementStatus.js +54 -0
- package/dist/admin/PreviewLinks.d.ts +2 -1
- package/dist/admin/PreviewLinks.js +47 -26
- package/dist/admin/TestsNavLink.d.ts +3 -1
- package/dist/admin/TestsNavLink.js +2 -2
- package/dist/admin/actions/ConfirmDialog.d.ts +2 -1
- package/dist/admin/actions/ConfirmDialog.js +2 -2
- package/dist/admin/actions/DecideDialog.d.ts +2 -1
- package/dist/admin/actions/DecideDialog.js +2 -2
- package/dist/admin/api.d.ts +11 -2
- package/dist/admin/api.js +20 -2
- package/dist/admin/armSelect/ArmSelectField.js +14 -22
- package/dist/admin/arms/arms.model.d.ts +1 -21
- package/dist/admin/arms/arms.model.js +1 -153
- package/dist/admin/blocks/BlockPanel.d.ts +4 -0
- package/dist/admin/blocks/BlockPanel.js +153 -0
- package/dist/admin/blocks/TestBlockField.d.ts +5 -0
- package/dist/admin/blocks/TestBlockField.js +163 -0
- package/dist/admin/blocks/formApi.d.ts +43 -0
- package/dist/admin/blocks/formApi.js +153 -0
- package/dist/admin/blocks/inject.d.ts +7 -0
- package/dist/admin/blocks/inject.js +41 -0
- package/dist/admin/blocks/model.d.ts +17 -0
- package/dist/admin/blocks/model.js +45 -0
- package/dist/admin/blocks/statusCache.d.ts +7 -0
- package/dist/admin/blocks/statusCache.js +83 -0
- package/dist/admin/blocks/useBlockForm.d.ts +17 -0
- package/dist/admin/blocks/useBlockForm.js +46 -0
- package/dist/admin/copy/actions.d.ts +2 -0
- package/dist/admin/copy/actions.js +9 -0
- package/dist/admin/copy/arms.d.ts +1 -6
- package/dist/admin/copy/arms.js +1 -16
- package/dist/admin/copy/blocks.d.ts +27 -0
- package/dist/admin/copy/blocks.js +38 -0
- package/dist/admin/copy/goals.d.ts +2 -0
- package/dist/admin/copy/goals.js +4 -1
- package/dist/admin/copy/newTest.d.ts +10 -0
- package/dist/admin/copy/newTest.js +10 -0
- package/dist/admin/copy/pages.d.ts +26 -0
- package/dist/admin/copy/pages.js +26 -0
- package/dist/admin/copy/readiness.d.ts +25 -3
- package/dist/admin/copy/readiness.js +25 -3
- package/dist/admin/copy/results.d.ts +3 -0
- package/dist/admin/copy/results.js +4 -1
- package/dist/admin/copy/review.d.ts +1 -1
- package/dist/admin/copy/review.js +1 -1
- package/dist/admin/copy/surface.d.ts +0 -1
- package/dist/admin/copy/surface.js +0 -1
- package/dist/admin/copy/testPage.d.ts +4 -0
- package/dist/admin/copy/testPage.js +4 -0
- package/dist/admin/goals/XpGoalField.d.ts +2 -0
- package/dist/admin/goals/XpGoalField.js +152 -0
- package/dist/admin/metricSource.model.d.ts +2 -0
- package/dist/admin/metricSource.model.js +4 -0
- package/dist/admin/pageCollections.d.ts +10 -0
- package/dist/admin/pageCollections.js +20 -0
- package/dist/admin/paths.d.ts +8 -1
- package/dist/admin/paths.js +8 -1
- package/dist/admin/placementStatus.model.d.ts +2 -0
- package/dist/admin/placementStatus.model.js +19 -0
- package/dist/admin/placements/placementLabels.d.ts +11 -0
- package/dist/admin/placements/placementLabels.js +21 -0
- package/dist/admin/placements/placementLabels.model.d.ts +13 -0
- package/dist/admin/placements/placementLabels.model.js +12 -0
- package/dist/admin/placements/usePlacementLabels.d.ts +12 -0
- package/dist/admin/placements/usePlacementLabels.js +102 -0
- package/dist/admin/previewLinks.model.d.ts +2 -0
- package/dist/admin/previewLinks.model.js +3 -0
- package/dist/admin/results/DataQualityPanel.js +4 -1
- package/dist/admin/results/legacy.d.ts +16 -0
- package/dist/admin/results/legacy.js +27 -0
- package/dist/admin/results/results.model.d.ts +9 -6
- package/dist/admin/results/results.model.js +22 -9
- package/dist/admin/results/snapshot.d.ts +14 -0
- package/dist/admin/results/snapshot.js +3 -0
- package/dist/admin/surface/TestsHome.js +43 -15
- package/dist/admin/surface/TestsSurface.js +5 -1
- package/dist/admin/surface/contracts.d.ts +6 -5
- package/dist/admin/surface/goals/GoalForm.js +67 -34
- package/dist/admin/surface/goals/GoalsList.js +3 -3
- package/dist/admin/surface/goals/data.js +19 -16
- package/dist/admin/surface/goals/goals.model.d.ts +1 -2
- package/dist/admin/surface/goals/goals.model.js +1 -3
- package/dist/admin/surface/home.model.d.ts +2 -1
- package/dist/admin/surface/home.model.js +8 -4
- package/dist/admin/surface/newTest/NewTest.js +58 -17
- package/dist/admin/surface/newTest/PageWizard.d.ts +7 -0
- package/dist/admin/surface/newTest/PageWizard.js +209 -0
- package/dist/admin/surface/pagePicker/CopyRoute.d.ts +6 -0
- package/dist/admin/surface/pagePicker/CopyRoute.js +53 -0
- package/dist/admin/surface/pagePicker/PagePicker.d.ts +10 -0
- package/dist/admin/surface/pagePicker/PagePicker.js +172 -0
- package/dist/admin/surface/pagePicker/pagePicker.model.d.ts +25 -0
- package/dist/admin/surface/pagePicker/pagePicker.model.js +25 -0
- package/dist/admin/surface/readiness/ActionDialogs.js +103 -69
- package/dist/admin/surface/readiness/Banner.js +30 -6
- package/dist/admin/surface/readiness/readiness.model.d.ts +8 -1
- package/dist/admin/surface/readiness/readiness.model.js +95 -6
- package/dist/admin/surface/results/ReleasePages.d.ts +3 -1
- package/dist/admin/surface/results/ReleasePages.js +3 -7
- package/dist/admin/surface/results/ResultsBody.js +23 -3
- package/dist/admin/surface/results/ResultsHeader.js +2 -5
- package/dist/admin/surface/results/recommendation.model.d.ts +3 -2
- package/dist/admin/surface/results/recommendation.model.js +10 -3
- package/dist/admin/surface/results/useResults.d.ts +10 -1
- package/dist/admin/surface/surface.css +0 -1
- package/dist/admin/surface/test/AdvancedSection.js +111 -104
- package/dist/admin/surface/test/GoalSection.js +97 -22
- package/dist/admin/surface/test/LengthSection.js +1 -1
- package/dist/admin/surface/test/PageHoldout.d.ts +3 -0
- package/dist/admin/surface/test/PageHoldout.js +67 -0
- package/dist/admin/surface/test/TestPage.js +2 -1
- package/dist/admin/surface/test/VersionsSection.js +12 -5
- package/dist/admin/surface/test/fields.d.ts +13 -4
- package/dist/admin/surface/test/fields.js +46 -14
- package/dist/admin/surface/test/jumpToSection.d.ts +1 -1
- package/dist/admin/surface/test/jumpToSection.js +4 -3
- package/dist/admin/surface/test/pageParts.d.ts +4 -1
- package/dist/admin/surface/test/pageParts.js +79 -2
- package/dist/admin/surface/test/testPage.css +8 -1
- package/dist/admin/surface/test/testPage.model.d.ts +2 -3
- package/dist/admin/surface/test/testPage.model.js +1 -21
- package/dist/admin/surface/testDocument.model.d.ts +1 -0
- package/dist/admin/surface/testDocument.model.js +2 -1
- package/dist/admin/surface/useTestDocument.d.ts +11 -2
- package/dist/admin/surface/useTestDocument.js +55 -16
- package/dist/admin/useApi.d.ts +11 -2
- package/dist/admin/useApi.js +3 -5
- package/dist/admin/useLayerUsage.js +14 -23
- package/dist/admin/views/ExperimentsRootView.js +58 -16
- package/dist/admin/views/TestsNavLinkServer.d.ts +2 -0
- package/dist/admin/views/TestsNavLinkServer.js +43 -0
- package/dist/arms/copy.d.ts +6 -0
- package/dist/arms/copy.js +16 -0
- package/dist/arms/model.d.ts +23 -0
- package/dist/arms/model.js +173 -0
- package/dist/blocks/bindings.d.ts +3 -0
- package/dist/blocks/bindings.js +34 -0
- package/dist/blocks/createExperimentBlock.d.ts +6 -0
- package/dist/blocks/createExperimentBlock.js +77 -0
- package/dist/blocks/formState.d.ts +31 -0
- package/dist/blocks/formState.js +146 -0
- package/dist/blocks/model.d.ts +41 -0
- package/dist/blocks/model.js +34 -0
- package/dist/client/ExperimentGroup.d.ts +6 -0
- package/dist/client/ExperimentGroup.js +9 -0
- package/dist/client/ExperimentsProvider.js +20 -4
- package/dist/client/clickGoals.d.ts +4 -0
- package/dist/client/clickGoals.js +43 -0
- package/dist/collections/experiments/config.js +12 -2
- package/dist/collections/experiments/deleteGuard.d.ts +2 -1
- package/dist/collections/experiments/deleteGuard.js +12 -1
- package/dist/collections/experiments/editability.js +4 -2
- package/dist/collections/experiments/pickSlug.d.ts +2 -0
- package/dist/collections/experiments/pickSlug.js +8 -0
- package/dist/collections/labels.d.ts +3 -0
- package/dist/collections/labels.js +20 -0
- package/dist/collections/metrics.js +12 -2
- package/dist/collections/phases.js +1 -0
- package/dist/collections/serialize.js +7 -0
- package/dist/db/appends.d.ts +6 -2
- package/dist/db/appends.js +10 -4
- package/dist/db/clickGoals.d.ts +9 -0
- package/dist/db/clickGoals.js +22 -0
- package/dist/db/delete.d.ts +1 -0
- package/dist/db/delete.js +1 -0
- package/dist/db/locks.d.ts +14 -0
- package/dist/db/locks.js +36 -1
- package/dist/db/names.d.ts +4 -0
- package/dist/db/names.js +4 -0
- package/dist/db/pageBindings.d.ts +4 -0
- package/dist/db/pageBindings.js +17 -0
- package/dist/db/pageBusy.d.ts +2 -0
- package/dist/db/pageBusy.js +10 -0
- package/dist/db/pageContent.d.ts +10 -0
- package/dist/db/pageContent.js +79 -0
- package/dist/db/pageOccupancy.d.ts +12 -0
- package/dist/db/pageOccupancy.js +45 -0
- package/dist/db/placements.d.ts +33 -0
- package/dist/db/placements.js +54 -0
- package/dist/db/slugs.d.ts +8 -0
- package/dist/db/slugs.js +40 -0
- package/dist/db/snapshots.d.ts +6 -0
- package/dist/db/snapshots.js +9 -5
- package/dist/endpoints/clickKeys.d.ts +5 -0
- package/dist/endpoints/clickKeys.js +17 -0
- package/dist/endpoints/events.js +4 -1
- package/dist/endpoints/index.js +28 -0
- package/dist/endpoints/pages.d.ts +5 -0
- package/dist/endpoints/pages.js +120 -0
- package/dist/endpoints/placementStatus.d.ts +6 -0
- package/dist/endpoints/placementStatus.js +238 -0
- package/dist/endpoints/readBody.d.ts +2 -0
- package/dist/endpoints/readBody.js +24 -0
- package/dist/endpoints/results.js +5 -1
- package/dist/endpoints/summary.js +11 -4
- package/dist/endpoints/wrapBlock.d.ts +6 -0
- package/dist/endpoints/wrapBlock.js +76 -0
- package/dist/errors.d.ts +3 -3
- package/dist/errors.js +4 -4
- package/dist/events/rows.d.ts +1 -1
- package/dist/events/rows.js +5 -3
- package/dist/exports/adminClient.d.ts +5 -0
- package/dist/exports/adminClient.js +5 -0
- package/dist/exports/adminRsc.d.ts +1 -0
- package/dist/exports/adminRsc.js +1 -0
- package/dist/exports/blocks.d.ts +2 -0
- package/dist/exports/blocks.js +1 -0
- package/dist/exports/client.d.ts +1 -0
- package/dist/exports/client.js +1 -0
- package/dist/exports/index.d.ts +6 -1
- package/dist/exports/index.js +3 -0
- package/dist/exports/puck.d.ts +5 -2
- package/dist/exports/puck.js +3 -1
- package/dist/exports/puckEditor.d.ts +6 -1
- package/dist/exports/puckEditor.js +4 -1
- package/dist/exports/rsc.d.ts +4 -0
- package/dist/exports/rsc.js +2 -0
- package/dist/fields/xpGoal.d.ts +3 -0
- package/dist/fields/xpGoal.js +28 -0
- package/dist/globals/state.js +10 -0
- package/dist/goals/cache.d.ts +3 -0
- package/dist/goals/cache.js +35 -0
- package/dist/goals/fairness.d.ts +40 -0
- package/dist/goals/fairness.js +200 -0
- package/dist/goals/key.d.ts +4 -0
- package/dist/goals/key.js +3 -0
- package/dist/goals/picker/clickGoalPicker.model.d.ts +29 -0
- package/dist/goals/picker/clickGoalPicker.model.js +45 -0
- package/dist/goals/picker/copy.d.ts +16 -0
- package/dist/goals/picker/copy.js +15 -0
- package/dist/goals/picker/useClickGoalPicker.d.ts +30 -0
- package/dist/goals/picker/useClickGoalPicker.js +220 -0
- package/dist/hooks/pageGuard.d.ts +5 -1
- package/dist/hooks/pageGuard.js +160 -80
- package/dist/lib/fetchAllDocs.d.ts +16 -0
- package/dist/lib/fetchAllDocs.js +56 -0
- package/dist/onInit.js +29 -0
- package/dist/options.d.ts +2 -0
- package/dist/options.js +43 -10
- package/dist/plugin.js +45 -28
- package/dist/puck/ExperimentPicker.js +47 -16
- package/dist/puck/ExperimentSlot.d.ts +51 -1
- package/dist/puck/ExperimentSlot.js +23 -12
- package/dist/puck/copy.d.ts +6 -4
- package/dist/puck/copy.js +6 -4
- package/dist/puck/editor/CreateExperimentDrawer.js +46 -56
- package/dist/puck/editor/ExperimentSlotEditor.d.ts +52 -1
- package/dist/puck/editor/ExperimentSlotEditor.js +292 -137
- package/dist/puck/editor/ResultsReadout.d.ts +6 -0
- package/dist/puck/editor/ResultsReadout.js +99 -0
- package/dist/puck/editor/VersionBuilder.d.ts +34 -0
- package/dist/puck/editor/VersionBuilder.js +317 -0
- package/dist/puck/editor/armWrites.d.ts +7 -0
- package/dist/puck/editor/armWrites.js +71 -0
- package/dist/puck/editor/armsCache.d.ts +14 -0
- package/dist/puck/editor/armsCache.js +78 -0
- package/dist/puck/editor/copy.d.ts +33 -1
- package/dist/puck/editor/copy.js +45 -8
- package/dist/puck/editor/createTest.d.ts +1 -1
- package/dist/puck/editor/createTest.js +25 -14
- package/dist/puck/editor/editor.chrome.css +48 -161
- package/dist/puck/editor/editor.css +48 -161
- package/dist/puck/editor/editor.model.d.ts +8 -3
- package/dist/puck/editor/editor.model.js +16 -10
- package/dist/puck/editor/experimentOutlinePlugin.d.ts +3 -0
- package/dist/puck/editor/experimentOutlinePlugin.js +89 -0
- package/dist/puck/editor/integration.d.ts +10 -0
- package/dist/puck/editor/integration.js +30 -0
- package/dist/puck/editor/xpGoalPuckEditorField.d.ts +7 -0
- package/dist/puck/editor/xpGoalPuckEditorField.js +134 -0
- package/dist/puck/experimentSlotField.d.ts +2 -0
- package/dist/puck/experimentSlotField.js +11 -3
- package/dist/puck/hostContext.d.ts +10 -0
- package/dist/puck/hostContext.js +12 -0
- package/dist/puck/integration.d.ts +6 -0
- package/dist/puck/integration.js +21 -0
- package/dist/puck/routes.d.ts +12 -0
- package/dist/puck/routes.js +11 -0
- package/dist/puck/slotConfig.d.ts +1 -1
- package/dist/puck/slotConfig.js +2 -2
- package/dist/puck/xpGoalField.d.ts +3 -0
- package/dist/puck/xpGoalField.js +6 -0
- package/dist/results/health.d.ts +4 -0
- package/dist/results/health.js +27 -0
- package/dist/results/snapshot.js +15 -2
- package/dist/sanitizeReadinessDetail.js +51 -1
- package/dist/server/metadata.js +9 -2
- package/dist/server/normaliseContentRelationships.d.ts +7 -0
- package/dist/server/normaliseContentRelationships.js +62 -0
- package/dist/server/pageKindSeoContract.d.ts +27 -0
- package/dist/server/pageKindSeoContract.js +80 -0
- package/dist/server/recordContentChange.d.ts +1 -1
- package/dist/server/recordContentChange.js +72 -13
- package/dist/server/resolveExperimentBlocks.d.ts +5 -0
- package/dist/server/resolveExperimentBlocks.js +54 -0
- package/dist/server/runResults.js +92 -19
- package/dist/server/toClientAssignments.js +2 -1
- package/dist/transitions/addMetric.d.ts +2 -1
- package/dist/transitions/addMetric.js +28 -1
- package/dist/transitions/checkStart.d.ts +17 -1
- package/dist/transitions/checkStart.js +99 -7
- package/dist/transitions/decide.js +40 -1
- package/dist/transitions/deleteExperiment.d.ts +3 -1
- package/dist/transitions/deleteExperiment.js +27 -1
- package/dist/transitions/kernel.js +4 -0
- package/dist/transitions/makeContent.d.ts +5 -0
- package/dist/transitions/makeContent.js +39 -11
- package/dist/transitions/pageCollection.d.ts +10 -0
- package/dist/transitions/pageCollection.js +20 -0
- package/dist/transitions/pageVersions.d.ts +10 -0
- package/dist/transitions/pageVersions.js +44 -0
- package/dist/transitions/phaseRecord.d.ts +1 -0
- package/dist/transitions/placementInteractions.d.ts +40 -0
- package/dist/transitions/placementInteractions.js +94 -0
- package/dist/transitions/placements.d.ts +10 -6
- package/dist/transitions/placements.js +156 -2
- package/dist/transitions/preflightData.d.ts +8 -0
- package/dist/transitions/preflightData.js +248 -36
- package/dist/transitions/resolveCaps.d.ts +1 -0
- package/dist/transitions/resolveCaps.js +6 -2
- package/dist/transitions/start.js +77 -3
- package/dist/types/admin.d.ts +1 -1
- package/dist/types/assignments.d.ts +1 -0
- package/dist/types/contracts.d.ts +31 -10
- package/dist/types/endpoints.d.ts +92 -5
- package/dist/types/index.d.ts +3 -0
- package/dist/types/puck.d.ts +7 -1
- package/dist/types/snapshot.d.ts +17 -4
- package/dist/types/surface.d.ts +13 -0
- package/dist/ui/Tabs.d.ts +4 -2
- package/dist/ui/Tabs.js +33 -26
- package/dist/ui/accent.d.ts +22 -3
- package/dist/ui/accent.js +74 -16
- package/package.json +12 -2
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { BLOCK_SLOTS, EXPERIMENT_BLOCK_SLUG, isObject, slotBlocks } from './model.js';
|
|
2
|
+
/** Mirror Payload 3.90.2's collections/operations/utilities/copyDataWithFreshRowIDs.js:
|
|
3
|
+
* walk configured row/subfield containers only. Mint ids for array/blocks rows, leaving
|
|
4
|
+
* opaque values (including json, richText, relationships and uploads) unchanged.
|
|
5
|
+
* Global block definitions resolve blockReferences; inline slot blocks retain host configs.
|
|
6
|
+
*/ export function freshBlockIds(value, field, config = {}, mint = ()=>globalThis.crypto.randomUUID()) {
|
|
7
|
+
function copyObject(data, fields, parentIsLocalized = false) {
|
|
8
|
+
const result = {
|
|
9
|
+
...data
|
|
10
|
+
};
|
|
11
|
+
for (const child of fields){
|
|
12
|
+
if (child.type === 'row' || child.type === 'collapsible' || child.type === 'group' && !('name' in child && child.name)) {
|
|
13
|
+
Object.assign(result, copyObject(result, child.fields, parentIsLocalized));
|
|
14
|
+
} else if (child.type === 'tabs') {
|
|
15
|
+
for (const tab of child.tabs){
|
|
16
|
+
if ('name' in tab && tab.name) {
|
|
17
|
+
if (Object.hasOwn(result, tab.name)) result[tab.name] = subfields(result[tab.name], tab.fields, tab.localized, parentIsLocalized);
|
|
18
|
+
} else Object.assign(result, copyObject(result, tab.fields, parentIsLocalized));
|
|
19
|
+
}
|
|
20
|
+
} else if (child.type === 'group' && 'name' in child && child.name) {
|
|
21
|
+
if (Object.hasOwn(result, child.name)) result[child.name] = subfields(result[child.name], child.fields, child.localized, parentIsLocalized);
|
|
22
|
+
} else if (child.type === 'array' || child.type === 'blocks') {
|
|
23
|
+
if (Object.hasOwn(result, child.name)) result[child.name] = copyRows(result[child.name], child, parentIsLocalized || Boolean(child.localized));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
function subfields(data, fields, localized, parentIsLocalized) {
|
|
29
|
+
if (!isObject(data)) return data;
|
|
30
|
+
const locales = config.localization ? config.localization.localeCodes : [];
|
|
31
|
+
if (localized && !parentIsLocalized && Object.keys(data).length > 0 && Object.keys(data).every((key)=>locales.includes(key))) return Object.fromEntries(Object.entries(data).map(([locale, value])=>[
|
|
32
|
+
locale,
|
|
33
|
+
isObject(value) ? copyObject(value, fields, true) : value
|
|
34
|
+
]));
|
|
35
|
+
return copyObject(data, fields, parentIsLocalized || Boolean(localized));
|
|
36
|
+
}
|
|
37
|
+
function copyRows(data, rowsField, parentIsLocalized = false) {
|
|
38
|
+
if (Array.isArray(data)) return data.map((row)=>{
|
|
39
|
+
if (!isObject(row)) return row;
|
|
40
|
+
const fields = rowsField.type === 'array' ? rowsField.fields : (config.blocks?.find((block)=>block.slug === row.blockType) ?? [
|
|
41
|
+
...rowsField.blocks,
|
|
42
|
+
...(rowsField.blockReferences ?? []).filter((block)=>typeof block !== 'string')
|
|
43
|
+
].find((block)=>block.slug === row.blockType))?.fields ?? [];
|
|
44
|
+
return {
|
|
45
|
+
...copyObject(row, fields, parentIsLocalized),
|
|
46
|
+
id: mint()
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
if (isObject(data)) return Object.fromEntries(Object.entries(data).map(([locale, rows])=>[
|
|
50
|
+
locale,
|
|
51
|
+
Array.isArray(rows) ? copyRows(rows, rowsField, true) : rows
|
|
52
|
+
]));
|
|
53
|
+
return data;
|
|
54
|
+
}
|
|
55
|
+
return copyRows(value, field);
|
|
56
|
+
}
|
|
57
|
+
export function wrapExperimentBlock(block, experiment, field, config = {}, blockSlug = EXPERIMENT_BLOCK_SLUG) {
|
|
58
|
+
if (block.blockType === blockSlug) throw new Error('Experiment blocks cannot be nested.');
|
|
59
|
+
return {
|
|
60
|
+
id: globalThis.crypto.randomUUID(),
|
|
61
|
+
blockType: blockSlug,
|
|
62
|
+
experiment,
|
|
63
|
+
slots: [
|
|
64
|
+
{
|
|
65
|
+
id: globalThis.crypto.randomUUID(),
|
|
66
|
+
slot: 'control',
|
|
67
|
+
blocks: [
|
|
68
|
+
block
|
|
69
|
+
]
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: globalThis.crypto.randomUUID(),
|
|
73
|
+
slot: 'variantA',
|
|
74
|
+
blocks: freshBlockIds([
|
|
75
|
+
block
|
|
76
|
+
], field, config)
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function copySlotBlocks(block, slot, field, config) {
|
|
82
|
+
// Traverse the wrapper too: slot content may use blocks defined only inside
|
|
83
|
+
// createExperimentBlock, rather than in the host's outer blocks field.
|
|
84
|
+
return slotBlocks(freshBlockIds([
|
|
85
|
+
block
|
|
86
|
+
], field, config)[0], slot);
|
|
87
|
+
}
|
|
88
|
+
export function syncExperimentSlots(block, arms, field, config = {}) {
|
|
89
|
+
const wanted = arms.filter((a)=>a.kind !== 'holdout' && a.key !== 'holdout').map((a)=>a.slot ?? a.key).filter((slot)=>BLOCK_SLOTS.includes(slot));
|
|
90
|
+
const slots = [
|
|
91
|
+
...new Set(wanted)
|
|
92
|
+
].map((slot)=>block.slots.find((row)=>row.slot === slot) ?? {
|
|
93
|
+
id: globalThis.crypto.randomUUID(),
|
|
94
|
+
slot,
|
|
95
|
+
blocks: copySlotBlocks(block, 'control', field, config)
|
|
96
|
+
});
|
|
97
|
+
return {
|
|
98
|
+
...block,
|
|
99
|
+
slots
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export function copyOriginalIntoVersion(block, slot, field, config = {}) {
|
|
103
|
+
return {
|
|
104
|
+
...block,
|
|
105
|
+
slots: block.slots.map((row)=>row.slot === slot && slot !== 'control' ? {
|
|
106
|
+
...row,
|
|
107
|
+
blocks: copySlotBlocks(block, 'control', field, config)
|
|
108
|
+
} : row)
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
export function unwrapExperimentBlock(block, field, slot = 'control', config = {}) {
|
|
112
|
+
return copySlotBlocks(block, slot, field, config);
|
|
113
|
+
}
|
|
114
|
+
/** Config coordinates include unnamed layout containers. No field component is injected here. */ export function findRegisteredBlocksField(fields, name) {
|
|
115
|
+
function visit(fields, path) {
|
|
116
|
+
for(let i = 0; i < fields.length; i++){
|
|
117
|
+
const field = fields[i], here = [
|
|
118
|
+
...path,
|
|
119
|
+
i
|
|
120
|
+
];
|
|
121
|
+
if ('name' in field && field.name === name && field.type === 'blocks') return {
|
|
122
|
+
field,
|
|
123
|
+
path: here
|
|
124
|
+
};
|
|
125
|
+
if (field.type === 'tabs') for(let t = 0; t < field.tabs.length; t++){
|
|
126
|
+
const tab = field.tabs[t];
|
|
127
|
+
if ('name' in tab && tab.name) continue;
|
|
128
|
+
const found = visit(field.tabs[t].fields, [
|
|
129
|
+
...here,
|
|
130
|
+
'tabs',
|
|
131
|
+
t,
|
|
132
|
+
'fields'
|
|
133
|
+
]);
|
|
134
|
+
if (found) return found;
|
|
135
|
+
}
|
|
136
|
+
if (field.type === 'row' || field.type === 'collapsible') {
|
|
137
|
+
const found = visit(field.fields, [
|
|
138
|
+
...here,
|
|
139
|
+
'fields'
|
|
140
|
+
]);
|
|
141
|
+
if (found) return found;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return visit(fields, []);
|
|
146
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { RenderedExposure } from '../types/assignments.js';
|
|
2
|
+
export declare const EXPERIMENT_BLOCK_SLUG = "xpExperiment";
|
|
3
|
+
export declare const BLOCK_SLOTS: readonly ["control", "variantA", "variantB", "variantC"];
|
|
4
|
+
export interface HostBlock {
|
|
5
|
+
id?: string;
|
|
6
|
+
blockType: string;
|
|
7
|
+
blockName?: string;
|
|
8
|
+
[field: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
export interface ExperimentBlock extends HostBlock {
|
|
11
|
+
experiment: string;
|
|
12
|
+
slots: Array<{
|
|
13
|
+
id?: string;
|
|
14
|
+
slot: string;
|
|
15
|
+
blocks: HostBlock[];
|
|
16
|
+
}>;
|
|
17
|
+
}
|
|
18
|
+
export interface ExperimentBlockGroup {
|
|
19
|
+
slug: string;
|
|
20
|
+
expose?: RenderedExposure;
|
|
21
|
+
}
|
|
22
|
+
export type ResolvedHostBlock = HostBlock & {
|
|
23
|
+
xpGroup?: ExperimentBlockGroup;
|
|
24
|
+
};
|
|
25
|
+
export interface ResolvedExperimentBlocks {
|
|
26
|
+
blocks: ResolvedHostBlock[];
|
|
27
|
+
rendered: RenderedExposure[];
|
|
28
|
+
groups: Array<ExperimentBlockGroup & {
|
|
29
|
+
start: number;
|
|
30
|
+
end: number;
|
|
31
|
+
}>;
|
|
32
|
+
}
|
|
33
|
+
export declare const isObject: (v: unknown) => v is Record<string, unknown>;
|
|
34
|
+
export declare const hostBlocks: (v: unknown) => HostBlock[];
|
|
35
|
+
export declare function slotBlocks(block: HostBlock, slot: string): HostBlock[];
|
|
36
|
+
/** Experiments-off rendering preserves stored row ids; only top-level wrappers are read. */
|
|
37
|
+
export declare function controlOnlyBlocks(blocks: unknown, blockSlug?: string): HostBlock[];
|
|
38
|
+
export declare function segmentExperimentBlocks(blocks: readonly ResolvedHostBlock[]): Array<{
|
|
39
|
+
group?: ExperimentBlockGroup;
|
|
40
|
+
blocks: HostBlock[];
|
|
41
|
+
}>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const EXPERIMENT_BLOCK_SLUG = 'xpExperiment';
|
|
2
|
+
export const BLOCK_SLOTS = [
|
|
3
|
+
'control',
|
|
4
|
+
'variantA',
|
|
5
|
+
'variantB',
|
|
6
|
+
'variantC'
|
|
7
|
+
];
|
|
8
|
+
export const isObject = (v)=>!!v && typeof v === 'object' && !Array.isArray(v);
|
|
9
|
+
export const hostBlocks = (v)=>Array.isArray(v) ? v.filter((b)=>isObject(b) && typeof b.blockType === 'string') : [];
|
|
10
|
+
export function slotBlocks(block, slot) {
|
|
11
|
+
const row = Array.isArray(block.slots) ? block.slots.find((row)=>isObject(row) && row.slot === slot) : undefined;
|
|
12
|
+
return isObject(row) ? hostBlocks(row.blocks) : [];
|
|
13
|
+
}
|
|
14
|
+
/** Experiments-off rendering preserves stored row ids; only top-level wrappers are read. */ export function controlOnlyBlocks(blocks, blockSlug = EXPERIMENT_BLOCK_SLUG) {
|
|
15
|
+
return hostBlocks(blocks).flatMap((block)=>block.blockType === blockSlug ? slotBlocks(block, 'control') : [
|
|
16
|
+
block
|
|
17
|
+
]);
|
|
18
|
+
}
|
|
19
|
+
export function segmentExperimentBlocks(blocks) {
|
|
20
|
+
const runs = [];
|
|
21
|
+
for (const block of blocks){
|
|
22
|
+
const group = block.xpGroup, last = runs.at(-1);
|
|
23
|
+
if (last && last.group === group) last.blocks.push(block);
|
|
24
|
+
else runs.push({
|
|
25
|
+
...group ? {
|
|
26
|
+
group
|
|
27
|
+
} : {},
|
|
28
|
+
blocks: [
|
|
29
|
+
block
|
|
30
|
+
]
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return runs;
|
|
34
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { ViewportExposure } from './ViewportExposure.js';
|
|
4
|
+
export function ExperimentGroup({ expose, children }) {
|
|
5
|
+
return /*#__PURE__*/ _jsx(ViewportExposure, {
|
|
6
|
+
expose: expose,
|
|
7
|
+
children: children
|
|
8
|
+
});
|
|
9
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import { useLayoutEffect, useRef } from 'react';
|
|
3
|
+
import { createContext, useContext, useLayoutEffect, useRef } from 'react';
|
|
4
4
|
import { registerApiRoute, unregisterApiRoute } from './beacon.js';
|
|
5
|
+
import { registerClickGoals, unregisterClickGoals } from './clickGoals.js';
|
|
5
6
|
import { ExperimentsContext } from './context.js';
|
|
7
|
+
const DepthContext = /*#__PURE__*/ createContext(0);
|
|
6
8
|
export function ExperimentsProvider(props) {
|
|
9
|
+
const depth = useContext(DepthContext) + 1;
|
|
7
10
|
const token = useRef(Symbol()).current;
|
|
8
11
|
// Register committed providers before descendants can send from passive effects.
|
|
9
12
|
useLayoutEffect(()=>{
|
|
@@ -15,8 +18,21 @@ export function ExperimentsProvider(props) {
|
|
|
15
18
|
useLayoutEffect(()=>()=>unregisterApiRoute(token), [
|
|
16
19
|
token
|
|
17
20
|
]);
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
useLayoutEffect(()=>{
|
|
22
|
+
registerClickGoals(token, depth, props.value);
|
|
23
|
+
}, [
|
|
24
|
+
token,
|
|
25
|
+
depth,
|
|
26
|
+
props.value
|
|
27
|
+
]);
|
|
28
|
+
useLayoutEffect(()=>()=>unregisterClickGoals(token), [
|
|
29
|
+
token
|
|
30
|
+
]);
|
|
31
|
+
return /*#__PURE__*/ _jsx(DepthContext.Provider, {
|
|
32
|
+
value: depth,
|
|
33
|
+
children: /*#__PURE__*/ _jsx(ExperimentsContext.Provider, {
|
|
34
|
+
value: props.value,
|
|
35
|
+
children: props.children
|
|
36
|
+
})
|
|
21
37
|
});
|
|
22
38
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ClientAssignments } from '../types/assignments.js';
|
|
2
|
+
/** One delegated listener; depth, rather than React's child-first commit order, decides ownership. */
|
|
3
|
+
export declare function registerClickGoals(token: symbol, depth: number, value: ClientAssignments): void;
|
|
4
|
+
export declare function unregisterClickGoals(token: symbol): void;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { CLICK_KEY } from '../goals/key.js';
|
|
3
|
+
import { sendEvent } from './beacon.js';
|
|
4
|
+
import { idempotencyKey } from './idempotency.js';
|
|
5
|
+
const providers = new Map();
|
|
6
|
+
let listening;
|
|
7
|
+
function onClick(event) {
|
|
8
|
+
let owner;
|
|
9
|
+
for (const provider of providers.values()){
|
|
10
|
+
if (provider.value.clickGoalsArmed && (!owner || provider.depth >= owner.depth)) owner = provider;
|
|
11
|
+
}
|
|
12
|
+
if (!owner) return;
|
|
13
|
+
const target = event.target;
|
|
14
|
+
const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
|
|
15
|
+
const key = element?.closest('[data-xp-goal]')?.getAttribute('data-xp-goal');
|
|
16
|
+
if (!key || !CLICK_KEY.test(key)) return;
|
|
17
|
+
sendEvent(owner.value.apiRoute, {
|
|
18
|
+
type: 'conversion',
|
|
19
|
+
eventName: `click:${key}`,
|
|
20
|
+
idempotencyKey: `${owner.pageviewId}:${key}`
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/** One delegated listener; depth, rather than React's child-first commit order, decides ownership. */ export function registerClickGoals(token, depth, value) {
|
|
24
|
+
const previous = providers.get(token);
|
|
25
|
+
const pageviewId = previous?.value === value ? previous.pageviewId : idempotencyKey();
|
|
26
|
+
providers.delete(token);
|
|
27
|
+
providers.set(token, {
|
|
28
|
+
depth,
|
|
29
|
+
value,
|
|
30
|
+
pageviewId
|
|
31
|
+
});
|
|
32
|
+
if (!listening && typeof document !== 'undefined') {
|
|
33
|
+
listening = document;
|
|
34
|
+
listening.addEventListener('click', onClick);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function unregisterClickGoals(token) {
|
|
38
|
+
providers.delete(token);
|
|
39
|
+
if (!providers.size && listening) {
|
|
40
|
+
listening.removeEventListener('click', onClick);
|
|
41
|
+
listening = undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -5,8 +5,9 @@ import { TARGETING_OPS, isOpValid } from '../../targeting/ops.js';
|
|
|
5
5
|
import { readListValue } from '../../targeting/values.js';
|
|
6
6
|
import { createDefaults } from './createDefaults.js';
|
|
7
7
|
import { flagControlParams } from './flagControlParams.js';
|
|
8
|
+
import { pickExperimentSlug } from './pickSlug.js';
|
|
8
9
|
import { lockHook } from './lockHook.js';
|
|
9
|
-
import { deleteGuard, deleteGuardAfter } from './deleteGuard.js';
|
|
10
|
+
import { deleteGuard, deleteGuardAfter, deleteGuardBeforeOperation } from './deleteGuard.js';
|
|
10
11
|
import { checkArms } from './armInvariants.js';
|
|
11
12
|
import { XpNotImplemented, XpRefusal } from '../../errors.js';
|
|
12
13
|
const targetingValue = (rule)=>rule.op === 'in' || rule.op === 'nin' ? readListValue(rule.value) : rule.value;
|
|
@@ -81,12 +82,16 @@ export function experimentsCollection(options) {
|
|
|
81
82
|
}
|
|
82
83
|
},
|
|
83
84
|
hooks: {
|
|
85
|
+
beforeOperation: [
|
|
86
|
+
deleteGuardBeforeOperation
|
|
87
|
+
],
|
|
84
88
|
beforeValidate: [
|
|
85
89
|
createDefaults
|
|
86
90
|
],
|
|
87
91
|
beforeChange: [
|
|
88
92
|
flagControlParams,
|
|
89
|
-
lockHook
|
|
93
|
+
lockHook,
|
|
94
|
+
pickExperimentSlug
|
|
90
95
|
],
|
|
91
96
|
beforeDelete: [
|
|
92
97
|
deleteGuard
|
|
@@ -166,6 +171,11 @@ export function experimentsCollection(options) {
|
|
|
166
171
|
read: async (args)=>args.doc?.kind !== 'code' || adminOnly(args)
|
|
167
172
|
}
|
|
168
173
|
},
|
|
174
|
+
{
|
|
175
|
+
name: 'pageCollection',
|
|
176
|
+
type: 'text',
|
|
177
|
+
validate: (value, { previousValue })=>value === previousValue || value == null || value === '' || options.pages?.collections.some((entry)=>entry.slug === value) || 'Choose a registered page collection.'
|
|
178
|
+
},
|
|
169
179
|
{
|
|
170
180
|
name: 'flagPoint',
|
|
171
181
|
type: 'text',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import type { CollectionAfterDeleteHook, CollectionBeforeDeleteHook } from 'payload';
|
|
1
|
+
import type { CollectionBeforeOperationHook, CollectionAfterDeleteHook, CollectionBeforeDeleteHook } from 'payload';
|
|
2
2
|
export declare const deleteGuard: CollectionBeforeDeleteHook;
|
|
3
3
|
export declare const deleteGuardAfter: CollectionAfterDeleteHook;
|
|
4
|
+
export declare const deleteGuardBeforeOperation: CollectionBeforeOperationHook;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { deleteExperiment, deleteExperimentAfter } from '../../transitions/deleteExperiment.js';
|
|
1
|
+
import { deleteExperiment, deleteExperimentAfter, prepareBulkDelete } from '../../transitions/deleteExperiment.js';
|
|
2
|
+
import { xpOptionsFromReq } from '../../options/fromReq.js';
|
|
2
3
|
export const deleteGuard = async ({ req, id })=>{
|
|
3
4
|
await deleteExperiment(req, id);
|
|
4
5
|
};
|
|
@@ -6,3 +7,13 @@ export const deleteGuardAfter = async ({ req, id, doc })=>{
|
|
|
6
7
|
await deleteExperimentAfter(req, id);
|
|
7
8
|
return doc;
|
|
8
9
|
};
|
|
10
|
+
export const deleteGuardBeforeOperation = async ({ operation, args, req })=>{
|
|
11
|
+
if (operation === 'delete' && 'where' in args && args.where) {
|
|
12
|
+
// Payload runs beforeOperation before access; only a manager, or an explicit overrideAccess call, may make the plugin discover and lock the delete scope.
|
|
13
|
+
if (args.overrideAccess !== true && await xpOptionsFromReq(req).access.manage({
|
|
14
|
+
req
|
|
15
|
+
}) !== true) return args;
|
|
16
|
+
await prepareBulkDelete(req, args.where);
|
|
17
|
+
}
|
|
18
|
+
return args;
|
|
19
|
+
};
|
|
@@ -8,7 +8,8 @@ export const EDITABLE_BY_STATUS = {
|
|
|
8
8
|
'flagPoint',
|
|
9
9
|
'metrics',
|
|
10
10
|
'analysis',
|
|
11
|
-
'exposurePolicy'
|
|
11
|
+
'exposurePolicy',
|
|
12
|
+
'pageCollection'
|
|
12
13
|
],
|
|
13
14
|
stopped: [
|
|
14
15
|
'name',
|
|
@@ -18,7 +19,8 @@ export const EDITABLE_BY_STATUS = {
|
|
|
18
19
|
'allocation',
|
|
19
20
|
'metrics',
|
|
20
21
|
'analysis',
|
|
21
|
-
'exposurePolicy'
|
|
22
|
+
'exposurePolicy',
|
|
23
|
+
'pageCollection'
|
|
22
24
|
],
|
|
23
25
|
running: [
|
|
24
26
|
'name',
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { pickSlug } from '../../db/slugs.js';
|
|
2
|
+
import { pooledExecutor, txExecutor } from '../../db/executor.js';
|
|
3
|
+
export const pickExperimentSlug = async ({ req, data, operation })=>{
|
|
4
|
+
if (operation !== 'create' || typeof data.slug !== 'string' || !data.slug) return data;
|
|
5
|
+
const transactional = await req.transactionID != null;
|
|
6
|
+
data.slug = await pickSlug(transactional ? txExecutor(req) : pooledExecutor(req.payload), data.slug, transactional);
|
|
7
|
+
return data;
|
|
8
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Payload labels may be translated strings or functions. Keep server entries dependency-safe. */ export function collectionLabel(label, slug, req) {
|
|
2
|
+
try {
|
|
3
|
+
if (typeof label === 'function') {
|
|
4
|
+
const value = label({
|
|
5
|
+
i18n: req.i18n,
|
|
6
|
+
t: req.i18n.t
|
|
7
|
+
});
|
|
8
|
+
return typeof value === 'string' ? value : slug;
|
|
9
|
+
}
|
|
10
|
+
if (typeof label === 'string') return label;
|
|
11
|
+
const translations = label && typeof label === 'object' ? label : {};
|
|
12
|
+
return [
|
|
13
|
+
translations[req.i18n?.language ?? 'en'],
|
|
14
|
+
translations.en,
|
|
15
|
+
...Object.values(translations)
|
|
16
|
+
].find((value)=>typeof value === 'string') ?? slug;
|
|
17
|
+
} catch {
|
|
18
|
+
return slug;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CLICK_KEY, isClickSource, validClickSource } from '../goals/key.js';
|
|
1
2
|
import { XP_METRIC_SOURCE_SELECT } from '../admin/paths.js';
|
|
2
3
|
import { managedAccess } from './access.js';
|
|
3
4
|
import { serializeMetricSources } from './serialize.js';
|
|
@@ -30,6 +31,14 @@ export function metricsCollection(options) {
|
|
|
30
31
|
beforeValidate: [
|
|
31
32
|
({ data, originalDoc })=>{
|
|
32
33
|
if (!data) return data;
|
|
34
|
+
if (isClickSource(data.source ?? originalDoc?.source)) {
|
|
35
|
+
data.source = `click:${data.slug ?? originalDoc?.slug ?? ''}`;
|
|
36
|
+
data.direction = 'increase';
|
|
37
|
+
data.capping = {
|
|
38
|
+
type: null,
|
|
39
|
+
value: null
|
|
40
|
+
};
|
|
41
|
+
}
|
|
33
42
|
const source = sources.find((source)=>source.id === (data.source ?? originalDoc?.source));
|
|
34
43
|
const capping = data.capping === undefined ? originalDoc?.capping : data.capping;
|
|
35
44
|
// Payload may submit an empty optional group as { type: null, value: null }.
|
|
@@ -58,6 +67,7 @@ export function metricsCollection(options) {
|
|
|
58
67
|
type: 'text',
|
|
59
68
|
required: true,
|
|
60
69
|
unique: true,
|
|
70
|
+
validate: (value, { data })=>!isClickSource(data?.source) || typeof value === 'string' && CLICK_KEY.test(value) || 'Use 1–64 lowercase letters, numbers, underscores or hyphens, starting with a letter or number.',
|
|
61
71
|
admin: {
|
|
62
72
|
description: 'Use a unique short name for code that refers to this goal.'
|
|
63
73
|
}
|
|
@@ -67,7 +77,7 @@ export function metricsCollection(options) {
|
|
|
67
77
|
label: 'Measured by',
|
|
68
78
|
type: 'text',
|
|
69
79
|
required: true,
|
|
70
|
-
validate: (value)=>sources.some((source)=>source.id === value) || 'Choose what this goal is measured by.',
|
|
80
|
+
validate: (value)=>(isClickSource(value) ? validClickSource(value) : sources.some((source)=>source.id === value)) || 'Choose what this goal is measured by.',
|
|
71
81
|
admin: {
|
|
72
82
|
description: 'Choose the outcome this goal measures.',
|
|
73
83
|
components: {
|
|
@@ -135,7 +145,7 @@ export function metricsCollection(options) {
|
|
|
135
145
|
},
|
|
136
146
|
validate: (value, { data, siblingData })=>{
|
|
137
147
|
const source = sources.find((source)=>source.id === data?.source);
|
|
138
|
-
return validateCappingType(value, siblingData?.value, source?.kind);
|
|
148
|
+
return validateCappingType(value, siblingData?.value, isClickSource(data?.source) ? 'binomial' : source?.kind);
|
|
139
149
|
}
|
|
140
150
|
},
|
|
141
151
|
{
|
|
@@ -12,6 +12,13 @@ export function serializeFlagPoints(options) {
|
|
|
12
12
|
}
|
|
13
13
|
export function serializeMetricSources(options) {
|
|
14
14
|
return [
|
|
15
|
+
{
|
|
16
|
+
id: 'click:',
|
|
17
|
+
label: 'A click on the page',
|
|
18
|
+
kind: 'binomial',
|
|
19
|
+
keyedBy: 'visitor',
|
|
20
|
+
clientWritable: true
|
|
21
|
+
},
|
|
15
22
|
...(options.metricSources ?? []).map(({ id, label, kind, keyedBy, clientWritable })=>({
|
|
16
23
|
id,
|
|
17
24
|
label,
|
package/dist/db/appends.d.ts
CHANGED
|
@@ -19,9 +19,13 @@ type AppendArgs<T> = {
|
|
|
19
19
|
phase: number;
|
|
20
20
|
entry: T;
|
|
21
21
|
};
|
|
22
|
+
/** Raw main-row read on the host transaction, without afterRead expansion.
|
|
23
|
+
* Adapter reads avoid Local API error handling killing the host transaction
|
|
24
|
+
* before the audit savepoint can recover.
|
|
25
|
+
*/
|
|
26
|
+
export declare function storedContent(req: PayloadRequest, collection: string, id: DocId): Promise<Record<string, unknown> | undefined>;
|
|
22
27
|
/** Read the main collection row BEFORE the host write. Draft version retention
|
|
23
|
-
* and unpublishing cannot remove this baseline.
|
|
24
|
-
* error handling killing the host transaction before savepoint recovery.
|
|
28
|
+
* and unpublishing cannot remove this baseline.
|
|
25
29
|
*/
|
|
26
30
|
export declare function previousPublishedContent(req: PayloadRequest, collection: string, id: DocId): Promise<Record<string, unknown> | undefined>;
|
|
27
31
|
/** Serialize flagDefaults read/modify/write before the manifest builder bumps. */
|
package/dist/db/appends.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { COLUMNS, TABLES } from './names.js';
|
|
2
2
|
import { XpRefusal } from '../errors.js';
|
|
3
3
|
import { qualifiedTable, quoteIdentifier as quote } from './sql.js';
|
|
4
|
-
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*/ export async function
|
|
4
|
+
/** Raw main-row read on the host transaction, without afterRead expansion.
|
|
5
|
+
* Adapter reads avoid Local API error handling killing the host transaction
|
|
6
|
+
* before the audit savepoint can recover.
|
|
7
|
+
*/ export async function storedContent(req, collection, id) {
|
|
8
8
|
const doc = await req.payload.db.findOne({
|
|
9
9
|
collection,
|
|
10
10
|
req,
|
|
@@ -16,6 +16,12 @@ import { qualifiedTable, quoteIdentifier as quote } from './sql.js';
|
|
|
16
16
|
// Adapter reads have no access layer; document the audit's access posture.
|
|
17
17
|
overrideAccess: false
|
|
18
18
|
});
|
|
19
|
+
return doc ?? undefined;
|
|
20
|
+
}
|
|
21
|
+
/** Read the main collection row BEFORE the host write. Draft version retention
|
|
22
|
+
* and unpublishing cannot remove this baseline.
|
|
23
|
+
*/ export async function previousPublishedContent(req, collection, id) {
|
|
24
|
+
const doc = await storedContent(req, collection, id);
|
|
19
25
|
if (!doc) return undefined;
|
|
20
26
|
// A draft created with content is already in the main table. Only the
|
|
21
27
|
// existence of a published version matters here; never use its content as
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PayloadRequest } from 'payload';
|
|
2
|
+
import type { DocId } from '../types/contracts.js';
|
|
3
|
+
import type { Executor } from './executor.js';
|
|
4
|
+
/** Defined click goals only; called on the pool by the public events endpoint. */
|
|
5
|
+
export declare function definedClickKeys(exec: Executor): Promise<Set<string>>;
|
|
6
|
+
/** §7.8: click-goal fairness reads published content raw. Never read draft versions. */
|
|
7
|
+
export declare function clickGoalContent(req: PayloadRequest, collection: string, id: DocId): Promise<(Record<string, unknown> & {
|
|
8
|
+
id: DocId;
|
|
9
|
+
}) | undefined>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { COLUMNS, TABLES } from './names.js';
|
|
2
|
+
import { qualifiedTable, quoteIdentifier as q } from './sql.js';
|
|
3
|
+
/** Defined click goals only; called on the pool by the public events endpoint. */ export async function definedClickKeys(exec) {
|
|
4
|
+
const c = COLUMNS.metrics;
|
|
5
|
+
const { rows } = await exec.query(`SELECT ${q(c.slug)} AS slug FROM ${qualifiedTable(exec, TABLES.metrics)} WHERE ${q(c.source)} = 'click:' || ${q(c.slug)}`);
|
|
6
|
+
return new Set(rows.map((row)=>row.slug));
|
|
7
|
+
}
|
|
8
|
+
/** §7.8: click-goal fairness reads published content raw. Never read draft versions. */ export async function clickGoalContent(req, collection, id) {
|
|
9
|
+
const doc = await req.payload.db.findOne({
|
|
10
|
+
collection,
|
|
11
|
+
req,
|
|
12
|
+
where: {
|
|
13
|
+
id: {
|
|
14
|
+
equals: id
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
// Adapter read: click-goal fairness reads published content raw.
|
|
18
|
+
overrideAccess: true
|
|
19
|
+
});
|
|
20
|
+
const drafts = req.payload.config.collections?.find((c)=>c.slug === collection)?.versions;
|
|
21
|
+
return doc && (!(drafts && drafts.drafts) || doc._status === 'published') ? doc : undefined;
|
|
22
|
+
}
|
package/dist/db/delete.d.ts
CHANGED