@delmaredigital/payload-experiments 0.3.0-rc.4 → 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 +113 -20
- 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/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/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/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 +22 -3
- package/dist/admin/copy/readiness.js +23 -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 +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 +6 -0
- package/dist/admin/paths.js +6 -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 +37 -6
- 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 +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 +1 -1
- package/dist/admin/surface/home.model.js +8 -4
- package/dist/admin/surface/newTest/NewTest.js +54 -13
- 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 +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 +91 -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 +2 -5
- package/dist/admin/surface/results/useResults.d.ts +10 -1
- package/dist/admin/surface/test/AdvancedSection.js +111 -104
- package/dist/admin/surface/test/GoalSection.js +90 -7
- 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/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 +53 -15
- package/dist/admin/useApi.d.ts +11 -2
- package/dist/admin/useApi.js +3 -5
- package/dist/admin/views/ExperimentsRootView.js +58 -16
- 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/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/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 +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 +157 -95
- package/dist/onInit.js +29 -0
- package/dist/options.d.ts +2 -0
- package/dist/options.js +41 -1
- package/dist/plugin.js +41 -28
- package/dist/puck/copy.d.ts +1 -0
- package/dist/puck/copy.js +1 -0
- package/dist/puck/editor/editor.chrome.css +1 -0
- package/dist/puck/editor/editor.css +1 -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 +134 -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 +41 -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 +13 -1
- package/dist/transitions/checkStart.js +89 -7
- package/dist/transitions/decide.js +24 -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 +45 -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/package.json +8 -1
|
@@ -3,6 +3,7 @@ import { pooledExecutor, txExecutor } from '../db/executor.js';
|
|
|
3
3
|
import { lockTransitionScope } from '../db/locks.js';
|
|
4
4
|
import { currentTxid, txidStatus } from '../db/txid.js';
|
|
5
5
|
import { contentChangeSavepoint } from '../db/appends.js';
|
|
6
|
+
import { isPageBusy } from '../db/pageBusy.js';
|
|
6
7
|
import { XpRefusal } from '../errors.js';
|
|
7
8
|
import { buildAndStoreManifest } from '../manifest/compile.js';
|
|
8
9
|
/** Runs transition writes on the request's transaction under ordered locks.
|
|
@@ -61,6 +62,9 @@ import { buildAndStoreManifest } from '../manifest/compile.js';
|
|
|
61
62
|
};
|
|
62
63
|
} catch (error) {
|
|
63
64
|
if (!borrowedAudit) await killTransaction(req);
|
|
65
|
+
if (!scope.audit && isPageBusy(error)) throw new XpRefusal('page-busy', "One of this test's pages is being saved right now. Try again in a moment.", undefined, 409, {
|
|
66
|
+
cause: error
|
|
67
|
+
});
|
|
64
68
|
throw error;
|
|
65
69
|
} finally{
|
|
66
70
|
if (hadTransition) req.context.xpTransition = previousTransition;
|
|
@@ -10,6 +10,11 @@ type ContentDoc = {
|
|
|
10
10
|
dismissedAt?: string;
|
|
11
11
|
dismissedBy?: string | null;
|
|
12
12
|
};
|
|
13
|
+
/** Each placement commits independently while the outer transition retains its
|
|
14
|
+
* experiment locks. Keep the manager's exact req/user, temporarily lending it a
|
|
15
|
+
* fresh transaction; a denied host write must not kill the outer transaction.
|
|
16
|
+
*/
|
|
17
|
+
export declare function hostTransaction(req: PayloadRequest, experimentId: DocId, fn: () => Promise<void>): Promise<void>;
|
|
13
18
|
export declare function makeContent<TUser extends {
|
|
14
19
|
id: DocId;
|
|
15
20
|
}>(req: PayloadRequest, { experimentId, options }: {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Forbidden, NotFound, ValidationError, Locked, commitTransaction, initTransaction, killTransaction } from 'payload';
|
|
2
2
|
import { latestDraftContent, publishedContent as readPublishedContent } from '../db/publishedContent.js';
|
|
3
|
+
import { EXPERIMENT_BLOCK_SLUG, hostBlocks, slotBlocks } from '../blocks/model.js';
|
|
4
|
+
import { findRegisteredBlocksField, freshBlockIds } from '../blocks/formState.js';
|
|
3
5
|
import { contentBlocks, hasLocalizedFields } from './contentFields.js';
|
|
4
6
|
import { safeError } from '../log.js';
|
|
5
7
|
import { XpRefusal } from '../errors.js';
|
|
@@ -7,7 +9,7 @@ import { lockContentDefaults } from '../db/appends.js';
|
|
|
7
9
|
import { pooledExecutor, txExecutor } from '../db/executor.js';
|
|
8
10
|
import { currentTxid, txidStatus } from '../db/txid.js';
|
|
9
11
|
import { buildAndStoreManifest } from '../manifest/compile.js';
|
|
10
|
-
import { puckCollections } from '../options.js';
|
|
12
|
+
import { documentEditor, puckCollections } from '../options.js';
|
|
11
13
|
import { contentDone } from '../contentDone.js';
|
|
12
14
|
import { archive } from './archive.js';
|
|
13
15
|
import { relationshipId } from './checkStart.js';
|
|
@@ -162,9 +164,14 @@ async function fieldRefusal(fields, req, doc, operation = 'update', data = doc,
|
|
|
162
164
|
/** Each placement commits independently while the outer transition retains its
|
|
163
165
|
* experiment locks. Keep the manager's exact req/user, temporarily lending it a
|
|
164
166
|
* fresh transaction; a denied host write must not kill the outer transaction.
|
|
165
|
-
*/ async function hostTransaction(req, fn) {
|
|
167
|
+
*/ export async function hostTransaction(req, experimentId, fn) {
|
|
166
168
|
const transactionID = req.transactionID;
|
|
167
169
|
const transition = req.context.xpTransition;
|
|
170
|
+
const hadHeld = Object.hasOwn(req.context, 'xpHeldExperiments');
|
|
171
|
+
const held = req.context.xpHeldExperiments;
|
|
172
|
+
req.context.xpHeldExperiments = [
|
|
173
|
+
experimentId
|
|
174
|
+
];
|
|
168
175
|
delete req.transactionID;
|
|
169
176
|
delete req.context.xpTransition;
|
|
170
177
|
try {
|
|
@@ -178,6 +185,8 @@ async function fieldRefusal(fields, req, doc, operation = 'update', data = doc,
|
|
|
178
185
|
throw error;
|
|
179
186
|
} finally{
|
|
180
187
|
req.transactionID = transactionID;
|
|
188
|
+
if (hadHeld) req.context.xpHeldExperiments = held;
|
|
189
|
+
else delete req.context.xpHeldExperiments;
|
|
181
190
|
if (transition === undefined) delete req.context.xpTransition;
|
|
182
191
|
else req.context.xpTransition = transition;
|
|
183
192
|
}
|
|
@@ -217,7 +226,7 @@ export async function makeContent(req, { experimentId, options }) {
|
|
|
217
226
|
if (retainDismissed(collection, id)) return;
|
|
218
227
|
let missing = false;
|
|
219
228
|
try {
|
|
220
|
-
await hostTransaction(req, async ()=>{
|
|
229
|
+
await hostTransaction(req, experimentId, async ()=>{
|
|
221
230
|
try {
|
|
222
231
|
await fn();
|
|
223
232
|
} catch (error) {
|
|
@@ -264,7 +273,10 @@ export async function makeContent(req, { experimentId, options }) {
|
|
|
264
273
|
};
|
|
265
274
|
if (current.kind === 'block') {
|
|
266
275
|
if (!winner.slot) refuse('invalid-winner');
|
|
267
|
-
const registrations =
|
|
276
|
+
const registrations = [
|
|
277
|
+
...puckCollections(options),
|
|
278
|
+
...options.blocks?.collections ?? []
|
|
279
|
+
];
|
|
268
280
|
const scanned = [];
|
|
269
281
|
for (const registration of registrations){
|
|
270
282
|
const transactionID = req.transactionID;
|
|
@@ -272,9 +284,8 @@ export async function makeContent(req, { experimentId, options }) {
|
|
|
272
284
|
try {
|
|
273
285
|
scanned.push(...await scanBlockPlacements(req, {
|
|
274
286
|
slug: current.slug,
|
|
275
|
-
puckCollections:
|
|
276
|
-
|
|
277
|
-
]
|
|
287
|
+
puckCollections: puckCollections(options).filter((r)=>r === registration),
|
|
288
|
+
blocksCollections: (options.blocks?.collections ?? []).filter((r)=>r === registration)
|
|
278
289
|
}));
|
|
279
290
|
} catch (error) {
|
|
280
291
|
// A collection made unreadable since the last attempt still has
|
|
@@ -330,7 +341,7 @@ export async function makeContent(req, { experimentId, options }) {
|
|
|
330
341
|
let detail = isNotFound(error) ? 'forbidden' : contentFailureReason(error);
|
|
331
342
|
if (isNotFound(error)) {
|
|
332
343
|
try {
|
|
333
|
-
await hostTransaction(req, async ()=>{
|
|
344
|
+
await hostTransaction(req, experimentId, async ()=>{
|
|
334
345
|
missing = !await readPublishedContent(req, collection, id);
|
|
335
346
|
});
|
|
336
347
|
} catch (checkError) {
|
|
@@ -367,7 +378,7 @@ export async function makeContent(req, { experimentId, options }) {
|
|
|
367
378
|
for (const [key, { collection, id }] of placements){
|
|
368
379
|
if (skipped.has(key)) continue;
|
|
369
380
|
await attempt(collection, id, async ()=>{
|
|
370
|
-
|
|
381
|
+
let field = registrations.find((entry)=>entry.slug === collection)?.field;
|
|
371
382
|
if (!field) throw new Error('This content collection is no longer configured.');
|
|
372
383
|
const config = req.payload.config.collections.find((config)=>config.slug === collection);
|
|
373
384
|
if (hasLocalizedFields(config?.fields ?? [], req.payload.config)) throw new LocalizedContent();
|
|
@@ -382,10 +393,27 @@ export async function makeContent(req, { experimentId, options }) {
|
|
|
382
393
|
const drafts = Boolean(config?.versions && config.versions.drafts);
|
|
383
394
|
const published = await readPublishedContent(req, collection, id);
|
|
384
395
|
if (!published) throw new NotFound();
|
|
396
|
+
const editor = documentEditor(options, collection, published);
|
|
397
|
+
const blockRegistration = options.blocks?.collections.find((r)=>r.slug === collection);
|
|
398
|
+
const contentField = editor === 'blocks' ? blockRegistration?.field : field;
|
|
399
|
+
if (!contentField) throw new Error('This content field is no longer configured.');
|
|
400
|
+
field = contentField;
|
|
401
|
+
const transform = (value)=>editor === 'blocks' ? !hostBlocks(value).some((block)=>block.blockType === (blockRegistration?.blockSlug ?? EXPERIMENT_BLOCK_SLUG) && block.experiment === current.slug) ? value : hostBlocks(value).flatMap((block)=>{
|
|
402
|
+
if (block.blockType !== (blockRegistration?.blockSlug ?? EXPERIMENT_BLOCK_SLUG) || block.experiment !== current.slug) return [
|
|
403
|
+
block
|
|
404
|
+
];
|
|
405
|
+
const rows = Array.isArray(block.slots) ? block.slots : [];
|
|
406
|
+
if (!rows.some((row)=>row?.slot === winner.slot)) throw new Error('The winning content slot is missing.');
|
|
407
|
+
const registered = findRegisteredBlocksField(config?.fields ?? [], field);
|
|
408
|
+
if (!registered) throw new Error('This content field is no longer configured.');
|
|
409
|
+
return slotBlocks(freshBlockIds([
|
|
410
|
+
block
|
|
411
|
+
], registered.field, req.payload.config)[0], winner.slot);
|
|
412
|
+
}) : unwrap(value, current.slug, winner.slot);
|
|
385
413
|
const draft = drafts ? await latestDraftContent(req, collection, id) ?? published : published;
|
|
386
414
|
const draftValue = draft[field] ?? null;
|
|
387
|
-
const draftContent =
|
|
388
|
-
const publishedContent =
|
|
415
|
+
const draftContent = transform(draftValue);
|
|
416
|
+
const publishedContent = transform(published[field]);
|
|
389
417
|
const changedDraft = JSON.stringify(draftContent) !== JSON.stringify(draftValue);
|
|
390
418
|
const changedPublished = JSON.stringify(publishedContent) !== JSON.stringify(published[field]);
|
|
391
419
|
if (!changedDraft && !changedPublished) return;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PayloadRequest } from 'payload';
|
|
2
|
+
import type { ExperimentsPageCollection, ResolvedOptions } from '../types/contracts.js';
|
|
3
|
+
/** A stored collection is authoritative. Legacy tests fall back by the Original's path. */
|
|
4
|
+
export declare function resolvePageCollection(req: PayloadRequest, options: Pick<ResolvedOptions, 'pages'>, experiment: {
|
|
5
|
+
pageCollection?: string | null;
|
|
6
|
+
arms: readonly {
|
|
7
|
+
kind?: string;
|
|
8
|
+
pageTarget?: string;
|
|
9
|
+
}[];
|
|
10
|
+
}, propagateErrors?: boolean): Promise<ExperimentsPageCollection | undefined>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { safeError } from '../log.js';
|
|
2
|
+
/** A stored collection is authoritative. Legacy tests fall back by the Original's path. */ export async function resolvePageCollection(req, options, experiment, propagateErrors = false) {
|
|
3
|
+
const collections = options.pages?.collections ?? [];
|
|
4
|
+
if (experiment.pageCollection) return collections.find((entry)=>entry.slug === experiment.pageCollection);
|
|
5
|
+
if (collections.length === 1) return collections[0];
|
|
6
|
+
const path = experiment.arms.find((arm)=>arm.kind === 'control')?.pageTarget;
|
|
7
|
+
if (!path) return undefined;
|
|
8
|
+
for (const entry of collections){
|
|
9
|
+
try {
|
|
10
|
+
if (await entry.findByPath(path, req)) return entry;
|
|
11
|
+
} catch (cause) {
|
|
12
|
+
if (propagateErrors) throw cause;
|
|
13
|
+
req.payload.logger.error({
|
|
14
|
+
err: safeError(cause),
|
|
15
|
+
collection: entry.slug
|
|
16
|
+
}, 'Experiment page lookup failed');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PayloadRequest } from 'payload';
|
|
2
|
+
import type { Arm, ExperimentsPageCollection } from '../types/contracts.js';
|
|
3
|
+
/** Page copies have fresh nested row ids and editor labels; neither is rendered content. */
|
|
4
|
+
export declare function comparableContent(value: unknown): unknown;
|
|
5
|
+
export declare function identicalPageVersions(req: PayloadRequest, collection: ExperimentsPageCollection | undefined, arms: Arm[]): Promise<{
|
|
6
|
+
arm: import("../types/contracts.js").ArmKey;
|
|
7
|
+
path: string;
|
|
8
|
+
collection: string;
|
|
9
|
+
id: string | number;
|
|
10
|
+
}[]>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { safeError } from '../log.js';
|
|
2
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
3
|
+
import { pageVersionContent } from '../db/pageContent.js';
|
|
4
|
+
/** Page copies have fresh nested row ids and editor labels; neither is rendered content. */ export function comparableContent(value) {
|
|
5
|
+
if (Array.isArray(value)) return value.map(comparableContent);
|
|
6
|
+
if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).filter(([key])=>key !== 'id' && key !== 'blockName').map(([key, nested])=>[
|
|
7
|
+
key,
|
|
8
|
+
comparableContent(nested)
|
|
9
|
+
]));
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
export async function identicalPageVersions(req, collection, arms) {
|
|
13
|
+
if (!collection?.contentOf) return [];
|
|
14
|
+
const content = await Promise.all(arms.filter((arm)=>arm.kind !== 'holdout' && arm.pageTarget).map(async (arm)=>{
|
|
15
|
+
try {
|
|
16
|
+
const page = await collection.findByPath(arm.pageTarget, req);
|
|
17
|
+
if (!page?.published) return null;
|
|
18
|
+
const raw = await pageVersionContent(req, collection.slug, page.id);
|
|
19
|
+
if (!raw || raw._status === 'draft') return null;
|
|
20
|
+
return {
|
|
21
|
+
arm,
|
|
22
|
+
id: page.id,
|
|
23
|
+
value: comparableContent(collection.contentOf(raw))
|
|
24
|
+
};
|
|
25
|
+
} catch (cause) {
|
|
26
|
+
req.payload.logger.error({
|
|
27
|
+
err: safeError(cause),
|
|
28
|
+
collection: collection.slug,
|
|
29
|
+
id: arm.pageTarget
|
|
30
|
+
}, 'Experiment version comparison failed');
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}));
|
|
34
|
+
const control = content.find((row)=>row?.arm.kind === 'control');
|
|
35
|
+
if (!control) return [];
|
|
36
|
+
return content.flatMap((row)=>row && row.arm.kind === 'variant' && isDeepStrictEqual(row.value, control.value) ? [
|
|
37
|
+
{
|
|
38
|
+
arm: row.arm.key,
|
|
39
|
+
path: row.arm.pageTarget,
|
|
40
|
+
collection: collection.slug,
|
|
41
|
+
id: row.id
|
|
42
|
+
}
|
|
43
|
+
] : []);
|
|
44
|
+
}
|
|
@@ -16,6 +16,7 @@ export interface PhaseRecord {
|
|
|
16
16
|
analysis?: Record<string, unknown> & {
|
|
17
17
|
sequentialTuningParameter?: number | null;
|
|
18
18
|
};
|
|
19
|
+
placementDocs?: import('../db/placements.js').PlacementDoc[] | null;
|
|
19
20
|
contentChanges?: ContentChange[];
|
|
20
21
|
}
|
|
21
22
|
export declare function readPhaseRecord(req: PayloadRequest, experimentId: DocId, phase: number): Promise<PhaseRecord & {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { PayloadRequest } from 'payload';
|
|
2
|
+
import type { Arm, DocId, ResolvedOptions } from '../types/contracts.js';
|
|
3
|
+
import type { Executor } from '../db/executor.js';
|
|
4
|
+
import { type PlacementDoc } from '../db/placements.js';
|
|
5
|
+
import { type BlockPlacement } from './placements.js';
|
|
6
|
+
export declare function detached<T>(req: PayloadRequest, fn: () => Promise<T>): Promise<T>;
|
|
7
|
+
export declare function uniquePlacementDocs(placements: readonly Pick<BlockPlacement, 'collection' | 'id'>[]): PlacementDoc[];
|
|
8
|
+
export declare function scanPlacements<TUser extends {
|
|
9
|
+
id: DocId;
|
|
10
|
+
}>(req: PayloadRequest, options: ResolvedOptions<TUser>, slug: string): Promise<BlockPlacement[]>;
|
|
11
|
+
export declare function placementPaths<TUser extends {
|
|
12
|
+
id: DocId;
|
|
13
|
+
}>(req: PayloadRequest, options: ResolvedOptions<TUser>, docs: readonly PlacementDoc[]): Promise<string[]>;
|
|
14
|
+
export interface InteractionDetail {
|
|
15
|
+
experimentId: DocId;
|
|
16
|
+
slug: string;
|
|
17
|
+
name: string;
|
|
18
|
+
status: string;
|
|
19
|
+
path: string;
|
|
20
|
+
version: boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare function placementInteractions<TUser extends {
|
|
23
|
+
id: DocId;
|
|
24
|
+
}>(req: PayloadRequest, exec: Executor, options: ResolvedOptions<TUser>, experiment: {
|
|
25
|
+
id: DocId;
|
|
26
|
+
kind: string;
|
|
27
|
+
arms: Arm[];
|
|
28
|
+
pageCollection?: string | null;
|
|
29
|
+
}, paths: string[]): Promise<InteractionDetail[]>;
|
|
30
|
+
export declare function lockAndRescanPlacements<TUser extends {
|
|
31
|
+
id: DocId;
|
|
32
|
+
}>(req: PayloadRequest, exec: Executor, options: ResolvedOptions<TUser>, experiment: {
|
|
33
|
+
id: DocId;
|
|
34
|
+
slug: string;
|
|
35
|
+
kind: string;
|
|
36
|
+
arms: Arm[];
|
|
37
|
+
}, before: readonly BlockPlacement[]): Promise<{
|
|
38
|
+
placements: BlockPlacement[];
|
|
39
|
+
docs: PlacementDoc[];
|
|
40
|
+
}>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { placementPathContent, placementTests } from '../db/placements.js';
|
|
2
|
+
import { lockPagePaths } from '../db/locks.js';
|
|
3
|
+
import { normalisePath } from '../runtime/normalisePath.js';
|
|
4
|
+
import { scanBlockPlacements } from './placements.js';
|
|
5
|
+
import { interactingTestsCopy } from '../admin/copy/readiness.js';
|
|
6
|
+
import { XpRefusal } from '../errors.js';
|
|
7
|
+
import { pageTargetPaths } from './checkStart.js';
|
|
8
|
+
export async function detached(req, fn) {
|
|
9
|
+
const transactionID = req.transactionID;
|
|
10
|
+
delete req.transactionID;
|
|
11
|
+
try {
|
|
12
|
+
return await fn();
|
|
13
|
+
} finally{
|
|
14
|
+
req.transactionID = transactionID;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function uniquePlacementDocs(placements) {
|
|
18
|
+
return [
|
|
19
|
+
...new Map(placements.map((p)=>[
|
|
20
|
+
JSON.stringify([
|
|
21
|
+
p.collection,
|
|
22
|
+
String(p.id)
|
|
23
|
+
]),
|
|
24
|
+
{
|
|
25
|
+
collection: p.collection,
|
|
26
|
+
id: p.id
|
|
27
|
+
}
|
|
28
|
+
])).values()
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
export function scanPlacements(req, options, slug) {
|
|
32
|
+
return scanBlockPlacements(req, {
|
|
33
|
+
slug,
|
|
34
|
+
puckCollections: options.puck?.collections ?? [],
|
|
35
|
+
blocksCollections: options.blocks?.collections ?? []
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
export async function placementPaths(req, options, docs) {
|
|
39
|
+
const paths = new Set();
|
|
40
|
+
for (const doc of docs){
|
|
41
|
+
const registration = options.pages?.collections.find((r)=>r.slug === doc.collection);
|
|
42
|
+
if (!registration) continue;
|
|
43
|
+
const row = await placementPathContent(req, doc.collection, doc.id);
|
|
44
|
+
if (!row || row._status === 'draft') continue;
|
|
45
|
+
const path = registration.pathOf(row);
|
|
46
|
+
if (path !== null) paths.add(normalisePath(path));
|
|
47
|
+
}
|
|
48
|
+
return [
|
|
49
|
+
...paths
|
|
50
|
+
].sort();
|
|
51
|
+
}
|
|
52
|
+
const active = (test)=>test.status === 'running' || test.status === 'decided';
|
|
53
|
+
export async function placementInteractions(req, exec, options, experiment, paths) {
|
|
54
|
+
const result = [];
|
|
55
|
+
for (const test of (await placementTests(exec))){
|
|
56
|
+
if (!active(test) || String(test.id) === String(experiment.id)) continue;
|
|
57
|
+
if (experiment.kind === 'block' && test.kind === 'page') {
|
|
58
|
+
for (const path of paths.filter((path)=>pageTargetPaths(test.arms).includes(path)))result.push({
|
|
59
|
+
experimentId: test.id,
|
|
60
|
+
slug: test.slug,
|
|
61
|
+
name: test.name,
|
|
62
|
+
status: test.status,
|
|
63
|
+
path,
|
|
64
|
+
version: test.arms.some((a)=>a.kind === 'variant' && a.pageTarget && normalisePath(a.pageTarget) === path)
|
|
65
|
+
});
|
|
66
|
+
} else if (experiment.kind === 'page' && test.kind === 'block') {
|
|
67
|
+
const docs = test.placementDocs ?? uniquePlacementDocs(await detached(req, ()=>scanPlacements(req, options, test.slug)));
|
|
68
|
+
const relevant = docs.filter((doc)=>!experiment.pageCollection || doc.collection === experiment.pageCollection);
|
|
69
|
+
for (const path of (await placementPaths(req, options, relevant)).filter((path)=>paths.includes(path)))result.push({
|
|
70
|
+
experimentId: test.id,
|
|
71
|
+
slug: test.slug,
|
|
72
|
+
name: test.name,
|
|
73
|
+
status: test.status,
|
|
74
|
+
path,
|
|
75
|
+
version: experiment.arms.some((a)=>a.kind === 'variant' && a.pageTarget && normalisePath(a.pageTarget) === path)
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
export async function lockAndRescanPlacements(req, exec, options, experiment, before) {
|
|
82
|
+
const paths = await placementPaths(req, options, uniquePlacementDocs(before));
|
|
83
|
+
await lockPagePaths(exec, paths);
|
|
84
|
+
const placements = await detached(req, ()=>scanPlacements(req, options, experiment.slug));
|
|
85
|
+
const docs = uniquePlacementDocs(placements);
|
|
86
|
+
const afterPaths = await placementPaths(req, options, docs);
|
|
87
|
+
if (afterPaths.some((path)=>!paths.includes(path))) throw new XpRefusal('placements-changed', "This test's pages changed while it was starting. Try again.", undefined, 409);
|
|
88
|
+
const conflicts = await placementInteractions(req, exec, options, experiment, paths);
|
|
89
|
+
if (conflicts.length) throw new XpRefusal('interacting-tests', interactingTestsCopy(conflicts[0].path, conflicts[0].status, conflicts[0].name, conflicts[0].version), conflicts, 409);
|
|
90
|
+
return {
|
|
91
|
+
placements,
|
|
92
|
+
docs
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -2,7 +2,11 @@ import type { PayloadRequest } from 'payload';
|
|
|
2
2
|
import type { Arm, DocId, FlagPoint, PreviewPlacement, ResolvedOptions } from '../types/contracts.js';
|
|
3
3
|
import type { ArmSlot } from '../types/puck.js';
|
|
4
4
|
export interface BlockPlacement extends PreviewPlacement {
|
|
5
|
+
editor?: 'puck' | 'blocks' | 'page';
|
|
6
|
+
doc?: Record<string, unknown>;
|
|
5
7
|
slots: Record<ArmSlot, number>;
|
|
8
|
+
duplicateRows?: boolean;
|
|
9
|
+
identicalSlots?: string[];
|
|
6
10
|
locked?: boolean;
|
|
7
11
|
localized?: boolean;
|
|
8
12
|
}
|
|
@@ -13,10 +17,14 @@ export type FlagPlacement = Awaited<ReturnType<NonNullable<FlagPoint['placements
|
|
|
13
17
|
export declare function scanBlockPlacements(req: PayloadRequest, args: {
|
|
14
18
|
slug: string;
|
|
15
19
|
puckCollections: NonNullable<ResolvedOptions['puck']>['collections'];
|
|
20
|
+
blocksCollections: NonNullable<ResolvedOptions['blocks']>['collections'];
|
|
21
|
+
compareVersions?: boolean;
|
|
16
22
|
}): Promise<BlockPlacement[]>;
|
|
17
23
|
export declare function scanBlockPlacements(req: PayloadRequest, args: {
|
|
18
24
|
slugs: ReadonlySet<string>;
|
|
19
25
|
puckCollections: NonNullable<ResolvedOptions['puck']>['collections'];
|
|
26
|
+
blocksCollections: NonNullable<ResolvedOptions['blocks']>['collections'];
|
|
27
|
+
compareVersions?: boolean;
|
|
20
28
|
}): Promise<Map<string, BlockPlacement[]>>;
|
|
21
29
|
export declare function flagPlacements<TUser extends {
|
|
22
30
|
id: DocId;
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import { BLOCK_SLOTS, EXPERIMENT_BLOCK_SLUG, hostBlocks, slotBlocks } from '../blocks/model.js';
|
|
2
|
+
import { safeError } from '../log.js';
|
|
3
|
+
import { pageVersionContent } from '../db/pageContent.js';
|
|
4
|
+
import { comparableContent } from './pageVersions.js';
|
|
5
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
1
6
|
import { hasLocalizedFields } from './contentFields.js';
|
|
2
|
-
import { puckCollections as normalisePuckCollections } from '../options.js';
|
|
7
|
+
import { documentEditor, puckCollections as normalisePuckCollections } from '../options.js';
|
|
3
8
|
const slots = [
|
|
4
9
|
'control',
|
|
5
10
|
'variantA',
|
|
@@ -23,6 +28,7 @@ export async function scanBlockPlacements(req, args) {
|
|
|
23
28
|
collections: puckCollections
|
|
24
29
|
}
|
|
25
30
|
})){
|
|
31
|
+
const editors = new Map();
|
|
26
32
|
const publishedIds = new Set();
|
|
27
33
|
const publishedSlugs = new Map();
|
|
28
34
|
const draftsEnabled = req.payload.config.collections?.some((config)=>config.slug === collection && config.versions && config.versions.drafts);
|
|
@@ -53,9 +59,28 @@ export async function scanBlockPlacements(req, args) {
|
|
|
53
59
|
if (!draft && doc._status === 'draft') continue;
|
|
54
60
|
if (draft && doc._status !== 'draft' && publishedIds.has(String(doc.id))) continue;
|
|
55
61
|
if (!draft) {
|
|
62
|
+
editors.set(String(doc.id), documentEditor({
|
|
63
|
+
puck: {
|
|
64
|
+
enabled: true,
|
|
65
|
+
collections: puckCollections
|
|
66
|
+
},
|
|
67
|
+
blocks: {
|
|
68
|
+
collections: args.blocksCollections
|
|
69
|
+
}
|
|
70
|
+
}, collection, doc));
|
|
56
71
|
publishedIds.add(String(doc.id));
|
|
57
72
|
if (typeof doc.slug === 'string') publishedSlugs.set(String(doc.id), doc.slug);
|
|
58
73
|
}
|
|
74
|
+
const editor = editors.get(String(doc.id)) ?? documentEditor({
|
|
75
|
+
puck: {
|
|
76
|
+
enabled: true,
|
|
77
|
+
collections: puckCollections
|
|
78
|
+
},
|
|
79
|
+
blocks: {
|
|
80
|
+
collections: args.blocksCollections
|
|
81
|
+
}
|
|
82
|
+
}, collection, doc);
|
|
83
|
+
if (editor !== 'puck') continue;
|
|
59
84
|
function walk(value) {
|
|
60
85
|
if (!value || typeof value !== 'object') return;
|
|
61
86
|
if (Array.isArray(value)) {
|
|
@@ -70,6 +95,8 @@ export async function scanBlockPlacements(req, args) {
|
|
|
70
95
|
placements.get(experiment).push({
|
|
71
96
|
collection,
|
|
72
97
|
id: doc.id,
|
|
98
|
+
editor: 'puck',
|
|
99
|
+
doc,
|
|
73
100
|
title: String(doc.title ?? doc.name ?? doc.id),
|
|
74
101
|
published: !draft || doc._status === 'published',
|
|
75
102
|
...publishedSlugs.has(String(doc.id)) ? {
|
|
@@ -87,7 +114,115 @@ export async function scanBlockPlacements(req, args) {
|
|
|
87
114
|
}
|
|
88
115
|
Object.values(node).forEach(walk);
|
|
89
116
|
}
|
|
117
|
+
const offsets = new Map([
|
|
118
|
+
...placements
|
|
119
|
+
].map(([slug, list])=>[
|
|
120
|
+
slug,
|
|
121
|
+
list.length
|
|
122
|
+
]));
|
|
90
123
|
walk(doc[field]);
|
|
124
|
+
if (args.compareVersions && !draft && [
|
|
125
|
+
...placements
|
|
126
|
+
].some(([slug, list])=>list.length > offsets.get(slug))) {
|
|
127
|
+
const raw = await comparisonContent(req, collection, doc.id);
|
|
128
|
+
const rawSlots = new Map();
|
|
129
|
+
const collect = (value)=>{
|
|
130
|
+
if (Array.isArray(value)) {
|
|
131
|
+
value.forEach(collect);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (!value || typeof value !== 'object') return;
|
|
135
|
+
const node = value, props = node.props;
|
|
136
|
+
const ref = props?.experiment;
|
|
137
|
+
const slug = typeof ref === 'string' ? ref : ref && typeof ref === 'object' && 'slug' in ref ? ref.slug : undefined;
|
|
138
|
+
if (node.type === 'ExperimentSlot' && props && typeof slug === 'string') {
|
|
139
|
+
const list = rawSlots.get(slug) ?? [];
|
|
140
|
+
list.push(props);
|
|
141
|
+
rawSlots.set(slug, list);
|
|
142
|
+
}
|
|
143
|
+
Object.values(node).forEach(collect);
|
|
144
|
+
};
|
|
145
|
+
if (raw && raw._status !== 'draft') collect(raw[field]);
|
|
146
|
+
for (const [slug, list] of placements)list.slice(offsets.get(slug)).forEach((placement, index)=>{
|
|
147
|
+
const props = rawSlots.get(slug)?.[index];
|
|
148
|
+
if (props) placement.identicalSlots = slots.filter((slot)=>slot !== 'control' && Array.isArray(props[slot]) && props[slot].length > 0 && isDeepStrictEqual(comparableContent(props.control ?? []), comparableContent(props[slot])));
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (!result.hasNextPage) break;
|
|
153
|
+
page++;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
for (const registration of args.blocksCollections){
|
|
158
|
+
const { slug: collection, field } = registration;
|
|
159
|
+
const published = new Map();
|
|
160
|
+
const draftsEnabled = req.payload.config.collections?.some((c)=>c.slug === collection && c.versions && c.versions.drafts);
|
|
161
|
+
for (const draft of [
|
|
162
|
+
false,
|
|
163
|
+
true
|
|
164
|
+
]){
|
|
165
|
+
let page = 1;
|
|
166
|
+
while(true){
|
|
167
|
+
const result = await req.payload.find({
|
|
168
|
+
collection,
|
|
169
|
+
req,
|
|
170
|
+
overrideAccess: false,
|
|
171
|
+
depth: 0,
|
|
172
|
+
limit: 100,
|
|
173
|
+
page,
|
|
174
|
+
draft,
|
|
175
|
+
includeLockStatus: true,
|
|
176
|
+
...!draft && draftsEnabled ? {
|
|
177
|
+
where: {
|
|
178
|
+
_status: {
|
|
179
|
+
equals: 'published'
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} : {}
|
|
183
|
+
});
|
|
184
|
+
for (const doc of result.docs){
|
|
185
|
+
if (!draft && doc._status === 'draft') continue;
|
|
186
|
+
if (!draft) published.set(String(doc.id), doc);
|
|
187
|
+
else if (doc._status !== 'draft' && published.has(String(doc.id))) continue;
|
|
188
|
+
const baseline = published.get(String(doc.id)) ?? doc;
|
|
189
|
+
if (documentEditor({
|
|
190
|
+
blocks: {
|
|
191
|
+
collections: args.blocksCollections
|
|
192
|
+
},
|
|
193
|
+
puck: {
|
|
194
|
+
enabled: true,
|
|
195
|
+
collections: puckCollections
|
|
196
|
+
}
|
|
197
|
+
}, collection, baseline) !== 'blocks') continue;
|
|
198
|
+
const raw = args.compareVersions && !draft && hostBlocks(doc[field]).some((block)=>block.blockType === (registration.blockSlug ?? EXPERIMENT_BLOCK_SLUG) && typeof block.experiment === 'string' && slugs.has(block.experiment)) ? await comparisonContent(req, collection, doc.id) : undefined;
|
|
199
|
+
for (const [index, block] of hostBlocks(doc[field]).entries()){
|
|
200
|
+
if (block.blockType !== (registration.blockSlug ?? EXPERIMENT_BLOCK_SLUG) || typeof block.experiment !== 'string' || !slugs.has(block.experiment)) continue;
|
|
201
|
+
const stored = raw ? hostBlocks(raw[field]).find((row)=>row.id === block.id) ?? hostBlocks(raw[field])[index] ?? block : block;
|
|
202
|
+
const rows = Array.isArray(stored.slots) ? stored.slots : [];
|
|
203
|
+
const keys = rows.map((row)=>row?.slot);
|
|
204
|
+
placements.get(block.experiment).push({
|
|
205
|
+
collection,
|
|
206
|
+
id: doc.id,
|
|
207
|
+
editor: 'blocks',
|
|
208
|
+
doc,
|
|
209
|
+
title: String(doc.title ?? doc.name ?? doc.id),
|
|
210
|
+
published: !draft || doc._status === 'published',
|
|
211
|
+
...typeof baseline.slug === 'string' ? {
|
|
212
|
+
slug: baseline.slug
|
|
213
|
+
} : {},
|
|
214
|
+
slots: Object.fromEntries(BLOCK_SLOTS.map((slot)=>[
|
|
215
|
+
slot,
|
|
216
|
+
slotBlocks(block, slot).length
|
|
217
|
+
])),
|
|
218
|
+
duplicateRows: new Set(keys).size !== keys.length,
|
|
219
|
+
identicalSlots: raw ? BLOCK_SLOTS.filter((slot)=>slot !== 'control' && slotBlocks(block, slot).length > 0 && isDeepStrictEqual(comparableContent(slotBlocks(stored, 'control')), comparableContent(slotBlocks(stored, slot)))) : [],
|
|
220
|
+
locked: doc._isLocked === true || doc.locked === true,
|
|
221
|
+
...hasLocalizedFields(req.payload.config.collections?.find((c)=>c.slug === collection)?.fields ?? [], req.payload.config) ? {
|
|
222
|
+
localized: true
|
|
223
|
+
} : {}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
91
226
|
}
|
|
92
227
|
if (!result.hasNextPage) break;
|
|
93
228
|
page++;
|
|
@@ -105,3 +240,15 @@ export async function flagPlacements(options, experiment, req) {
|
|
|
105
240
|
req
|
|
106
241
|
});
|
|
107
242
|
}
|
|
243
|
+
/** A warning-only comparison must never turn an adapter failure into a Start refusal. */ async function comparisonContent(req, collection, id) {
|
|
244
|
+
try {
|
|
245
|
+
return await pageVersionContent(req, collection, id);
|
|
246
|
+
} catch (cause) {
|
|
247
|
+
req.payload.logger.error({
|
|
248
|
+
err: safeError(cause),
|
|
249
|
+
collection,
|
|
250
|
+
id
|
|
251
|
+
}, 'Experiment version comparison failed');
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -17,10 +17,10 @@ export declare function loadStartInputs<TUser extends {
|
|
|
17
17
|
* Start's locked recheck. Raw SQL on the request's transaction when there is one. */
|
|
18
18
|
export declare function loadPageTestsInUse(req: PayloadRequest): Promise<NonNullable<CheckStartInput['pageTestsInUse']>>;
|
|
19
19
|
/** Page targets no registered collection reports as published. A lookup that throws counts as
|
|
20
|
-
* unpublished (logged). Start calls it again under its page-path locks on a detached request. */
|
|
20
|
+
* unpublished (logged) unless propagateErrors is requested by decide. Start calls it again under its page-path locks on a detached request. */
|
|
21
21
|
export declare function unpublishedPageTargets<TUser extends {
|
|
22
22
|
id: DocId;
|
|
23
|
-
}>(req: PayloadRequest, options: ResolvedOptions<TUser>, experiment: Pick<StartExperiment, 'id' | 'arms'
|
|
23
|
+
}>(req: PayloadRequest, options: ResolvedOptions<TUser>, experiment: Pick<StartExperiment, 'id' | 'arms' | 'pageCollection'>, propagateErrors?: boolean): Promise<string[]>;
|
|
24
24
|
/** Shared, request-scoped reads for summary; a failed row is null in tolerant mode.
|
|
25
25
|
* Preflight/Start use the same builder and retain their throwing read behaviour.
|
|
26
26
|
*/
|