@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.
Files changed (338) hide show
  1. package/README.md +172 -21
  2. package/dist/admin/MetricSourceSelect.js +3 -3
  3. package/dist/admin/PlacementStatus.d.ts +3 -0
  4. package/dist/admin/PlacementStatus.js +54 -0
  5. package/dist/admin/PreviewLinks.d.ts +2 -1
  6. package/dist/admin/PreviewLinks.js +47 -26
  7. package/dist/admin/TestsNavLink.d.ts +3 -1
  8. package/dist/admin/TestsNavLink.js +2 -2
  9. package/dist/admin/actions/ConfirmDialog.d.ts +2 -1
  10. package/dist/admin/actions/ConfirmDialog.js +2 -2
  11. package/dist/admin/actions/DecideDialog.d.ts +2 -1
  12. package/dist/admin/actions/DecideDialog.js +2 -2
  13. package/dist/admin/api.d.ts +11 -2
  14. package/dist/admin/api.js +20 -2
  15. package/dist/admin/armSelect/ArmSelectField.js +14 -22
  16. package/dist/admin/arms/arms.model.d.ts +1 -21
  17. package/dist/admin/arms/arms.model.js +1 -153
  18. package/dist/admin/blocks/BlockPanel.d.ts +4 -0
  19. package/dist/admin/blocks/BlockPanel.js +153 -0
  20. package/dist/admin/blocks/TestBlockField.d.ts +5 -0
  21. package/dist/admin/blocks/TestBlockField.js +163 -0
  22. package/dist/admin/blocks/formApi.d.ts +43 -0
  23. package/dist/admin/blocks/formApi.js +153 -0
  24. package/dist/admin/blocks/inject.d.ts +7 -0
  25. package/dist/admin/blocks/inject.js +41 -0
  26. package/dist/admin/blocks/model.d.ts +17 -0
  27. package/dist/admin/blocks/model.js +45 -0
  28. package/dist/admin/blocks/statusCache.d.ts +7 -0
  29. package/dist/admin/blocks/statusCache.js +83 -0
  30. package/dist/admin/blocks/useBlockForm.d.ts +17 -0
  31. package/dist/admin/blocks/useBlockForm.js +46 -0
  32. package/dist/admin/copy/actions.d.ts +2 -0
  33. package/dist/admin/copy/actions.js +9 -0
  34. package/dist/admin/copy/arms.d.ts +1 -6
  35. package/dist/admin/copy/arms.js +1 -16
  36. package/dist/admin/copy/blocks.d.ts +27 -0
  37. package/dist/admin/copy/blocks.js +38 -0
  38. package/dist/admin/copy/goals.d.ts +2 -0
  39. package/dist/admin/copy/goals.js +4 -1
  40. package/dist/admin/copy/newTest.d.ts +10 -0
  41. package/dist/admin/copy/newTest.js +10 -0
  42. package/dist/admin/copy/pages.d.ts +26 -0
  43. package/dist/admin/copy/pages.js +26 -0
  44. package/dist/admin/copy/readiness.d.ts +25 -3
  45. package/dist/admin/copy/readiness.js +25 -3
  46. package/dist/admin/copy/results.d.ts +3 -0
  47. package/dist/admin/copy/results.js +4 -1
  48. package/dist/admin/copy/review.d.ts +1 -1
  49. package/dist/admin/copy/review.js +1 -1
  50. package/dist/admin/copy/surface.d.ts +0 -1
  51. package/dist/admin/copy/surface.js +0 -1
  52. package/dist/admin/copy/testPage.d.ts +4 -0
  53. package/dist/admin/copy/testPage.js +4 -0
  54. package/dist/admin/goals/XpGoalField.d.ts +2 -0
  55. package/dist/admin/goals/XpGoalField.js +152 -0
  56. package/dist/admin/metricSource.model.d.ts +2 -0
  57. package/dist/admin/metricSource.model.js +4 -0
  58. package/dist/admin/pageCollections.d.ts +10 -0
  59. package/dist/admin/pageCollections.js +20 -0
  60. package/dist/admin/paths.d.ts +8 -1
  61. package/dist/admin/paths.js +8 -1
  62. package/dist/admin/placementStatus.model.d.ts +2 -0
  63. package/dist/admin/placementStatus.model.js +19 -0
  64. package/dist/admin/placements/placementLabels.d.ts +11 -0
  65. package/dist/admin/placements/placementLabels.js +21 -0
  66. package/dist/admin/placements/placementLabels.model.d.ts +13 -0
  67. package/dist/admin/placements/placementLabels.model.js +12 -0
  68. package/dist/admin/placements/usePlacementLabels.d.ts +12 -0
  69. package/dist/admin/placements/usePlacementLabels.js +102 -0
  70. package/dist/admin/previewLinks.model.d.ts +2 -0
  71. package/dist/admin/previewLinks.model.js +3 -0
  72. package/dist/admin/results/DataQualityPanel.js +4 -1
  73. package/dist/admin/results/legacy.d.ts +16 -0
  74. package/dist/admin/results/legacy.js +27 -0
  75. package/dist/admin/results/results.model.d.ts +9 -6
  76. package/dist/admin/results/results.model.js +22 -9
  77. package/dist/admin/results/snapshot.d.ts +14 -0
  78. package/dist/admin/results/snapshot.js +3 -0
  79. package/dist/admin/surface/TestsHome.js +43 -15
  80. package/dist/admin/surface/TestsSurface.js +5 -1
  81. package/dist/admin/surface/contracts.d.ts +6 -5
  82. package/dist/admin/surface/goals/GoalForm.js +67 -34
  83. package/dist/admin/surface/goals/GoalsList.js +3 -3
  84. package/dist/admin/surface/goals/data.js +19 -16
  85. package/dist/admin/surface/goals/goals.model.d.ts +1 -2
  86. package/dist/admin/surface/goals/goals.model.js +1 -3
  87. package/dist/admin/surface/home.model.d.ts +2 -1
  88. package/dist/admin/surface/home.model.js +8 -4
  89. package/dist/admin/surface/newTest/NewTest.js +58 -17
  90. package/dist/admin/surface/newTest/PageWizard.d.ts +7 -0
  91. package/dist/admin/surface/newTest/PageWizard.js +209 -0
  92. package/dist/admin/surface/pagePicker/CopyRoute.d.ts +6 -0
  93. package/dist/admin/surface/pagePicker/CopyRoute.js +53 -0
  94. package/dist/admin/surface/pagePicker/PagePicker.d.ts +10 -0
  95. package/dist/admin/surface/pagePicker/PagePicker.js +172 -0
  96. package/dist/admin/surface/pagePicker/pagePicker.model.d.ts +25 -0
  97. package/dist/admin/surface/pagePicker/pagePicker.model.js +25 -0
  98. package/dist/admin/surface/readiness/ActionDialogs.js +103 -69
  99. package/dist/admin/surface/readiness/Banner.js +30 -6
  100. package/dist/admin/surface/readiness/readiness.model.d.ts +8 -1
  101. package/dist/admin/surface/readiness/readiness.model.js +95 -6
  102. package/dist/admin/surface/results/ReleasePages.d.ts +3 -1
  103. package/dist/admin/surface/results/ReleasePages.js +3 -7
  104. package/dist/admin/surface/results/ResultsBody.js +23 -3
  105. package/dist/admin/surface/results/ResultsHeader.js +2 -5
  106. package/dist/admin/surface/results/recommendation.model.d.ts +3 -2
  107. package/dist/admin/surface/results/recommendation.model.js +10 -3
  108. package/dist/admin/surface/results/useResults.d.ts +10 -1
  109. package/dist/admin/surface/surface.css +0 -1
  110. package/dist/admin/surface/test/AdvancedSection.js +111 -104
  111. package/dist/admin/surface/test/GoalSection.js +97 -22
  112. package/dist/admin/surface/test/LengthSection.js +1 -1
  113. package/dist/admin/surface/test/PageHoldout.d.ts +3 -0
  114. package/dist/admin/surface/test/PageHoldout.js +67 -0
  115. package/dist/admin/surface/test/TestPage.js +2 -1
  116. package/dist/admin/surface/test/VersionsSection.js +12 -5
  117. package/dist/admin/surface/test/fields.d.ts +13 -4
  118. package/dist/admin/surface/test/fields.js +46 -14
  119. package/dist/admin/surface/test/jumpToSection.d.ts +1 -1
  120. package/dist/admin/surface/test/jumpToSection.js +4 -3
  121. package/dist/admin/surface/test/pageParts.d.ts +4 -1
  122. package/dist/admin/surface/test/pageParts.js +79 -2
  123. package/dist/admin/surface/test/testPage.css +8 -1
  124. package/dist/admin/surface/test/testPage.model.d.ts +2 -3
  125. package/dist/admin/surface/test/testPage.model.js +1 -21
  126. package/dist/admin/surface/testDocument.model.d.ts +1 -0
  127. package/dist/admin/surface/testDocument.model.js +2 -1
  128. package/dist/admin/surface/useTestDocument.d.ts +11 -2
  129. package/dist/admin/surface/useTestDocument.js +55 -16
  130. package/dist/admin/useApi.d.ts +11 -2
  131. package/dist/admin/useApi.js +3 -5
  132. package/dist/admin/useLayerUsage.js +14 -23
  133. package/dist/admin/views/ExperimentsRootView.js +58 -16
  134. package/dist/admin/views/TestsNavLinkServer.d.ts +2 -0
  135. package/dist/admin/views/TestsNavLinkServer.js +43 -0
  136. package/dist/arms/copy.d.ts +6 -0
  137. package/dist/arms/copy.js +16 -0
  138. package/dist/arms/model.d.ts +23 -0
  139. package/dist/arms/model.js +173 -0
  140. package/dist/blocks/bindings.d.ts +3 -0
  141. package/dist/blocks/bindings.js +34 -0
  142. package/dist/blocks/createExperimentBlock.d.ts +6 -0
  143. package/dist/blocks/createExperimentBlock.js +77 -0
  144. package/dist/blocks/formState.d.ts +31 -0
  145. package/dist/blocks/formState.js +146 -0
  146. package/dist/blocks/model.d.ts +41 -0
  147. package/dist/blocks/model.js +34 -0
  148. package/dist/client/ExperimentGroup.d.ts +6 -0
  149. package/dist/client/ExperimentGroup.js +9 -0
  150. package/dist/client/ExperimentsProvider.js +20 -4
  151. package/dist/client/clickGoals.d.ts +4 -0
  152. package/dist/client/clickGoals.js +43 -0
  153. package/dist/collections/experiments/config.js +12 -2
  154. package/dist/collections/experiments/deleteGuard.d.ts +2 -1
  155. package/dist/collections/experiments/deleteGuard.js +12 -1
  156. package/dist/collections/experiments/editability.js +4 -2
  157. package/dist/collections/experiments/pickSlug.d.ts +2 -0
  158. package/dist/collections/experiments/pickSlug.js +8 -0
  159. package/dist/collections/labels.d.ts +3 -0
  160. package/dist/collections/labels.js +20 -0
  161. package/dist/collections/metrics.js +12 -2
  162. package/dist/collections/phases.js +1 -0
  163. package/dist/collections/serialize.js +7 -0
  164. package/dist/db/appends.d.ts +6 -2
  165. package/dist/db/appends.js +10 -4
  166. package/dist/db/clickGoals.d.ts +9 -0
  167. package/dist/db/clickGoals.js +22 -0
  168. package/dist/db/delete.d.ts +1 -0
  169. package/dist/db/delete.js +1 -0
  170. package/dist/db/locks.d.ts +14 -0
  171. package/dist/db/locks.js +36 -1
  172. package/dist/db/names.d.ts +4 -0
  173. package/dist/db/names.js +4 -0
  174. package/dist/db/pageBindings.d.ts +4 -0
  175. package/dist/db/pageBindings.js +17 -0
  176. package/dist/db/pageBusy.d.ts +2 -0
  177. package/dist/db/pageBusy.js +10 -0
  178. package/dist/db/pageContent.d.ts +10 -0
  179. package/dist/db/pageContent.js +79 -0
  180. package/dist/db/pageOccupancy.d.ts +12 -0
  181. package/dist/db/pageOccupancy.js +45 -0
  182. package/dist/db/placements.d.ts +33 -0
  183. package/dist/db/placements.js +54 -0
  184. package/dist/db/slugs.d.ts +8 -0
  185. package/dist/db/slugs.js +40 -0
  186. package/dist/db/snapshots.d.ts +6 -0
  187. package/dist/db/snapshots.js +9 -5
  188. package/dist/endpoints/clickKeys.d.ts +5 -0
  189. package/dist/endpoints/clickKeys.js +17 -0
  190. package/dist/endpoints/events.js +4 -1
  191. package/dist/endpoints/index.js +28 -0
  192. package/dist/endpoints/pages.d.ts +5 -0
  193. package/dist/endpoints/pages.js +120 -0
  194. package/dist/endpoints/placementStatus.d.ts +6 -0
  195. package/dist/endpoints/placementStatus.js +238 -0
  196. package/dist/endpoints/readBody.d.ts +2 -0
  197. package/dist/endpoints/readBody.js +24 -0
  198. package/dist/endpoints/results.js +5 -1
  199. package/dist/endpoints/summary.js +11 -4
  200. package/dist/endpoints/wrapBlock.d.ts +6 -0
  201. package/dist/endpoints/wrapBlock.js +76 -0
  202. package/dist/errors.d.ts +3 -3
  203. package/dist/errors.js +4 -4
  204. package/dist/events/rows.d.ts +1 -1
  205. package/dist/events/rows.js +5 -3
  206. package/dist/exports/adminClient.d.ts +5 -0
  207. package/dist/exports/adminClient.js +5 -0
  208. package/dist/exports/adminRsc.d.ts +1 -0
  209. package/dist/exports/adminRsc.js +1 -0
  210. package/dist/exports/blocks.d.ts +2 -0
  211. package/dist/exports/blocks.js +1 -0
  212. package/dist/exports/client.d.ts +1 -0
  213. package/dist/exports/client.js +1 -0
  214. package/dist/exports/index.d.ts +6 -1
  215. package/dist/exports/index.js +3 -0
  216. package/dist/exports/puck.d.ts +5 -2
  217. package/dist/exports/puck.js +3 -1
  218. package/dist/exports/puckEditor.d.ts +6 -1
  219. package/dist/exports/puckEditor.js +4 -1
  220. package/dist/exports/rsc.d.ts +4 -0
  221. package/dist/exports/rsc.js +2 -0
  222. package/dist/fields/xpGoal.d.ts +3 -0
  223. package/dist/fields/xpGoal.js +28 -0
  224. package/dist/globals/state.js +10 -0
  225. package/dist/goals/cache.d.ts +3 -0
  226. package/dist/goals/cache.js +35 -0
  227. package/dist/goals/fairness.d.ts +40 -0
  228. package/dist/goals/fairness.js +200 -0
  229. package/dist/goals/key.d.ts +4 -0
  230. package/dist/goals/key.js +3 -0
  231. package/dist/goals/picker/clickGoalPicker.model.d.ts +29 -0
  232. package/dist/goals/picker/clickGoalPicker.model.js +45 -0
  233. package/dist/goals/picker/copy.d.ts +16 -0
  234. package/dist/goals/picker/copy.js +15 -0
  235. package/dist/goals/picker/useClickGoalPicker.d.ts +30 -0
  236. package/dist/goals/picker/useClickGoalPicker.js +220 -0
  237. package/dist/hooks/pageGuard.d.ts +5 -1
  238. package/dist/hooks/pageGuard.js +160 -80
  239. package/dist/lib/fetchAllDocs.d.ts +16 -0
  240. package/dist/lib/fetchAllDocs.js +56 -0
  241. package/dist/onInit.js +29 -0
  242. package/dist/options.d.ts +2 -0
  243. package/dist/options.js +43 -10
  244. package/dist/plugin.js +45 -28
  245. package/dist/puck/ExperimentPicker.js +47 -16
  246. package/dist/puck/ExperimentSlot.d.ts +51 -1
  247. package/dist/puck/ExperimentSlot.js +23 -12
  248. package/dist/puck/copy.d.ts +6 -4
  249. package/dist/puck/copy.js +6 -4
  250. package/dist/puck/editor/CreateExperimentDrawer.js +46 -56
  251. package/dist/puck/editor/ExperimentSlotEditor.d.ts +52 -1
  252. package/dist/puck/editor/ExperimentSlotEditor.js +292 -137
  253. package/dist/puck/editor/ResultsReadout.d.ts +6 -0
  254. package/dist/puck/editor/ResultsReadout.js +99 -0
  255. package/dist/puck/editor/VersionBuilder.d.ts +34 -0
  256. package/dist/puck/editor/VersionBuilder.js +317 -0
  257. package/dist/puck/editor/armWrites.d.ts +7 -0
  258. package/dist/puck/editor/armWrites.js +71 -0
  259. package/dist/puck/editor/armsCache.d.ts +14 -0
  260. package/dist/puck/editor/armsCache.js +78 -0
  261. package/dist/puck/editor/copy.d.ts +33 -1
  262. package/dist/puck/editor/copy.js +45 -8
  263. package/dist/puck/editor/createTest.d.ts +1 -1
  264. package/dist/puck/editor/createTest.js +25 -14
  265. package/dist/puck/editor/editor.chrome.css +48 -161
  266. package/dist/puck/editor/editor.css +48 -161
  267. package/dist/puck/editor/editor.model.d.ts +8 -3
  268. package/dist/puck/editor/editor.model.js +16 -10
  269. package/dist/puck/editor/experimentOutlinePlugin.d.ts +3 -0
  270. package/dist/puck/editor/experimentOutlinePlugin.js +89 -0
  271. package/dist/puck/editor/integration.d.ts +10 -0
  272. package/dist/puck/editor/integration.js +30 -0
  273. package/dist/puck/editor/xpGoalPuckEditorField.d.ts +7 -0
  274. package/dist/puck/editor/xpGoalPuckEditorField.js +134 -0
  275. package/dist/puck/experimentSlotField.d.ts +2 -0
  276. package/dist/puck/experimentSlotField.js +11 -3
  277. package/dist/puck/hostContext.d.ts +10 -0
  278. package/dist/puck/hostContext.js +12 -0
  279. package/dist/puck/integration.d.ts +6 -0
  280. package/dist/puck/integration.js +21 -0
  281. package/dist/puck/routes.d.ts +12 -0
  282. package/dist/puck/routes.js +11 -0
  283. package/dist/puck/slotConfig.d.ts +1 -1
  284. package/dist/puck/slotConfig.js +2 -2
  285. package/dist/puck/xpGoalField.d.ts +3 -0
  286. package/dist/puck/xpGoalField.js +6 -0
  287. package/dist/results/health.d.ts +4 -0
  288. package/dist/results/health.js +27 -0
  289. package/dist/results/snapshot.js +15 -2
  290. package/dist/sanitizeReadinessDetail.js +51 -1
  291. package/dist/server/metadata.js +9 -2
  292. package/dist/server/normaliseContentRelationships.d.ts +7 -0
  293. package/dist/server/normaliseContentRelationships.js +62 -0
  294. package/dist/server/pageKindSeoContract.d.ts +27 -0
  295. package/dist/server/pageKindSeoContract.js +80 -0
  296. package/dist/server/recordContentChange.d.ts +1 -1
  297. package/dist/server/recordContentChange.js +72 -13
  298. package/dist/server/resolveExperimentBlocks.d.ts +5 -0
  299. package/dist/server/resolveExperimentBlocks.js +54 -0
  300. package/dist/server/runResults.js +92 -19
  301. package/dist/server/toClientAssignments.js +2 -1
  302. package/dist/transitions/addMetric.d.ts +2 -1
  303. package/dist/transitions/addMetric.js +28 -1
  304. package/dist/transitions/checkStart.d.ts +17 -1
  305. package/dist/transitions/checkStart.js +99 -7
  306. package/dist/transitions/decide.js +40 -1
  307. package/dist/transitions/deleteExperiment.d.ts +3 -1
  308. package/dist/transitions/deleteExperiment.js +27 -1
  309. package/dist/transitions/kernel.js +4 -0
  310. package/dist/transitions/makeContent.d.ts +5 -0
  311. package/dist/transitions/makeContent.js +39 -11
  312. package/dist/transitions/pageCollection.d.ts +10 -0
  313. package/dist/transitions/pageCollection.js +20 -0
  314. package/dist/transitions/pageVersions.d.ts +10 -0
  315. package/dist/transitions/pageVersions.js +44 -0
  316. package/dist/transitions/phaseRecord.d.ts +1 -0
  317. package/dist/transitions/placementInteractions.d.ts +40 -0
  318. package/dist/transitions/placementInteractions.js +94 -0
  319. package/dist/transitions/placements.d.ts +10 -6
  320. package/dist/transitions/placements.js +156 -2
  321. package/dist/transitions/preflightData.d.ts +8 -0
  322. package/dist/transitions/preflightData.js +248 -36
  323. package/dist/transitions/resolveCaps.d.ts +1 -0
  324. package/dist/transitions/resolveCaps.js +6 -2
  325. package/dist/transitions/start.js +77 -3
  326. package/dist/types/admin.d.ts +1 -1
  327. package/dist/types/assignments.d.ts +1 -0
  328. package/dist/types/contracts.d.ts +31 -10
  329. package/dist/types/endpoints.d.ts +92 -5
  330. package/dist/types/index.d.ts +3 -0
  331. package/dist/types/puck.d.ts +7 -1
  332. package/dist/types/snapshot.d.ts +17 -4
  333. package/dist/types/surface.d.ts +13 -0
  334. package/dist/ui/Tabs.d.ts +4 -2
  335. package/dist/ui/Tabs.js +33 -26
  336. package/dist/ui/accent.d.ts +22 -3
  337. package/dist/ui/accent.js +74 -16
  338. package/package.json +12 -2
@@ -1,8 +1,15 @@
1
+ import { goalContent, OUTSIDE_GOAL } from '../goals/fairness.js';
2
+ import { placementInteractions, placementPaths, uniquePlacementDocs } from './placementInteractions.js';
1
3
  import { Forbidden, NotFound } from 'payload';
4
+ import { normalisePath } from '../runtime/normalisePath.js';
5
+ import { resolvePageCollection } from './pageCollection.js';
6
+ import { identicalPageVersions } from './pageVersions.js';
2
7
  import { safeError } from '../log.js';
3
8
  import { powerMetricWeeks } from '@delmaredigital/experiments-stats';
4
9
  import { checkStart, relationshipId } from './checkStart.js';
5
10
  import { flagPlacements, scanBlockPlacements } from './placements.js';
11
+ import { routedPageTests } from '../db/pageOccupancy.js';
12
+ import { pooledExecutor, txExecutor } from '../db/executor.js';
6
13
  const failureDetail = (cause)=>{
7
14
  const name = cause instanceof Error ? cause.name : undefined;
8
15
  return cause instanceof Forbidden || name === 'Forbidden' ? 'forbidden' : cause instanceof NotFound || name === 'NotFound' ? 'not-found' : 'placements-failed';
@@ -28,6 +35,35 @@ const failureDetail = (cause)=>{
28
35
  experiment
29
36
  ])).get(String(experiment.id)) ?? null;
30
37
  }
38
+ /** Occupancy from the arms the runtime routes on (frozen rounds), shared by preflight and
39
+ * Start's locked recheck. Raw SQL on the request's transaction when there is one. */ export async function loadPageTestsInUse(req) {
40
+ return routedPageTests(await req.transactionID != null ? txExecutor(req) : pooledExecutor(req.payload));
41
+ }
42
+ /** Page targets no registered collection reports as published. A lookup that throws counts as
43
+ * unpublished (logged) unless propagateErrors is requested by decide. Start calls it again under its page-path locks on a detached request. */ export async function unpublishedPageTargets(req, options, experiment, propagateErrors = false) {
44
+ const resolved = await resolvePageCollection(req, options, experiment, propagateErrors);
45
+ const collections = resolved ? [
46
+ resolved
47
+ ] : [];
48
+ const targets = await Promise.all(experiment.arms.filter((arm)=>arm.kind !== 'holdout' && arm.pageTarget?.trim()).map(async (arm)=>{
49
+ for (const collection of collections){
50
+ try {
51
+ const page = await collection.findByPath(arm.pageTarget, req);
52
+ if (page?.published === true) return null;
53
+ } catch (cause) {
54
+ if (propagateErrors) throw cause;
55
+ req.payload.logger.error({
56
+ err: safeError(cause),
57
+ experimentId: experiment.id,
58
+ collection: collection.slug,
59
+ id: arm.pageTarget
60
+ }, 'Experiment page lookup failed');
61
+ }
62
+ }
63
+ return arm.pageTarget;
64
+ }));
65
+ return targets.filter((target)=>target !== null);
66
+ }
31
67
  const metricIds = (experiment)=>[
32
68
  experiment.metrics?.primary,
33
69
  ...experiment.metrics?.secondary ?? [],
@@ -58,6 +94,7 @@ const metricIds = (experiment)=>[
58
94
  })();
59
95
  const blockSlugs = new Set(experiments.filter((e)=>e.kind === 'block').map((e)=>e.slug));
60
96
  // Lazy promises prevent unhandled rejections and share failed reads as well as successful ones.
97
+ let pageTestsInUse;
61
98
  let blocks;
62
99
  let state;
63
100
  const busyPoints = new Map();
@@ -84,33 +121,22 @@ const metricIds = (experiment)=>[
84
121
  return failureDetail(cause);
85
122
  };
86
123
  if (experiment.kind === 'page') {
124
+ pageTestsInUse ??= loadPageTestsInUse(req);
125
+ input.pageTestsInUse = await pageTestsInUse;
87
126
  input.pageTargetsUnchecked = options.pages === undefined;
127
+ input.unknownPageCollection = Boolean(experiment.pageCollection && !options.pages?.collections.some((entry)=>entry.slug === experiment.pageCollection));
88
128
  if (options.pages) {
89
- input.unpublishedPageTargets = [];
90
- for (const arm of experiment.arms.filter((arm)=>arm.kind !== 'holdout' && arm.pageTarget?.trim())){
91
- let published = false;
92
- for (const collection of options.pages.collections){
93
- try {
94
- const page = await collection.findByPath(arm.pageTarget, req);
95
- if (page?.published === true) published = true;
96
- } catch (cause) {
97
- req.payload.logger.error({
98
- err: safeError(cause),
99
- experimentId: experiment.id,
100
- collection: collection.slug,
101
- id: arm.pageTarget
102
- }, 'Experiment page lookup failed');
103
- }
104
- }
105
- if (!published) input.unpublishedPageTargets.push(arm.pageTarget);
106
- }
129
+ input.unpublishedPageTargets = await unpublishedPageTargets(req, options, experiment);
130
+ if (options.pages.collections.some((entry)=>entry.contentOf)) input.identicalVersions = await identicalPageVersions(req, await resolvePageCollection(req, options, experiment), experiment.arms);
107
131
  }
108
132
  }
109
133
  if (experiment.kind === 'block') {
110
134
  try {
111
135
  blocks ??= scanBlockPlacements(req, {
112
136
  slugs: blockSlugs,
113
- puckCollections: options.puck?.collections ?? []
137
+ compareVersions: true,
138
+ puckCollections: options.puck?.collections ?? [],
139
+ blocksCollections: options.blocks?.collections ?? []
114
140
  });
115
141
  input.placements = (await blocks).get(experiment.slug) ?? [];
116
142
  } catch (cause) {
@@ -240,6 +266,13 @@ const metricIds = (experiment)=>[
240
266
  })());
241
267
  input.layer = await layers.get(key);
242
268
  }
269
+ if ((experiment.kind === 'page' || experiment.kind === 'block') && options.pages?.collections.length) {
270
+ const paths = experiment.kind === 'page' ? experiment.arms.flatMap((a)=>typeof a.pageTarget === 'string' ? [
271
+ a.pageTarget
272
+ ] : []) : await placementPaths(req, options, uniquePlacementDocs(input.placements ?? []));
273
+ input.interactions = await placementInteractions(req, await req.transactionID != null ? txExecutor(req) : pooledExecutor(req.payload), options, experiment, paths.map(normalisePath));
274
+ }
275
+ if (input.metrics.some((metric)=>metric.source.startsWith('click:'))) input.goalContent = await goalContent(req, options, experiment, input.placements ? uniquePlacementDocs(input.placements.filter((placement)=>placement.published)) : undefined);
243
276
  inputs.set(String(experiment.id), input);
244
277
  } catch (cause) {
245
278
  if (!tolerateErrors) throw cause;
@@ -248,26 +281,72 @@ const metricIds = (experiment)=>[
248
281
  }
249
282
  return inputs;
250
283
  }
251
- function placementUrls(options, placement) {
284
+ // Bound each optional host preview lookup so it cannot indefinitely hold preflight open.
285
+ const PREVIEW_URL_TIMEOUT_MS = 2000;
286
+ // Resolve host preview lookups in a bounded pool instead of launching one per slot.
287
+ const PREVIEW_URL_CONCURRENCY = 8;
288
+ async function placementUrls(options, placement, pendingPreviews, onFailure, previews) {
252
289
  const result = {};
253
290
  const { collection, id, published } = placement;
254
- try {
255
- const url = options.admin?.previewUrl?.({
256
- collection,
257
- id
258
- });
259
- if (typeof url === 'string') result.url = url;
260
- } catch {}
291
+ // One host lookup per page version: several slots on a page share it.
292
+ const key = JSON.stringify([
293
+ collection,
294
+ String(id),
295
+ published
296
+ ]);
297
+ let preview = previews.get(key);
298
+ if (!preview) previews.set(key, preview = previewLookup(options, placement, pendingPreviews, onFailure));
299
+ const url = await preview;
300
+ if (url !== undefined) result.url = url;
261
301
  try {
262
302
  const url = options.admin?.editorUrl?.({
263
303
  collection,
264
304
  id,
265
- draft: !published
305
+ draft: !published,
306
+ editor: placement.editor ?? 'puck',
307
+ doc: placement.doc
266
308
  });
267
309
  if (typeof url === 'string') result.editorUrl = url;
268
310
  } catch {}
269
311
  return result;
270
312
  }
313
+ async function previewLookup(options, placement, pendingPreviews, onFailure) {
314
+ const { collection, id, title, published, slug } = placement;
315
+ const previewUrl = options.admin?.previewUrl;
316
+ let found;
317
+ if (previewUrl) {
318
+ let timer;
319
+ try {
320
+ if (pendingPreviews.count >= PREVIEW_URL_CONCURRENCY) throw new Error('Preview URL lookups at capacity');
321
+ pendingPreviews.count++;
322
+ // A timeout cannot cancel host work: retain its permit until that work settles.
323
+ const lookup = Promise.resolve().then(()=>previewUrl({
324
+ collection,
325
+ id,
326
+ title,
327
+ published,
328
+ ...slug !== undefined ? {
329
+ slug
330
+ } : {}
331
+ })).finally(()=>{
332
+ pendingPreviews.count--;
333
+ });
334
+ const url = await Promise.race([
335
+ lookup,
336
+ new Promise((_, reject)=>{
337
+ timer = setTimeout(()=>reject(new Error('Preview URL lookup timed out')), PREVIEW_URL_TIMEOUT_MS);
338
+ })
339
+ ]);
340
+ if (typeof url === 'string') found = url;
341
+ else if (url !== null && url !== undefined) throw new TypeError('Invalid preview URL result');
342
+ } catch (cause) {
343
+ onFailure(cause, placement);
344
+ } finally{
345
+ clearTimeout(timer);
346
+ }
347
+ }
348
+ return found;
349
+ }
271
350
  async function primaryHistory(source, now) {
272
351
  if (!source) return null;
273
352
  const from = new Date(now.getTime() - 28 * 86400000);
@@ -378,15 +457,41 @@ export async function preflightData(req, { experimentId, options }, { now = new
378
457
  }
379
458
  }
380
459
  const requiredSlots = e.arms.filter((arm)=>arm.kind !== 'holdout').map((arm)=>arm.slot);
381
- const placements = (input.placements ?? []).map((placement)=>({
460
+ const blocks = input.placements ?? [];
461
+ const placements = new Array(blocks.length);
462
+ let nextPlacement = 0;
463
+ const pendingPreviews = {
464
+ count: 0
465
+ };
466
+ const previews = new Map();
467
+ let loggedPreviewFailure = false;
468
+ const onPreviewFailure = (cause, placement)=>{
469
+ if (loggedPreviewFailure) return;
470
+ loggedPreviewFailure = true;
471
+ req.payload.logger.warn({
472
+ err: safeError(cause),
473
+ experimentId: e.id,
382
474
  collection: placement.collection,
383
- id: placement.id,
384
- title: placement.title,
385
- slots: placement.slots,
386
- ...placementUrls(options, placement),
387
- ok: requiredSlots.every((slot)=>slot && placement.slots[slot] > 0),
388
- detail: placement.published ? 'Published' : 'Draft'
389
- }));
475
+ id: placement.id
476
+ }, 'Experiment preview URL lookup failed');
477
+ };
478
+ await Promise.all(Array.from({
479
+ length: Math.min(PREVIEW_URL_CONCURRENCY, blocks.length)
480
+ }, async ()=>{
481
+ while(nextPlacement < blocks.length){
482
+ const index = nextPlacement++;
483
+ const placement = blocks[index];
484
+ placements[index] = {
485
+ collection: placement.collection,
486
+ id: placement.id,
487
+ title: placement.title,
488
+ slots: placement.slots,
489
+ ...await placementUrls(options, placement, pendingPreviews, onPreviewFailure, previews),
490
+ ok: requiredSlots.every((slot)=>slot && placement.slots[slot] > 0),
491
+ detail: placement.published ? 'Published' : 'Draft'
492
+ };
493
+ }
494
+ }));
390
495
  for (const placement of input.flagPlacements ?? [])placements.push({
391
496
  collection: e.flagPoint ?? '',
392
497
  id: placement.arm,
@@ -396,9 +501,116 @@ export async function preflightData(req, { experimentId, options }, { now = new
396
501
  detail: placement.detail
397
502
  } : {}
398
503
  });
504
+ const pagePlacements = [];
505
+ if (e.kind === 'page') {
506
+ const collection = await resolvePageCollection(req, options, e);
507
+ for (const arm of e.arms.filter((arm)=>arm.kind !== 'holdout')){
508
+ const path = arm.pageTarget ?? '';
509
+ let page = null;
510
+ try {
511
+ page = path && collection ? await collection.findByPath(path, req) : null;
512
+ } catch (cause) {
513
+ onPreviewFailure(cause, {
514
+ collection: collection.slug,
515
+ id: path,
516
+ title: path,
517
+ published: false,
518
+ slots: {
519
+ control: 0,
520
+ variantA: 0,
521
+ variantB: 0,
522
+ variantC: 0
523
+ }
524
+ });
525
+ }
526
+ if (!page || !collection) {
527
+ pagePlacements.push({
528
+ arm: arm.key,
529
+ path,
530
+ published: false,
531
+ id: null,
532
+ ...collection ? {
533
+ collection: collection.slug
534
+ } : {}
535
+ });
536
+ continue;
537
+ }
538
+ let doc;
539
+ try {
540
+ doc = await req.payload.findByID({
541
+ collection: collection.slug,
542
+ id: page.id,
543
+ req,
544
+ overrideAccess: false,
545
+ depth: 0,
546
+ draft: !page.published
547
+ });
548
+ } catch (cause) {
549
+ onPreviewFailure(cause, {
550
+ collection: collection.slug,
551
+ id: page.id,
552
+ title: path,
553
+ published: false,
554
+ slots: {
555
+ control: 0,
556
+ variantA: 0,
557
+ variantB: 0,
558
+ variantC: 0
559
+ }
560
+ });
561
+ pagePlacements.push({
562
+ arm: arm.key,
563
+ path,
564
+ collection: collection.slug,
565
+ published: false,
566
+ id: null
567
+ });
568
+ continue;
569
+ }
570
+ const titleField = req.payload.config.collections?.find((entry)=>entry.slug === collection.slug)?.admin.useAsTitle ?? 'title';
571
+ const placement = {
572
+ collection: collection.slug,
573
+ id: page.id,
574
+ title: String(doc[titleField] ?? doc.id),
575
+ published: page.published,
576
+ ...page.published && typeof doc.slug === 'string' ? {
577
+ slug: doc.slug
578
+ } : {},
579
+ slots: {
580
+ control: 0,
581
+ variantA: 0,
582
+ variantB: 0,
583
+ variantC: 0
584
+ },
585
+ editor: 'page',
586
+ doc
587
+ };
588
+ pagePlacements.push({
589
+ arm: arm.key,
590
+ path,
591
+ collection: collection.slug,
592
+ id: page.id,
593
+ title: placement.title,
594
+ published: placement.published,
595
+ ...placement.slug ? {
596
+ slug: placement.slug
597
+ } : {},
598
+ ...await placementUrls(options, placement, pendingPreviews, onPreviewFailure, previews)
599
+ });
600
+ }
601
+ }
399
602
  return {
400
603
  checks: checkStart(input),
604
+ ...input.goalContent ? {
605
+ goalNotes: input.metrics.filter((m)=>m.source.startsWith('click:') && input.goalContent && !input.goalContent.skipped && !input.goalContent.unchecked.length && !input.goalContent.keys.some((k)=>`click:${k.key}` === m.source)).map((m)=>({
606
+ metricId: m.id,
607
+ message: OUTSIDE_GOAL
608
+ }))
609
+ } : {},
401
610
  placements,
611
+ ...e.kind === 'page' ? {
612
+ pagePlacements
613
+ } : {},
402
614
  baseline,
403
615
  trafficPerDay,
404
616
  estimate: power
@@ -2,6 +2,7 @@ import type { DocId, MetricSource } from '../types/contracts.js';
2
2
  export interface MetricDefinition extends Record<string, unknown> {
3
3
  id: DocId;
4
4
  source: string;
5
+ direction?: 'increase' | 'decrease';
5
6
  attributionWindowHours?: number | null;
6
7
  minSampleSize?: number | null;
7
8
  guardrailThreshold?: number | null;
@@ -8,7 +8,7 @@ import { perUnitHistory } from '../results/history.js';
8
8
  const from = new Date(start.getTime() - 90 * 86400000);
9
9
  for (const metric of metrics){
10
10
  const source = sources.find((source)=>source.id === metric.source);
11
- const kind = metric.source.startsWith('events:') ? 'binomial' : source?.kind;
11
+ const kind = metric.source.startsWith('events:') || metric.source.startsWith('click:') ? 'binomial' : source?.kind;
12
12
  const definition = {
13
13
  ...structuredClone(metric),
14
14
  ...source ? {
@@ -19,11 +19,15 @@ import { perUnitHistory } from '../results/history.js';
19
19
  historyUnits: undefined,
20
20
  capNotice: undefined
21
21
  };
22
+ if (metric.source.startsWith('click:')) {
23
+ definition.direction = 'increase';
24
+ definition.capping = null;
25
+ }
22
26
  const cap = metric.capping?.type ? metric.capping : kind === 'revenue' ? {
23
27
  type: 'percentile',
24
28
  value: 0.99
25
29
  } : null;
26
- if (kind === 'revenue' || !source && cap && !metric.source.startsWith('events:')) {
30
+ if (kind === 'revenue' || !source && cap && !(metric.source.startsWith('events:') || metric.source.startsWith('click:'))) {
27
31
  definition.capping = cap;
28
32
  if (cap?.type === 'absolute' && typeof cap.value === 'number' && Number.isFinite(cap.value) && cap.value >= 0) {
29
33
  definition.resolvedCap = cap.value;
@@ -1,14 +1,19 @@
1
+ import { goalContent } from '../goals/fairness.js';
2
+ import { detached } from './placementInteractions.js';
3
+ import { lockAndRescanPlacements, placementInteractions } from './placementInteractions.js';
4
+ import { lockPagePaths } from '../db/locks.js';
1
5
  import { lockMetricsForFreeze } from '../db/metricUsage.js';
2
6
  import { DEFAULT_SEQUENTIAL_TUNING_PARAMETER } from '../constants.js';
3
7
  import { claimSlices } from '../db/slices.js';
4
- import { unpublishedPageCopy } from '../admin/copy/readiness.js';
8
+ import { asymmetricGoalsCopy, sameAddressCopy, interactingTestsCopy, pageInUseCopy, unpublishedPageCopy } from '../admin/copy/readiness.js';
9
+ import { GROUP_LABELS } from '../admin/copy/arms.js';
5
10
  import { XpRefusal } from '../errors.js';
6
11
  import { isOpValid } from '../targeting/ops.js';
7
12
  import { newSeed } from '../runtime/seed.js';
8
- import { checkStart, relationshipId } from './checkStart.js';
13
+ import { checkStart, pageTargetPaths, relationshipId } from './checkStart.js';
9
14
  import { withTransition } from './kernel.js';
10
15
  import { writePhaseRecord } from './phaseRecord.js';
11
- import { loadStartInputs } from './preflightData.js';
16
+ import { loadPageTestsInUse, loadStartInputs, unpublishedPageTargets } from './preflightData.js';
12
17
  import { resolveCaps } from './resolveCaps.js';
13
18
  export async function readExperiment(req, id) {
14
19
  // §7.8: internal lookup serves the authorised transition write. Preserve
@@ -30,6 +35,19 @@ export async function readExperiment(req, id) {
30
35
  }
31
36
  export function refuse(code, detail) {
32
37
  const messages = {
38
+ 'goal-in-some-versions': asymmetricGoalsCopy(detail),
39
+ 'placements-changed': "This test's pages changed while it was starting. Try again.",
40
+ 'page-busy': "One of this test's pages is being saved right now. Try again in a moment.",
41
+ 'interacting-tests': (()=>{
42
+ const d = detail?.[0];
43
+ return d ? interactingTestsCopy(d.path, d.status, d.name, d.version) : 'Finish one of the tests first.';
44
+ })(),
45
+ 'unknown-collection': "That page type isn't set up for tests.",
46
+ 'page-in-use': pageInUseCopy(detail?.paths?.[0] ?? 'this page'),
47
+ 'version-same-address': (()=>{
48
+ const d = detail;
49
+ return sameAddressCopy(GROUP_LABELS[d?.arm ?? 'variantA'], GROUP_LABELS[d?.otherArm ?? 'control'], d?.path ?? 'this address');
50
+ })(),
33
51
  'page-target-unpublished': unpublishedPageCopy(typeof detail === 'string' ? detail : 'This page'),
34
52
  'invalid-status': 'This action is not available in the current status.',
35
53
  'stale-draft': 'The experiment changed. Review it again before starting.',
@@ -44,8 +62,12 @@ export function refuse(code, detail) {
44
62
  'entry-not-dismissed': 'That page is not dismissed.'
45
63
  };
46
64
  const conflict = [
65
+ 'page-busy',
66
+ 'placements-changed',
67
+ 'interacting-tests',
47
68
  'invalid-status',
48
69
  'stale-draft',
70
+ 'page-in-use',
49
71
  'flag-point-busy',
50
72
  'layer-full',
51
73
  'allocation-lower',
@@ -107,6 +129,46 @@ export async function start(req, { experimentId, options }) {
107
129
  const current = await readExperiment(req, experimentId);
108
130
  if (current.updatedAt !== updatedAt) refuse('stale-draft');
109
131
  if (current.status !== 'draft' && current.status !== 'stopped') refuse('invalid-status');
132
+ let placementDocs;
133
+ if (current.kind === 'block') {
134
+ const rescan = await lockAndRescanPlacements(req, exec, options, current, input.placements ?? []);
135
+ placementDocs = rescan.docs;
136
+ const failure = checkStart({
137
+ ...input,
138
+ experiment: current,
139
+ placements: rescan.placements
140
+ }).find((check)=>!check.ok);
141
+ if (failure && failure.code !== 'page-targets-unchecked' && failure.code !== 'make-content-unsupported') refuse(failure.code, failure.detail);
142
+ }
143
+ if (current.kind === 'page') {
144
+ // Independent experiment/layer locks do not serialize overlapping pages.
145
+ // Re-read occupancy after acquiring sorted path locks; hold through commit.
146
+ // A waiting Start then sees the first Start's live test on this transaction.
147
+ await lockPagePaths(exec, pageTargetPaths(current.arms));
148
+ const recheck = checkStart({
149
+ ...input,
150
+ experiment: current,
151
+ pageTestsInUse: await loadPageTestsInUse(req)
152
+ }).find((check)=>check.code === 'page-in-use' && !check.ok);
153
+ if (recheck) refuse('page-in-use', recheck.detail);
154
+ const conflicts = await placementInteractions(req, exec, options, current, pageTargetPaths(current.arms));
155
+ if (conflicts.length) refuse('interacting-tests', conflicts);
156
+ if (options.pages) {
157
+ // The page guard takes the same path locks, so a delete, unpublish or rename that committed
158
+ // while Start waited is visible now. Read it on a detached request (committed state, outside
159
+ // this transaction): a refused host read must not kill the transition (CORE-21).
160
+ // (the make-content pattern: drop the transaction id for the reads, restore it after).
161
+ const transactionID = req.transactionID;
162
+ delete req.transactionID;
163
+ let unpublished;
164
+ try {
165
+ unpublished = await unpublishedPageTargets(req, options, current);
166
+ } finally{
167
+ req.transactionID = transactionID;
168
+ }
169
+ if (unpublished.length) refuse('page-target-unpublished', unpublished[0]);
170
+ }
171
+ }
110
172
  if (current.kind === 'flag' && current.flagPoint) {
111
173
  const busy = await req.payload.find({
112
174
  collection: 'xp-experiments',
@@ -151,6 +213,15 @@ export async function start(req, { experimentId, options }) {
151
213
  }).find((check)=>!check.ok);
152
214
  if (recheck && recheck.code !== 'page-targets-unchecked' && recheck.code !== 'make-content-unsupported') refuse(recheck.code, recheck.detail);
153
215
  }
216
+ if (input.metrics.some((metric)=>metric.source.startsWith('click:'))) {
217
+ const content = await detached(req, ()=>goalContent(req, options, current, placementDocs));
218
+ const failure = checkStart({
219
+ ...input,
220
+ experiment: current,
221
+ goalContent: content
222
+ }).find((check)=>check.code === 'goal-in-some-versions' && !check.ok);
223
+ if (failure) refuse('goal-in-some-versions', failure.detail);
224
+ }
154
225
  const lockedMetrics = await lockMetricsForFreeze(exec, caps.definitions.map((metric)=>metric.id));
155
226
  if (!lockedMetrics) {
156
227
  throw new XpRefusal('stale-draft', 'A goal in this test was deleted. Review the goals before starting.', undefined, 409);
@@ -203,6 +274,9 @@ export async function start(req, { experimentId, options }) {
203
274
  allocationChanges: [],
204
275
  targeting,
205
276
  metricDefinitions: definitions,
277
+ ...placementDocs ? {
278
+ placementDocs
279
+ } : {},
206
280
  analysis: {
207
281
  ...current.analysis,
208
282
  method: current.analysis?.method ?? 'sequential',
@@ -18,4 +18,4 @@ export type SerializedAttributeKeys = Record<string, {
18
18
  type: 'string' | 'number' | 'boolean';
19
19
  values?: string[];
20
20
  }>;
21
- export type EditableField = 'name' | 'hypothesis' | 'arms' | 'targeting' | 'allocation' | 'flagPoint' | 'metrics' | 'analysis' | 'exposurePolicy';
21
+ export type EditableField = 'name' | 'hypothesis' | 'arms' | 'targeting' | 'allocation' | 'flagPoint' | 'metrics' | 'analysis' | 'exposurePolicy' | 'pageCollection';
@@ -32,6 +32,7 @@ export interface FlagPointResult {
32
32
  exposable: boolean;
33
33
  }
34
34
  export interface ClientAssignments {
35
+ clickGoalsArmed?: boolean;
35
36
  v: 1;
36
37
  revision: number;
37
38
  apiRoute: string;
@@ -3,6 +3,14 @@ import type { ExperimentsServerRuntime } from './runtime.js';
3
3
  import type { ArmSlot } from './puck.js';
4
4
  import type { TargetingOp } from '../targeting/ops.js';
5
5
  export type DocId = string | number;
6
+ export interface PreviewPlacement {
7
+ collection: string;
8
+ id: DocId;
9
+ title: string;
10
+ published: boolean;
11
+ /** The published main row's string slug, including on draft placements; absent for unpublished documents, never a draft/version row's slug. */
12
+ slug?: string;
13
+ }
6
14
  export type Attributes = Record<string, string | number | boolean>;
7
15
  export type ExposurePolicy = 'render' | 'viewport' | 'pageview' | 'manual';
8
16
  export type ExperimentKind = 'block' | 'page' | 'flag' | 'code';
@@ -93,6 +101,19 @@ export type RateLimiter = (args: {
93
101
  export type ExperimentsAccess = (args: {
94
102
  req: PayloadRequest;
95
103
  }) => boolean | Promise<boolean>;
104
+ export interface ExperimentsPageCollection {
105
+ slug: string;
106
+ contentOf?: (doc: Record<string, unknown>) => unknown;
107
+ copyHint?: string;
108
+ copyUrl?: string;
109
+ copyLabel?: string;
110
+ pathOf: (doc: Record<string, unknown>) => string | null;
111
+ /** Lookup with the supplied req and overrideAccess: false. Under an anonymous req (results checks) only published pages are visible, so a hit is { published: true } or null. Under a req that can read drafts (Start), a draft-only page may be reported as { published: false }. */
112
+ findByPath: (path: string, req: PayloadRequest) => Promise<{
113
+ id: string | number;
114
+ published: boolean;
115
+ } | null>;
116
+ }
96
117
  export interface ExperimentsPluginOptions<TUser extends {
97
118
  id: string | number;
98
119
  } = {
@@ -113,12 +134,12 @@ export interface ExperimentsPluginOptions<TUser extends {
113
134
  collection: string;
114
135
  id: DocId;
115
136
  draft?: boolean;
137
+ editor: 'puck' | 'blocks' | 'page';
138
+ doc?: Record<string, unknown>;
116
139
  }) => string;
117
- previewUrl?: (args: {
118
- collection: string;
119
- id: DocId;
120
- }) => string;
140
+ previewUrl?: (placement: PreviewPlacement) => string | null | undefined | Promise<string | null | undefined>;
121
141
  descriptions?: {
142
+ page?: string;
122
143
  block?: string;
123
144
  document?: string;
124
145
  setting?: string;
@@ -135,14 +156,14 @@ export interface ExperimentsPluginOptions<TUser extends {
135
156
  values?: string[];
136
157
  }>;
137
158
  pages?: {
159
+ collections: ExperimentsPageCollection[];
160
+ };
161
+ blocks?: {
138
162
  collections: Array<{
139
163
  slug: string;
140
- pathOf: (doc: Record<string, unknown>) => string | null;
141
- /** Published-only lookup using the supplied req and overrideAccess: false. Results checks supply an anonymous req. */
142
- findByPath: (path: string, req: PayloadRequest) => Promise<{
143
- id: string | number;
144
- published: boolean;
145
- } | null>;
164
+ field: string;
165
+ blockSlug?: string;
166
+ appliesTo?: (doc: Record<string, unknown>) => boolean;
146
167
  }>;
147
168
  };
148
169
  puck?: {