@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
@@ -0,0 +1,120 @@
1
+ import { normalisePath } from '../runtime/normalisePath.js';
2
+ import { XpRefusal } from '../errors.js';
3
+ export async function handlePages(req, { options }) {
4
+ const query = new URL(req.url ?? 'http://xp.invalid').searchParams;
5
+ const collection = options.pages?.collections.find((entry)=>entry.slug === query.get('collection'));
6
+ if (!collection) throw new XpRefusal('unknown-collection', "That page type isn't set up for tests.", undefined, 422);
7
+ const path = query.get('path');
8
+ if (path !== null && (!path.length || path.length > 2048 || query.has('q'))) throw new XpRefusal('invalid-body', 'Enter a valid request body.', undefined, 422);
9
+ const page = Number(query.get('page') ?? 1);
10
+ if (!Number.isSafeInteger(page) || page < 1 || page > 1000) throw new XpRefusal('invalid-body', 'Choose a valid page.', undefined, 422);
11
+ const config = req.payload.config.collections.find((entry)=>entry.slug === collection.slug);
12
+ const titleField = config?.admin.useAsTitle ?? 'title';
13
+ const drafts = Boolean(config?.versions && config.versions.drafts);
14
+ const q = query.get('q')?.trim().slice(0, 100);
15
+ if (path !== null) {
16
+ const found = await collection.findByPath(path, req);
17
+ if (!found) return Response.json({
18
+ docs: [],
19
+ hasNextPage: false
20
+ }, {
21
+ headers: {
22
+ 'Cache-Control': 'no-store'
23
+ }
24
+ });
25
+ const main = await req.payload.findByID({
26
+ collection: collection.slug,
27
+ id: found.id,
28
+ req,
29
+ overrideAccess: false,
30
+ depth: 0,
31
+ draft: false,
32
+ disableErrors: true
33
+ });
34
+ if (!main) return Response.json({
35
+ docs: [],
36
+ hasNextPage: false
37
+ }, {
38
+ headers: {
39
+ 'Cache-Control': 'no-store'
40
+ }
41
+ });
42
+ const published = !drafts || main._status === 'published';
43
+ const doc = published ? main : await req.payload.findByID({
44
+ collection: collection.slug,
45
+ id: main.id,
46
+ req,
47
+ overrideAccess: false,
48
+ depth: 0,
49
+ draft: true,
50
+ disableErrors: true
51
+ });
52
+ if (!doc) return Response.json({
53
+ docs: [],
54
+ hasNextPage: false
55
+ }, {
56
+ headers: {
57
+ 'Cache-Control': 'no-store'
58
+ }
59
+ });
60
+ const rawPath = collection.pathOf(doc);
61
+ return Response.json({
62
+ docs: [
63
+ {
64
+ id: main.id,
65
+ title: titleField !== 'id' && typeof doc[titleField] === 'string' && String(doc[titleField]).trim() ? String(doc[titleField]) : '',
66
+ path: rawPath,
67
+ pathKey: rawPath === null ? null : normalisePath(rawPath),
68
+ published
69
+ }
70
+ ],
71
+ hasNextPage: false
72
+ }, {
73
+ headers: {
74
+ 'Cache-Control': 'no-store'
75
+ }
76
+ });
77
+ }
78
+ const result = await req.payload.find({
79
+ collection: collection.slug,
80
+ req,
81
+ overrideAccess: false,
82
+ depth: 0,
83
+ draft: false,
84
+ sort: '-createdAt',
85
+ limit: 20,
86
+ page,
87
+ where: q && titleField !== 'id' ? {
88
+ [titleField]: {
89
+ like: q
90
+ }
91
+ } : undefined
92
+ });
93
+ const docs = await Promise.all(result.docs.map(async (main)=>{
94
+ const published = !drafts || main._status === 'published';
95
+ const doc = published ? main : await req.payload.findByID({
96
+ collection: collection.slug,
97
+ id: main.id,
98
+ req,
99
+ overrideAccess: false,
100
+ depth: 0,
101
+ draft: true
102
+ });
103
+ const path = collection.pathOf(doc);
104
+ return {
105
+ id: main.id,
106
+ title: String(doc[titleField] ?? main.id),
107
+ path,
108
+ pathKey: path === null ? null : normalisePath(path),
109
+ published
110
+ };
111
+ }));
112
+ return Response.json({
113
+ docs,
114
+ hasNextPage: result.hasNextPage
115
+ }, {
116
+ headers: {
117
+ 'Cache-Control': 'no-store'
118
+ }
119
+ });
120
+ }
@@ -0,0 +1,6 @@
1
+ import type { PayloadRequest } from 'payload';
2
+ import type { DocId } from '../types/contracts.js';
3
+ import type { EndpointContext } from '../types/endpoints.js';
4
+ export declare function handlePlacementStatus<TUser extends {
5
+ id: DocId;
6
+ }>(req: PayloadRequest, { options }: EndpointContext<TUser>): Promise<Response>;
@@ -0,0 +1,238 @@
1
+ import { readBody } from './readBody.js';
2
+ import { safeError } from '../log.js';
3
+ import { statusPageContent } from '../db/pageContent.js';
4
+ import { normalisePath } from '../runtime/normalisePath.js';
5
+ import { puckCollections } from '../options.js';
6
+ import { resolvePageCollection } from '../transitions/pageCollection.js';
7
+ const error = (status, code, message)=>Response.json({
8
+ errors: [
9
+ {
10
+ code,
11
+ message
12
+ }
13
+ ]
14
+ }, {
15
+ status
16
+ });
17
+ const object = (v)=>!!v && typeof v === 'object' && !Array.isArray(v);
18
+ const id = (v)=>typeof v === 'string' && !!v.trim() || typeof v === 'number' && Number.isSafeInteger(v);
19
+ export async function handlePlacementStatus(req, { options }) {
20
+ let value;
21
+ try {
22
+ value = await readBody(req);
23
+ } catch (cause) {
24
+ return error(cause instanceof RangeError ? 413 : 422, cause instanceof RangeError ? 'body-too-large' : 'invalid-body', 'Enter a valid request body.');
25
+ }
26
+ if (!object(value) || Object.keys(value).some((key)=>key !== 'docs' && key !== 'slugs') || value.docs !== undefined && !Array.isArray(value.docs) || value.slugs !== undefined && !Array.isArray(value.slugs)) return error(422, 'invalid-body', 'Enter a valid request body.');
27
+ if ((value.docs?.length ?? 0) > 100 || (value.slugs?.length ?? 0) > 100) return error(422, 'too-many-ids', 'Choose at most 100 documents and slugs.');
28
+ if (value.docs?.some((doc)=>!object(doc) || typeof doc.collection !== 'string' || !id(doc.id)) || value.slugs?.some((slug)=>typeof slug !== 'string' || !slug || slug.length > 64)) return error(422, 'invalid-body', 'Enter valid documents and slugs.');
29
+ const body = value;
30
+ const registered = new Set([
31
+ ...options.pages?.collections.map((entry)=>entry.slug) ?? [],
32
+ ...options.blocks?.collections.map((entry)=>entry.slug) ?? [],
33
+ ...puckCollections(options).map((entry)=>entry.slug)
34
+ ]);
35
+ if (body.docs?.some((doc)=>!registered.has(doc.collection))) return error(422, 'unknown-collection', "That page type isn't set up for tests.");
36
+ const result = {
37
+ docs: Object.create(null),
38
+ slugs: Object.create(null)
39
+ };
40
+ const wanted = body.slugs ?? [];
41
+ const { docs } = await req.payload.find({
42
+ collection: 'xp-experiments',
43
+ req,
44
+ overrideAccess: false,
45
+ depth: 0,
46
+ pagination: false,
47
+ limit: 0,
48
+ where: !body.docs?.length ? {
49
+ slug: {
50
+ in: wanted
51
+ }
52
+ } : !wanted.length ? {
53
+ kind: {
54
+ equals: 'page'
55
+ }
56
+ } : undefined
57
+ });
58
+ const tests = docs;
59
+ const state = wanted.length ? await req.payload.findGlobal({
60
+ slug: 'xp-state',
61
+ req,
62
+ overrideAccess: false,
63
+ depth: 0
64
+ }) : null;
65
+ const retired = Array.isArray(state?.retiredSlugs) ? state.retiredSlugs : [];
66
+ const rounds = new Map();
67
+ for (const test of tests){
68
+ let arms = test.arms, rollout = false;
69
+ if ([
70
+ 'running',
71
+ 'decided',
72
+ 'archived'
73
+ ].includes(test.status)) {
74
+ const phases = await req.payload.find({
75
+ collection: 'xp-phases',
76
+ req,
77
+ overrideAccess: false,
78
+ depth: 0,
79
+ limit: 1,
80
+ sort: '-phase',
81
+ where: {
82
+ experiment: {
83
+ equals: test.id
84
+ }
85
+ }
86
+ });
87
+ const latest = phases.docs[0];
88
+ rollout = test.status === 'decided' && latest?.phaseKind === 'rollout' && !latest.endedAt && !test.decision?.holdoutReleasedAt;
89
+ if (latest?.phaseKind === 'test') arms = latest.arms;
90
+ else if (latest) {
91
+ const frozen = await req.payload.find({
92
+ collection: 'xp-phases',
93
+ req,
94
+ overrideAccess: false,
95
+ depth: 0,
96
+ limit: 1,
97
+ sort: '-phase',
98
+ where: {
99
+ and: [
100
+ {
101
+ experiment: {
102
+ equals: test.id
103
+ }
104
+ },
105
+ {
106
+ phaseKind: {
107
+ equals: 'test'
108
+ }
109
+ }
110
+ ]
111
+ }
112
+ });
113
+ arms = frozen.docs[0]?.arms ?? arms;
114
+ }
115
+ }
116
+ rounds.set(test.slug, {
117
+ arms,
118
+ rollout
119
+ });
120
+ }
121
+ for (const slug of wanted){
122
+ const test = tests.find((test)=>test.slug === slug);
123
+ if (!test) {
124
+ result.slugs[slug] = retired.includes(slug) ? {
125
+ deleted: true
126
+ } : {
127
+ missing: true
128
+ };
129
+ continue;
130
+ }
131
+ const round = rounds.get(slug);
132
+ result.slugs[slug] = {
133
+ name: test.name,
134
+ status: test.status,
135
+ kind: test.kind,
136
+ arms: round.arms.map(({ key, label, slot })=>({
137
+ key,
138
+ label,
139
+ ...slot ? {
140
+ slot
141
+ } : {}
142
+ })),
143
+ ...test.decision?.winnerArm ? {
144
+ winnerArm: test.decision.winnerArm
145
+ } : {},
146
+ rollout: round.rollout
147
+ };
148
+ }
149
+ const collections = new Map();
150
+ const winnerTitles = new Map();
151
+ for (const { collection, id } of body.docs ?? []){
152
+ // Check document access before the raw path read. A refused read propagates.
153
+ await req.payload.findByID({
154
+ collection,
155
+ id,
156
+ req,
157
+ overrideAccess: false,
158
+ depth: 0,
159
+ draft: false
160
+ });
161
+ const registration = options.pages?.collections.find((entry)=>entry.slug === collection);
162
+ const raw = registration ? await statusPageContent(req, collection, id) : null;
163
+ const rawPath = raw && registration ? registration.pathOf(raw) : null;
164
+ const path = rawPath;
165
+ const pathKey = path === null ? null : normalisePath(path);
166
+ const status = {
167
+ path,
168
+ tests: []
169
+ };
170
+ for (const test of tests){
171
+ if (test.kind !== 'page' || path === null || test.decision?.pagesReleasedAt || test.decision?.discardedAt || test.status === 'archived' && !test.decision?.winnerArm) continue;
172
+ const round = rounds.get(test.slug);
173
+ if (!collections.has(test.slug)) collections.set(test.slug, await resolvePageCollection(req, options, {
174
+ ...test,
175
+ arms: round.arms
176
+ }));
177
+ const testCollection = collections.get(test.slug);
178
+ if (testCollection?.slug !== collection) continue;
179
+ const arm = round.arms.find((arm)=>arm.kind !== 'holdout' && arm.pageTarget && normalisePath(arm.pageTarget) === pathKey);
180
+ if (!arm) continue;
181
+ const winner = round.arms.find((arm)=>arm.key === test.decision?.winnerArm);
182
+ const original = round.arms.find((arm)=>arm.kind === 'control');
183
+ if (!winnerTitles.has(test.slug)) {
184
+ let title;
185
+ try {
186
+ const winnerPage = winner?.pageTarget && registration ? await registration.findByPath(winner.pageTarget, req) : null;
187
+ const winnerDoc = winnerPage ? await req.payload.findByID({
188
+ collection,
189
+ id: winnerPage.id,
190
+ req,
191
+ overrideAccess: false,
192
+ depth: 0,
193
+ draft: false,
194
+ disableErrors: true
195
+ }) : null;
196
+ const titleField = req.payload.config.collections.find((entry)=>entry.slug === collection)?.admin.useAsTitle ?? 'title';
197
+ if (winnerDoc) title = String(winnerDoc[titleField] ?? winnerDoc.id);
198
+ } catch (cause) {
199
+ req.payload.logger.error({
200
+ err: safeError(cause),
201
+ experimentId: test.id
202
+ }, 'Experiment winner title lookup failed');
203
+ }
204
+ winnerTitles.set(test.slug, title);
205
+ }
206
+ const winnerTitle = winnerTitles.get(test.slug);
207
+ status.tests.push({
208
+ slug: test.slug,
209
+ name: test.name,
210
+ status: test.status,
211
+ role: arm.kind === 'control' ? 'original' : 'version',
212
+ armLabel: arm.label,
213
+ winner: [
214
+ 'decided',
215
+ 'archived'
216
+ ].includes(test.status) && winner?.key === arm.key,
217
+ ...winner ? {
218
+ winnerLabel: winner.label,
219
+ winnerPath: winner.pageTarget
220
+ } : {},
221
+ ...winnerTitle ? {
222
+ winnerTitle
223
+ } : {},
224
+ ...original?.pageTarget ? {
225
+ originalPath: normalisePath(original.pageTarget)
226
+ } : {},
227
+ rollout: round.rollout,
228
+ pagesReleased: false
229
+ });
230
+ }
231
+ result.docs[`${collection}:${id}`] = status;
232
+ }
233
+ return Response.json(result, {
234
+ headers: {
235
+ 'Cache-Control': 'no-store'
236
+ }
237
+ });
238
+ }
@@ -0,0 +1,2 @@
1
+ import type { PayloadRequest } from 'payload';
2
+ export declare function readBody(req: PayloadRequest): Promise<unknown>;
@@ -0,0 +1,24 @@
1
+ export async function readBody(req) {
2
+ if (req.data !== undefined) {
3
+ if (Buffer.byteLength(JSON.stringify(req.data)) > 16 * 1024) throw new RangeError();
4
+ return req.data;
5
+ }
6
+ if (!req.body) return {};
7
+ const reader = req.body.getReader(), chunks = [];
8
+ let size = 0;
9
+ try {
10
+ while(true){
11
+ const { done, value } = await reader.read();
12
+ if (done) break;
13
+ size += value.byteLength;
14
+ if (size > 16 * 1024) {
15
+ void reader.cancel().catch(()=>{});
16
+ throw new RangeError();
17
+ }
18
+ chunks.push(value);
19
+ }
20
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'));
21
+ } finally{
22
+ reader.releaseLock();
23
+ }
24
+ }
@@ -1,6 +1,7 @@
1
1
  import { resultsMetadata, newestSnapshotIds } from '../db/snapshots.js';
2
2
  import { pooledExecutor } from '../db/executor.js';
3
3
  import { routeId } from './routeId.js';
4
+ import { isClickSource } from '../goals/key.js';
4
5
  const headers = {
5
6
  'Cache-Control': 'no-store'
6
7
  };
@@ -85,11 +86,14 @@ export async function handleResults(req, _ctx) {
85
86
  label,
86
87
  kind
87
88
  })),
88
- metrics: record.metricDefinitions.map(({ metricId, name, kind, role, resolvedCap, historyUnits, capNotice })=>({
89
+ metrics: record.metricDefinitions.map(({ metricId, name, kind, role, source, resolvedCap, historyUnits, capNotice })=>({
89
90
  metricId,
90
91
  name,
91
92
  kind,
92
93
  role,
94
+ ...isClickSource(source) ? {
95
+ clickGoal: true
96
+ } : {},
93
97
  ...resolvedCap === undefined ? {} : {
94
98
  resolvedCap
95
99
  },
@@ -43,10 +43,17 @@ export async function handleSummary(req, { options }) {
43
43
  headline: snapshot.verdict.headline
44
44
  },
45
45
  ...snapshot.health ? {
46
- health: snapshot.health.slice(0, 100).map(({ code, path })=>({
47
- code,
48
- path: path.slice(0, 128)
49
- }))
46
+ health: snapshot.health.slice(0, 100).map((item)=>item.code === 'placement-unchecked' ? {
47
+ code: item.code,
48
+ detail: sanitizeReadinessDetail('placement-unchecked', item.detail)
49
+ } : item.code === 'goal-in-some-versions' ? {
50
+ code: item.code,
51
+ metricId: item.metricId,
52
+ name: item.name.slice(0, 128)
53
+ } : {
54
+ code: item.code,
55
+ path: item.path.slice(0, 128)
56
+ })
50
57
  } : {}
51
58
  } : {
52
59
  exposures: 0,
@@ -0,0 +1,6 @@
1
+ import { type PayloadRequest } from 'payload';
2
+ import type { EndpointContext } from '../types/endpoints.js';
3
+ import type { DocId } from '../types/contracts.js';
4
+ export declare function handleWrapBlock<TUser extends {
5
+ id: DocId;
6
+ }>(req: PayloadRequest, { options }: EndpointContext<TUser>): Promise<Response>;
@@ -0,0 +1,76 @@
1
+ import { Locked } from 'payload';
2
+ import { readBody } from './readBody.js';
3
+ import { findRegisteredBlocksField, wrapExperimentBlock } from '../blocks/formState.js';
4
+ import { EXPERIMENT_BLOCK_SLUG, hostBlocks, isObject } from '../blocks/model.js';
5
+ import { XpRefusal } from '../errors.js';
6
+ export async function handleWrapBlock(req, { options }) {
7
+ let value;
8
+ try {
9
+ value = await readBody(req);
10
+ } catch (cause) {
11
+ throw new XpRefusal(cause instanceof RangeError ? 'body-too-large' : 'invalid-body', 'Enter a valid request body.', undefined, cause instanceof RangeError ? 413 : 422);
12
+ }
13
+ if (!isObject(value) || Object.keys(value).some((k)=>![
14
+ 'collection',
15
+ 'id',
16
+ 'field',
17
+ 'blockId',
18
+ 'experiment'
19
+ ].includes(k)) || ![
20
+ 'collection',
21
+ 'field',
22
+ 'blockId',
23
+ 'experiment'
24
+ ].every((k)=>typeof value[k] === 'string' && value[k].trim()) || !(typeof value.id === 'string' && value.id.trim() || typeof value.id === 'number' && Number.isSafeInteger(value.id))) throw new XpRefusal('invalid-body', 'Enter a valid request body.', undefined, 422);
25
+ const { collection, field, blockId, experiment } = value;
26
+ const id = value.id;
27
+ const registration = options.blocks?.collections.find((r)=>r.slug === collection && r.field === field);
28
+ if (!registration) throw new XpRefusal('unknown-collection', "That page type isn't set up for tests.", undefined, 422);
29
+ const tests = await req.payload.find({
30
+ collection: 'xp-experiments',
31
+ req,
32
+ overrideAccess: false,
33
+ depth: 0,
34
+ limit: 1,
35
+ where: {
36
+ slug: {
37
+ equals: experiment
38
+ }
39
+ }
40
+ });
41
+ const test = tests.docs[0];
42
+ if (test?.kind !== 'block') throw new XpRefusal('invalid-body', 'Choose a block test.', undefined, 422);
43
+ if (test.status !== 'draft') throw new XpRefusal('invalid-status', 'This action is not available in the current status.', undefined, 409);
44
+ const doc = await req.payload.findByID({
45
+ collection,
46
+ id,
47
+ req,
48
+ overrideAccess: false,
49
+ depth: 0,
50
+ draft: true,
51
+ includeLockStatus: true
52
+ });
53
+ const blocks = hostBlocks(doc[field]), blockSlug = registration.blockSlug ?? EXPERIMENT_BLOCK_SLUG;
54
+ const index = blocks.findIndex((block)=>block.id === blockId && block.blockType !== blockSlug);
55
+ if (index < 0) throw new XpRefusal('wrap-target-missing', 'Reload the page and try again.', undefined, 409);
56
+ const config = findRegisteredBlocksField(req.payload.config.collections.find((c)=>c.slug === collection)?.fields ?? [], field);
57
+ if (!config) throw new Error(`Experiments blocks field ${collection}.${field} is not configured.`);
58
+ blocks[index] = wrapExperimentBlock(blocks[index], experiment, config.field, req.payload.config, blockSlug);
59
+ try {
60
+ const result = await req.payload.update({
61
+ collection,
62
+ id,
63
+ req,
64
+ overrideAccess: false,
65
+ overrideLock: false,
66
+ draft: true,
67
+ data: {
68
+ [field]: blocks
69
+ }
70
+ });
71
+ return Response.json(result);
72
+ } catch (error) {
73
+ if (error instanceof Locked || error instanceof Error && error.name === 'Locked') throw new XpRefusal('locked-placement', 'Another editor has this page open. Try again when it is unlocked.', undefined, 409);
74
+ throw error;
75
+ }
76
+ }
package/dist/errors.d.ts CHANGED
@@ -15,15 +15,15 @@ export declare class XpLockedFieldError extends Error {
15
15
  }
16
16
  export declare class XpNoTransactionError extends Error {
17
17
  name: string;
18
- constructor();
18
+ constructor(message?: string);
19
19
  }
20
20
  export declare class XpRefusal extends Error {
21
21
  readonly code: RefusalCode;
22
22
  readonly detail: unknown | undefined;
23
- readonly status: 409 | 422 | 500 | 404;
23
+ readonly status: 409 | 413 | 422 | 500 | 404;
24
24
  name: string;
25
25
  readonly isPublic = true;
26
- constructor(code: RefusalCode, message: string, detail: unknown | undefined, status: 409 | 422 | 500 | 404);
26
+ constructor(code: RefusalCode, message: string, detail: unknown | undefined, status: 409 | 413 | 422 | 500 | 404, options?: ErrorOptions);
27
27
  }
28
28
  /** Fixed analysis diagnostics: callers can choose a code, never supply log text. */
29
29
  export type AnalysisErrorCode = 'no-baseline' | 'primary-metric-count' | 'duplicate-metric' | 'invalid-timestamp' | 'window-hours' | 'window-before-start' | 'unknown-arm' | 'metric-not-supplied' | 'non-finite-values' | 'non-finite-covariates' | 'source-unavailable' | 'unit-type-unavailable' | 'srm-weights' | 'cuped-non-finite' | 'cuped-binomial' | 'seed-format' | 'snapshot-shape' | 'snapshot-invariants';
package/dist/errors.js CHANGED
@@ -17,8 +17,8 @@ export class XpLockedFieldError extends Error {
17
17
  }
18
18
  export class XpNoTransactionError extends Error {
19
19
  name = 'XpNoTransactionError';
20
- constructor(){
21
- super('An active transaction is required');
20
+ constructor(message = 'An active transaction is required'){
21
+ super(message);
22
22
  }
23
23
  }
24
24
  export class XpRefusal extends Error {
@@ -27,8 +27,8 @@ export class XpRefusal extends Error {
27
27
  status;
28
28
  name = 'XpRefusal';
29
29
  isPublic = true;
30
- constructor(code, message, detail, status){
31
- super(message), this.code = code, this.detail = detail, this.status = status;
30
+ constructor(code, message, detail, status, options){
31
+ super(message, options), this.code = code, this.detail = detail, this.status = status;
32
32
  }
33
33
  }
34
34
  export const ANALYSIS_ERROR_MESSAGES = Object.freeze({
@@ -4,4 +4,4 @@ import type { ResolvedOptions } from '../types/contracts.js';
4
4
  import type { Manifest } from '../types/manifest.js';
5
5
  export declare function eventRows<TUser extends {
6
6
  id: string | number;
7
- }>(events: unknown[], manifest: Manifest, assignments: AssignmentResult, options: ResolvedOptions<TUser>, visitorId: string, userId: string | undefined, ts: string, requestAttributes?: Readonly<Record<string, unknown>>): EventRow[];
7
+ }>(events: unknown[], manifest: Manifest, assignments: AssignmentResult, options: ResolvedOptions<TUser>, visitorId: string, userId: string | undefined, ts: string, requestAttributes?: Readonly<Record<string, unknown>>, clickKeys?: ReadonlySet<string>): EventRow[];
@@ -1,3 +1,4 @@
1
+ import { validClickSource, isClickSource } from '../goals/key.js';
1
2
  import { exposureDimensions } from './dimensions.js';
2
3
  function record(value) {
3
4
  return value !== null && typeof value === 'object' && !Array.isArray(value);
@@ -9,7 +10,7 @@ function record(value) {
9
10
  return definition && typeof item === definition.type && (typeof item !== 'number' || Number.isFinite(item)) && (!definition.values || typeof item === 'string' && definition.values.includes(item));
10
11
  }));
11
12
  }
12
- export function eventRows(events, manifest, assignments, options, visitorId, userId, ts, requestAttributes = {}) {
13
+ export function eventRows(events, manifest, assignments, options, visitorId, userId, ts, requestAttributes = {}, clickKeys) {
13
14
  const rows = new Map();
14
15
  for (const event of events){
15
16
  if (!record(event)) continue;
@@ -42,7 +43,8 @@ export function eventRows(events, manifest, assignments, options, visitorId, use
42
43
  } else if (event.type === 'conversion') {
43
44
  if (typeof event.eventName !== 'string' || typeof event.idempotencyKey !== 'string' || !event.idempotencyKey.trim() || event.value !== undefined && (typeof event.value !== 'number' || !Number.isFinite(event.value))) continue;
44
45
  const source = options.metricSources?.find((source)=>source.id === event.eventName);
45
- if (source ? source.clientWritable !== true || source.kind === 'revenue' : !options.eventSources?.some((source)=>source.name === event.eventName && source.clientWritable)) continue;
46
+ const click = isClickSource(event.eventName);
47
+ if (click ? !validClickSource(event.eventName) || !clickKeys?.has(event.eventName.slice(6)) : source ? source.clientWritable !== true || source.kind === 'revenue' : !options.eventSources?.some((source)=>source.name === event.eventName && source.clientWritable)) continue;
46
48
  row = {
47
49
  type: 'conversion',
48
50
  eventName: event.eventName,
@@ -52,7 +54,7 @@ export function eventRows(events, manifest, assignments, options, visitorId, use
52
54
  userId,
53
55
  ts,
54
56
  dimensions: null,
55
- attributes: attributes(event.attributes, options.attributeKeys),
57
+ attributes: click ? undefined : attributes(event.attributes, options.attributeKeys),
56
58
  // §5.5: scoped by unit and event so one client can never pre-claim another's key.
57
59
  dedupeKey: `conversion:c:${visitorId}:${event.eventName}:${event.idempotencyKey}`
58
60
  };
@@ -5,3 +5,8 @@ export { ArmSelectField } from '../admin/armSelect/ArmSelectField.js';
5
5
  export { TestsNavLink } from '../admin/TestsNavLink.js';
6
6
  export { TestsSurface } from '../admin/surface/TestsSurface.js';
7
7
  export { TestsDashboardCards } from '../admin/TestsDashboardCards.js';
8
+ export { PlacementStatus } from '../admin/PlacementStatus.js';
9
+ export { wrapExperimentBlock, syncExperimentSlots, copyOriginalIntoVersion, unwrapExperimentBlock, findRegisteredBlocksField } from '../blocks/formState.js';
10
+ export { TestBlockField } from '../admin/blocks/TestBlockField.js';
11
+ export { BlockStatusField, BlockSlotRowLabel, CopyOriginalField } from '../admin/blocks/BlockPanel.js';
12
+ export { XpGoalField } from '../admin/goals/XpGoalField.js';
@@ -6,3 +6,8 @@ export { ArmSelectField } from '../admin/armSelect/ArmSelectField.js';
6
6
  export { TestsNavLink } from '../admin/TestsNavLink.js';
7
7
  export { TestsSurface } from '../admin/surface/TestsSurface.js';
8
8
  export { TestsDashboardCards } from '../admin/TestsDashboardCards.js';
9
+ export { PlacementStatus } from '../admin/PlacementStatus.js';
10
+ export { wrapExperimentBlock, syncExperimentSlots, copyOriginalIntoVersion, unwrapExperimentBlock, findRegisteredBlocksField } from '../blocks/formState.js';
11
+ export { TestBlockField } from '../admin/blocks/TestBlockField.js';
12
+ export { BlockStatusField, BlockSlotRowLabel, CopyOriginalField } from '../admin/blocks/BlockPanel.js';
13
+ export { XpGoalField } from '../admin/goals/XpGoalField.js';
@@ -1,3 +1,4 @@
1
1
  export { ExperimentsRootView } from '../admin/views/ExperimentsRootView.js';
2
2
  export { CollectionListRedirect } from '../admin/views/CollectionListRedirect.js';
3
3
  export { CollectionEditRedirect } from '../admin/views/CollectionEditRedirect.js';
4
+ export { TestsNavLinkServer } from '../admin/views/TestsNavLinkServer.js';
@@ -1,3 +1,4 @@
1
1
  export { ExperimentsRootView } from '../admin/views/ExperimentsRootView.js';
2
2
  export { CollectionListRedirect } from '../admin/views/CollectionListRedirect.js';
3
3
  export { CollectionEditRedirect } from '../admin/views/CollectionEditRedirect.js';
4
+ export { TestsNavLinkServer } from '../admin/views/TestsNavLinkServer.js';
@@ -0,0 +1,2 @@
1
+ export { controlOnlyBlocks, segmentExperimentBlocks, EXPERIMENT_BLOCK_SLUG } from '../blocks/model.js';
2
+ export type { HostBlock, ExperimentBlock, ExperimentBlockGroup, ResolvedHostBlock, ResolvedExperimentBlocks } from '../blocks/model.js';
@@ -0,0 +1 @@
1
+ export { controlOnlyBlocks, segmentExperimentBlocks, EXPERIMENT_BLOCK_SLUG } from '../blocks/model.js';
@@ -5,3 +5,4 @@ export { ExperimentPageview } from '../client/ExperimentPageview.js';
5
5
  export { trackConversionBeacon } from '../client/trackConversionBeacon.js';
6
6
  export { MAX_EVENTS_PER_BODY, DEFAULT_MAX_BODY_BYTES } from '../constants.js';
7
7
  export type { ClientAssignments } from '../types/assignments.js';
8
+ export { ExperimentGroup } from '../client/ExperimentGroup.js';