@delmaredigital/payload-experiments 0.3.0-rc.4 → 0.4.0-rc.2
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 +122 -27
- 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 +2 -2
- package/dist/admin/TestsDashboardCards.d.ts +1 -0
- package/dist/admin/TestsDashboardCards.js +4 -8
- package/dist/admin/TestsNavLink.d.ts +1 -0
- package/dist/admin/TestsNavLink.js +4 -10
- 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 +18 -2
- package/dist/admin/api.js +47 -2
- package/dist/admin/blocks/BlockPanel.d.ts +4 -0
- package/dist/admin/blocks/BlockPanel.js +199 -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 +46 -0
- package/dist/admin/blocks/formApi.js +159 -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 +9 -0
- package/dist/admin/blocks/statusCache.js +92 -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/blocks.d.ts +43 -0
- package/dist/admin/copy/blocks.js +64 -0
- package/dist/admin/copy/common.d.ts +1 -0
- package/dist/admin/copy/common.js +1 -1
- 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 +24 -0
- package/dist/admin/copy/pages.js +24 -0
- package/dist/admin/copy/readiness.d.ts +23 -3
- package/dist/admin/copy/readiness.js +24 -3
- package/dist/admin/copy/results.d.ts +1 -0
- package/dist/admin/copy/results.js +1 -0
- package/dist/admin/copy/testPage.d.ts +4 -0
- package/dist/admin/copy/testPage.js +4 -0
- package/dist/admin/goals/XpGoalField.d.ts +4 -0
- package/dist/admin/goals/XpGoalField.js +168 -0
- package/dist/admin/goals/xpGoalField.css +48 -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 +9 -0
- package/dist/admin/paths.js +9 -0
- 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/results/DataQualityPanel.js +4 -1
- package/dist/admin/results/results.model.d.ts +3 -0
- package/dist/admin/results/results.model.js +8 -4
- package/dist/admin/surface/TestsHome.js +39 -10
- package/dist/admin/surface/TestsSurface.js +7 -6
- package/dist/admin/surface/contracts.d.ts +6 -5
- package/dist/admin/surface/goals/GoalForm.js +71 -44
- package/dist/admin/surface/goals/GoalsList.js +5 -4
- package/dist/admin/surface/goals/data.js +4 -1
- 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 +3 -3
- package/dist/admin/surface/home.model.js +8 -4
- package/dist/admin/surface/newTest/NewTest.js +56 -17
- package/dist/admin/surface/newTest/PageWizard.d.ts +7 -0
- package/dist/admin/surface/newTest/PageWizard.js +214 -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/PageCombobox.d.ts +10 -0
- package/dist/admin/surface/pagePicker/PageCombobox.js +395 -0
- package/dist/admin/surface/pagePicker/pagePicker.css +10 -0
- package/dist/admin/surface/pagePicker/pagePicker.model.d.ts +27 -0
- package/dist/admin/surface/pagePicker/pagePicker.model.js +25 -0
- package/dist/admin/surface/readiness/ActionDialogs.js +99 -64
- 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 +92 -9
- 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 +21 -2
- package/dist/admin/surface/results/ResultsHeader.js +5 -13
- package/dist/admin/surface/results/useResults.d.ts +20 -1
- package/dist/admin/surface/test/AdvancedSection.js +111 -104
- package/dist/admin/surface/test/GoalSection.js +94 -14
- 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 +5 -9
- 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 +49 -1
- package/dist/admin/surface/test/testPage.css +3 -1
- 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 +15 -2
- package/dist/admin/surface/useTestDocument.js +53 -15
- package/dist/admin/surfaceUrls.d.ts +7 -0
- package/dist/admin/surfaceUrls.js +19 -0
- package/dist/admin/theme.css +2 -0
- package/dist/admin/useApi.d.ts +21 -2
- package/dist/admin/useApi.js +3 -5
- package/dist/admin/views/CollectionListRedirect.d.ts +1 -0
- package/dist/admin/views/CollectionListRedirect.js +1 -1
- package/dist/admin/views/ExperimentsRootView.js +28 -133
- package/dist/admin/views/GoalEditRedirect.d.ts +4 -0
- package/dist/admin/views/GoalEditRedirect.js +14 -0
- package/dist/admin/views/GoalsCollectionListView.d.ts +2 -0
- package/dist/admin/views/GoalsCollectionListView.js +4 -0
- package/dist/admin/views/TestsCollectionListView.d.ts +2 -0
- package/dist/admin/views/TestsCollectionListView.js +4 -0
- package/dist/admin/views/TestsNavLinkServer.d.ts +1 -0
- package/dist/admin/views/TestsNavLinkServer.js +1 -1
- package/dist/admin/views/buildSurfaceProps.d.ts +8 -0
- package/dist/admin/views/buildSurfaceProps.js +143 -0
- package/dist/admin/views/renderCollectionList.d.ts +2 -0
- package/dist/admin/views/renderCollectionList.js +34 -0
- package/dist/admin/views/renderTestsSurface.d.ts +3 -0
- package/dist/admin/views/renderTestsSurface.js +34 -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 +21 -7
- 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 +28 -6
- package/dist/collections/phases.js +1 -0
- package/dist/collections/serialize.js +7 -0
- 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 +8 -0
- package/dist/db/locks.js +27 -4
- 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/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 +8 -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 +3 -0
- package/dist/exports/adminRsc.js +3 -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 +3 -1
- package/dist/exports/puck.js +2 -0
- package/dist/exports/puckEditor.d.ts +3 -0
- package/dist/exports/puckEditor.js +2 -0
- package/dist/exports/rsc.d.ts +3 -0
- package/dist/exports/rsc.js +2 -0
- package/dist/fields/xpGoal.d.ts +3 -0
- package/dist/fields/xpGoal.js +16 -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/goals/validate.d.ts +3 -0
- package/dist/goals/validate.js +14 -0
- package/dist/hooks/pageGuard.d.ts +5 -1
- package/dist/hooks/pageGuard.js +157 -95
- package/dist/hooks/puckGoals.d.ts +3 -0
- package/dist/hooks/puckGoals.js +38 -0
- package/dist/onInit.js +29 -0
- package/dist/options.d.ts +2 -0
- package/dist/options.js +41 -1
- package/dist/plugin.js +50 -43
- package/dist/puck/copy.d.ts +1 -0
- package/dist/puck/copy.js +1 -0
- package/dist/puck/editor/editor.chrome.css +14 -0
- package/dist/puck/editor/editor.css +106 -0
- package/dist/puck/editor/editor.model.d.ts +4 -2
- package/dist/puck/editor/editor.model.js +6 -3
- 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 +142 -0
- package/dist/puck/integration.d.ts +6 -0
- package/dist/puck/integration.js +21 -0
- 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 +43 -0
- package/dist/server/pageKindSeoContract.d.ts +27 -0
- package/dist/server/pageKindSeoContract.js +80 -0
- package/dist/server/recordContentChange.js +58 -8
- package/dist/server/resolveExperimentBlocks.d.ts +5 -0
- package/dist/server/resolveExperimentBlocks.js +54 -0
- package/dist/server/runResults.js +56 -15
- 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 +14 -1
- package/dist/transitions/checkStart.js +92 -8
- package/dist/transitions/decide.js +25 -0
- 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 +8 -0
- package/dist/transitions/placements.js +148 -1
- package/dist/transitions/preflightData.d.ts +2 -2
- package/dist/transitions/preflightData.js +137 -5
- package/dist/transitions/resolveCaps.d.ts +1 -0
- package/dist/transitions/resolveCaps.js +6 -2
- package/dist/transitions/start.js +46 -1
- package/dist/types/admin.d.ts +1 -1
- package/dist/types/assignments.d.ts +1 -0
- package/dist/types/contracts.d.ts +15 -0
- 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/ui.css +4 -0
- package/package.json +8 -1
|
@@ -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
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SURFACE_COPY } from '../../admin/copy/surface.js';
|
|
1
2
|
import * as paths from '../../admin/paths.js';
|
|
2
3
|
import { managedAccess } from '../access.js';
|
|
3
4
|
import { serializeAttributeKeys, serializeFlagPoints } from '../serialize.js';
|
|
@@ -5,8 +6,9 @@ import { TARGETING_OPS, isOpValid } from '../../targeting/ops.js';
|
|
|
5
6
|
import { readListValue } from '../../targeting/values.js';
|
|
6
7
|
import { createDefaults } from './createDefaults.js';
|
|
7
8
|
import { flagControlParams } from './flagControlParams.js';
|
|
9
|
+
import { pickExperimentSlug } from './pickSlug.js';
|
|
8
10
|
import { lockHook } from './lockHook.js';
|
|
9
|
-
import { deleteGuard, deleteGuardAfter } from './deleteGuard.js';
|
|
11
|
+
import { deleteGuard, deleteGuardAfter, deleteGuardBeforeOperation } from './deleteGuard.js';
|
|
10
12
|
import { checkArms } from './armInvariants.js';
|
|
11
13
|
import { XpNotImplemented, XpRefusal } from '../../errors.js';
|
|
12
14
|
const targetingValue = (rule)=>rule.op === 'in' || rule.op === 'nin' ? readListValue(rule.value) : rule.value;
|
|
@@ -36,6 +38,10 @@ export function experimentsCollection(options) {
|
|
|
36
38
|
drafts: false,
|
|
37
39
|
maxPerDoc: 50
|
|
38
40
|
},
|
|
41
|
+
labels: {
|
|
42
|
+
singular: SURFACE_COPY.test,
|
|
43
|
+
plural: SURFACE_COPY.navAllTests
|
|
44
|
+
},
|
|
39
45
|
access: {
|
|
40
46
|
...managedAccess(options),
|
|
41
47
|
delete: async ({ req })=>await options.access.manage({
|
|
@@ -50,8 +56,7 @@ export function experimentsCollection(options) {
|
|
|
50
56
|
} : false
|
|
51
57
|
},
|
|
52
58
|
admin: {
|
|
53
|
-
|
|
54
|
-
group: false,
|
|
59
|
+
group: SURFACE_COPY.title,
|
|
55
60
|
useAsTitle: 'name',
|
|
56
61
|
defaultColumns: [
|
|
57
62
|
'name',
|
|
@@ -62,7 +67,7 @@ export function experimentsCollection(options) {
|
|
|
62
67
|
components: {
|
|
63
68
|
views: {
|
|
64
69
|
list: {
|
|
65
|
-
Component: paths.
|
|
70
|
+
Component: paths.XP_TESTS_COLLECTION_LIST
|
|
66
71
|
},
|
|
67
72
|
edit: {
|
|
68
73
|
default: {
|
|
@@ -81,12 +86,16 @@ export function experimentsCollection(options) {
|
|
|
81
86
|
}
|
|
82
87
|
},
|
|
83
88
|
hooks: {
|
|
89
|
+
beforeOperation: [
|
|
90
|
+
deleteGuardBeforeOperation
|
|
91
|
+
],
|
|
84
92
|
beforeValidate: [
|
|
85
93
|
createDefaults
|
|
86
94
|
],
|
|
87
95
|
beforeChange: [
|
|
88
96
|
flagControlParams,
|
|
89
|
-
lockHook
|
|
97
|
+
lockHook,
|
|
98
|
+
pickExperimentSlug
|
|
90
99
|
],
|
|
91
100
|
beforeDelete: [
|
|
92
101
|
deleteGuard
|
|
@@ -96,8 +105,8 @@ export function experimentsCollection(options) {
|
|
|
96
105
|
],
|
|
97
106
|
afterError: [
|
|
98
107
|
({ error })=>{
|
|
99
|
-
// Payload preserves public Error status/message, but omits custom codes.
|
|
100
|
-
if (error instanceof XpRefusal
|
|
108
|
+
// Payload preserves public Error status/message, but omits custom codes; the admin maps refusals by code.
|
|
109
|
+
if (error instanceof XpRefusal) {
|
|
101
110
|
return {
|
|
102
111
|
status: error.status,
|
|
103
112
|
response: {
|
|
@@ -166,6 +175,11 @@ export function experimentsCollection(options) {
|
|
|
166
175
|
read: async (args)=>args.doc?.kind !== 'code' || adminOnly(args)
|
|
167
176
|
}
|
|
168
177
|
},
|
|
178
|
+
{
|
|
179
|
+
name: 'pageCollection',
|
|
180
|
+
type: 'text',
|
|
181
|
+
validate: (value, { previousValue })=>value === previousValue || value == null || value === '' || options.pages?.collections.some((entry)=>entry.slug === value) || 'Choose a registered page collection.'
|
|
182
|
+
},
|
|
169
183
|
{
|
|
170
184
|
name: 'flagPoint',
|
|
171
185
|
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,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SURFACE_COPY } from '../admin/copy/surface.js';
|
|
2
|
+
import { CLICK_KEY, isClickSource, validClickSource } from '../goals/key.js';
|
|
3
|
+
import { XP_METRIC_SOURCE_SELECT, XP_GOALS_COLLECTION_LIST, XP_GOAL_EDIT_REDIRECT } from '../admin/paths.js';
|
|
2
4
|
import { managedAccess } from './access.js';
|
|
3
5
|
import { serializeMetricSources } from './serialize.js';
|
|
4
6
|
import { validateCappingType, validateCappingValue } from './metricValidation.js';
|
|
@@ -10,14 +12,25 @@ export function metricsCollection(options) {
|
|
|
10
12
|
graphQL: false,
|
|
11
13
|
disableDuplicate: true,
|
|
12
14
|
versions: false,
|
|
13
|
-
// group:false hides navigation while keeping collection routes reachable (hidden causes 404s).
|
|
14
15
|
labels: {
|
|
15
16
|
singular: 'Goal',
|
|
16
17
|
plural: 'Goals'
|
|
17
18
|
},
|
|
18
19
|
admin: {
|
|
19
|
-
group:
|
|
20
|
-
useAsTitle: 'name'
|
|
20
|
+
group: SURFACE_COPY.title,
|
|
21
|
+
useAsTitle: 'name',
|
|
22
|
+
components: {
|
|
23
|
+
views: {
|
|
24
|
+
list: {
|
|
25
|
+
Component: XP_GOALS_COLLECTION_LIST
|
|
26
|
+
},
|
|
27
|
+
edit: {
|
|
28
|
+
default: {
|
|
29
|
+
Component: XP_GOAL_EDIT_REDIRECT
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
21
34
|
},
|
|
22
35
|
access: managedAccess(options),
|
|
23
36
|
hooks: {
|
|
@@ -30,6 +43,14 @@ export function metricsCollection(options) {
|
|
|
30
43
|
beforeValidate: [
|
|
31
44
|
({ data, originalDoc })=>{
|
|
32
45
|
if (!data) return data;
|
|
46
|
+
if (isClickSource(data.source ?? originalDoc?.source)) {
|
|
47
|
+
data.source = `click:${data.slug ?? originalDoc?.slug ?? ''}`;
|
|
48
|
+
data.direction = 'increase';
|
|
49
|
+
data.capping = {
|
|
50
|
+
type: null,
|
|
51
|
+
value: null
|
|
52
|
+
};
|
|
53
|
+
}
|
|
33
54
|
const source = sources.find((source)=>source.id === (data.source ?? originalDoc?.source));
|
|
34
55
|
const capping = data.capping === undefined ? originalDoc?.capping : data.capping;
|
|
35
56
|
// Payload may submit an empty optional group as { type: null, value: null }.
|
|
@@ -58,6 +79,7 @@ export function metricsCollection(options) {
|
|
|
58
79
|
type: 'text',
|
|
59
80
|
required: true,
|
|
60
81
|
unique: true,
|
|
82
|
+
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
83
|
admin: {
|
|
62
84
|
description: 'Use a unique short name for code that refers to this goal.'
|
|
63
85
|
}
|
|
@@ -67,7 +89,7 @@ export function metricsCollection(options) {
|
|
|
67
89
|
label: 'Measured by',
|
|
68
90
|
type: 'text',
|
|
69
91
|
required: true,
|
|
70
|
-
validate: (value)=>sources.some((source)=>source.id === value) || 'Choose what this goal is measured by.',
|
|
92
|
+
validate: (value)=>(isClickSource(value) ? validClickSource(value) : sources.some((source)=>source.id === value)) || 'Choose what this goal is measured by.',
|
|
71
93
|
admin: {
|
|
72
94
|
description: 'Choose the outcome this goal measures.',
|
|
73
95
|
components: {
|
|
@@ -135,7 +157,7 @@ export function metricsCollection(options) {
|
|
|
135
157
|
},
|
|
136
158
|
validate: (value, { data, siblingData })=>{
|
|
137
159
|
const source = sources.find((source)=>source.id === data?.source);
|
|
138
|
-
return validateCappingType(value, siblingData?.value, source?.kind);
|
|
160
|
+
return validateCappingType(value, siblingData?.value, isClickSource(data?.source) ? 'binomial' : source?.kind);
|
|
139
161
|
}
|
|
140
162
|
},
|
|
141
163
|
{
|
|
@@ -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,
|
|
@@ -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